diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a5fb97b85a..b3accd39cfe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,15 +71,6 @@ parameters: devnet-metrics-collect: type: boolean default: false - flake-shake-dispatch: - type: boolean - default: false - flake-shake-iterations: - type: integer - default: 300 - flake-shake-workers: - type: integer - default: 50 # go-cache-version can be used as a cache buster when making breaking changes to caching strategy go-cache-version: type: string @@ -2740,9 +2731,6 @@ workflows: .* c-github-event-action << pipeline.parameters.github-event-action >> .circleci/continue/main.yml .* c-github-event-base64 << pipeline.parameters.github-event-base64 >> .circleci/continue/main.yml .* c-devnet-metrics-collect << pipeline.parameters.devnet-metrics-collect >> .circleci/continue/main.yml - .* c-flake-shake-dispatch << pipeline.parameters.flake-shake-dispatch >> .circleci/continue/main.yml - .* c-flake-shake-iterations << pipeline.parameters.flake-shake-iterations >> .circleci/continue/main.yml - .* c-flake-shake-workers << pipeline.parameters.flake-shake-workers >> .circleci/continue/main.yml .* c-go-cache-version << pipeline.parameters.go-cache-version >> .circleci/continue/main.yml rust/.* c-rust_files_changed true .circleci/continue/main.yml diff --git a/.circleci/continue/main.yml b/.circleci/continue/main.yml index 804357b2da2..848080a40b8 100644 --- a/.circleci/continue/main.yml +++ b/.circleci/continue/main.yml @@ -1,10 +1,10 @@ version: 2.1 parameters: - default_docker_image: + c-default_docker_image: type: string - default: cimg/base:2024.01 - base_image: + default: cimg/base:2026.03 + c-base_image: type: string default: default # The dispatch parameters are used to manually dispatch pipelines that normally only run post-merge on develop @@ -12,7 +12,7 @@ parameters: # when: # or: # - equal: [ "develop", <> ] - # - equal: [ true, <> ] + # - equal: [ true, <> ] # Add a new `*_dispatch` parameter for any pipeline you want manual dispatch for. c-main_dispatch: type: boolean @@ -32,7 +32,7 @@ parameters: c-sdk_dispatch: type: boolean default: false - c-docker_publish_dispatch: + c-publish_contract_artifacts_dispatch: type: boolean default: false c-stale_check_dispatch: @@ -59,21 +59,17 @@ parameters: c-devnet-metrics-collect: type: boolean default: false - c-flake-shake-dispatch: - type: boolean - default: false - c-flake-shake-iterations: - type: integer - default: 300 - c-flake-shake-workers: - type: integer - default: 50 # Set to true by path-filtering when files under rust/ change. # When false on feature branches, kona-build-release skips cargo build and # reuses cached binaries — saving ~9 minutes on PRs that don't touch Rust. c-rust_files_changed: type: boolean default: false + # Set to true by path-filtering when files outside docs/public-docs/ change. + # When false, the main workflow is skipped entirely (docs-only PR). + c-non_docs_changes: + type: boolean + default: false # go-cache-version can be used as a cache buster when making breaking changes to caching strategy c-go-cache-version: type: string @@ -90,7 +86,7 @@ parameters: default: default main_dispatch: type: boolean - default: true # default to running main in case the manual run cancelled an automatic run + default: true fault_proofs_dispatch: type: boolean default: false @@ -109,9 +105,6 @@ parameters: docker_publish_dispatch: type: boolean default: false - publish_contract_artifacts_dispatch: - type: boolean - default: false stale_check_dispatch: type: boolean default: false @@ -121,15 +114,18 @@ parameters: heavy_fuzz_dispatch: type: boolean default: false - acceptance_tests_dispatch: - type: boolean - default: false sync_test_op_node_dispatch: type: boolean default: false ai_contracts_test_dispatch: type: boolean default: false + rust_ci_dispatch: + type: boolean + default: false + rust_e2e_dispatch: + type: boolean + default: false github-event-type: type: string default: "__not_set__" @@ -142,16 +138,6 @@ parameters: devnet-metrics-collect: type: boolean default: false - flake-shake-dispatch: - type: boolean - default: false - flake-shake-iterations: - type: integer - default: 300 - flake-shake-workers: - type: integer - default: 50 - # go-cache-version can be used as a cache buster when making breaking changes to caching strategy go-cache-version: type: string default: "v0.0" @@ -162,7 +148,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.23 + utils: ethereum-optimism/circleci-utils@1.0.24 docker: circleci/docker@2.8.2 github-cli: circleci/github-cli@2.7.0 @@ -208,21 +194,6 @@ commands: # Configure ADC echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a "$BASH_ENV" - clean-old-acceptor-logs: - description: "Delete op-acceptor testrun logs older than 60 days (prevents disk bloat across workspace attaches/reruns)." - steps: - - run: - name: Cleanup old op-acceptor testrun logs (>60 days) - command: | - set -eu - for base in "op-acceptor/logs" "op-acceptance-tests/logs"; do - if [ -d "$base" ]; then - echo "Scanning $base for old testrun-* directories..." - # Remove any testrun-* directories older than 60 days - find "$base" -type d -name 'testrun-*' -mtime +60 -print -exec rm -rf {} + - fi - done - check-changed: description: "Conditionally halts a step if certain modules change" parameters: @@ -379,24 +350,6 @@ commands: fi fi - run-contracts-check: - parameters: - command: - description: Just command that runs the check - type: string - steps: - - run: - name: <> - command: | - git reset --hard - git clean -df - just <> - git status --porcelain - [ -z "$(git status --porcelain)" ] || exit 1 - working_directory: packages/contracts-bedrock - when: always - environment: - FOUNDRY_PROFILE: ci go-restore-cache: parameters: @@ -411,7 +364,7 @@ commands: # Version can be used as a cache buster when making breaking changes to caching strategy version: type: string - default: <> + default: <> steps: - restore_cache: name: Restore go cache for <> (<>/go.mod) @@ -432,7 +385,7 @@ commands: type: string version: type: string - default: <> + default: <> steps: - save_cache: name: Save go cache for <> (<>/go.mod) @@ -441,14 +394,6 @@ commands: - ~/go/pkg/mod key: go-<>-<>-<>-{{ checksum "<>/go.mod" }}-{{ checksum "<>/go.sum" }} - pull-artifacts-conditional: - description: "Pull artifacts with conditional fallback based on branch and PR labels" - steps: - - run: - name: Pull artifacts - command: bash scripts/ops/use-latest-fallback.sh - working_directory: packages/contracts-bedrock - # --- Rust environment setup commands --- rust-install-toolchain: description: "Install Rust toolchain via rustup" @@ -497,6 +442,7 @@ commands: ROOT_DIR="$(pwd)" BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" echo "export RUST_BINARY_PATH_KONA_NODE=$ROOT_DIR/rust/target/release/kona-node" >> "$BASH_ENV" + echo "export OP_RETH_EXEC_PATH=$ROOT_DIR/rust/target/release/op-reth" >> "$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" @@ -664,6 +610,10 @@ commands: description: "Whether to save the cache at the end of the build" type: boolean default: false + rust_files_changed: + description: "Set to false to skip cargo build and reuse cached binaries (feature branches only)." + type: boolean + default: true steps: - utils/checkout-with-mise: checkout-method: blobless @@ -684,6 +634,22 @@ commands: PWD=$(pwd) export CARGO_TARGET_DIR="$PWD/<< parameters.directory >>/target" echo "CARGO_TARGET_DIR: $CARGO_TARGET_DIR" + + # On feature branches where rust/ hasn't changed, reuse binaries from + # the restored target cache instead of running cargo build (~9 min saving). + # Always build on develop/main as a safety backstop. + if [ "<< parameters.rust_files_changed >>" = "false" ] \ + && [ "$CIRCLE_BRANCH" != "develop" ] \ + && [ "$CIRCLE_BRANCH" != "main" ]; then + binary_dir="$CARGO_TARGET_DIR/<< parameters.profile >>" + if [ -d "$binary_dir" ] && ls "$binary_dir"/* >/dev/null 2>&1; then + echo "No rust/ changes on feature branch — skipping cargo build" + ls -lh "$binary_dir"/ | head -20 || true + exit 0 + fi + echo "WARNING: no cached binaries found, building from source" + fi + export PROFILE="--profile << parameters.profile >>" # Debug profile is specified as "debug" in the config/target, but cargo build expects "dev" @@ -696,7 +662,7 @@ commands: export PACKAGE="--package << parameters.package >>" fi - export BINARY="--all-targets" + export BINARY="" if [ -n "<< parameters.binary >>" ]; then export BINARY="--bin << parameters.binary >>" fi @@ -706,7 +672,7 @@ commands: export FEATURES="--all-features" fi - cd << parameters.directory >> && cargo build $PROFILE $TARGET $PACKAGE $FEATURES + cd << parameters.directory >> && cargo build $PROFILE $TARGET $PACKAGE $FEATURES $BINARY no_output_timeout: 30m - when: condition: << parameters.save_cache >> @@ -722,7 +688,7 @@ jobs: rust-build-binary: description: "Build a Rust workspace with target directory caching" docker: - - image: <> + - image: <> resource_class: xlarge parameters: directory: @@ -760,6 +726,14 @@ jobs: description: "Whether to save the cache at the end of the build" type: boolean default: true + persist_to_workspace: + description: "Whether to persist the built binaries to the CircleCI workspace" + type: boolean + default: false + rust_files_changed: + description: "Set to false to skip cargo build and reuse cached binaries (feature branches only)." + type: boolean + default: true steps: - rust-build: directory: << parameters.directory >> @@ -771,11 +745,21 @@ jobs: binary: << parameters.binary >> toolchain: << parameters.toolchain >> save_cache: << parameters.save_cache >> + rust_files_changed: << parameters.rust_files_changed >> + - when: + condition: << parameters.persist_to_workspace >> + steps: + - persist_to_workspace: + root: "." + paths: + - "<< parameters.directory >>/target/<< parameters.profile >>/kona-*" + - "<< parameters.directory >>/target/<< parameters.profile >>/op-*" + - "<< parameters.directory >>/target/<< parameters.profile >>/rollup-boost" # Build a single Rust binary from a submodule. rust-build-submodule: docker: - - image: <> + - image: <> resource_class: xlarge parameters: directory: @@ -867,127 +851,9 @@ jobs: paths: - ".circleci-cache/rust-binaries" - # Kurtosis-based acceptance tests - op-acceptance-tests-kurtosis: - parameters: - devnet: - description: | - The name of the pre-defined Kurtosis devnet to run the acceptance tests against - (e.g. 'simple', 'interop', 'jovian'). Empty string uses in-process testing (sysgo orchestrator). - type: string - default: "interop" - gate: - description: The gate to run the acceptance tests against. Must be defined in op-acceptance-tests/acceptance-tests.yaml. - type: string - default: "interop" - no_output_timeout: - description: Timeout for when CircleCI kills the job if there's no output - type: string - default: 30m - docker: - - image: <> - resource_class: xlarge - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true - - setup_remote_docker: - docker_layer_caching: true - - run: - name: Lint/Vet/Build op-acceptance-tests/cmd - working_directory: op-acceptance-tests - command: | - just cmd-check - - run: - name: Setup Kurtosis - command: | - echo "Setting up Kurtosis for external devnet testing..." - echo "Using Kurtosis from: $(which kurtosis || echo 'not found')" - kurtosis version || true - echo "Starting Kurtosis engine..." - kurtosis engine start || true - echo "Cleaning old instances..." - kurtosis clean -a || true - kurtosis engine status || true - echo "Kurtosis setup complete" - - run: - name: Dump kurtosis logs (pre-run) - command: | - # Best-effort: show engine status and existing enclaves before the test run - kurtosis engine status || true - kurtosis enclave ls || true - - run: - name: Run acceptance tests (devnet=<>, gate=<>) - working_directory: op-acceptance-tests - no_output_timeout: 1h - environment: - GOFLAGS: "-mod=mod" - GO111MODULE: "on" - GOGC: "0" - command: | - LOG_LEVEL=info just acceptance-test "<>" "<>" - - run: - name: Dump kurtosis logs - when: on_fail - command: | - # Dump logs & specs - kurtosis dump ./.kurtosis-dump - - # Remove spec.json files - rm -rf ./.kurtosis-dump/enclaves/**/*.json - - # Remove all unnecessary logs - rm -rf ./.kurtosis-dump/enclaves/*/kurtosis-api--* - rm -rf ./.kurtosis-dump/enclaves/*/kurtosis-logs-collector--* - rm -rf ./.kurtosis-dump/enclaves/*/task-* - - # Print enclaves and try to show service logs for the most recent devnet - kurtosis enclave ls || true - # Dump logs for all enclaves to aid debugging - for e in $(kurtosis enclave ls --output json 2>/dev/null | jq -r '.[].identifier' 2>/dev/null); do - echo "\n==== Kurtosis logs for enclave: $e ====" - kurtosis enclave inspect "$e" || true - kurtosis service logs "$e" --all-services --follow=false || true - done - - run: - name: Print results (summary) - working_directory: op-acceptance-tests - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - cat "$LOG_DIR/summary.log" || true - - run: - name: Print results (failures) - working_directory: op-acceptance-tests - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - cat "$LOG_DIR/failed/*.log" || true - when: on_fail - - run: - name: Print results (all) - working_directory: op-acceptance-tests - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - cat "$LOG_DIR/all.log" || true - - run: - name: Generate JUnit XML test report for CircleCI - working_directory: op-acceptance-tests - when: always - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - gotestsum --junitfile results/results.xml --raw-command cat $LOG_DIR/raw_go_events.log || true - - when: - condition: always - steps: - - store_test_results: - path: ./op-acceptance-tests/results - - when: - condition: always - steps: - - store_artifacts: - path: ./op-acceptance-tests/logs initialize: docker: - - image: <> + - image: <> resource_class: large steps: - run: @@ -995,7 +861,7 @@ jobs: cannon-go-lint-and-test: docker: - - image: <> + - image: <> resource_class: xlarge parameters: skip_slow_tests: @@ -1024,12 +890,12 @@ jobs: mkdir -p ./tmp/testlogs - run: name: build Cannon example binaries - command: make elf # only compile ELF binaries with Go, we do not have MIPS GCC for creating the debug-dumps. + command: just elf # only compile ELF binaries with Go, we do not have MIPS GCC for creating the debug-dumps. working_directory: cannon/testdata - run: name: Cannon Go lint command: | - make lint + just lint working_directory: cannon - run: name: Cannon Go 64-bit tests @@ -1060,7 +926,7 @@ jobs: contracts-bedrock-build: docker: - - image: <> + - image: <> resource_class: 2xlarge parameters: build_args: @@ -1075,15 +941,10 @@ jobs: - utils/checkout-with-mise: checkout-method: blobless enable-mise-cache: true - - install-zstd - install-contracts-dependencies - run: name: Print forge version command: forge --version - - run: - name: Pull artifacts - command: bash scripts/ops/pull-artifacts.sh - working_directory: packages/contracts-bedrock - run: name: Build contracts command: just forge-build <> @@ -1102,11 +963,12 @@ jobs: - "packages/contracts-bedrock/artifacts" - "packages/contracts-bedrock/forge-artifacts" - "op-deployer/pkg/deployer/artifacts/forge-artifacts" - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" check-kontrol-build: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -1126,251 +988,13 @@ jobs: name: Build Kontrol summary files command: just forge-build ./test/kontrol/proofs working_directory: packages/contracts-bedrock - - notify-failures-on-develop - - docker-build: - environment: - DOCKER_BUILDKIT: 1 - parameters: - docker_tags: - description: Docker image tags, comma-separated - type: string - docker_name: - description: "Docker buildx bake target" - type: string - default: "" - registry: - description: Docker registry - type: string - default: "us-docker.pkg.dev" - repo: - description: Docker repo - type: string - default: "oplabs-tools-artifacts/images" - save_image_tag: - description: Save docker image with given tag - type: string - default: "" - platforms: - description: Platforms to build for, comma-separated - type: string - default: "linux/amd64" - publish: - description: Publish the docker image (multi-platform, all tags) - type: boolean - default: false - release: - description: Run the release script - type: boolean - default: false - resource_class: - description: Docker resource class - type: string - default: medium - machine: - image: <> - resource_class: "<>" - docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true - - attach_workspace: - at: . - - run: - command: mkdir -p /tmp/docker_images - - when: - condition: - or: - - "<>" - - "<>" - steps: - - gcp-cli/install - - when: - condition: - or: - - "<>" - - "<>" - steps: - - gcp-oidc-authenticate - - run: - name: Build - command: | - # Check to see if DOCKER_HUB_READ_ONLY_TOKEN is set (i.e. we are in repo) before attempting to use secrets. - # Building should work without this read only login, but may get rate limited. - if [[ -v DOCKER_HUB_READ_ONLY_TOKEN ]]; then - echo "$DOCKER_HUB_READ_ONLY_TOKEN" | docker login -u "$DOCKER_HUB_READ_ONLY_USER" --password-stdin - fi - - export REGISTRY="<>" - export REPOSITORY="<>" - export IMAGE_TAGS="$(echo -ne "<>" | sed "s/[^a-zA-Z0-9\n,]/-/g")" - export GIT_COMMIT="$(git rev-parse HEAD)" - export GIT_DATE="$(git show -s --format='%ct')" - export PLATFORMS="<>" - - echo "Checking git tags pointing at $GIT_COMMIT:" - tags_at_commit=$(git tag --points-at $GIT_COMMIT) - echo "Tags at commit:\n$tags_at_commit" - - filtered_tags=$(echo "$tags_at_commit" | grep "^<>/" || true) - echo "Filtered tags: $filtered_tags" - - if [ -z "$filtered_tags" ]; then - export GIT_VERSION="untagged" - else - sorted_tags=$(echo "$filtered_tags" | sed "s/<>\///" | sort -V) - echo "Sorted tags: $sorted_tags" - - # prefer full release tag over "-rc" release candidate tag if both exist - full_release_tag=$(echo "$sorted_tags" | grep -v -- "-rc" || true) - if [ -z "$full_release_tag" ]; then - export GIT_VERSION=$(echo "$sorted_tags" | tail -n 1) - else - export GIT_VERSION=$(echo "$full_release_tag" | tail -n 1) - fi - fi - - echo "Setting GIT_VERSION=$GIT_VERSION" - - # Create, start (bootstrap) and use a *named* docker builder - # This allows us to cross-build multi-platform, - # and naming allows us to use the DLC (docker-layer-cache) - docker buildx create --driver=docker-container --name=buildx-build --bootstrap --use - - DOCKER_OUTPUT_DESTINATION="" - if [ "<>" == "true" ]; then - gcloud auth configure-docker <> - echo "Building for platforms $PLATFORMS and then publishing to registry" - DOCKER_OUTPUT_DESTINATION="--push" - if [ "<>" != "" ]; then - echo "ERROR: cannot save image to docker when publishing to registry" - exit 1 - fi - else - if [ "<>" == "" ]; then - echo "Running $PLATFORMS build without destination (cache warm-up)" - DOCKER_OUTPUT_DESTINATION="" - elif [[ $PLATFORMS == *,* ]]; then - echo "ERROR: cannot perform multi-arch (platforms: $PLATFORMS) build while also loading the result into regular docker" - exit 1 - else - echo "Running single-platform $PLATFORMS build and loading into docker" - DOCKER_OUTPUT_DESTINATION="--load" - fi - fi - - # Let them cook! - docker buildx bake \ - --progress plain \ - --builder=buildx-build \ - -f docker-bake.hcl \ - $DOCKER_OUTPUT_DESTINATION \ - <> - - no_output_timeout: 45m - - when: - condition: "<>" - steps: - - notify-failures-on-develop - - when: - condition: "<>" - steps: - - run: - name: Save - command: | - IMAGE_NAME="<>/<>/<>:<>" - docker save -o /tmp/docker_images/<>.tar $IMAGE_NAME - - persist_to_workspace: - root: /tmp/docker_images - paths: # only write the one file, to avoid concurrent workspace-file additions - - "<>.tar" - - when: - condition: "<>" - steps: - - run: - name: Tag - command: | - ./ops/scripts/ci-docker-tag-op-stack-release.sh <>/<> $CIRCLE_TAG $CIRCLE_SHA1 - - when: - condition: - or: - - and: - - "<>" - - "<>" - - and: - - "<>" - - equal: [develop, << pipeline.git.branch >>] - steps: - - gcp-oidc-authenticate: - service_account_email: GCP_SERVICE_ATTESTOR_ACCOUNT_EMAIL - - run: - name: Sign - command: | - VER=$(yq '.tools.binary_signer' mise.toml) - wget -O - "https://github.com/ethereum-optimism/binary_signer/archive/refs/tags/v${VER}.tar.gz" | tar xz - cd "binary_signer-${VER}/signer" - - IMAGE_PATH="<>/<>/<>:<>" - echo $IMAGE_PATH - pip3 install -r requirements.txt - - python3 ./sign_image.py --command="sign"\ - --attestor-project-name="$ATTESTOR_PROJECT_NAME"\ - --attestor-name="$ATTESTOR_NAME"\ - --image-path="$IMAGE_PATH"\ - --signer-logging-level="INFO"\ - --attestor-key-id="//cloudkms.googleapis.com/v1/projects/$ATTESTOR_PROJECT_NAME/locations/global/keyRings/$ATTESTOR_NAME-key-ring/cryptoKeys/$ATTESTOR_NAME-key/cryptoKeyVersions/1" - - # Verify newly published images (built on AMD machine) will run on ARM - check-cross-platform: - docker: - - image: <> - resource_class: arm.medium - parameters: - registry: - description: Docker registry - type: string - default: "us-docker.pkg.dev" - repo: - description: Docker repo - type: string - default: "oplabs-tools-artifacts/images" - op_component: - description: "Name of op-stack component (e.g. op-node)" - type: string - default: "" - docker_tag: - description: "Tag of docker image" - type: string - default: "<>" - steps: - - setup_remote_docker - - run: - name: "Verify Image Platform" - command: | - image_name="<>/<>/<>:<>" - echo "Retrieving Docker image manifest: $image_name" - MANIFEST=$(docker manifest inspect $image_name) - - echo "Verifying 'linux/arm64' is supported..." - SUPPORTED_PLATFORM=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture == "arm64" and .platform.os == "linux")') - echo $SUPPORT_PLATFORM - if [ -z "$SUPPORTED_PLATFORM" ]; then - echo "Platform 'linux/arm64' not supported by this image" - exit 1 - fi - - run: - name: "Pull and run docker image" - command: | - image_name="<>/<>/<>:<>" - docker pull $image_name || exit 1 - docker run $image_name <> --version || exit 1 + - notify-failures-on-develop: + mentions: "@security-oncall" contracts-bedrock-tests: circleci_ip_ranges: true docker: - - image: <> + - image: <> resource_class: 2xlarge parameters: test_list: @@ -1396,7 +1020,6 @@ jobs: - utils/checkout-with-mise: checkout-method: full enable-mise-cache: true - - install-zstd - run: name: Check if test list is empty command: | @@ -1417,7 +1040,6 @@ jobs: name: Print forge version command: forge --version working_directory: packages/contracts-bedrock - - pull-artifacts-conditional - go-restore-cache: namespace: packages/contracts-bedrock/scripts/go-ffi - run: @@ -1445,31 +1067,29 @@ jobs: name: Print failed test traces command: just test-rerun environment: - FOUNDRY_PROFILE: ci + FOUNDRY_PROFILE: <> working_directory: packages/contracts-bedrock when: on_fail - - when: - condition: always - steps: - - store_test_results: - path: packages/contracts-bedrock/results/results.xml + - store_test_results: + path: packages/contracts-bedrock/results + when: always - run: name: Lint forge test names command: just lint-forge-tests-check-no-build working_directory: packages/contracts-bedrock - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" contracts-bedrock-heavy-fuzz-nightly: circleci_ip_ranges: true docker: - - image: <> + - image: <> resource_class: 2xlarge steps: - utils/checkout-with-mise: checkout-method: full enable-mise-cache: true - install-contracts-dependencies - - install-zstd - run: name: Print dependencies command: just dep-status @@ -1478,10 +1098,6 @@ jobs: name: Print forge version command: forge --version working_directory: packages/contracts-bedrock - - run: - name: Pull artifacts - command: bash scripts/ops/pull-artifacts.sh - working_directory: packages/contracts-bedrock - run: name: Build go-ffi command: just build-go-ffi @@ -1508,19 +1124,24 @@ 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 + # Store raw JUnit XML as artifact for debugging when store_test_results + # shows 0 results (see ethereum-optimism/optimism#19577) + - store_artifacts: + path: packages/contracts-bedrock/results + destination: junit-results + when: always + - store_test_results: + path: packages/contracts-bedrock/results + when: always + - notify-failures-on-develop: + mentions: "@security-oncall" # AI Contracts Test Maintenance System # Runbook: https://github.com/ethereum-optimism/optimism/blob/develop/ops/ai-eng/contracts-test-maintenance/docs/runbook.md ai-contracts-test: circleci_ip_ranges: true docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -1546,12 +1167,13 @@ jobs: channel: C050F1GUHDG event: always template: AI_PR_SLACK_TEMPLATE - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" contracts-bedrock-coverage: circleci_ip_ranges: true docker: - - image: <> + - image: <> resource_class: 2xlarge parameters: test_timeout: @@ -1571,9 +1193,6 @@ jobs: checkout-method: full enable-mise-cache: true - install-contracts-dependencies - - install-zstd - - attach_workspace: - at: . - check-changed: patterns: contracts-bedrock - install-solc-compilers @@ -1585,7 +1204,6 @@ jobs: name: Print forge version command: forge --version working_directory: packages/contracts-bedrock - - pull-artifacts-conditional - run: name: Install lcov command: | @@ -1641,7 +1259,8 @@ jobs: - store_artifacts: path: packages/contracts-bedrock/failed-test-traces.log when: on_fail - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" contracts-bedrock-tests-upgrade: circleci_ip_ranges: true @@ -1657,20 +1276,21 @@ jobs: fork_base_rpc: description: Fork Base RPC type: string + test_profile: + description: Profile to use for testing + type: string + default: ci features: description: Comma-separated list of features to enable (e.g., "OPTIMISM_PORTAL_INTEROP", "CUSTOM_GAS_TOKEN") type: string default: "" docker: - - image: <> + - image: <> resource_class: 2xlarge steps: - utils/checkout-with-mise: enable-mise-cache: true - install-contracts-dependencies - - install-zstd - - attach_workspace: - at: . - check-changed: patterns: contracts-bedrock - install-solc-compilers @@ -1682,7 +1302,6 @@ jobs: name: Print forge version command: forge --version working_directory: packages/contracts-bedrock - - pull-artifacts-conditional - run: name: Write pinned block number for cache key command: | @@ -1706,7 +1325,7 @@ jobs: JUNIT_TEST_PATH: results/results.xml FOUNDRY_FUZZ_SEED: 42424242 FOUNDRY_FUZZ_RUNS: 1 - FOUNDRY_PROFILE: ci + FOUNDRY_PROFILE: <> ETH_RPC_URL: <> FORK_OP_CHAIN: <> FORK_BASE_CHAIN: <> @@ -1719,7 +1338,7 @@ jobs: environment: FOUNDRY_FUZZ_SEED: 42424242 FOUNDRY_FUZZ_RUNS: 1 - FOUNDRY_PROFILE: ci + FOUNDRY_PROFILE: <> ETH_RPC_URL: <> FORK_OP_CHAIN: <> FORK_BASE_CHAIN: <> @@ -1739,12 +1358,17 @@ 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 + # Store raw JUnit XML as artifact for debugging when store_test_results + # shows 0 results (see ethereum-optimism/optimism#19577) + - store_artifacts: + path: packages/contracts-bedrock/results + destination: junit-results + when: always + - store_test_results: + path: packages/contracts-bedrock/results + when: always + - notify-failures-on-develop: + mentions: "@security-oncall" contracts-bedrock-upload: machine: true @@ -1762,76 +1386,25 @@ jobs: command: just update-selectors working_directory: packages/contracts-bedrock - contracts-bedrock-checks: + contracts-bedrock-checks-fast: docker: - - image: <> - resource_class: xlarge + - image: <> + resource_class: 2xlarge steps: - utils/checkout-with-mise: enable-mise-cache: true - install-contracts-dependencies - - attach_workspace: - at: . - - check-changed: - patterns: contracts-bedrock - - get-target-branch - - run: - name: print forge version - command: forge --version - - run-contracts-check: - command: check-kontrol-summaries-unchanged - - run-contracts-check: - command: semgrep-test-validity-check - - run-contracts-check: - command: semgrep - - run-contracts-check: - command: semver-lock-no-build - - run-contracts-check: - command: semver-diff-check-no-build - - run-contracts-check: - command: validate-deploy-configs - - run-contracts-check: - command: lint - - run-contracts-check: - command: snapshots-check-no-build - - run-contracts-check: - command: interfaces-check-no-build - - run-contracts-check: - command: reinitializer-check-no-build - - run-contracts-check: - command: size-check - - run-contracts-check: - command: unused-imports-check-no-build - - run-contracts-check: - command: strict-pragma-check-no-build - - run-contracts-check: - command: validate-spacers-no-build - - 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 - working_directory: packages/contracts-bedrock - run: name: Run checks command: just check-fast working_directory: packages/contracts-bedrock - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" todo-issues: parameters: @@ -1839,7 +1412,7 @@ jobs: type: boolean default: true machine: - image: <> + image: <> steps: - utils/checkout-with-mise: checkout-method: blobless @@ -1865,7 +1438,7 @@ jobs: type: boolean default: false docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -1882,7 +1455,7 @@ jobs: name: Fuzz no_output_timeout: 15m command: | - make fuzz + just fuzz working_directory: "<>" - go-save-cache: namespace: fuzz-<> @@ -1898,7 +1471,7 @@ jobs: go-lint: docker: - - image: <> + - image: <> resource_class: large steps: - utils/checkout-with-mise: @@ -1909,15 +1482,37 @@ jobs: - run: name: run Go linter command: | - make lint-go + just lint-go - save_cache: key: golangci-v1-{{ checksum ".golangci.yaml" }} paths: - "/home/circleci/.cache/golangci-lint" + go-binaries-for-sysgo: + docker: + - image: <> + resource_class: large + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - go-restore-cache: + namespace: sysgo-go-binaries + - run: + name: Build Go binaries for sysgo + command: just cannon op-program + - go-save-cache: + namespace: sysgo-go-binaries + - persist_to_workspace: + root: . + paths: + - "cannon/bin" + - "op-program/bin/op-program" + - "op-program/bin/op-program-client" + check-op-geth-version: docker: - - image: <> + - image: <> resource_class: small steps: - utils/checkout-with-mise: @@ -1926,7 +1521,20 @@ jobs: - run: name: check op-geth version command: | - make check-op-geth-version + just check-op-geth-version + + check-nut-locks: + docker: + - image: <> + resource_class: small + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - run: + name: check nut locks + command: | + go run ./ops/scripts/check-nut-locks go-tests: parameters: @@ -1959,7 +1567,7 @@ jobs: type: integer default: 1 docker: - - image: <> + - image: <> resource_class: 2xlarge circleci_ip_ranges: true parallelism: <> @@ -1972,7 +1580,7 @@ jobs: - restore_cache: key: go-tests-v2-{{ checksum "go.mod" }} - run: - name: Run Go tests via Makefile + name: Run Go tests no_output_timeout: <> command: | <> @@ -1980,7 +1588,7 @@ jobs: # set to less than number CPUs (xlarge Docker is 16 CPU) so there's some buffer for things # like Geth export PARALLEL=12 - make <> + just <> - save_cache: key: go-tests-v2-{{ checksum "go.mod" }} paths: @@ -2042,22 +1650,22 @@ jobs: at: . - run: name: build op-program-client - command: make op-program-client + command: just op-program-client working_directory: op-program - run: name: build op-program-host - command: make op-program-host + command: just op-program-host working_directory: op-program - run: name: build cannon - command: make cannon + command: just cannon - run: name: run tests no_output_timeout: <> command: | <> export TEST_TIMEOUT=<> - make go-tests-fraud-proofs-ci + just go-tests-fraud-proofs-ci - codecov/upload: disable_search: true files: ./coverage.out @@ -2079,9 +1687,9 @@ jobs: op-acceptance-tests: parameters: gate: - description: The gate to run the acceptance tests against. This gate should be defined in op-acceptance-tests/acceptance-tests.yaml. + description: The gate to run. Reads package list from op-acceptance-tests/gates/.txt. If empty, runs all tests. type: string - default: "base" + default: "" l2_cl_kind: description: "L2 consensus layer client (op-node or kona)" type: string @@ -2090,16 +1698,12 @@ jobs: description: "L2 execution layer client (op-geth or op-reth)" type: string default: "op-geth" - run_all: - description: When true, run all tests in gateless mode. - type: boolean - default: false no_output_timeout: description: Timeout for when CircleCI kills the job if there's no output type: string default: 30m docker: - - image: <> + - image: <> circleci_ip_ranges: true resource_class: 2xlarge+ steps: @@ -2108,18 +1712,21 @@ jobs: enable-mise-cache: true - attach_workspace: at: . - # Build kona-node for the acceptance tests. This automatically gets kona from the cache. - - rust-build: - directory: rust - profile: "release" - 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=$ROOT_DIR/rust/target/release/kona-node" >> "$BASH_ENV" + echo "export OP_RETH_EXEC_PATH=$ROOT_DIR/rust/target/release/op-reth" >> "$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" + - run: + name: Configure L2 stack + command: | + echo "export DEVSTACK_L2CL_KIND=<>" >> "$BASH_ENV" + echo "export DEVSTACK_L2EL_KIND=<>" >> "$BASH_ENV" # Restore cached Go modules - restore_cache: keys: @@ -2130,29 +1737,13 @@ jobs: name: Download Go dependencies working_directory: op-acceptance-tests command: go mod download - - run: - name: Lint/Vet/Build op-acceptance-tests/cmd - working_directory: op-acceptance-tests - command: | - just cmd-check - # Prepare the test environment - - run: - name: Prepare test environment (compile tests and cache build results) - working_directory: op-acceptance-tests - command: go test -v -c -o /dev/null $(go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./tests/...) # Run the acceptance tests (if the devnet is running) - run: name: Run acceptance tests working_directory: op-acceptance-tests - no_output_timeout: 1h + no_output_timeout: <> command: | - if <>; then - echo "Running in gateless mode - auto-discovering all tests in ./op-acceptance-tests/..." - LOG_LEVEL=info just acceptance-test-all - else - echo "Running in gate mode (gate=<>)" - LOG_LEVEL=info just acceptance-test "<>" - fi + LOG_LEVEL=info just acceptance-test "<>" - run: name: Print results (summary) working_directory: op-acceptance-tests @@ -2164,21 +1755,8 @@ jobs: working_directory: op-acceptance-tests command: | LOG_DIR=$(ls -td -- logs/* | head -1) - cat "$LOG_DIR/failed/*.log" || true + cat "$LOG_DIR"/failed/*.log || true when: on_fail - - run: - name: Print results (all) - working_directory: op-acceptance-tests - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - cat "$LOG_DIR/all.log" || true - - run: - name: Generate JUnit XML test report for CircleCI - working_directory: op-acceptance-tests - when: always - command: | - LOG_DIR=$(ls -td -- logs/* | head -1) - gotestsum --junitfile results/results.xml --raw-command cat $LOG_DIR/raw_go_events.log || true # Save the module cache for future runs - save_cache: key: go-mod-v1-{{ checksum "go.sum" }} @@ -2201,206 +1779,19 @@ jobs: - notify-failures-on-develop: mentions: "@protocol-devx-pod @changwan" - op-acceptance-tests-flake-shake: - parameters: - gate: - type: string - default: "flake-shake" - machine: - image: ubuntu-2404:current - resource_class: xlarge - parallelism: << pipeline.parameters.flake-shake-workers >> - 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_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" }} - - go-mod-v1- - - run: - name: Download Go dependencies - working_directory: op-acceptance-tests - command: go mod download - - run: - name: Lint/Vet/Build op-acceptance-tests/cmd - working_directory: op-acceptance-tests - command: | - just cmd-check - - run: - name: Calculate iterations for worker - command: | - bash ./op-acceptance-tests/scripts/ci_flake_shake_calc_iterations.sh << pipeline.parameters.flake-shake-iterations >> - - run: - name: Run flake-shake iterations - no_output_timeout: 3h - working_directory: op-acceptance-tests - command: | - OUTPUT_DIR="logs/flake-shake-results-worker-${FLAKE_SHAKE_WORKER_ID}" - mkdir -p "$OUTPUT_DIR" - op-acceptor \ - --validators ./acceptance-tests.yaml \ - --gate << parameters.gate >> \ - --testdir tests \ - --flake-shake \ - --flake-shake-iterations "$FLAKE_SHAKE_ITERATIONS" \ - --log.level debug \ - --logdir "./$OUTPUT_DIR" - - persist_to_workspace: - root: op-acceptance-tests - paths: - - logs/flake-shake-results-worker-*/ - - store_artifacts: - path: ./op-acceptance-tests/logs - destination: flake-shake-workers - - op-acceptance-tests-flake-shake-report: - machine: - image: ubuntu-2404:current - resource_class: large - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true - - attach_workspace: - at: . - - clean-old-acceptor-logs - - run: - name: Lint/Vet/Build op-acceptance-tests/cmd - working_directory: op-acceptance-tests - command: | - just cmd-check - - go-restore-cache: - namespace: op-acceptance-tests-flake-shake-aggregator - - run: - name: Build flake-shake aggregator - working_directory: op-acceptance-tests - command: | - go mod download - go build -o ../flake-shake-aggregator ./cmd/flake-shake-aggregator/main.go - - go-save-cache: - namespace: op-acceptance-tests-flake-shake-aggregator - - run: - name: Aggregate results - command: | - mkdir -p final-report - ./flake-shake-aggregator \ - --input-pattern "logs/flake-shake-results-worker-*/testrun-*/flake-shake-report.json" \ - --output-dir final-report \ - --verbose - - run: - name: Generate summary - command: | - bash ./op-acceptance-tests/scripts/ci_flake_shake_generate_summary.sh final-report/flake-shake-report.json final-report - - run: - name: Bundle consolidated flake-shake worker logs - command: | - set -euo pipefail - # Create a single tarball with all worker logs, if any exist in the workspace - if compgen -G "op-acceptance-tests/logs/flake-shake-results-worker-*" > /dev/null; then - tar -czf final-report/flake-shake-workers-logs.tar.gz op-acceptance-tests/logs/flake-shake-results-worker-* - echo "Created final-report/flake-shake-workers-logs.tar.gz" - else - echo "No worker log directories found to bundle" - fi - - store_artifacts: - path: ./final-report - destination: flake-shake-report - - op-acceptance-tests-flake-shake-promote: - machine: - image: ubuntu-2404:current - resource_class: large + # Aggregator job - allows downstream jobs and required checks to depend on a single memory-all job. + memory-all: + docker: + - image: <> + resource_class: small 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 - command: | - just cmd-check - - go-restore-cache: - namespace: op-acceptance-tests-flake-shake-promoter - - run: - name: Build flake-shake promoter - working_directory: op-acceptance-tests - command: | - go mod download - go build -o ../flake-shake-promoter ./cmd/flake-shake-promoter/main.go - - go-save-cache: - namespace: op-acceptance-tests-flake-shake-promoter - - run: - name: Set GH_TOKEN - command: | - if [ -n "${GITHUB_TOKEN_GOVERNANCE:-}" ]; then - echo "export GH_TOKEN=${GITHUB_TOKEN_GOVERNANCE}" >> "$BASH_ENV" - fi - - run: - name: Validate GH_TOKEN is present - command: | - if [ -z "${GH_TOKEN:-}" ]; then - echo "GH_TOKEN is required for PR creation" >&2 - exit 1 - fi - - run: - name: Run flake-shake promoter - command: | - ./flake-shake-promoter \ - --org ethereum-optimism \ - --repo optimism \ - --branch "<< pipeline.git.branch >>" \ - --workflow scheduled-flake-shake \ - --report-job op-acceptance-tests-flake-shake-report \ - --days 3 \ - --gate flake-shake \ - --min-runs 300 \ - --max-failure-rate 0.0 \ - --min-age-days 2 \ - --dry-run=false \ - --require-clean-24h \ - --out ./final-promotion \ - --verbose - - store_artifacts: - path: ./final-promotion - destination: flake-shake-promotion - - run: - name: Prepare Slack message (promotion candidates) - command: | - bash ./op-acceptance-tests/scripts/ci_flake_shake_prepare_slack.sh ./final-promotion/promotion-ready.json - run: - name: Slack - Sending Notification - command: | - set -euo pipefail - if [ -z "${SLACK_BLOCKS_PAYLOAD:-}" ] || [ "${SLACK_BLOCKS_PAYLOAD}" = "[]" ]; then - echo "SLACK_BLOCKS is empty or doesn't exist. Skipping Slack notification." - # Halt this job here to avoid invoking the Slack orb step. - circleci-agent step halt - else - printf '%s' "$SLACK_BLOCKS_PAYLOAD" | jq '.' > /tmp/blocks.json - jq -c '{blocks: .}' /tmp/blocks.json > /tmp/slack_template.json - fi - echo 'export SLACK_TEMPLATE=$(cat /tmp/slack_template.json)' >> "$BASH_ENV" - - slack/notify: - channel: C03N11M0BBN # "notify-ci" channel - event: always - retries: 1 - retry_delay: 3 - template: SLACK_TEMPLATE + name: All memory-all tests passed + command: echo "All memory-all acceptance test variants passed" sanitize-op-program: docker: - - image: <> + - image: <> resource_class: large steps: - utils/checkout-with-mise: @@ -2413,104 +1804,62 @@ jobs: sudo apt-get install -y binutils-mips-linux-gnu - run: name: Build cannon - command: make cannon + command: just cannon - run: name: Build op-program - command: make op-program + command: just op-program - run: name: Sanitize op-program client - command: make sanitize-program GUEST_PROGRAM=../op-program/bin/op-program-client64.elf + command: GUEST_PROGRAM=../op-program/bin/op-program-client64.elf just sanitize-program working_directory: cannon - cannon-prestate-quick: - docker: - - image: <> - resource_class: xlarge - 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" }} - - run: - name: Build prestates - command: make cannon-prestates - - save_cache: - key: cannon-prestate-{{ checksum "./cannon/bin/cannon" }}-{{ checksum "op-program/bin/op-program-client.elf" }} - name: Save Cannon prestate to cache - paths: - - "op-program/bin/prestate*.bin.gz" - - "op-program/bin/meta*.json" - - "op-program/bin/prestate-proof*.json" - - persist_to_workspace: - root: . - paths: - - "op-program/bin/prestate*" - - "op-program/bin/meta*" - - "op-program/bin/op-program" - - "op-program/bin/op-program-client" - - "cannon/bin" - cannon-prestate: - docker: - - image: <> + machine: + docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages steps: - utils/checkout-with-mise: checkout-method: blobless enable-mise-cache: true - - setup_remote_docker - run: name: Build prestates - command: make reproducible-prestate + command: just reproducible-prestate + - run: + name: Capture kona prestate debug artifacts + command: | + mkdir -p /tmp/prestate-debug + + # chainList.json from the checkout (baked into the binary via include_str!) + cp rust/kona/crates/protocol/registry/etc/chainList.json /tmp/prestate-debug/chainList.json + + # The kona-client ELF binaries (for offline inspection with strings/objdump) + cp rust/kona/prestate-artifacts-cannon/kona-client-elf /tmp/prestate-debug/kona-client-elf-cannon 2>/dev/null || true + cp rust/kona/prestate-artifacts-cannon-interop/kona-client-elf /tmp/prestate-debug/kona-client-elf-cannon-interop 2>/dev/null || true + - store_artifacts: + path: /tmp/prestate-debug + destination: prestate-debug - persist_to_workspace: root: . paths: - "op-program/bin/prestate*" - "op-program/bin/meta*" - - cannon-kona-prestate: - machine: - docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true - - restore_cache: - name: Restore kona cache - key: kona-prestate-{{ checksum "./rust/justfile" }}-{{ checksum "./rust/kona/docker/fpvm-prestates/cannon-repro.dockerfile" }} - - run: - name: Build kona prestates - command: just build-kona-prestates - working_directory: rust - - save_cache: - key: kona-prestate-{{ checksum "./rust/justfile" }}-{{ checksum "./rust/kona/docker/fpvm-prestates/cannon-repro.dockerfile" }} - name: Save Kona to cache - paths: - "rust/kona/prestate-artifacts-*/" - - persist_to_workspace: - root: . - paths: - - "rust/kona/prestate-artifacts-*/*" - # Aggregator job - allows downstream jobs to depend on a single job instead of listing all build jobs. rust-binaries-for-sysgo: docker: - - image: <> + - 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 docker: - - image: <> + - image: <> steps: - utils/checkout-with-mise: checkout-method: blobless @@ -2579,7 +1928,8 @@ jobs: enable-mise-cache: true - run: name: Verify reproducibility - command: make -C op-program verify-reproducibility + command: just verify-reproducibility + working_directory: op-program - store_artifacts: path: ./op-program/temp/logs when: always @@ -2588,7 +1938,7 @@ jobs: cannon-stf-verify: docker: - - image: <> + - image: <> steps: - utils/checkout-with-mise: checkout-method: blobless @@ -2596,10 +1946,11 @@ jobs: - setup_remote_docker - run: name: Build cannon - command: make cannon + command: just cannon - run: name: Verify the Cannon STF - command: make -C ./cannon cannon-stf-verify + command: just cannon-stf-verify + working_directory: cannon - notify-failures-on-develop: mentions: "@proofs-team" @@ -2649,14 +2000,14 @@ jobs: bedrock-go-tests: # just a helper, that depends on all the actual test jobs docker: - - image: <> + - image: <> resource_class: medium steps: - run: echo Done analyze-op-program-client: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -2666,12 +2017,16 @@ jobs: - run: name: Run Analyzer command: | - make run-vm-compat + just run-vm-compat working_directory: op-program + - store_artifacts: + path: op-program/bin/vm-compat-output/vm-compat-findings.json + destination: vm-compat-findings.json + when: always op-program-compat: docker: - - image: <> + - image: <> resource_class: large steps: - utils/checkout-with-mise: @@ -2680,12 +2035,12 @@ jobs: - run: name: Verify Compatibility command: | - make verify-compat + just verify-compat working_directory: op-program check-generated-mocks-op-node: docker: - - image: <> + - image: <> resource_class: large steps: - utils/checkout-with-mise: @@ -2695,11 +2050,11 @@ jobs: patterns: op-node - run: name: check-generated-mocks - command: make generate-mocks-op-node && git diff --exit-code + command: just generate-mocks-op-node && git diff --exit-code check-generated-mocks-op-service: docker: - - image: <> + - image: <> resource_class: large steps: - utils/checkout-with-mise: @@ -2709,11 +2064,11 @@ jobs: patterns: op-service - run: name: check-generated-mocks - command: make generate-mocks-op-service && git diff --exit-code + command: just generate-mocks-op-service && git diff --exit-code op-deployer-forge-version: docker: - - image: <> + - image: <> steps: - utils/checkout-with-mise: checkout-method: blobless @@ -2724,7 +2079,7 @@ jobs: kontrol-tests: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -2754,11 +2109,12 @@ jobs: } }' working_directory: ./packages/contracts-bedrock - - notify-failures-on-develop + - notify-failures-on-develop: + mentions: "@security-oncall" publish-contract-artifacts: docker: - - image: <> + - image: <> resource_class: 2xlarge steps: - gcp-cli/install @@ -2772,10 +2128,6 @@ jobs: enable-mise-cache: true - install-contracts-dependencies - install-zstd - - run: - name: Pull artifacts - command: bash scripts/ops/pull-artifacts.sh - working_directory: packages/contracts-bedrock - run: name: Build contracts environment: @@ -2787,7 +2139,6 @@ jobs: command: bash scripts/ops/publish-artifacts.sh working_directory: packages/contracts-bedrock - go-release: parameters: module: @@ -2798,7 +2149,7 @@ jobs: default: .goreleaser.yaml type: string docker: - - image: <> + - image: <> resource_class: large steps: - setup_remote_docker: @@ -2821,7 +2172,7 @@ jobs: diff-fetcher-forge-artifacts: docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -2872,7 +2223,7 @@ jobs: steps: - github-cli/install - utils/github-event-handler-setup: - github_event_base64: << pipeline.parameters.github-event-base64 >> + github_event_base64: << pipeline.parameters.c-github-event-base64 >> env_prefix: "github_" - run: name: Close issue if label is added @@ -2888,7 +2239,7 @@ jobs: devnet-metrics-collect-authorship: docker: - - image: <> + - image: <> steps: - utils/checkout-with-mise: checkout-method: blobless @@ -2946,12 +2297,14 @@ workflows: and: - equal: ["", << pipeline.git.tag >>] - or: - - equal: ["webhook", << pipeline.trigger_source >>] - and: - - equal: [true, <>] + - equal: ["webhook", << pipeline.trigger_source >>] + - << pipeline.parameters.c-non_docs_changes >> + - and: + - equal: [true, <>] - equal: ["api", << pipeline.trigger_source >>] - equal: [ - << pipeline.parameters.github-event-type >>, + << pipeline.parameters.c-github-event-type >>, "__not_set__", ] #this is to prevent triggering this workflow as the default value is always set for main_dispatch jobs: @@ -2984,12 +2337,16 @@ workflows: - OPCM_V2 - OPCM_V2,CUSTOM_GAS_TOKEN - OPCM_V2,OPTIMISM_PORTAL_INTEROP + - OPCM_V2,ZK_DISPUTE_GAME + - OPCM_V2,CANNON_KONA context: - circleci-repo-readonly-authenticated-github-token - slack + # On PRs, run tests with lite profile for better build times. - contracts-bedrock-tests: name: contracts-bedrock-tests <> test_list: find test -name "*.t.sol" + test_profile: liteci features: <> matrix: parameters: @@ -2998,6 +2355,25 @@ workflows: - circleci-repo-readonly-authenticated-github-token - slack check_changed_patterns: contracts-bedrock,op-node + filters: + branches: + ignore: develop + # On develop, run tests with ci profile to mirror production. + - contracts-bedrock-tests: + name: contracts-bedrock-tests-develop <> + test_list: find test -name "*.t.sol" + test_profile: ci + features: <> + matrix: + parameters: + features: *features_matrix + context: + - circleci-repo-readonly-authenticated-github-token + - slack + check_changed_patterns: contracts-bedrock,op-node + filters: + branches: + only: develop - contracts-bedrock-coverage: # Generate coverage reports. name: contracts-bedrock-coverage <> @@ -3010,11 +2386,13 @@ workflows: context: - circleci-repo-readonly-authenticated-github-token - slack + # On PRs, run upgrade tests with lite profile for better build times. - contracts-bedrock-tests-upgrade: name: contracts-bedrock-tests-upgrade op-mainnet <> fork_op_chain: op fork_base_chain: mainnet fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + test_profile: liteci features: <> matrix: parameters: @@ -3022,23 +2400,58 @@ workflows: context: - circleci-repo-readonly-authenticated-github-token - slack + filters: + branches: + ignore: develop + # On develop, run upgrade tests with ci profile to mirror production. + - contracts-bedrock-tests-upgrade: + name: contracts-bedrock-tests-upgrade-develop op-mainnet <> + fork_op_chain: op + fork_base_chain: mainnet + fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + test_profile: ci + features: <> + matrix: + parameters: + features: *features_matrix + context: + - circleci-repo-readonly-authenticated-github-token + - slack + filters: + branches: + only: develop + # On PRs, run chain-specific upgrade tests with lite profile for better build times. - contracts-bedrock-tests-upgrade: name: contracts-bedrock-tests-upgrade <>-mainnet fork_op_chain: <> fork_base_chain: mainnet fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + test_profile: liteci matrix: parameters: - fork_op_chain: ["base", "ink", "unichain"] + fork_op_chain: ["op", "ink", "unichain"] context: - circleci-repo-readonly-authenticated-github-token - slack - - contracts-bedrock-checks: - requires: - - contracts-bedrock-build + filters: + branches: + ignore: develop + # On develop, run chain-specific upgrade tests with ci profile to mirror production. + - contracts-bedrock-tests-upgrade: + name: contracts-bedrock-tests-upgrade-develop <>-mainnet + fork_op_chain: <> + fork_base_chain: mainnet + fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + test_profile: ci + matrix: + parameters: + fork_op_chain: ["op", "ink", "unichain"] context: - circleci-repo-readonly-authenticated-github-token - slack + filters: + branches: + only: develop - contracts-bedrock-checks-fast: context: - circleci-repo-readonly-authenticated-github-token @@ -3073,6 +2486,9 @@ workflows: - check-op-geth-version: context: - circleci-repo-readonly-authenticated-github-token + - check-nut-locks: + context: + - circleci-repo-readonly-authenticated-github-token - fuzz-golang: name: fuzz-golang-<> on_changes: <> @@ -3103,7 +2519,7 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - contracts-bedrock-build - - cannon-prestate-quick + - cannon-prestate - go-tests: name: go-tests-short parallelism: 12 @@ -3111,7 +2527,8 @@ workflows: test_timeout: 20m requires: - contracts-bedrock-build - - cannon-prestate-quick + - cannon-prestate + - go-binaries-for-sysgo context: - circleci-repo-readonly-authenticated-github-token filters: @@ -3129,7 +2546,8 @@ workflows: only: develop # Only runs on develop branch (post-merge) requires: - contracts-bedrock-build - - cannon-prestate-quick + - cannon-prestate + - go-binaries-for-sysgo context: - circleci-repo-readonly-authenticated-github-token - slack @@ -3150,24 +2568,12 @@ workflows: - sanitize-op-program context: - circleci-repo-readonly-authenticated-github-tokens - - docker-build: - name: <>-docker-build - docker_tags: <>,<> - save_image_tag: <> - matrix: - parameters: - docker_name: - - op-deployer - context: - - circleci-repo-readonly-authenticated-github-token - requires: - - contracts-bedrock-build - - cannon-prestate-quick: + - cannon-prestate: context: - circleci-repo-readonly-authenticated-github-token - sanitize-op-program: requires: - - cannon-prestate-quick + - cannon-prestate context: - circleci-repo-readonly-authenticated-github-token - check-generated-mocks-op-node: @@ -3194,7 +2600,9 @@ workflows: name: shell-check # We don't need the `exclude` key as the orb detects the `.shellcheckrc` dir: . - ignore-dirs: ./packages/contracts-bedrock/lib + ignore-dirs: | + ./packages/contracts-bedrock/lib + ./docs/public-docs context: - circleci-repo-readonly-authenticated-github-token # Acceptance test jobs (formerly in separate acceptance-tests workflow) @@ -3233,8 +2641,7 @@ workflows: # IN-MEMORY (all) - op-node/op-geth - op-acceptance-tests: name: memory-all-opn-op-geth - run_all: true - no_output_timeout: 120m # Allow longer runs for memory-all gate + no_output_timeout: 120m context: - circleci-repo-readonly-authenticated-github-token - slack @@ -3244,12 +2651,12 @@ workflows: - cannon-prestate - rust-binaries-for-sysgo - go-binaries-for-sysgo - # IN-MEMORY (all) - op-node/op-reth + # IN-MEMORY (base gate) - op-node/op-reth - op-acceptance-tests: name: memory-all-opn-op-reth gate: "base" l2_el_kind: op-reth - no_output_timeout: 120m # Allow longer runs for memory-all gate + no_output_timeout: 120m context: - circleci-repo-readonly-authenticated-github-token - slack @@ -3259,13 +2666,13 @@ workflows: - cannon-prestate - rust-binaries-for-sysgo - go-binaries-for-sysgo - # IN-MEMORY (all) - kona/op-reth + # IN-MEMORY (base gate) - kona/op-reth - op-acceptance-tests: name: memory-all-kona-op-reth gate: "base" l2_cl_kind: kona l2_el_kind: op-reth - no_output_timeout: 120m # Allow longer runs for memory-all gate + no_output_timeout: 120m context: - circleci-repo-readonly-authenticated-github-token - slack @@ -3351,46 +2758,6 @@ workflows: only: /^(da-server|cannon|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/ branches: ignore: /.*/ - - contracts-bedrock-build: - context: - - circleci-repo-readonly-authenticated-github-token - requires: - - initialize - filters: - tags: - only: /^op-deployer.*/ # ensure contract artifacts are embedded in op-deployer binary - branches: - ignore: /.*/ - - docker-build: - matrix: - parameters: - docker_name: - - op-deployer - name: <>-docker-release - docker_tags: <> - platforms: "linux/amd64,linux/arm64" - publish: true - release: true - filters: - tags: - only: /^<>\/v.*/ - branches: - ignore: /.*/ - context: - - oplabs-gcr-release - requires: - - initialize - - contracts-bedrock-build - - check-cross-platform: - matrix: - parameters: - op_component: - - op-deployer - name: <>-cross-platform - requires: - - op-deployer-docker-release - context: - - circleci-repo-readonly-authenticated-github-token - cannon-prestate: filters: tags: @@ -3424,21 +2791,14 @@ workflows: - slack - circleci-repo-readonly-authenticated-github-token - develop-publish-contract-artifacts: - when: - or: - - and: - - equal: ["develop", <>] - - equal: ["webhook", << pipeline.trigger_source >>] - - and: - - equal: - [ - true, - <>, - ] - - equal: ["api", << pipeline.trigger_source >>] + publish-contract-artifacts-on-tag: jobs: - publish-contract-artifacts: + filters: + tags: + only: /^op-contracts\/v.*/ + branches: + ignore: /.*/ context: - circleci-repo-readonly-authenticated-github-token @@ -3449,7 +2809,7 @@ workflows: - equal: ["develop", <>] - equal: ["webhook", << pipeline.trigger_source >>] - and: - - equal: [true, <>] + - equal: [true, <>] - equal: ["api", << pipeline.trigger_source >>] jobs: - cannon-prestate: @@ -3493,7 +2853,7 @@ workflows: - equal: ["develop", <>] - equal: ["webhook", << pipeline.trigger_source >>] - and: - - equal: [true, <>] + - equal: [true, <>] - equal: ["api", << pipeline.trigger_source >>] jobs: - kontrol-tests: @@ -3506,7 +2866,7 @@ workflows: when: or: - equal: [build_four_hours, <>] - - equal: [true, << pipeline.parameters.cannon_full_test_dispatch >>] + - equal: [true, << pipeline.parameters.c-cannon_full_test_dispatch >>] jobs: - contracts-bedrock-build: build_args: --deny-warnings --skip test @@ -3522,109 +2882,12 @@ workflows: - slack - circleci-repo-readonly-authenticated-github-token - scheduled-docker-publish: - when: - or: - - equal: [build_daily, <>] - # Trigger on manual triggers if explicitly requested - - equal: [true, << pipeline.parameters.docker_publish_dispatch >>] - jobs: - - contracts-bedrock-build: - context: - - circleci-repo-readonly-authenticated-github-token - - docker-build: - matrix: - parameters: - docker_name: - - op-deployer - name: <>-docker-publish - docker_tags: <>,<> - platforms: "linux/amd64,linux/arm64" - publish: true - context: - - oplabs-gcr - - slack - - circleci-repo-readonly-authenticated-github-token - requires: - - contracts-bedrock-build - - check-cross-platform: - matrix: - parameters: - op_component: - - op-deployer - name: <>-cross-platform - requires: - - <>-docker-publish - context: - - circleci-repo-readonly-authenticated-github-token - - scheduled-flake-shake: - when: - or: - - equal: [build_daily, << pipeline.schedule.name >>] - - and: - - equal: [true, << pipeline.parameters.flake-shake-dispatch >>] - - equal: ["api", << pipeline.trigger_source >>] - jobs: - - contracts-bedrock-build: - build_args: --skip test - context: - - circleci-repo-readonly-authenticated-github-token - - cannon-prestate-quick: - context: - - circleci-repo-readonly-authenticated-github-token - - rust-build-binary: - name: kona-build-release - directory: rust - needs_clang: true - profile: "release" - context: - - circleci-repo-readonly-authenticated-github-token - - rust-build-submodule: &rust-build-op-rbuilder - 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-build-submodule: &rust-build-rollup-boost - 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: - - kona-build-release - - 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 - - op-acceptance-tests-flake-shake-promote: - requires: - - op-acceptance-tests-flake-shake-report - context: - - circleci-repo-readonly-authenticated-github-token - - circleci-repo-optimism - - circleci-api-token - - slack - scheduled-preimage-reproducibility: when: or: - equal: [build_daily, <>] # Trigger on manual triggers if explicitly requested - - equal: [true, << pipeline.parameters.reproducibility_dispatch >>] + - equal: [true, << pipeline.parameters.c-reproducibility_dispatch >>] jobs: - preimage-reproducibility: context: @@ -3636,7 +2899,7 @@ workflows: or: - equal: [build_daily, <>] # Trigger on manual triggers if explicitly requested - - equal: [true, << pipeline.parameters.stale_check_dispatch >>] + - equal: [true, << pipeline.parameters.c-stale_check_dispatch >>] jobs: - stale-check: context: @@ -3646,84 +2909,18 @@ workflows: when: or: - equal: [build_daily, <>] - - equal: [true, << pipeline.parameters.heavy_fuzz_dispatch >>] + - equal: [true, << pipeline.parameters.c-heavy_fuzz_dispatch >>] jobs: - contracts-bedrock-heavy-fuzz-nightly: context: - slack - circleci-repo-readonly-authenticated-github-token - - # Acceptance tests - acceptance-tests: - when: - or: - - equal: ["webhook", << pipeline.trigger_source >>] - # Manual dispatch - - and: - - equal: [true, <>] - - equal: ["api", << pipeline.trigger_source >>] - jobs: - - contracts-bedrock-build: # needed for sysgo tests - build_args: --skip test - context: - - circleci-repo-readonly-authenticated-github-token - - cannon-prestate-quick: # needed for sysgo tests - context: - - circleci-repo-readonly-authenticated-github-token - - cannon-kona-prestate: # needed for sysgo tests (if any package is in-memory) - context: - - circleci-repo-readonly-authenticated-github-token - - rust-build-binary: &cannon-kona-host - name: cannon-kona-host - directory: rust - profile: "release" - binary: "kona-host" - save_cache: true - context: - - circleci-repo-readonly-authenticated-github-token - - rust-build-binary: &kona-build-release - name: kona-build-release - directory: rust - profile: "release" - features: "default" - save_cache: true - context: - - circleci-repo-readonly-authenticated-github-token - - rust-build-submodule: *rust-build-op-rbuilder - - rust-build-submodule: *rust-build-rollup-boost - - rust-binaries-for-sysgo: - requires: - - kona-build-release - - rust-build-op-rbuilder - - rust-build-rollup-boost - # IN-MEMORY (all) - - op-acceptance-tests: - name: memory-all - gate: "" # Empty gate = gateless mode - no_output_timeout: 120m # Allow longer runs for memory-all gate - context: - - circleci-repo-readonly-authenticated-github-token - - slack - - discord - requires: - - contracts-bedrock-build - - 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 - context: - - circleci-repo-readonly-authenticated-github-token - - circleci-api-token - close-issue-workflow: when: and: - equal: [<< pipeline.trigger_source >>, "api"] - - equal: [<< pipeline.parameters.github-event-type >>, "pull_request"] - - equal: [<< pipeline.parameters.github-event-action >>, "labeled"] + - equal: [<< pipeline.parameters.c-github-event-type >>, "pull_request"] + - equal: [<< pipeline.parameters.c-github-event-action >>, "labeled"] jobs: - close-issue: label_name: "auto-close-trivial-contribution" @@ -3739,7 +2936,7 @@ workflows: or: - equal: [<< pipeline.trigger_source >>, "webhook"] - and: - - equal: [true, << pipeline.parameters.devnet-metrics-collect >>] + - equal: [true, << pipeline.parameters.c-devnet-metrics-collect >>] - equal: [<< pipeline.trigger_source >>, "api"] jobs: - devnet-metrics-collect-authorship: @@ -3751,7 +2948,7 @@ workflows: when: or: - equal: [build_mon_thu, <>] - - equal: [true, << pipeline.parameters.ai_contracts_test_dispatch >>] + - equal: [true, << pipeline.parameters.c-ai_contracts_test_dispatch >>] jobs: - ai-contracts-test: context: diff --git a/.circleci/continue/rust-ci.yml b/.circleci/continue/rust-ci.yml index 488c5a4b064..3a04042072d 100644 --- a/.circleci/continue/rust-ci.yml +++ b/.circleci/continue/rust-ci.yml @@ -4,20 +4,93 @@ version: 2.1 # This file contains all Rust CI commands, parameterized jobs, crate-specific jobs, and workflows. orbs: - utils: ethereum-optimism/circleci-utils@1.0.23 + utils: ethereum-optimism/circleci-utils@1.0.24 gcp-cli: circleci/gcp-cli@3.0.1 codecov: codecov/codecov@5.0.3 parameters: + c-default_docker_image: + type: string + default: cimg/base:2026.03 + c-base_image: + type: string + default: default + c-rust_ci_dispatch: + type: boolean + default: false + c-go-cache-version: + type: string + default: "v0.0" + c-rust_changes_detected: + type: boolean + default: false + c-non_docs_changes: + type: boolean + default: false + # Passthrough declarations for setup config parameters. + # CircleCI forwards all explicitly-passed pipeline parameters to continuation configs. + # Without these declarations, manually triggered pipelines fail with "Unexpected argument(s)". + # These are not referenced by any job — the c- prefixed versions above are used instead. default_docker_image: type: string - default: cimg/base:2024.01 + default: cimg/base:2026.03 base_image: type: string default: default + main_dispatch: + type: boolean + default: true + fault_proofs_dispatch: + type: boolean + default: false + reproducibility_dispatch: + type: boolean + default: false + kontrol_dispatch: + type: boolean + default: false + cannon_full_test_dispatch: + type: boolean + default: false + sdk_dispatch: + type: boolean + default: false + docker_publish_dispatch: + type: boolean + default: false + stale_check_dispatch: + type: boolean + default: false + contracts_coverage_dispatch: + type: boolean + default: false + heavy_fuzz_dispatch: + type: boolean + default: false + sync_test_op_node_dispatch: + type: boolean + default: false + ai_contracts_test_dispatch: + type: boolean + default: false rust_ci_dispatch: type: boolean default: false + rust_e2e_dispatch: + type: boolean + default: false + github-event-type: + type: string + default: "__not_set__" + github-event-action: + type: string + default: "__not_set__" + github-event-base64: + type: string + default: "__not_set__" + devnet-metrics-collect: + type: boolean + default: false go-cache-version: type: string default: "v0.0" @@ -329,7 +402,7 @@ jobs: type: string default: "just fmt-check" docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -362,7 +435,7 @@ jobs: type: string default: "stable" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -395,7 +468,7 @@ jobs: type: string default: "cargo deny check" docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -425,7 +498,7 @@ jobs: type: string default: "zepter run check" docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -452,7 +525,7 @@ jobs: description: "Directory containing the Cargo workspace" type: string docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -490,7 +563,7 @@ jobs: type: string default: "just check-no-std" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -519,7 +592,7 @@ jobs: type: string default: "just lint-docs" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -550,7 +623,7 @@ jobs: type: string default: "cargo test --workspace --doc" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -588,7 +661,7 @@ jobs: type: string default: "release" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -622,7 +695,7 @@ jobs: type: string default: "just check-udeps" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -661,7 +734,7 @@ jobs: type: string default: "--workspace" docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -689,7 +762,7 @@ jobs: type: integer default: 1 docker: - - image: <> + - image: <> resource_class: xlarge parallelism: <> steps: @@ -723,7 +796,7 @@ jobs: # OP-Reth compact codec backwards compatibility op-reth-compact-codec: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -763,7 +836,7 @@ jobs: kona-host-client-offline: parameters: machine: - image: <> + image: <> docker_layer_caching: true resource_class: xlarge steps: @@ -779,7 +852,7 @@ jobs: - run: name: Build cannon command: | - cd cannon && make + cd cannon && just cannon sudo mv ./bin/cannon /usr/local/bin/ - run: name: Set run environment @@ -819,7 +892,7 @@ jobs: description: The lint target (native, cannon) type: string machine: - image: <> + image: <> docker_layer_caching: true resource_class: xlarge steps: @@ -844,7 +917,7 @@ jobs: description: The build target (cannon-client) type: string machine: - image: <> + image: <> docker_layer_caching: true resource_class: xlarge steps: @@ -861,7 +934,7 @@ jobs: # Kona Coverage kona-coverage: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -898,7 +971,7 @@ jobs: # Unified Rust Docs Build rust-docs-build: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -931,7 +1004,7 @@ jobs: # OP-Reth docs build op-reth-docs-build: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -958,7 +1031,7 @@ jobs: # Kona Link Checker kona-link-checker: docker: - - image: <> + - image: <> resource_class: medium steps: - utils/checkout-with-mise: @@ -985,7 +1058,7 @@ jobs: description: The version to build (kona-client, kona-client-int) type: string machine: - image: <> + image: <> docker_layer_caching: true resource_class: xlarge steps: @@ -1009,6 +1082,7 @@ jobs: just "<>" "<>" "../../prestate-artifacts-<>" - run: name: Upload prestates to GCS + working_directory: rust/kona command: | PRESTATE_HASH=$(jq -r .pre ./prestate-artifacts-<>/prestate-proof.json) BRANCH_NAME=$(echo "<< pipeline.git.branch >>" | tr '/' '-') @@ -1025,20 +1099,30 @@ jobs: echo "Successfully published prestates artifacts to GCS" - rust-save-build-cache: *kona-publish-prestate-cache + required-rust-ci: + docker: + - image: <> + resource_class: small + steps: + - run: echo "Required Rust CI checks passed" + # ============================================================================ # WORKFLOWS # ============================================================================ workflows: # ========================================================================== # Unified Rust CI workflow - # Runs on any rust/.* change or manual dispatch with rust_ci_dispatch=true + # Runs on rust path changes or manual dispatch with rust_ci_dispatch=true # ========================================================================== rust-ci: when: or: - - equal: ["webhook", << pipeline.trigger_source >>] - and: - - equal: [true, <>] + - equal: ["webhook", << pipeline.trigger_source >>] + - << pipeline.parameters.c-rust_changes_detected >> + - << pipeline.parameters.c-non_docs_changes >> + - and: + - equal: [true, <>] - equal: ["api", << pipeline.trigger_source >>] jobs: # ----------------------------------------------------------------------- @@ -1170,17 +1254,13 @@ workflows: # Kona crate-specific jobs (lint, FPVM builds, benches, coverage) # ----------------------------------------------------------------------- - kona-cargo-lint: - name: kona-lint-<> - matrix: - parameters: - target: ["cannon"] + name: kona-lint-cannon + target: "cannon" context: *rust-ci-context - kona-build-fpvm: - name: kona-build-fpvm-<> - matrix: - parameters: - target: ["cannon-client"] + name: kona-build-fpvm-cannon-client + target: "cannon-client" context: *rust-ci-context - kona-coverage: @@ -1192,6 +1272,57 @@ workflows: name: kona-host-client-offline-cannon context: *rust-ci-context + # ----------------------------------------------------------------------- + # Required gate — fans in on required Rust CI jobs + # ----------------------------------------------------------------------- + - required-rust-ci: + requires: + - rust-tests + - rust-clippy + - rust-docs + + # ========================================================================== + # Required Rust CI gate (skip) — runs when no rust changes and no dispatch + # Just runs the rust cargo tests for the different packages in the repo and try to build the fpvm-prestates + # ========================================================================== + rust-ci-gate-short: + when: + and: + - equal: ["webhook", << pipeline.trigger_source >>] + - not: << pipeline.parameters.c-rust_changes_detected >> + jobs: + - rust-ci-cargo-tests: + name: rust-tests + directory: rust + context: *rust-ci-context + + - rust-ci-cargo-tests: + name: op-reth-integration-tests + directory: rust + command: "--justfile op-reth/justfile test-integration" + cache_profile: debug + context: *rust-ci-context + + - rust-ci-cargo-tests: + name: op-reth-tests-edge + directory: rust + command: "--justfile op-reth/justfile test" + flags: "edge" + cache_profile: debug + context: *rust-ci-context + + - kona-build-fpvm: + name: kona-build-fpvm-cannon-client + target: "cannon-client" + context: *rust-ci-context + + - required-rust-ci: + requires: + - rust-tests + - op-reth-integration-tests + - op-reth-tests-edge + - kona-build-fpvm-cannon-client + # ========================================================================== # Kona scheduled workflows @@ -1207,8 +1338,10 @@ workflows: # Kona publish prestate artifacts - on push to develop kona-publish-prestates: when: - or: + and: - equal: ["develop", <>] + - equal: ["webhook", << pipeline.trigger_source >>] # Only trigger on push to develop, not scheduled runs + - << pipeline.parameters.c-rust_changes_detected >> # Only publish when rust paths changed jobs: - kona-publish-prestate-artifacts: name: kona-publish-<> @@ -1218,4 +1351,3 @@ workflows: context: - circleci-repo-readonly-authenticated-github-token - oplabs-network-optimism-io-bucket - diff --git a/.circleci/continue/rust-e2e.yml b/.circleci/continue/rust-e2e.yml index 1b8edd429ee..2b36c6dad4c 100644 --- a/.circleci/continue/rust-e2e.yml +++ b/.circleci/continue/rust-e2e.yml @@ -7,12 +7,85 @@ version: 2.1 parameters: # Required parameters (also in main.yml, merged during continuation) + c-default_docker_image: + type: string + default: cimg/base:2026.03 + c-rust_e2e_dispatch: + type: boolean + default: false + c-go-cache-version: + type: string + default: "v0.0" + c-rust_changes_detected: + type: boolean + default: false + c-non_docs_changes: + type: boolean + default: false + # Passthrough declarations for setup config parameters. + # CircleCI forwards all explicitly-passed pipeline parameters to continuation configs. + # Without these declarations, manually triggered pipelines fail with "Unexpected argument(s)". + # These are not referenced by any job — the c- prefixed versions above are used instead. default_docker_image: type: string - default: cimg/base:2024.01 + default: cimg/base:2026.03 + base_image: + type: string + default: default + main_dispatch: + type: boolean + default: true + fault_proofs_dispatch: + type: boolean + default: false + reproducibility_dispatch: + type: boolean + default: false + kontrol_dispatch: + type: boolean + default: false + cannon_full_test_dispatch: + type: boolean + default: false + sdk_dispatch: + type: boolean + default: false + docker_publish_dispatch: + type: boolean + default: false + stale_check_dispatch: + type: boolean + default: false + contracts_coverage_dispatch: + type: boolean + default: false + heavy_fuzz_dispatch: + type: boolean + default: false + sync_test_op_node_dispatch: + type: boolean + default: false + ai_contracts_test_dispatch: + type: boolean + default: false + rust_ci_dispatch: + type: boolean + default: false rust_e2e_dispatch: type: boolean default: false + github-event-type: + type: string + default: "__not_set__" + github-event-action: + type: string + default: "__not_set__" + github-event-base64: + type: string + default: "__not_set__" + devnet-metrics-collect: + type: boolean + default: false go-cache-version: type: string default: "v0.0" @@ -28,7 +101,7 @@ commands: type: string version: type: string - default: <> + default: <> steps: - restore_cache: name: Restore go cache for <> (<>/go.mod) @@ -46,7 +119,7 @@ commands: type: string version: type: string - default: <> + default: <> steps: - save_cache: name: Save go cache for <> (<>/go.mod) @@ -70,7 +143,7 @@ jobs: type: boolean default: false docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -116,7 +189,7 @@ jobs: # Kona Node Restart Tests (from node_e2e_sysgo_tests.yaml) rust-restart-sysgo-tests: docker: - - image: <> + - image: <> resource_class: xlarge steps: - utils/checkout-with-mise: @@ -181,7 +254,7 @@ jobs: description: The kind of action test (single or interop) type: string docker: - - image: <> + - image: <> resource_class: xlarge parallelism: 4 steps: @@ -210,27 +283,36 @@ jobs: path: op-e2e/actions/proofs/tmp/testlogs when: always + required-rust-e2e: + docker: + - image: <> + resource_class: small + steps: + - run: echo "Required Rust E2E checks passed" + # ============================================================================ # RUST E2E WORKFLOWS # ============================================================================ workflows: + # Rust E2E CI — runs on rust path changes or manual dispatch rust-e2e-ci: when: or: - - equal: ["webhook", << pipeline.trigger_source >>] - and: - - equal: [true, <>] + - equal: ["webhook", << pipeline.trigger_source >>] + - << pipeline.parameters.c-rust_changes_detected >> + - << pipeline.parameters.c-non_docs_changes >> + - and: + - equal: [true, <>] - equal: ["api", << pipeline.trigger_source >>] jobs: - contracts-bedrock-build: build_args: --skip test context: - circleci-repo-readonly-authenticated-github-token - - cannon-prestate-quick: &rust-e2e-job-base + - cannon-prestate: &rust-e2e-job-base context: - circleci-repo-readonly-authenticated-github-token - - cannon-kona-prestate: - <<: *rust-e2e-job-base - rust-build-binary: &cannon-kona-host name: cannon-kona-host directory: rust @@ -260,8 +342,7 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - contracts-bedrock-build - - cannon-prestate-quick - - cannon-kona-prestate + - cannon-prestate - cannon-kona-host - kona-build-release - op-reth-build @@ -275,8 +356,7 @@ workflows: <<: *rust-e2e-job-base requires: - contracts-bedrock-build - - cannon-prestate-quick - - cannon-kona-prestate + - cannon-prestate - cannon-kona-host - kona-build-release # Proof tests - single kind only, interop excluded per original config @@ -331,4 +411,3 @@ workflows: requires: - kona-proof-action-single - diff --git a/go.mod b/go.mod index 48c915a9f03..8a71c3ca5e8 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/golang/snappy v1.0.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v55 v55.0.0 github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 @@ -58,18 +57,17 @@ require ( github.com/prometheus/client_model v0.6.2 github.com/protolambda/ctxlock v0.1.0 github.com/schollz/progressbar/v3 v3.18.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.0 github.com/urfave/cli/v2 v2.27.6 go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/trace v1.34.0 golang.org/x/crypto v0.43.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c - golang.org/x/mod v0.28.0 - golang.org/x/oauth2 v0.25.0 - golang.org/x/sync v0.17.0 - golang.org/x/term v0.36.0 - golang.org/x/text v0.30.0 - golang.org/x/time v0.11.0 + golang.org/x/mod v0.29.0 + golang.org/x/sync v0.18.0 + golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 + golang.org/x/time v0.14.0 gonum.org/v1/plot v0.16.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -77,6 +75,8 @@ require ( require ( github.com/fatih/color v1.18.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect ) require ( @@ -86,8 +86,7 @@ require ( git.sr.ht/~sbinet/gg v0.6.0 // indirect github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect - github.com/VictoriaMetrics/fastcache v1.12.2 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/adrg/xdg v0.4.0 // indirect github.com/aead/siphash v1.0.1 // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect @@ -102,7 +101,6 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/campoy/embedmd v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.3.3 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect @@ -148,7 +146,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect github.com/graph-gophers/graphql-go v1.3.0 // indirect @@ -277,10 +274,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/image v0.25.0 // indirect - golang.org/x/net v0.45.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect google.golang.org/grpc v1.69.4 // indirect diff --git a/go.sum b/go.sum index b0211893cc1..bec05084b97 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -39,10 +41,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= -github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= @@ -56,7 +56,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= @@ -111,15 +110,15 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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= @@ -142,9 +141,6 @@ github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= @@ -166,6 +162,12 @@ github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -211,9 +213,15 @@ github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdo github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -251,6 +259,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -345,8 +355,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= -github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -359,11 +369,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg= -github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 h1:Ep/joEub9YwcjRY6ND3+Y/w0ncE540RtGatVhtZL0/Q= github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -564,6 +570,9 @@ github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCy github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lmittmann/w3 v0.19.5 h1:WwVRyIwhRLfIahmpB1EglsB3o1XWsgydgrxIUp5upFQ= github.com/lmittmann/w3 v0.19.5/go.mod h1:pN97sGGYGvsbqOYj/ms3Pd+7k/aiK/9OpNcxMmmzSOI= +github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0= +github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -622,10 +631,22 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw= github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -687,6 +708,10 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -765,6 +790,8 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= @@ -829,6 +856,12 @@ github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8G github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= +github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -925,6 +958,8 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= @@ -992,8 +1027,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1027,14 +1062,12 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1047,8 +1080,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1082,6 +1115,7 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1108,12 +1142,11 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1122,8 +1155,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1136,14 +1169,14 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1164,8 +1197,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/op-acceptance-tests/README.md b/op-acceptance-tests/README.md index 11e1ac0df90..faeda8a0b03 100644 --- a/op-acceptance-tests/README.md +++ b/op-acceptance-tests/README.md @@ -2,227 +2,71 @@ ## Overview -This directory contains the acceptance tests and configuration for the OP Stack. These tests are executed by `op-acceptor`, which serves as an automated gatekeeper for OP Stack network promotions. +This directory contains the OP Stack acceptance tests. They run against the in-process `sysgo` devstack and are executed as normal Go tests. -Think of acceptance testing as Gandalf 🧙, standing at the gates and shouting, "You shall not pass!" to networks that don't meet our standards. It enforces the "Don't trust, verify" principle by: +The supported execution path is: -- Running automated acceptance tests -- Providing clear pass/fail results (and tracking these over time) -- Gating network promotions based on test results -- Providing insight into test feature/functional coverage +- `just` or `just acceptance-test-all` +- `gotestsum -- go test ./op-acceptance-tests/tests/...` -The `op-acceptor` ensures network quality and readiness by running a comprehensive suite of acceptance tests before features can advance through the promotion pipeline: - -Localnet → Alphanet → Betanet → Testnet - -This process helps maintain high-quality standards across all networks in the OP Stack ecosystem. - -## Architecture - -The acceptance testing system uses `sysgo` in-process orchestration: - -### **sysgo (In-process)** -- **Use case**: Fast, isolated testing without external dependencies -- **Benefits**: Quick startup, no external infrastructure needed -- **Dependencies**: None (pure Go services) +`devtest.T.MarkFlaky(...)` is used for tests that should downgrade failures to skips in the normal acceptance run. Set `DEVNET_FAIL_FLAKY_TESTS=true` to force those tests to fail normally. Acceptance runs also emit a `flaky-tests.txt` artifact in `op-acceptance-tests/logs/...` listing the current `MarkFlaky(...)` call sites. ## Dependencies -### Basic Dependencies -* Mise (install as instructed in CONTRIBUTING.md) - -Dependencies are managed using the repo-wide `mise` config. Run `mise install` at the repo root to install `op-acceptor` and other tools. +Install repo tools via `mise` as documented in the repository root. Local acceptance runs also build contract and Rust dependencies when needed. ## Usage ### Quick Start ```bash -# Run in-process tests (fast, no external dependencies) -just acceptance-test base +cd op-acceptance-tests +just ``` ### Available Commands ```bash -# Default: run in-process tests with base gate +# Default: run all acceptance test packages just -# Run a specific gate -just acceptance-test - -# Run all tests (gateless) +# Explicit alias +just acceptance-test just acceptance-test-all ``` ### Direct CLI Usage -You can also run `op-acceptor` directly: - ```bash -cd op-acceptance-tests - -# In-process testing -op-acceptor \ - --gate base \ - --testdir .. \ - --validators ./acceptance-tests.yaml \ - --log.level info \ - --allow-skips \ - --exclude-gates flake-shake +gotestsum --format testname --junitfile ./op-acceptance-tests/results/results.xml -- \ + -count=1 -p 4 -parallel 4 -timeout 2h ./op-acceptance-tests/tests/... ``` -## Development Usage +The `just` wrapper computes defaults from available CPUs: -### Fast Development Loop (In-process) +- package jobs: CPU count +- in-package `t.Parallel`: half the CPU count +- timeout: `2h` -For rapid test development, use in-process testing: +Override them with `ACCEPTANCE_TEST_JOBS`, `ACCEPTANCE_TEST_PARALLEL`, and `ACCEPTANCE_TEST_TIMEOUT`. -```bash -cd op-acceptance-tests -just acceptance-test base -``` +## Logging -### Configuration +When invoked with `go test`, devstack acceptance tests support configuring logging via CLI flags and environment variables: -- `acceptance-tests.yaml`: Defines the validation gates and the suites and tests that should be run for each gate. -- `justfile`: Contains the commands for running the acceptance tests. +- `--log.level LEVEL` or `LOG_LEVEL` +- `--log.format FORMAT` or `LOG_FORMAT` +- `--log.color` or `LOG_COLOR` +- `--log.pid` or `LOG_PID` -### Logging Configuration +Example: -When invoked with `go test`, devstack acceptance tests support configuring logging via CLI flags and environment variables. The following options are available: - -* `--log.level LEVEL` (env: `LOG_LEVEL`): Sets the minimum log level. Supported levels: `trace`, `debug`, `info`, `warn`, `error`, `crit`. Default: `trace`. -* `--log.format FORMAT` (env: `LOG_FORMAT`): Chooses the log output format. Supported formats: `text`, `terminal`, `logfmt`, `json`, `json-pretty`. Default: `text`. -* `--log.color` (env: `LOG_COLOR`): Enables colored output in terminal mode. Default: `true` if STDOUT is a TTY. -* `--log.pid` (env: `LOG_PID`): Includes the process ID in each log entry. Default: `false`. - -Environment variables override CLI flags. For example: ```bash -# Override log level via flag -go test -v ./op-acceptance-tests/tests/interop/sync/multisupervisor_interop/... -run TestL2CLAheadOfSupervisor -log.format=json | logdy - -# Override via env var LOG_LEVEL=info go test -v ./op-acceptance-tests/tests/interop/sync/multisupervisor_interop/... -run TestL2CLAheadOfSupervisor ``` -## Adding New Tests - -To add new acceptance tests: - -1. Create your test in the appropriate Go package under `tests` (as a regular Go test) -2. Register the test in `acceptance-tests.yaml` under the appropriate gate -3. Follow the existing pattern for test registration: - ```yaml - - name: YourTestName - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/your/package/path - ``` - -## Flake-Shake: Test Stability Validation - -Flake-shake is a test stability validation system that runs tests multiple times to detect flakiness before they reach production gates. It serves as a quarantine area where new or potentially unstable tests must prove their reliability. - -### Purpose - -- Detect flaky tests through repeated execution (100+ iterations) -- Prevent unstable tests from disrupting CI/CD pipelines -- Provide data-driven decisions for test promotion to production gates - -### How It Works - -Flake-shake runs tests multiple times and aggregates results to determine stability: -- **STABLE**: Tests with 100% pass rate across all iterations -- **UNSTABLE**: Tests with any failures (<100% pass rate) - -### Running Flake-Shake - -Flake-shake is integrated into op-acceptor and can be run locally or in CI: - -```bash -# Run flake-shake with op-acceptor (requires op-acceptor v3.4.0+) -op-acceptor \ - --validators ./acceptance-tests.yaml \ - --gate flake-shake \ - --flake-shake \ - --flake-shake-iterations 10 - -# Run with more iterations for thorough testing -op-acceptor \ - --validators ./acceptance-tests.yaml \ - --gate flake-shake \ - --flake-shake \ - --flake-shake-iterations 100 -``` - -### Adding Tests to Flake-Shake - -Add new or suspicious tests to the flake-shake gate in `acceptance-tests.yaml`: - -```yaml -gates: - - id: flake-shake - description: "Test stability validation gate" - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/yourtest - timeout: 10m - metatada: - owner: stefano -``` - -### Understanding Reports - -Flake-shake stores a daily summary artifact per run: -- **`final-report/daily-summary.json`**: Aggregated counts of stable/unstable tests and per-test pass/fail tallies. - -### CI Integration - -In CI, flake-shake runs tests across multiple parallel workers: -- 10 workers each run 10 iterations (100 total by default) -- Results are aggregated using the `flake-shake-aggregator` tool -- Reports are stored as CircleCI artifacts - -### Automated Promotion (Promoter CLI) - -We provide a small CLI that aggregates the last N daily summaries from CircleCI and proposes YAML edits to promote stable tests out of the `flake-shake` gate: - -```bash -export CIRCLE_API_TOKEN=... # CircleCI API token (read artifacts) -go build -o ./op-acceptance-tests/flake-shake-promoter ./op-acceptance-tests/cmd/flake-shake-promoter/main.go -./op-acceptance-tests/flake-shake-promoter \ - --org ethereum-optimism --repo optimism --branch develop \ - --workflow scheduled-flake-shake --report-job op-acceptance-tests-flake-shake-report \ - --days 3 --gate flake-shake --min-runs 300 --max-failure-rate 0.01 --min-age-days 3 \ - --out ./final-promotion --dry-run -``` - -Outputs written to `--out`: -- `aggregate.json`: Per-test aggregated totals across days -- `promotion-ready.json`: Candidates and skip reasons -- `promotion.yaml`: Proposed edits to `op-acceptance-tests/acceptance-tests.yaml` - -### Promotion Criteria - -Tests should remain in flake-shake until they demonstrate consistent stability: -- **Immediate promotion**: 100% pass rate across 100+ iterations -- **Investigation needed**: Any failures require fixing before promotion -- **Minimum soak time**: 3 days in flake-shake gate recommended - -### Quick Development - -For rapid development and testing: - -```bash -cd op-acceptance-tests - -# Run all tests (gateless mode) - most comprehensive coverage -just acceptance-test-all - -# Run specific gate-based tests (traditional mode) -just acceptance-test base -``` - -## Further Information +## Adding Tests -For more details about `op-acceptor` and the acceptance testing process, refer to the main documentation or ask the team for guidance. +Add new acceptance tests as ordinary Go tests under [`tests`](./tests). There is no external gate or manifest to update. -The source code for `op-acceptor` is available at [github.com/ethereum-optimism/infra/op-acceptor](https://github.com/ethereum-optimism/infra/tree/main/op-acceptor). If you discover any bugs or have feature requests, please open an issue in that repository. +If a test is currently flaky in the normal acceptance run, mark it in code with `devtest.T.MarkFlaky(...)`. That keeps the source of truth next to the test itself while the acceptance logs and flaky-test artifacts provide the review surface for recent failures. diff --git a/op-acceptance-tests/acceptance-tests.yaml b/op-acceptance-tests/acceptance-tests.yaml deleted file mode 100644 index 9b9417ba8ad..00000000000 --- a/op-acceptance-tests/acceptance-tests.yaml +++ /dev/null @@ -1,148 +0,0 @@ -# Configuration file for acceptance tests (op-acceptor) -# -# All acceptance tests need to be registered here for op-acceptor to run them. -# As a rule of thumb, we recommend that each fork gate inherits from the -# base gate as well as any earlier fork gates. - -gates: - # New tests should be added here first with an owner metadata. - # Once we're confident they're not flaky, a PR will be automatically created to remove them from this gate. - # Example entry format: - # - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/parallelism - # timeout: 10m - # metadata: - # owner: "team-infra" - - id: flake-shake - description: "Quarantine gate for new and potentially flaky tests requiring stability validation." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/depreqres/reqressyncdisabled - name: TestUnsafeChainNotStalling_DisabledReqRespSync - timeout: 10m - metadata: - owner: "anton evangelatov" - target_gate: "depreqres" - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/supernode/interop/activation - name: TestSupernodeInteropActivationAfterGenesis - timeout: 10m - metadata: - owner: "adrian sutton" - target_gate: "supernode-interop" - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/supernode/interop - name: TestSupernodeInteropChainLag - timeout: 15m - metadata: - owner: "axel kingsley" - target_gate: "supernode-interop" - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync - name: TestUnsafeChainNotStalling_ELSync_Short - timeout: 10m - metadata: - owner: "anton evangelatov" - target_gate: "depreqres" - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync - name: TestUnsafeChainNotStalling_ELSync_Long - timeout: 10m - metadata: - owner: "anton evangelatov" - target_gate: "depreqres" - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync - name: TestUnsafeChainNotStalling_ELSync_RestartOpNode_Long - timeout: 10m - metadata: - owner: "anton evangelatov" - target_gate: "depreqres" - - - id: jovian - description: "Jovian network tests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/jovian/bpo2 - timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/jovian/pectra - timeout: 10m - - - id: isthmus - description: "Isthmus network tests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus - timeout: 6h - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus/operator_fee - timeout: 6h - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus/withdrawal_root - timeout: 20m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus/erc20_bridge - timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus/pectra - timeout: 10m - - - id: base - description: "Sanity/smoke acceptance tests for all networks." - inherits: - - isthmus - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/base - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/base/deposit - timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/base/chain - timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/ecotone - timeout: 10m - # TODO(infra#401): Re-enable the test once the remaining infra gap is resolved. - #- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/base/withdrawal - # timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/fjord - - - id: conductor - description: "Sanity/smoke acceptance tests for networks with conductors." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/base/conductor - timeout: 10m - - - id: pre-interop - inherits: - - base - description: "Pre-interop network tests." - tests: - - name: TestInteropReadiness - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus/preinterop - timeout: 20m - - - id: interop - inherits: - - base - description: "Interop network tests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop - timeout: 10m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/message - timeout: 30m - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/sync/... - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/smoke - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/contract - - - id: interop-loadtest - description: "Interop network loadtests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/loadtest - timeout: 10m - - - id: flashblocks - inherits: - - base - description: "Flashblocks network tests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/flashblocks - timeout: 5m - - - id: flashblocks-with-isthmus - inherits: - - base - description: "Flashblocks network tests with Isthmus." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/flashblocks - timeout: 5m - - - id: cgt - description: "Custom Gas Token (CGT) network tests." - tests: - - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/custom_gas_token - timeout: 10m diff --git a/op-acceptance-tests/cmd/flake-shake-aggregator/main.go b/op-acceptance-tests/cmd/flake-shake-aggregator/main.go deleted file mode 100644 index dd08d037a14..00000000000 --- a/op-acceptance-tests/cmd/flake-shake-aggregator/main.go +++ /dev/null @@ -1,560 +0,0 @@ -// flake-shake-aggregator aggregates multiple flake-shake reports from parallel workers -// into a single comprehensive report. -package main - -import ( - "crypto/sha256" - "encoding/json" - "flag" - "fmt" - html_pkg "html" - "log" - "os" - "path/filepath" - "regexp" - "strings" - "time" -) - -// FlakeShakeResult represents a single test's flake-shake analysis -type FlakeShakeResult struct { - TestName string `json:"test_name"` - Package string `json:"package"` - TotalRuns int `json:"total_runs"` - Passes int `json:"passes"` - Failures int `json:"failures"` - Skipped int `json:"skipped"` - PassRate float64 `json:"pass_rate"` - AvgDuration time.Duration `json:"avg_duration"` - MinDuration time.Duration `json:"min_duration"` - MaxDuration time.Duration `json:"max_duration"` - FailureLogs []string `json:"failure_logs,omitempty"` - LastFailure *time.Time `json:"last_failure,omitempty"` - Recommendation string `json:"recommendation"` -} - -// FlakeShakeReport contains the complete flake-shake analysis -type FlakeShakeReport struct { - Date string `json:"date"` - Gate string `json:"gate"` - TotalRuns int `json:"total_runs"` - Iterations int `json:"iterations"` - Tests []FlakeShakeResult `json:"tests"` - GeneratedAt time.Time `json:"generated_at"` - RunID string `json:"run_id"` -} - -// AggregatedTestStats for accumulating results -type AggregatedTestStats struct { - TestName string - Package string - TotalRuns int - Passes int - Failures int - Skipped int - MinDuration time.Duration - MaxDuration time.Duration - FailureLogs []string - LastFailure *time.Time - durationSum time.Duration - durationCount int -} - -func main() { - var ( - inputPattern string - outputDir string - verbose bool - ) - - flag.StringVar(&inputPattern, "input-pattern", "flake-shake-results-worker-*/flake-shake-report.json", - "Glob pattern to find worker report files") - flag.StringVar(&outputDir, "output-dir", "final-report", - "Directory to write the aggregated report") - flag.BoolVar(&verbose, "verbose", false, "Enable verbose output") - flag.Parse() - - if err := run(inputPattern, outputDir, verbose); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} - -func run(inputPattern, outputDir string, verbose bool) error { - logger := log.New(os.Stdout, "[flake-shake-aggregator] ", log.LstdFlags) - // Create output directory - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) - } - - // Find all report files - reportFiles, err := filepath.Glob(inputPattern) - if err != nil { - return fmt.Errorf("failed to glob input files: %w", err) - } - - if len(reportFiles) == 0 { - // Try alternative patterns - alternatives := []string{ - "flake-shake-results-worker-*/flake-shake-report.json", - "*/flake-shake-report.json", - "flake-shake-report-*.json", - } - for _, alt := range alternatives { - reportFiles, err = filepath.Glob(alt) - if err == nil && len(reportFiles) > 0 { - break - } - } - - if len(reportFiles) == 0 { - return fmt.Errorf("no report files found matching pattern: %s", inputPattern) - } - } - - if verbose { - logger.Printf("Found %d report files to aggregate:", len(reportFiles)) - for _, f := range reportFiles { - logger.Printf(" - %s", f) - } - } - - // Aggregate all reports - aggregated := make(map[string]*AggregatedTestStats) - var gate string - var runID string - totalIterations := 0 - - for _, reportFile := range reportFiles { - if verbose { - logger.Printf("Processing %s...", reportFile) - } - - data, err := os.ReadFile(reportFile) - if err != nil { - logger.Printf("Warning: failed to read %s: %v", reportFile, err) - continue - } - - var report FlakeShakeReport - if err := json.Unmarshal(data, &report); err != nil { - logger.Printf("Warning: failed to parse %s: %v", reportFile, err) - continue - } - - // Use first report's metadata - if gate == "" { - gate = report.Gate - } - if runID == "" && report.RunID != "" { - runID = report.RunID - } - totalIterations += report.Iterations - - // Aggregate test results - for _, test := range report.Tests { - key := fmt.Sprintf("%s::%s", test.Package, test.TestName) - - if stats, exists := aggregated[key]; exists { - // Merge with existing stats - stats.TotalRuns += test.TotalRuns - stats.Passes += test.Passes - stats.Failures += test.Failures - stats.Skipped += test.Skipped - - // Update durations - if test.MinDuration < stats.MinDuration || stats.MinDuration == 0 { - stats.MinDuration = test.MinDuration - } - if test.MaxDuration > stats.MaxDuration { - stats.MaxDuration = test.MaxDuration - } - stats.durationSum += time.Duration(test.AvgDuration) * time.Duration(test.TotalRuns) - stats.durationCount += test.TotalRuns - - // Merge failure logs (keep first 50) - stats.FailureLogs = append(stats.FailureLogs, test.FailureLogs...) - if len(stats.FailureLogs) > 50 { - stats.FailureLogs = stats.FailureLogs[:50] - } - - // Update last failure time - if test.LastFailure != nil && (stats.LastFailure == nil || test.LastFailure.After(*stats.LastFailure)) { - stats.LastFailure = test.LastFailure - } - } else { - // First occurrence of this test - aggregated[key] = &AggregatedTestStats{ - TestName: test.TestName, - Package: test.Package, - TotalRuns: test.TotalRuns, - Passes: test.Passes, - Failures: test.Failures, - Skipped: test.Skipped, - MinDuration: test.MinDuration, - MaxDuration: test.MaxDuration, - durationSum: time.Duration(test.AvgDuration) * time.Duration(test.TotalRuns), - durationCount: test.TotalRuns, - FailureLogs: test.FailureLogs, - LastFailure: test.LastFailure, - } - } - } - } - - // Calculate final statistics - var finalTests []FlakeShakeResult - totalTestRuns := 0 - for _, stats := range aggregated { - // Calculate pass rate - passRate := 0.0 - if stats.TotalRuns > 0 { - passRate = float64(stats.Passes) / float64(stats.TotalRuns) * 100 - } - - // Calculate average duration - avgDuration := time.Duration(0) - if stats.durationCount > 0 { - avgDuration = stats.durationSum / time.Duration(stats.durationCount) - } - - // Determine recommendation - recommendation := "UNSTABLE" - if passRate == 100 { - recommendation = "STABLE" - } - - // Convert to final format - totalTestRuns += stats.TotalRuns - finalTests = append(finalTests, FlakeShakeResult{ - TestName: stats.TestName, - Package: stats.Package, - TotalRuns: stats.TotalRuns, - Passes: stats.Passes, - Failures: stats.Failures, - Skipped: stats.Skipped, - PassRate: passRate, - AvgDuration: avgDuration, - MinDuration: stats.MinDuration, - MaxDuration: stats.MaxDuration, - FailureLogs: stats.FailureLogs, - LastFailure: stats.LastFailure, - Recommendation: recommendation, - }) - } - - // Create final aggregated report - finalReport := FlakeShakeReport{ - Date: time.Now().Format("2006-01-02"), - Gate: gate, - TotalRuns: totalTestRuns, - Iterations: totalIterations, - Tests: finalTests, - GeneratedAt: time.Now(), - RunID: runID, - } - - // Save JSON report - jsonFile := filepath.Join(outputDir, "flake-shake-report.json") - jsonData, err := json.MarshalIndent(finalReport, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal report: %w", err) - } - - if err := os.WriteFile(jsonFile, jsonData, 0644); err != nil { - return fmt.Errorf("failed to write JSON report: %w", err) - } - - // Generate and save HTML report - htmlFile := filepath.Join(outputDir, "flake-shake-report.html") - htmlContent := generateHTMLReport(&finalReport) - if err := os.WriteFile(htmlFile, []byte(htmlContent), 0644); err != nil { - return fmt.Errorf("failed to write HTML report: %w", err) - } - - logger.Printf("✅ Aggregation complete!") - logger.Printf(" - Processed %d worker reports", len(reportFiles)) - logger.Printf(" - Aggregated %d unique tests", len(finalTests)) - logger.Printf(" - Total iterations: %d", totalIterations) - logger.Printf(" - Reports saved to:") - logger.Printf(" • %s", jsonFile) - logger.Printf(" • %s", htmlFile) - - // Print summary statistics - stableCount := 0 - unstableCount := 0 - for _, test := range finalTests { - if test.Recommendation == "STABLE" { - stableCount++ - } else { - unstableCount++ - } - } - - logger.Printf("\n📊 Test Stability Summary:") - if len(finalTests) > 0 { - logger.Printf(" - STABLE: %d tests (%.1f%%)", stableCount, - float64(stableCount)/float64(len(finalTests))*100) - logger.Printf(" - UNSTABLE: %d tests (%.1f%%)", unstableCount, - float64(unstableCount)/float64(len(finalTests))*100) - } else { - logger.Printf(" - No tests found") - } - - // List unstable tests if any - if unstableCount > 0 && verbose { - logger.Printf("\n⚠️ Unstable tests:") - for _, test := range finalTests { - if test.Recommendation == "UNSTABLE" { - logger.Printf(" - %s (%.1f%% pass rate)", - strings.TrimPrefix(test.TestName, test.Package+"::"), - test.PassRate) - } - } - } - - return nil -} - -func generateHTMLReport(report *FlakeShakeReport) string { - var html strings.Builder - - html.WriteString(` - - - Flake-Shake Report - - - -
-

Flake-Shake Report - ` + html_pkg.EscapeString(report.Gate) + `

-

Generated: ` + report.GeneratedAt.Format("2006-01-02 15:04:05") + `

- -
-
-

Total Tests

-
` + fmt.Sprintf("%d", len(report.Tests)) + `
-
-
-

Iterations

-
` + fmt.Sprintf("%d", report.Iterations) + `
-
-
-

Stable Tests

-
`) - - stableCount := 0 - for _, test := range report.Tests { - if test.Recommendation == "STABLE" { - stableCount++ - } - } - html.WriteString(fmt.Sprintf("%d", stableCount)) - - html.WriteString(`
-
-
-

Unstable Tests

-
`) - - html.WriteString(fmt.Sprintf("%d", len(report.Tests)-stableCount)) - - html.WriteString(`
-
-
- -

Stable Tests

`) - - if stableCount > 0 { - html.WriteString(` -
    `) - for _, test := range report.Tests { - if test.Recommendation == "STABLE" { - html.WriteString(fmt.Sprintf(` -
  • %s (%s)
  • `, - html_pkg.EscapeString(test.TestName), - html_pkg.EscapeString(test.Package), - )) - } - } - html.WriteString(` -
`) - } else { - html.WriteString(` -

No stable tests in this run.

`) - } - - html.WriteString(` - - - - - - - - - - - - - - `) - - for _, test := range report.Tests { - rowClass := "" - if test.PassRate == 100 { - rowClass = "pass-rate-100" - } else if test.PassRate < 95 { - rowClass = "pass-rate-low" - } - - html.WriteString(fmt.Sprintf(` - - - - - - - - - - `, - rowClass, - html_pkg.EscapeString(test.TestName), - html_pkg.EscapeString(test.Package), - test.PassRate, - test.TotalRuns, - test.Passes, - test.Failures, - test.AvgDuration.Round(time.Second), - strings.ToLower(test.Recommendation), - test.Recommendation, - )) - } - - html.WriteString(` - -
Test NamePackagePass RateRunsPassedFailedAvg DurationStatus
%s%s%.1f%%%d%d%d%s%s
-`) - - // Append grouped failure details - html.WriteString(` -

Failure Details

-`) - - normalizer := regexp.MustCompile(`(?m)^\s*\[?\d{4}-\d{2}-\d{2}.*$|\bt=\d{4}-\d{2}-\d{2}.*$|\b(duration|elapsed|took)[:=].*$`) - ansiStrip := regexp.MustCompile("\x1b\\[[0-9;]*m") - classify := func(s string) string { - ls := strings.ToLower(s) - switch { - case strings.Contains(ls, "context deadline exceeded"): - return "context deadline" - case strings.Contains(ls, "deadline exceeded"): - return "deadline exceeded" - case strings.Contains(ls, "timeout"): - return "timeout" - case strings.Contains(ls, "connection refused"): - return "connection refused" - case strings.Contains(ls, "connection reset"): - return "connection reset" - case strings.Contains(ls, "rpc error") || strings.Contains(ls, "rpc call failed"): - return "rpc error" - case strings.Contains(ls, "assert") || strings.Contains(ls, "require"): - return "assertion" - default: - return "unknown" - } - } - for _, test := range report.Tests { - if len(test.FailureLogs) == 0 { - continue - } - html.WriteString(fmt.Sprintf(`
%s — %s (failures: %d)`, - html_pkg.EscapeString(test.TestName), html_pkg.EscapeString(test.Package), test.Failures)) - - groups := map[string]struct { - Count int - Sample string - Type string - }{} - typeSummary := map[string]int{} - for _, raw := range test.FailureLogs { - // Extract human-readable content from Go test JSON events by keeping only non-empty Output fields. - processed := strings.TrimSpace(raw) - if strings.HasPrefix(processed, "{") { - var ev struct { - Output string `json:"Output"` - } - if json.Unmarshal([]byte(processed), &ev) == nil { - processed = strings.TrimSpace(ev.Output) - } - } - if processed == "" { - continue - } - // Strip ANSI color codes for readability - processed = ansiStrip.ReplaceAllString(processed, "") - // Normalize noisy timestamps/durations and trim - norm := normalizer.ReplaceAllString(processed, "") - norm = strings.TrimSpace(norm) - if norm == "" { - continue - } - sum := sha256.Sum256([]byte(norm)) - key := fmt.Sprintf("%x", sum[:]) - g := groups[key] - if g.Count == 0 { - g.Sample = norm - g.Type = classify(norm) - } - g.Count++ - groups[key] = g - } - // Build type summary - for _, g := range groups { - typeSummary[g.Type] += g.Count - } - // Render summary - html.WriteString(`
`) - html.WriteString(`Summary:
    `) - for t, c := range typeSummary { - html.WriteString(fmt.Sprintf(`
  • %s: %d
  • `, html_pkg.EscapeString(t), c)) - } - html.WriteString(`
`) - // Render groups - for _, g := range groups { - html.WriteString(`
`) - html.WriteString(fmt.Sprintf(`
Type: %s
`, html_pkg.EscapeString(g.Type))) - html.WriteString(fmt.Sprintf(`
Occurrences: %d
`, g.Count)) - html.WriteString(`
` + html_pkg.EscapeString(g.Sample) + `
`) - html.WriteString(`
`) - } - html.WriteString(`
`) - } - - html.WriteString(` -
- -`) - - return html.String() -} diff --git a/op-acceptance-tests/cmd/flake-shake-aggregator/main_test.go b/op-acceptance-tests/cmd/flake-shake-aggregator/main_test.go deleted file mode 100644 index 1f30f74afe5..00000000000 --- a/op-acceptance-tests/cmd/flake-shake-aggregator/main_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -func writeReport(t *testing.T, dir, name string, r FlakeShakeReport) string { - t.Helper() - b, err := json.Marshal(r) - if err != nil { - t.Fatalf("marshal: %v", err) - } - p := filepath.Join(dir, name) - if err := os.WriteFile(p, b, 0644); err != nil { - t.Fatalf("write: %v", err) - } - return p -} - -func TestRunAggregatesReports(t *testing.T) { - tmp := t.TempDir() - // create two worker reports - r1 := FlakeShakeReport{ - Date: "2025-01-01", - Gate: "flake-shake", - Iterations: 10, - Tests: []FlakeShakeResult{{ - TestName: "pkg::T1", - Package: "pkg", - TotalRuns: 10, - Passes: 9, - Failures: 1, - Skipped: 0, - AvgDuration: 100 * time.Millisecond, - MinDuration: 80 * time.Millisecond, - MaxDuration: 120 * time.Millisecond, - }}, - GeneratedAt: time.Now(), - RunID: "abc", - } - r2 := FlakeShakeReport{ - Date: "2025-01-01", - Gate: "flake-shake", - Iterations: 5, - Tests: []FlakeShakeResult{{ - TestName: "pkg::T1", - Package: "pkg", - TotalRuns: 5, - Passes: 5, - Failures: 0, - Skipped: 0, - AvgDuration: 90 * time.Millisecond, - MinDuration: 70 * time.Millisecond, - MaxDuration: 110 * time.Millisecond, - }}, - GeneratedAt: time.Now(), - RunID: "abc", - } - // Place files under pattern - d1 := filepath.Join(tmp, "flake-shake-results-worker-1") - d2 := filepath.Join(tmp, "flake-shake-results-worker-2") - if err := os.MkdirAll(d1, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(d2, 0755); err != nil { - t.Fatal(err) - } - writeReport(t, d1, "flake-shake-report.json", r1) - writeReport(t, d2, "flake-shake-report.json", r2) - - out := filepath.Join(tmp, "final") - if err := run(filepath.Join(tmp, "flake-shake-results-worker-*/flake-shake-report.json"), out, false); err != nil { - t.Fatalf("run error: %v", err) - } - // verify outputs exist - if _, err := os.Stat(filepath.Join(out, "flake-shake-report.json")); err != nil { - t.Fatalf("missing json report: %v", err) - } - if _, err := os.Stat(filepath.Join(out, "flake-shake-report.html")); err != nil { - t.Fatalf("missing html report: %v", err) - } -} - -func TestGenerateHTMLReportBasic(t *testing.T) { - r := &FlakeShakeReport{ - Gate: "flake-shake", - Iterations: 15, - Tests: []FlakeShakeResult{ - {TestName: "pkg::T1", Package: "pkg", TotalRuns: 10, Passes: 10, Failures: 0, PassRate: 100}, - {TestName: "pkg::T2", Package: "pkg", TotalRuns: 5, Passes: 4, Failures: 1, PassRate: 80}, - }, - GeneratedAt: time.Now(), - } - html := generateHTMLReport(r) - if len(html) == 0 { - t.Fatal("empty html") - } - if !strings.Contains(html, "Flake-Shake Report") { - t.Fatal("missing title") - } -} diff --git a/op-acceptance-tests/cmd/flake-shake-promoter/main.go b/op-acceptance-tests/cmd/flake-shake-promoter/main.go deleted file mode 100644 index 782305bada5..00000000000 --- a/op-acceptance-tests/cmd/flake-shake-promoter/main.go +++ /dev/null @@ -1,1271 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - "path/filepath" - "sort" - "strings" - "time" - - github "github.com/google/go-github/v55/github" // newer version of Go is needed for the latest GitHub API - "golang.org/x/oauth2" - yaml "gopkg.in/yaml.v3" -) - -var logger *log.Logger - -// Constants used across the promoter -const ( - flakeShakeGateID = "flake-shake" - flakeShakeWorkflowName = "scheduled-flake-shake" - flakeShakeReportJobName = "op-acceptance-tests-flake-shake-report" - flakeShakePRTitle = "chore(op-acceptance-tests): flake-shake; test promotions" - flakeShakePRBranchPrefix = "ci/flake-shake-promote/" - flakeShakeLabel = "M-ci" - flakeShakeBotAuthor = "opgitgovernance" - flakeShakeSupersedeDays = 2 // lookback window (days) when closing older PRs as superseded -) - -// CircleCI API models -type pipelineList struct { - Items []pipeline `json:"items"` - NextPageToken string `json:"next_page_token"` -} - -type pipeline struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - Number int `json:"number"` -} - -type workflowList struct { - Items []workflow `json:"items"` - NextPageToken string `json:"next_page_token"` -} - -type workflow struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type jobList struct { - Items []job `json:"items"` - NextPageToken string `json:"next_page_token"` -} - -type job struct { - Name string `json:"name"` - JobNumber int `json:"job_number"` - WebURL string `json:"web_url"` -} - -type artifactsList struct { - Items []artifact `json:"items"` -} - -type artifact struct { - URL string `json:"url"` - Path string `json:"path"` -} - -// Daily summary (as produced in CI job) -type DailySummary struct { - Date string `json:"date"` - Gate string `json:"gate"` - TotalRuns int `json:"total_runs"` - Iterations int `json:"iterations"` - Totals struct { - Stable int `json:"stable"` - Unstable int `json:"unstable"` - } `json:"totals"` - StableTests []struct { - TestName string `json:"test_name"` - Package string `json:"package"` - TotalRuns int `json:"total_runs"` - PassRate float64 `json:"pass_rate"` - } `json:"stable_tests"` - UnstableTests []struct { - TestName string `json:"test_name"` - Package string `json:"package"` - TotalRuns int `json:"total_runs"` - Passes int `json:"passes"` - Failures int `json:"failures"` - PassRate float64 `json:"pass_rate"` - } `json:"unstable_tests"` -} - -// Acceptance tests YAML models -type acceptanceYAML struct { - Gates []gateYAML `yaml:"gates"` -} - -type gateYAML struct { - ID string `yaml:"id"` - Description string `yaml:"description,omitempty"` - Inherits []string `yaml:"inherits,omitempty"` - Tests []testEntry `yaml:"tests,omitempty"` -} - -type testEntry struct { - Name string `yaml:"name,omitempty"` - Package string `yaml:"package"` - Timeout string `yaml:"timeout,omitempty"` - Metadata map[string]interface{} `yaml:"metadata,omitempty"` - Owner string `yaml:"owner,omitempty"` -} - -// Aggregated per test across days -type aggStats struct { - Package string `json:"package"` - TestName string `json:"test_name"` - TotalRuns int `json:"total_runs"` - Passes int `json:"passes"` - Failures int `json:"failures"` - FirstSeenDay string `json:"first_seen_day"` - LastSeenDay string `json:"last_seen_day"` - LastFailureAt *time.Time `json:"last_failure_at,omitempty"` - DaysObserved []string `json:"days_observed"` -} - -type promoteCandidate struct { - Package string `json:"package"` - TestName string `json:"test_name"` - TotalRuns int `json:"total_runs"` - PassRate float64 `json:"pass_rate"` - Timeout string `json:"timeout"` - FirstSeenDay string `json:"first_seen_day"` - Owner string `json:"owner,omitempty"` -} - -// Map tests in flake-shake: key -> (timeout, name) -type testInfo struct { - Timeout string - Name string - Meta map[string]interface{} - Owner string - GateIndex int - TestIndex int -} - -func main() { - opts := parsePromoterFlags() - - logger = log.New(os.Stdout, "[flake-shake-promoter] ", log.LstdFlags) - if opts.verbose { - logger.Printf("Flags: org=%s repo=%s branch=%s workflow=%s report_job=%s days=%d gate=%s min_runs=%d max_failure_rate=%.4f min_age_days=%d require_clean_24h=%t out=%s dry_run=%t", - opts.org, opts.repo, opts.branch, opts.workflowName, opts.reportJobName, opts.daysBack, opts.gateID, opts.minRuns, opts.maxFailureRate, opts.minAgeDays, opts.requireClean24h, opts.outDir, opts.dryRun, - ) - } - - token := requireEnv("CIRCLE_API_TOKEN") - if err := ensureDirExists(opts.outDir); err != nil { - fmt.Fprintf(os.Stderr, "failed to create out dir: %v\n", err) - os.Exit(1) - } - - now := time.Now().UTC() - since := now.AddDate(0, 0, -opts.daysBack) - - client := &http.Client{Timeout: 30 * time.Second} - ctx := &apiCtx{client: client, token: token} - - dailyReports, err := collectReports(ctx, opts.org, opts.repo, opts.branch, opts.workflowName, opts.reportJobName, since, opts.verbose) - if err != nil { - fmt.Fprintf(os.Stderr, "collection failed: %v\n", err) - os.Exit(1) - } - - agg := aggregate(dailyReports) - - logDailyReportSummary(dailyReports, opts.verbose) - - // Load acceptance-tests.yaml - yamlPath := filepath.Join("op-acceptance-tests", "acceptance-tests.yaml") - cfg, err := readAcceptanceYAML(yamlPath) - if err != nil { - fmt.Fprintf(os.Stderr, "failed reading %s: %v\n", yamlPath, err) - os.Exit(1) - } - - // Build indices for flake-shake tests and target gates - flakeTests, flakeGate, gateIndex := buildFlakeTests(&cfg, opts.gateID, yamlPath) - _ = gateIndex - - // Select promotion candidates - candidates, reasons := selectPromotionCandidates(agg, flakeTests, opts.minRuns, opts.maxFailureRate, opts.requireClean24h, opts.minAgeDays, now) - - // Write outputs - if err := writeJSON(filepath.Join(opts.outDir, "aggregate.json"), agg); err != nil { - fmt.Fprintf(os.Stderr, "failed writing aggregate: %v\n", err) - os.Exit(1) - } - sort.Slice(candidates, func(i, j int) bool { - if candidates[i].Package == candidates[j].Package { - return candidates[i].TestName < candidates[j].TestName - } - return candidates[i].Package < candidates[j].Package - }) - if err := writeJSON(filepath.Join(opts.outDir, "promotion-ready.json"), map[string]interface{}{"candidates": candidates, "skipped": reasons}); err != nil { - fmt.Fprintf(os.Stderr, "failed writing promotion-ready: %v\n", err) - os.Exit(1) - } - - if opts.verbose { - fmt.Printf("Promotion candidates: %d\n", len(candidates)) - for _, c := range candidates { - name := c.TestName - if strings.TrimSpace(name) == "" { - name = "(package)" - } - fmt.Printf(" - %s %s (runs=%d pass=%.2f%%)\n", c.Package, name, c.TotalRuns, c.PassRate) - } - } - - // Write metadata for downstream consumers (e.g., Slack) - meta := map[string]interface{}{ - "date": now.Format("2006-01-02"), - "candidates": len(candidates), - "flake_gate_tests": len(flakeGate.Tests), - } - if err := writeJSON(filepath.Join(opts.outDir, "metadata.json"), meta); err != nil { - fmt.Fprintf(os.Stderr, "failed writing metadata: %v\n", err) - os.Exit(1) - } - - // Generate updated YAML (proposal) - updated := computeUpdatedConfig(cfg, opts.gateID, candidates) - - // Write proposed YAML - outYAML := filepath.Join(opts.outDir, "promotion.yaml") - if err := writeYAML(outYAML, &updated); err != nil { - fmt.Fprintf(os.Stderr, "failed writing promotion.yaml: %v\n", err) - os.Exit(1) - } - - // Print short summary - if len(candidates) == 0 { - reason := buildNoCandidatesSummary(agg, flakeTests, opts.minAgeDays, opts.requireClean24h) - _ = os.WriteFile(filepath.Join(opts.outDir, "SUMMARY.txt"), []byte(reason+"\n"), 0o644) - logger.Println(reason) - return - } - var b bytes.Buffer - b.WriteString("Promotion candidates (dry-run):\n") - for _, c := range candidates { - b.WriteString(fmt.Sprintf("- %s %s (runs=%d, pass=%.2f%%)\n", c.Package, c.TestName, c.TotalRuns, c.PassRate)) - } - _ = os.WriteFile(filepath.Join(opts.outDir, "SUMMARY.txt"), b.Bytes(), 0o644) - logger.Print(b.String()) - - if opts.dryRun { - logger.Println("Dry-run enabled; skipping branch creation, file update, and PR creation.") - return - } - - // Prepare updated YAML content for PR by editing only the flake-shake gate in-place to preserve comments - var updatedYAMLBytes []byte - - prBranch := fmt.Sprintf("%s%s", flakeShakePRBranchPrefix, time.Now().UTC().Format("2006-01-02-150405")) - - // Prepare commit message and PR body - title := flakeShakePRTitle - var body bytes.Buffer - body.WriteString("## 🤖 Automated Flake-Shake Test Promotion\n\n") - - // Attempt to resolve the CircleCI report job web URL for artifacts page - reportArtifactsURL := resolveReportArtifactsURL(opts, ctx) - - body.WriteString(fmt.Sprintf("Promoting %d test(s) from gate `"+opts.gateID+"` based on stability criteria.\n\n", len(candidates))) - if reportArtifactsURL != "" { - body.WriteString(fmt.Sprintf("Artifacts: %s\n\n", reportArtifactsURL)) - } - body.WriteString("### Tests Being Promoted\n\n") - body.WriteString("| Test | Package | Total Runs | Pass Rate |\n|---|---|---:|---:|\n") - for _, c := range candidates { - name := c.TestName - if strings.TrimSpace(name) == "" { - name = "(package)" - } - body.WriteString(fmt.Sprintf("| %s | %s | %d | %.2f%% |\n", name, c.Package, c.TotalRuns, c.PassRate)) - } - body.WriteString("\nThis PR was auto-generated by flake-shake promoter.\n") - - // Use GitHub API to create branch, update file, and open PR - ghToken := os.Getenv("GH_TOKEN") - if ghToken == "" { - fmt.Fprintln(os.Stderr, "GH_TOKEN is required for PR creation but not set") - os.Exit(1) - } - ghCtx := context.Background() - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: ghToken}) - tc := oauth2.NewClient(ghCtx, ts) - ghc := github.NewClient(tc) - - if opts.verbose { - logger.Printf("PR: starting creation process (base_branch=%s candidates=%d)", opts.branch, len(candidates)) - } - - // 1) Get base branch ref - baseRef, _, err := ghc.Git.GetRef(ghCtx, opts.org, opts.repo, "refs/heads/"+opts.branch) - if err != nil || baseRef.Object == nil || baseRef.Object.SHA == nil { - fmt.Fprintf(os.Stderr, "failed to get base ref: %v\n", err) - os.Exit(1) - } - if opts.verbose { - logger.Printf("PR: base ref resolved sha=%s", baseRef.GetObject().GetSHA()) - } - - // 2) Create new branch ref - newRef := &github.Reference{ - Ref: github.String("refs/heads/" + prBranch), - Object: &github.GitObject{SHA: baseRef.Object.SHA}, - } - if _, _, err := ghc.Git.CreateRef(ghCtx, opts.org, opts.repo, newRef); err != nil { - fmt.Fprintf(os.Stderr, "failed to create ref: %v\n", err) - os.Exit(1) - } - if opts.verbose { - logger.Printf("PR: created branch %s", prBranch) - } - - // 3) Read current file to fetch SHA (if exists) on base branch - path := yamlPath - var sha *string - var originalYAML []byte - if fileContent, _, resp, err := ghc.Repositories.GetContents(ghCtx, opts.org, opts.repo, path, &github.RepositoryContentGetOptions{Ref: opts.branch}); err == nil && fileContent != nil { - sha = fileContent.SHA - // Retrieve decoded file content via client helper - rawContent, gcErr := fileContent.GetContent() - if gcErr == nil && rawContent != "" { - originalYAML = []byte(rawContent) - } - } else if resp != nil && resp.StatusCode == 404 { - sha = nil - } else if err != nil { - fmt.Fprintf(os.Stderr, "failed to get contents: %v\n", err) - os.Exit(1) - } - - // Build updated YAML by removing promoted tests only from flake-shake gate, preserving comments - promoteKeys := map[string]promoteCandidate{} - for _, c := range candidates { - promoteKeys[keyFor(c.Package, c.TestName)] = c - } - updatedYAMLBytes, err = updateFlakeShakeGateOnly(originalYAML, opts.gateID, promoteKeys) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to update YAML: %v\n", err) - os.Exit(1) - } - - // 4) Update file in new branch - commitMsg := title - if _, _, err = ghc.Repositories.UpdateFile(ghCtx, opts.org, opts.repo, path, &github.RepositoryContentFileOptions{ - Message: github.String(commitMsg), - Content: updatedYAMLBytes, - Branch: github.String(prBranch), - SHA: sha, - }); err != nil { - fmt.Fprintf(os.Stderr, "failed to update file: %v\n", err) - os.Exit(1) - } - if opts.verbose { - logger.Printf("PR: updated file %s on branch %s", path, prBranch) - } - - // 5) Create PR - prReq := &github.NewPullRequest{ - Title: github.String(title), - Head: github.String(prBranch), - Base: github.String(opts.branch), - Body: github.String(body.String()), - } - pr, _, err := ghc.PullRequests.Create(ghCtx, opts.org, opts.repo, prReq) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to create PR: %v\n", err) - os.Exit(1) - } - logger.Printf("PR created: %s (number=%d)", pr.GetHTMLURL(), pr.GetNumber()) - - // Update metadata with PR details for downstream Slack notification - meta["pr_url"] = pr.GetHTMLURL() - meta["pr_number"] = pr.GetNumber() - if err := writeJSON(filepath.Join(opts.outDir, "metadata.json"), meta); err != nil { - fmt.Fprintf(os.Stderr, "failed updating metadata with PR info: %v\n", err) - } - - // 6) Add labels - if _, _, err := ghc.Issues.AddLabelsToIssue(ghCtx, opts.org, opts.repo, pr.GetNumber(), []string{flakeShakeLabel, "A-acceptance-tests"}); err != nil { - fmt.Fprintf(os.Stderr, "failed to add labels: %v\n", err) - } - - // 7) Request reviewers (user and team slug) - if _, _, err := ghc.PullRequests.RequestReviewers(ghCtx, opts.org, opts.repo, pr.GetNumber(), github.ReviewersRequest{ - Reviewers: []string{"scharissis"}, - TeamReviewers: []string{"platforms-team"}, - }); err != nil { - fmt.Fprintf(os.Stderr, "failed to request reviewers: %v\n", err) - } - - // 8) Close any older open flake-shake PRs by this bot as superseded - if err := closeSupersededFlakeShakePRs(ghCtx, ghc, opts.org, opts.repo, pr, title, opts.verbose); err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to close superseded PRs: %v\n", err) - } -} - -// promoterOpts holds command-line options for the promoter tool. -type promoterOpts struct { - org string - repo string - branch string - workflowName string - reportJobName string - daysBack int - gateID string - minRuns int - maxFailureRate float64 - minAgeDays int - outDir string - dryRun bool - requireClean24h bool - verbose bool -} - -func parsePromoterFlags() promoterOpts { - var opts promoterOpts - flag.StringVar(&opts.org, "org", "ethereum-optimism", "GitHub org") - flag.StringVar(&opts.repo, "repo", "optimism", "GitHub repo") - flag.StringVar(&opts.branch, "branch", "develop", "Branch to scan") - flag.StringVar(&opts.workflowName, "workflow", flakeShakeWorkflowName, "Workflow name") - flag.StringVar(&opts.reportJobName, "report-job", flakeShakeReportJobName, "Report job name") - flag.IntVar(&opts.daysBack, "days", 3, "Number of days to aggregate") - flag.StringVar(&opts.gateID, "gate", flakeShakeGateID, "Gate id in acceptance-tests.yaml") - flag.IntVar(&opts.minRuns, "min-runs", 300, "Minimum total runs required") - flag.Float64Var(&opts.maxFailureRate, "max-failure-rate", 0.01, "Maximum allowed failure rate") - flag.IntVar(&opts.minAgeDays, "min-age-days", 2, "Minimum age in days in flake-shake") - flag.StringVar(&opts.outDir, "out", "./promotion-output", "Output directory") - flag.BoolVar(&opts.dryRun, "dry-run", true, "Do not modify repo or open PRs") - flag.BoolVar(&opts.requireClean24h, "require-clean-24h", false, "Require no failures in the last 24 hours") - flag.BoolVar(&opts.verbose, "verbose", false, "Enable verbose debug logging") - flag.Parse() - // Validate interdependent options early to avoid confusing outcomes later - if opts.daysBack < opts.minAgeDays { - fmt.Fprintf(os.Stderr, "invalid flags: --days (%d) must be >= --min-age-days (%d)\n", opts.daysBack, opts.minAgeDays) - os.Exit(2) - } - if opts.requireClean24h && opts.daysBack < 2 { - fmt.Fprintf(os.Stderr, "invalid flags: --days (%d) must be >= 2 when --require-clean-24h is set to ensure >24h coverage\n", opts.daysBack) - os.Exit(2) - } - return opts -} - -func requireEnv(name string) string { - v := os.Getenv(name) - if v == "" { - fmt.Fprintf(os.Stderr, "%s is not set\n", name) - os.Exit(1) - } - return v -} - -func ensureDirExists(dir string) error { - return os.MkdirAll(dir, 0o755) -} - -func logDailyReportSummary(dailyReports map[string]DailySummary, verbose bool) { - if !verbose { - return - } - logger.Printf("Collected %d day(s) of summaries.", len(dailyReports)) - totalTests := 0 - for date, ds := range dailyReports { - n := len(ds.StableTests) + len(ds.UnstableTests) - totalTests += n - logger.Printf(" - %s: %d tests (stable=%d unstable=%d)", date, n, len(ds.StableTests), len(ds.UnstableTests)) - } - logger.Printf("Total tests across days: %d", totalTests) -} - -// buildFlakeTests returns a map of tests in the flake-shake gate and also returns -// the flake gate reference and a gate index map for potential future use. -func buildFlakeTests(cfg *acceptanceYAML, gateID, yamlPath string) (map[string]testInfo, *gateYAML, map[string]*gateYAML) { - flakeGate := findGate(cfg, gateID) - if flakeGate == nil { - fmt.Fprintf(os.Stderr, "gate %s not found in %s\n", gateID, yamlPath) - os.Exit(1) - } - gateIndex := map[string]*gateYAML{} - for i := range cfg.Gates { - gateIndex[cfg.Gates[i].ID] = &cfg.Gates[i] - } - flakeTests := map[string]testInfo{} - for ti, t := range flakeGate.Tests { - key := keyFor(t.Package, t.Name) - // Prefer explicit YAML field owner; fallback to metadata.owner - owner := t.Owner - if owner == "" && t.Metadata != nil { - if v, ok := t.Metadata["owner"]; ok { - owner = fmt.Sprintf("%v", v) - } - } - flakeTests[key] = testInfo{Timeout: t.Timeout, Name: t.Name, Meta: t.Metadata, Owner: owner, GateIndex: indexOfGate(cfg, gateID), TestIndex: ti} - } - return flakeTests, flakeGate, gateIndex -} - -func selectPromotionCandidates(agg map[string]*aggStats, flakeTests map[string]testInfo, minRuns int, maxFailureRate float64, requireClean24h bool, minAgeDays int, now time.Time) ([]promoteCandidate, map[string]string) { - candidates := []promoteCandidate{} - reasons := map[string]string{} - // Identify wildcard package entries (tests with empty name in the flake-shake gate) - wildcardPkgs := map[string]testInfo{} - for k, info := range flakeTests { - if strings.TrimSpace(info.Name) == "" && strings.HasSuffix(k, "::") { - pkg := strings.TrimSuffix(k, "::") - if pkg != "" { - wildcardPkgs[pkg] = info - } - } - } - - // Produce package-level candidates for wildcard entries by aggregating all tests in the package - for pkg, info := range wildcardPkgs { - totalRuns := 0 - totalPasses := 0 - totalFailures := 0 - earliest := "" - var lastFailureAt *time.Time - for _, s := range agg { - if s.Package != pkg { - continue - } - totalRuns += s.TotalRuns - totalPasses += s.Passes - totalFailures += s.Failures - if earliest == "" || (s.FirstSeenDay != "" && s.FirstSeenDay < earliest) { - earliest = s.FirstSeenDay - } - if s.LastFailureAt != nil { - if lastFailureAt == nil || s.LastFailureAt.After(*lastFailureAt) { - lastFailureAt = s.LastFailureAt - } - } - } - if totalRuns == 0 { - reasons[keyFor(pkg, "")] = "no runs observed for package" - continue - } - if totalRuns < minRuns { - reasons[keyFor(pkg, "")] = fmt.Sprintf("insufficient runs: %d < %d (pkg)", totalRuns, minRuns) - continue - } - failureRate := 0.0 - if totalRuns > 0 { - failureRate = float64(totalFailures) / float64(totalRuns) - } - if !requireClean24h { - if failureRate > maxFailureRate { - reasons[keyFor(pkg, "")] = fmt.Sprintf("failure rate %.4f exceeds max %.4f (pkg)", failureRate, maxFailureRate) - continue - } - } - if requireClean24h && lastFailureAt != nil { - if time.Since(*lastFailureAt) < 24*time.Hour { - reasons[keyFor(pkg, "")] = "failure within last 24h (pkg)" - continue - } - } - if earliest == "" { - reasons[keyFor(pkg, "")] = "no age information (pkg)" - continue - } - firstDay, _ := time.Parse("2006-01-02", earliest) - daysInGate := int(now.Sub(firstDay).Hours()/24) + 1 - if daysInGate < minAgeDays { - reasons[keyFor(pkg, "")] = fmt.Sprintf("min age %dd not met (have %dd) (pkg)", minAgeDays, daysInGate) - continue - } - passRate := 0.0 - if totalRuns > 0 { - passRate = float64(totalPasses) / float64(totalRuns) - } - owner := info.Owner - if owner == "" { - if info.Meta != nil { - if v, ok := info.Meta["owner"]; ok { - owner = fmt.Sprintf("%v", v) - } - } - } - candidates = append(candidates, promoteCandidate{ - Package: pkg, - TestName: "", - TotalRuns: totalRuns, - PassRate: passRate * 100.0, - Timeout: info.Timeout, - FirstSeenDay: earliest, - Owner: owner, - }) - } - for key, s := range agg { - // Skip per-test candidates for any package that is handled via wildcard aggregation - if _, hasWildcard := wildcardPkgs[s.Package]; hasWildcard { - continue - } - info, ok := flakeTests[key] - if !ok { - // Support wildcard package entries in the flake-shake gate where name is omitted. - // Treat a gate entry with empty name as a wildcard that matches all tests in that package. - if wi, wok := flakeTests[keyFor(s.Package, "")]; wok { - info = wi - } else { - continue - } - } - if s.TotalRuns < minRuns { - reasons[key] = fmt.Sprintf("insufficient runs: %d < %d", s.TotalRuns, minRuns) - continue - } - failureRate := 0.0 - if s.TotalRuns > 0 { - failureRate = float64(s.Failures) / float64(s.TotalRuns) - } - if !requireClean24h { - if failureRate > maxFailureRate { - reasons[key] = fmt.Sprintf("failure rate %.4f exceeds max %.4f", failureRate, maxFailureRate) - continue - } - } - if requireClean24h && s.LastFailureAt != nil { - if time.Since(*s.LastFailureAt) < 24*time.Hour { - reasons[key] = "failure within last 24h" - continue - } - } - if s.FirstSeenDay == "" { - reasons[key] = "no age information" - continue - } - firstDay, _ := time.Parse("2006-01-02", s.FirstSeenDay) - daysInGate := int(now.Sub(firstDay).Hours()/24) + 1 - if daysInGate < minAgeDays { - reasons[key] = fmt.Sprintf("min age %dd not met (have %dd)", minAgeDays, daysInGate) - continue - } - passRate := 0.0 - if s.TotalRuns > 0 { - passRate = float64(s.Passes) / float64(s.TotalRuns) - } - owner := info.Owner - if owner == "" { - if info.Meta != nil { - if v, ok := info.Meta["owner"]; ok { - owner = fmt.Sprintf("%v", v) - } - } - } - candidates = append(candidates, promoteCandidate{ - Package: s.Package, - TestName: s.TestName, - TotalRuns: s.TotalRuns, - PassRate: passRate * 100.0, - Timeout: info.Timeout, - FirstSeenDay: s.FirstSeenDay, - Owner: owner, - }) - } - return candidates, reasons -} - -func computeUpdatedConfig(cfg acceptanceYAML, gateID string, candidates []promoteCandidate) acceptanceYAML { - updated := cfg - flakeIdx := indexOfGate(&updated, gateID) - if flakeIdx < 0 { - fmt.Fprintf(os.Stderr, "gate %s not found when updating\n", gateID) - os.Exit(1) - } - promoteKeys := map[string]promoteCandidate{} - for _, c := range candidates { - promoteKeys[keyFor(c.Package, c.TestName)] = c - } - newFlakeTests := make([]testEntry, 0, len(updated.Gates[flakeIdx].Tests)) - for _, t := range updated.Gates[flakeIdx].Tests { - k := keyFor(t.Package, t.Name) - if _, ok := promoteKeys[k]; !ok { - newFlakeTests = append(newFlakeTests, t) - } - } - updated.Gates[flakeIdx].Tests = newFlakeTests - return updated -} - -func buildNoCandidatesSummary(agg map[string]*aggStats, flakeTests map[string]testInfo, minAgeDays int, requireClean24h bool) string { - earliest := "" - totalRuns := 0 - totalPass := 0 - totalFail := 0 - daySet := map[string]struct{}{} - for key, s := range agg { - if _, ok := flakeTests[key]; !ok { - continue - } - totalRuns += s.TotalRuns - totalPass += s.Passes - totalFail += s.Failures - if earliest == "" || (s.FirstSeenDay != "" && s.FirstSeenDay < earliest) { - earliest = s.FirstSeenDay - } - for _, d := range s.DaysObserved { - daySet[d] = struct{}{} - } - } - daysObserved := len(daySet) - return fmt.Sprintf( - "No promotion candidates. Reason: min_age_days=%d; earliest_observation=%s; days_observed=%d; require_clean_24h=%t; total_runs=%d; passes=%d; failures=%d.", - minAgeDays, earliest, daysObserved, requireClean24h, totalRuns, totalPass, totalFail, - ) -} - -// HTTP helper context -type apiCtx struct { - client *http.Client - token string -} - -func (c *apiCtx) getJSON(u string, v interface{}) error { - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return err - } - req.Header.Set("Circle-Token", c.token) - req.Header.Set("Accept", "application/json") - resp, err := c.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("GET %s: status %d body=%s", u, resp.StatusCode, string(body)) - } - dec := json.NewDecoder(resp.Body) - return dec.Decode(v) -} - -func (c *apiCtx) getBytes(u string) ([]byte, error) { - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return nil, err - } - req.Header.Set("Circle-Token", c.token) - req.Header.Set("Accept", "application/json") - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("GET %s: status %d body=%s", u, resp.StatusCode, string(body)) - } - return io.ReadAll(resp.Body) -} - -// collectReports scans CircleCI pipelines for the given GitHub repo/branch, -// locates the specified workflow and report job, and downloads/merges the -// daily-summary.json artifacts into a map keyed by date (YYYY-MM-DD). -// Only runs created at or after 'since' are considered. When multiple -// summaries exist for the same day, totals are summed and test lists merged. -func collectReports(ctx *apiCtx, org, repo, branch, workflowName, reportJobName string, since time.Time, verbose bool) (map[string]DailySummary, error) { - dailyByDay := map[string]DailySummary{} - - basePipelines := fmt.Sprintf("https://circleci.com/api/v2/project/gh/%s/%s/pipeline?branch=%s", url.PathEscape(org), url.PathEscape(repo), url.QueryEscape(branch)) - pageURL := basePipelines - - for { - pl, nextToken, err := getPipelinesPage(ctx, pageURL) - if err != nil { - return nil, err - } - if verbose { - logger.Printf("Scanning pipelines page: %s", pageURL) - } - - stop, err := processPipelines(ctx, pl, org, repo, workflowName, reportJobName, since, verbose, dailyByDay) - if err != nil { - return nil, err - } - if stop { - break - } - if nextToken == "" { - break - } - pageURL = basePipelines + "&page-token=" + url.QueryEscape(nextToken) - } - return dailyByDay, nil -} - -// getPipelinesPage fetches a page of pipelines and returns the list along with the next page token. -func getPipelinesPage(ctx *apiCtx, pageURL string) (pipelineList, string, error) { - var pl pipelineList - if err := ctx.getJSON(pageURL, &pl); err != nil { - return pipelineList{}, "", err - } - return pl, pl.NextPageToken, nil -} - -// processPipelines iterates pipelines, filters by date/window, and merges daily summaries. -// It returns stop=true when it encounters pipelines older than the provided 'since' time. -func processPipelines(ctx *apiCtx, pl pipelineList, org, repo, workflowName, reportJobName string, since time.Time, verbose bool, dailyByDay map[string]DailySummary) (bool, error) { - for _, p := range pl.Items { - if verbose { - logger.Printf(" pipeline %s created_at=%s", p.ID, p.CreatedAt.Format(time.RFC3339)) - } - if p.CreatedAt.Before(since) { - return true, nil - } - - wfl, err := listWorkflows(ctx, p.ID) - if err != nil { - return false, err - } - for _, w := range wfl.Items { - if w.Name != workflowName { - continue - } - jl, err := listJobs(ctx, w.ID) - if err != nil { - return false, err - } - for _, j := range jl.Items { - if j.Name != reportJobName { - continue - } - al, err := listArtifacts(ctx, org, repo, j.JobNumber, verbose) - if err != nil { - return false, err - } - dailyURL := findDailySummaryArtifactURL(al) - if dailyURL == "" { - continue - } - if err := loadAndMergeDailySummary(ctx, dailyURL, dailyByDay, verbose); err != nil { - return false, err - } - } - } - } - return false, nil -} - -func listWorkflows(ctx *apiCtx, pipelineID string) (workflowList, error) { - wfURL := fmt.Sprintf("https://circleci.com/api/v2/pipeline/%s/workflow", pipelineID) - var wfl workflowList - if err := ctx.getJSON(wfURL, &wfl); err != nil { - return workflowList{}, err - } - return wfl, nil -} - -func listJobs(ctx *apiCtx, workflowID string) (jobList, error) { - jobsURL := fmt.Sprintf("https://circleci.com/api/v2/workflow/%s/job", workflowID) - var jl jobList - if err := ctx.getJSON(jobsURL, &jl); err != nil { - return jobList{}, err - } - return jl, nil -} - -// closeSupersededFlakeShakePRs finds any open flake-shake promotion PRs created by the bot -// and closes them with a comment pointing to the newly created PR. -func closeSupersededFlakeShakePRs(ctx context.Context, ghc *github.Client, org, repo string, newPR *github.PullRequest, title string, verbose bool) error { - // Search open PRs in this repo that match our title and bot author - // Using Issues.ListByRepo with filters - opt := &github.IssueListByRepoOptions{ - State: "open", - Labels: []string{flakeShakeLabel}, - Since: time.Now().AddDate(0, 0, -flakeShakeSupersedeDays), - ListOptions: github.ListOptions{PerPage: 50}, - } - for { - issues, resp, err := ghc.Issues.ListByRepo(ctx, org, repo, opt) - if err != nil { - return err - } - for _, is := range issues { - if is.IsPullRequest() && is.GetNumber() != newPR.GetNumber() { - // Check title contains our flake-shake marker; be robust to minor variations - // Use the provided title to derive a stable prefix (before the first ';') for matching - titlePrefix := strings.TrimSpace(strings.TrimSuffix(title, "; test promotions")) - if strings.Contains(strings.ToLower(is.GetTitle()), strings.ToLower(flakeShakeGateID)) && strings.Contains(is.GetTitle(), titlePrefix) { - // Author check - if is.User != nil && is.User.GetLogin() != flakeShakeBotAuthor { - continue - } - // Comment and close - msg := fmt.Sprintf("superseded by #%d", newPR.GetNumber()) - _, _, _ = ghc.Issues.CreateComment(ctx, org, repo, is.GetNumber(), &github.IssueComment{Body: github.String(msg)}) - state := "closed" - _, _, _ = ghc.PullRequests.Edit(ctx, org, repo, is.GetNumber(), &github.PullRequest{State: &state}) - if verbose { - logger.Printf("Closed superseded PR #%d: %s", is.GetNumber(), is.GetTitle()) - } - } - } - } - if resp.NextPage == 0 { - break - } - opt.Page = resp.NextPage - } - return nil -} - -// resolveReportArtifactsURL attempts to find the web URL to the report job's artifacts page -// by scanning recent pipelines/workflows for the configured workflow/report job names. -// Returns an empty string if not found. -func resolveReportArtifactsURL(opts promoterOpts, ctx *apiCtx) string { - // Scan the latest pipelines on the given branch; reuse collectReports traversal but short-circuit on first match - basePipelines := fmt.Sprintf("https://circleci.com/api/v2/project/gh/%s/%s/pipeline?branch=%s", url.PathEscape(opts.org), url.PathEscape(opts.repo), url.QueryEscape(opts.branch)) - pageURL := basePipelines - now := time.Now().UTC() - since := now.AddDate(0, 0, -opts.daysBack) - for { - pl, nextToken, err := getPipelinesPage(ctx, pageURL) - if err != nil { - return "" - } - for _, p := range pl.Items { - if p.CreatedAt.Before(since) { - return "" - } - wfl, err := listWorkflows(ctx, p.ID) - if err != nil { - return "" - } - for _, w := range wfl.Items { - if w.Name != opts.workflowName { - continue - } - jl, err := listJobs(ctx, w.ID) - if err != nil { - return "" - } - for _, j := range jl.Items { - if j.Name != opts.reportJobName { - continue - } - // Prefer constructing the app.circleci.com artifacts URL from pipeline number + workflow id + job number - if p.Number != 0 && j.JobNumber != 0 { - u := fmt.Sprintf("https://app.circleci.com/pipelines/github/%s/%s/%d/workflows/%s/jobs/%d/artifacts", opts.org, opts.repo, p.Number, w.ID, j.JobNumber) - return u - } - return "" - } - } - } - if nextToken == "" { - break - } - pageURL = basePipelines + "&page-token=" + url.QueryEscape(nextToken) - } - return "" -} - -func listArtifacts(ctx *apiCtx, org, repo string, jobNumber int, verbose bool) (artifactsList, error) { - artsURL := fmt.Sprintf("https://circleci.com/api/v2/project/gh/%s/%s/%d/artifacts", url.PathEscape(org), url.PathEscape(repo), jobNumber) - var al artifactsList - if err := ctx.getJSON(artsURL, &al); err != nil { - return artifactsList{}, err - } - if verbose { - logger.Printf(" job %d artifacts: %d", jobNumber, len(al.Items)) - for _, a := range al.Items { - logger.Printf(" - %s", a.Path) - } - } - return al, nil -} - -func findDailySummaryArtifactURL(al artifactsList) string { - for _, a := range al.Items { - // Accept any artifact path that ends with the filename, regardless of destination prefix - if strings.HasSuffix(a.Path, "daily-summary.json") { - return a.URL - } - } - return "" -} - -func loadAndMergeDailySummary(ctx *apiCtx, dailyURL string, dailyByDay map[string]DailySummary, verbose bool) error { - data, err := ctx.getBytes(dailyURL) - if err != nil { - return err - } - var ds DailySummary - if json.Unmarshal(data, &ds) != nil || ds.Date == "" { - return nil - } - if prev, seen := dailyByDay[ds.Date]; !seen { - dailyByDay[ds.Date] = ds - if verbose { - logger.Printf(" loaded daily summary for %s (runs=%d iterations=%d)", ds.Date, ds.TotalRuns, ds.Iterations) - } - return nil - } else { - merged := prev - merged.TotalRuns += ds.TotalRuns - merged.Iterations += ds.Iterations - merged.StableTests = append(merged.StableTests, ds.StableTests...) - merged.UnstableTests = append(merged.UnstableTests, ds.UnstableTests...) - dailyByDay[ds.Date] = merged - if verbose { - logger.Printf(" merged another run for %s (+runs=%d +iters=%d) now runs=%d iters=%d", ds.Date, ds.TotalRuns, ds.Iterations, merged.TotalRuns, merged.Iterations) - } - return nil - } -} - -// aggregate reduces per-day test summaries into a single map keyed by test, -// summing runs/passes/failures and tracking which days each test appeared. -// It also computes first/last seen day boundaries for each test. -func aggregate(daily map[string]DailySummary) map[string]*aggStats { - result := map[string]*aggStats{} - // Collect all days - days := make([]string, 0, len(daily)) - for d := range daily { - days = append(days, d) - } - sort.Strings(days) - - for _, day := range days { - if ds, ok := daily[day]; ok { - for _, t := range ds.StableTests { - k := keyFor(t.Package, t.TestName) - s := ensureAgg(result, k, t.Package, t.TestName, day) - s.TotalRuns += t.TotalRuns - s.Passes += t.TotalRuns - } - for _, t := range ds.UnstableTests { - k := keyFor(t.Package, t.TestName) - s := ensureAgg(result, k, t.Package, t.TestName, day) - s.TotalRuns += t.TotalRuns - s.Passes += t.Passes - s.Failures += t.Failures - approx := parseDayEnd(day) - if s.LastFailureAt == nil || approx.After(*s.LastFailureAt) { - s.LastFailureAt = &approx - } - } - } - } - return result -} - -// ensureAgg returns the aggregated stats bucket for the given test key, creating it -// if it does not exist. It also records the provided day in DaysObserved (without -// duplicates) and updates FirstSeenDay/LastSeenDay bounds accordingly. -func ensureAgg(m map[string]*aggStats, key, pkg, name, day string) *aggStats { - s, ok := m[key] - if !ok { - s = &aggStats{Package: pkg, TestName: name, DaysObserved: []string{}, FirstSeenDay: day, LastSeenDay: day} - m[key] = s - } - // Append day if new - found := false - for _, d := range s.DaysObserved { - if d == day { - found = true - break - } - } - if !found { - s.DaysObserved = append(s.DaysObserved, day) - if s.FirstSeenDay == "" || day < s.FirstSeenDay { - s.FirstSeenDay = day - } - if s.LastSeenDay == "" || day > s.LastSeenDay { - s.LastSeenDay = day - } - } - return s -} - -// parseDayEnd returns the exclusive end-of-day bound for the given date -// (YYYY-MM-DD) in UTC. This is the start of the next day, suitable for -// half-open intervals: [start, end). -func parseDayEnd(day string) time.Time { - t, err := time.Parse("2006-01-02", day) - if err != nil { - return time.Now().UTC() - } - return t.UTC().Add(24 * time.Hour) -} - -func keyFor(pkg, name string) string { - return pkg + "::" + strings.TrimSpace(name) -} - -func readAcceptanceYAML(path string) (acceptanceYAML, error) { - var acc acceptanceYAML - data, err := os.ReadFile(path) - if err != nil { - return acc, err - } - if err := yaml.Unmarshal(data, &acc); err != nil { - return acc, err - } - if len(acc.Gates) == 0 { - return acc, errors.New("no gates found") - } - return acc, nil -} - -func findGate(acc *acceptanceYAML, id string) *gateYAML { - for i, gate := range acc.Gates { - if gate.ID == id { - return &acc.Gates[i] - } - } - return nil -} - -func indexOfGate(acc *acceptanceYAML, id string) int { - for i := range acc.Gates { - if acc.Gates[i].ID == id { - return i - } - } - return -1 -} - -func writeJSON(path string, v interface{}) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - enc := json.NewEncoder(f) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - return enc.Encode(v) -} - -func writeYAML(path string, v interface{}) error { - data, err := yaml.Marshal(v) - if err != nil { - return err - } - // Normalize line endings - data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) - return os.WriteFile(path, data, 0o644) -} - -// updateFlakeShakeGateOnly updates only the flake-shake gate tests list in the original YAML bytes, -// preserving all comments and formatting elsewhere. It removes any test entries matching promoteKeys. -func updateFlakeShakeGateOnly(original []byte, gateID string, promoteKeys map[string]promoteCandidate) ([]byte, error) { - if len(original) == 0 { - // Fallback to structured marshal if original missing (should not happen in CI) - return yaml.Marshal(nil) - } - lines := strings.Split(string(original), "\n") - var out []string - inGates := false - inFlake := false - indentGate := "" - indentTests := "" - // simple state machine: copy all lines except tests under flake-shake that match promoteKeys - for i := 0; i < len(lines); i++ { - line := lines[i] - trimmed := strings.TrimSpace(line) - // Detect top-level 'gates:' - if strings.HasPrefix(trimmed, "gates:") { - inGates = true - out = append(out, line) - continue - } - if inGates && strings.HasPrefix(trimmed, "- id:") { - // Entering a gate block - // Determine indentation - indentGate = line[:len(line)-len(strings.TrimLeft(line, " \t"))] - // Gate id value - id := strings.TrimSpace(strings.TrimPrefix(trimmed, "- id:")) - id = strings.Trim(id, "\"')") - inFlake = (id == gateID) - out = append(out, line) - continue - } - if !inFlake { - out = append(out, line) - continue - } - // Within flake-shake gate only - if strings.HasPrefix(strings.TrimSpace(line), "tests:") && indentTests == "" { - // Capture tests indent from next line if present - out = append(out, line) - // From here, filter list items until we leave tests list (deduce by indentation decrease or new key at gate level) - pos := i + 1 - for ; pos < len(lines); pos++ { - cur := lines[pos] - curTrim := strings.TrimSpace(cur) - if curTrim == "" { - out = append(out, cur) - continue - } - // end of this gate block if next key aligns to indentGate and not list item - if !strings.HasPrefix(cur, indentGate+" ") || strings.HasPrefix(strings.TrimSpace(cur), "- id:") { - // set i so that outer loop reprocesses this line next - i = pos - 1 - break - } - // If this is a list item under tests: starts with indentGate + two spaces + two more (tests indent) + '- ' - // We can't rely on exact spaces; detect package line to gather block - if strings.HasPrefix(strings.TrimSpace(cur), "- package:") { - // start of a test block; buffer until next '- package:' or gate-level boundary - block := []string{cur} - pkg := strings.TrimSpace(strings.TrimPrefix(curTrim, "- package:")) - name := "" - j := pos + 1 - for ; j < len(lines); j++ { - nt := strings.TrimSpace(lines[j]) - if nt == "" { - block = append(block, lines[j]) - continue - } - // next test or end of tests - if strings.HasPrefix(nt, "- package:") || (!strings.HasPrefix(lines[j], indentGate+" ")) { - j-- - break - } - block = append(block, lines[j]) - if strings.HasPrefix(nt, "name:") { - name = strings.TrimSpace(strings.TrimPrefix(nt, "name:")) - } - } - // Decide keep or drop - k := keyFor(pkg, name) - if _, toPromote := promoteKeys[k]; !toPromote { - out = append(out, block...) - } - pos = j - continue - } - // Non-test line under tests; keep - out = append(out, cur) - } - continue - } - out = append(out, line) - } - return []byte(strings.Join(out, "\n")), nil -} diff --git a/op-acceptance-tests/cmd/flake-shake-promoter/main_test.go b/op-acceptance-tests/cmd/flake-shake-promoter/main_test.go deleted file mode 100644 index 6e2cb769885..00000000000 --- a/op-acceptance-tests/cmd/flake-shake-promoter/main_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "reflect" - "slices" - "sort" - "testing" - "time" -) - -func TestKeyFor(t *testing.T) { - if got := keyFor("pkg/a", ""); got != "pkg/a::" { - t.Fatalf("empty name => got %q", got) - } - if got := keyFor("pkg/a", " TestFoo "); got != "pkg/a::TestFoo" { - t.Fatalf("trim => got %q", got) - } -} - -func TestParseDayEnd(t *testing.T) { - end := parseDayEnd("2025-01-02") - want := time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC) - if !end.Equal(want) { - t.Fatalf("end != start-of-next-day: got %v want %v", end, want) - } -} - -func TestEnsureAggAndAggregate(t *testing.T) { - // ensureAgg behavior - m := map[string]*aggStats{} - s := ensureAgg(m, "pkg::T1", "pkg", "T1", "2025-01-01") - if s.FirstSeenDay != "2025-01-01" || s.LastSeenDay != "2025-01-01" { - t.Fatalf("first/last not set") - } - s2 := ensureAgg(m, "pkg::T1", "pkg", "T1", "2025-01-02") - if s2 != s { - t.Fatalf("ensureAgg did not return same pointer for same key") - } - if !slices.Contains(s.DaysObserved, "2025-01-01") || !slices.Contains(s.DaysObserved, "2025-01-02") { - t.Fatalf("days observed missing: %v", s.DaysObserved) - } - - // aggregate behavior across days - day1 := DailySummary{Date: "2025-01-01"} - day1.UnstableTests = append(day1.UnstableTests, struct { - TestName string `json:"test_name"` - Package string `json:"package"` - TotalRuns int `json:"total_runs"` - Passes int `json:"passes"` - Failures int `json:"failures"` - PassRate float64 `json:"pass_rate"` - }{TestName: "T1", Package: "pkg", TotalRuns: 10, Passes: 10, Failures: 0}) - - day2 := DailySummary{Date: "2025-01-02"} - day2.UnstableTests = append(day2.UnstableTests, struct { - TestName string `json:"test_name"` - Package string `json:"package"` - TotalRuns int `json:"total_runs"` - Passes int `json:"passes"` - Failures int `json:"failures"` - PassRate float64 `json:"pass_rate"` - }{TestName: "T1", Package: "pkg", TotalRuns: 5, Passes: 4, Failures: 1}) - - agg := aggregate(map[string]DailySummary{ - day1.Date: day1, - day2.Date: day2, - }) - a := agg["pkg::T1"] - if a == nil || a.TotalRuns != 15 || a.Passes != 14 || a.Failures != 1 { - t.Fatalf("bad aggregate: %+v", a) - } - // DaysObserved should be sorted and include both - wantDays := []string{"2025-01-01", "2025-01-02"} - gotDays := append([]string(nil), a.DaysObserved...) - sort.Strings(gotDays) - if !reflect.DeepEqual(gotDays, wantDays) { - t.Fatalf("days mismatch: got %v want %v", gotDays, wantDays) - } -} - -func TestSelectPromotionCandidates(t *testing.T) { - now := time.Date(2025, 1, 10, 0, 0, 0, 0, time.UTC) - flake := map[string]testInfo{"pkg::T1": {Timeout: "1m"}} - agg := map[string]*aggStats{ - "pkg::T1": { - Package: "pkg", - TestName: "T1", - TotalRuns: 100, - Passes: 100, - Failures: 0, - FirstSeenDay: "2025-01-01", - DaysObserved: []string{"2025-01-01", "2025-01-02"}, - }, - } - cands, reasons := selectPromotionCandidates(agg, flake, 50, 0.01, true, 3, now) - if len(reasons) != 0 { - t.Fatalf("unexpected reasons: %v", reasons) - } - if len(cands) != 1 || cands[0].Package != "pkg" || cands[0].TestName != "T1" { - t.Fatalf("unexpected candidates: %+v", cands) - } -} - -func TestComputeUpdatedConfig(t *testing.T) { - cfg := acceptanceYAML{Gates: []gateYAML{{ID: "flake-shake", Tests: []testEntry{{Package: "pkg", Name: "T1"}, {Package: "pkg", Name: "T2"}}}}} - cands := []promoteCandidate{{Package: "pkg", TestName: "T1"}} - updated := computeUpdatedConfig(cfg, "flake-shake", cands) - tests := updated.Gates[0].Tests - if len(tests) != 1 || tests[0].Name != "T2" { - t.Fatalf("expected only T2 to remain, got %+v", tests) - } -} diff --git a/op-acceptance-tests/gates/base.txt b/op-acceptance-tests/gates/base.txt new file mode 100644 index 00000000000..6f73a09ba86 --- /dev/null +++ b/op-acceptance-tests/gates/base.txt @@ -0,0 +1,12 @@ +./op-acceptance-tests/tests/base +./op-acceptance-tests/tests/base/deposit +./op-acceptance-tests/tests/base/chain +./op-acceptance-tests/tests/ecotone +./op-acceptance-tests/tests/fjord +./op-acceptance-tests/tests/isthmus +./op-acceptance-tests/tests/isthmus/operator_fee +./op-acceptance-tests/tests/isthmus/withdrawal_root +./op-acceptance-tests/tests/isthmus/erc20_bridge +./op-acceptance-tests/tests/isthmus/pectra +./op-acceptance-tests/tests/jovian/bpo2 +./op-acceptance-tests/tests/jovian/pectra diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index eea9131ae38..8f5839e531d 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -1,52 +1,18 @@ REPO_ROOT := `realpath ..` # path to the root of the optimism monorepo -ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.10.2") -DOCKER_REGISTRY := env_var_or_default("DOCKER_REGISTRY", "us-docker.pkg.dev/oplabs-tools-artifacts/images") -ACCEPTOR_IMAGE := env_var_or_default("ACCEPTOR_IMAGE", DOCKER_REGISTRY + "/op-acceptor:" + ACCEPTOR_VERSION) # Default recipe - runs acceptance tests default: - @just acceptance-test base + @just acceptance-test -jovian: - @just acceptance-test jovian - -interop: - @just acceptance-test interop - -cgt: - @just acceptance-test cgt - -acceptance-test-all: - @just _acceptance-test base all - -# Run acceptance tests with mise-managed binary -# Usage: just acceptance-test [gate] -# Examples: -# just acceptance-test base # In-process with specific gate -# just acceptance-test-all # In-process all-tests mode -acceptance-test gate="base": - @just _acceptance-test "{{gate}}" gate - -_acceptance-test gate mode: +# Run acceptance tests, optionally filtered by a gate (reads packages from gates/.txt) +acceptance-test gate="": #!/usr/bin/env bash set -euo pipefail - - MODE="{{mode}}" - - if [[ "$MODE" != "gate" && "$MODE" != "all" ]]; then - echo "error: invalid mode '$MODE' (expected gate|all)." >&2 - exit 1 - fi - - if [[ "$MODE" == "gate" && "{{gate}}" == "" ]]; then - echo "error: gate must be non-empty for gate mode; use 'just acceptance-test-all' for all tests." >&2 - exit 1 - fi - - if [[ "$MODE" == "all" ]]; then - echo -e "DEVNET: in-memory, MODE: all tests\n" + GATE="{{gate}}" + if [[ -n "$GATE" ]]; then + echo -e "DEVNET: in-memory, GATE: $GATE\n" else - echo -e "DEVNET: in-memory, GATE: {{gate}}\n" + echo -e "DEVNET: in-memory, MODE: all tests\n" fi # Build dependencies for in-process mode if not in CI. @@ -77,73 +43,92 @@ _acceptance-test gate mode: just build-rust-debug fi - cd {{REPO_ROOT}}/op-acceptance-tests + LOG_LEVEL="$(echo "${LOG_LEVEL:-info}" | grep -E '^(debug|info|warn|error)$' || echo 'info')" + echo "LOG_LEVEL: $LOG_LEVEL" - if ! command -v mise >/dev/null; then - echo "error: mise is required for acceptance-test runs." >&2 - exit 1 + CPU_COUNT="" + if command -v nproc >/dev/null 2>&1; then + CPU_COUNT="$(nproc)" + elif command -v getconf >/dev/null 2>&1; then + CPU_COUNT="$(getconf _NPROCESSORS_ONLN)" + elif command -v sysctl >/dev/null 2>&1; then + CPU_COUNT="$(sysctl -n hw.ncpu)" + fi + if [[ -z "$CPU_COUNT" || "$CPU_COUNT" -lt 1 ]]; then + CPU_COUNT=1 + fi + + DEFAULT_JOBS="$CPU_COUNT" + DEFAULT_TEST_PARALLEL="$(( CPU_COUNT / 2 ))" + if [[ "$DEFAULT_TEST_PARALLEL" -lt 1 ]]; then + DEFAULT_TEST_PARALLEL=1 fi - if ! mise install op-acceptor; then - echo "error: failed to install op-acceptor with mise." >&2 + JOBS="${ACCEPTANCE_TEST_JOBS:-$DEFAULT_JOBS}" + TEST_PARALLEL="${ACCEPTANCE_TEST_PARALLEL:-$DEFAULT_TEST_PARALLEL}" + TEST_TIMEOUT="${ACCEPTANCE_TEST_TIMEOUT:-2h}" + echo "CPU COUNT: $CPU_COUNT" + echo "PACKAGE JOBS: $JOBS" + echo "TEST PARALLELISM: $TEST_PARALLEL" + echo "PACKAGE TIMEOUT: $TEST_TIMEOUT" + + cd {{REPO_ROOT}} + if ! command -v gotestsum >/dev/null; then + echo "error: gotestsum is required for acceptance-test runs." >&2 exit 1 fi - BINARY_PATH=$(mise which op-acceptor) - echo "Using mise-managed binary: $BINARY_PATH" - LOG_LEVEL="$(echo "${LOG_LEVEL:-info}" | grep -E '^(debug|info|warn|error)$' || echo 'info')" - echo "LOG_LEVEL: $LOG_LEVEL" + LOG_ROOT="{{REPO_ROOT}}/op-acceptance-tests/logs" + RESULTS_DIR="{{REPO_ROOT}}/op-acceptance-tests/results" + mkdir -p "$LOG_ROOT" "$RESULTS_DIR" + LOG_DIR="$LOG_ROOT/testrun-$(date -u +%Y%m%d-%H%M%S)" + mkdir -p "$LOG_DIR/failed" + rm -f "$RESULTS_DIR/results.xml" - if [[ "$MODE" == "all" ]]; then - CMD_ARGS=( - "$BINARY_PATH" - "--orchestrator" "sysgo" - "--testdir" "{{REPO_ROOT}}/op-acceptance-tests/..." - "--validators" "./acceptance-tests.yaml" - "--log.level" "${LOG_LEVEL}" - "--exclude-gates" "flake-shake" - "--allow-skips" - "--timeout" "120m" - "--show-progress" - ) + FLAKY_REPORT="$LOG_DIR/flaky-tests.txt" + if command -v rg >/dev/null 2>&1; then + rg -n "MarkFlaky\\(" ./op-acceptance-tests/tests > "$FLAKY_REPORT" || true else - CMD_ARGS=( - "$BINARY_PATH" - "--orchestrator" "sysgo" - "--gate" "{{gate}}" - "--testdir" "{{REPO_ROOT}}" - "--validators" "./acceptance-tests.yaml" - "--log.level" "${LOG_LEVEL}" - "--allow-skips" - "--show-progress" - ) - - if [[ "{{gate}}" != "flake-shake" ]]; then - CMD_ARGS+=("--exclude-gates" "flake-shake") + grep -RIn "MarkFlaky(" ./op-acceptance-tests/tests > "$FLAKY_REPORT" || true + fi + + # Determine which packages to test + if [[ -n "$GATE" ]]; then + GATE_FILE="{{REPO_ROOT}}/op-acceptance-tests/gates/${GATE}.txt" + if [[ ! -f "$GATE_FILE" ]]; then + echo "error: gate file not found: $GATE_FILE" >&2 + exit 1 fi + # Read non-empty, non-comment lines from the gate file + TEST_PACKAGES=() + while IFS= read -r line; do + line="${line%%#*}" # strip comments + line="${line// /}" # strip whitespace + [[ -z "$line" ]] && continue + TEST_PACKAGES+=("$line") + done < "$GATE_FILE" + echo "Gate '$GATE': ${#TEST_PACKAGES[@]} packages" + else + TEST_PACKAGES=("./op-acceptance-tests/tests/...") fi - "${CMD_ARGS[@]}" + set +e + LOG_LEVEL="${LOG_LEVEL}" {{REPO_ROOT}}/ops/scripts/gotestsum-split.sh \ + --format testname \ + --junitfile "$RESULTS_DIR/results.xml" \ + --jsonfile "$LOG_DIR/raw_go_events.log" \ + -- \ + -count=1 \ + -p "${JOBS}" \ + -parallel "${TEST_PARALLEL}" \ + -timeout "${TEST_TIMEOUT}" \ + "${TEST_PACKAGES[@]}" \ + 2>&1 | tee "$LOG_DIR/all.log" + TEST_STATUS=${PIPESTATUS[0]} + set -e + + cp "$LOG_DIR/all.log" "$LOG_DIR/summary.log" + exit "$TEST_STATUS" clean: rm -rf tests/interop/loadtest/artifacts - - -# Build, vet, lint and test Go code in ./cmd -cmd-check: - #!/usr/bin/env bash - set -euo pipefail - - cd {{REPO_ROOT}}/op-acceptance-tests - - echo "Downloading Go modules..." - go mod download - - echo "Building ./cmd/..." - go build ./cmd/... - - echo "Running go vet on ./cmd/..." - go vet ./cmd/... - - echo "Running unit tests for ./cmd/..." - go test -v ./cmd/... diff --git a/op-acceptance-tests/scripts/ci_flake_shake_calc_iterations.sh b/op-acceptance-tests/scripts/ci_flake_shake_calc_iterations.sh deleted file mode 100644 index a5b839bbdf7..00000000000 --- a/op-acceptance-tests/scripts/ci_flake_shake_calc_iterations.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ci_flake_shake_calc_iterations.sh -# -# Purpose: -# Compute the number of iterations each CircleCI parallel worker should run -# and export FLAKE_SHAKE_ITERATIONS and FLAKE_SHAKE_WORKER_ID into $BASH_ENV. -# -# Usage: -# ci_flake_shake_calc_iterations.sh [WORKERS] [WORKER_ID] -# -# Arguments: -# TOTAL_ITER (required): total iterations across all workers. -# WORKERS (optional): number of parallel workers (defaults to $CIRCLE_NODE_TOTAL or 1). -# WORKER_ID (optional): 1-based worker id (defaults to $((CIRCLE_NODE_INDEX+1)) or 1). -# -# Notes: -# - Remainder iterations are distributed one-by-one to the first N workers. - -TOTAL_ITER=${1:?TOTAL_ITER is required} -WORKERS=${2:-${CIRCLE_NODE_TOTAL:-1}} -WORKER_ID=${3:-$((${CIRCLE_NODE_INDEX:-0} + 1))} - -ITER_PER_WORKER=$((TOTAL_ITER / WORKERS)) -REMAINDER=$((TOTAL_ITER % WORKERS)) - -# Distribute the remainder fairly: the first $REMAINDER workers get one extra iteration -if [ "$WORKER_ID" -le "$REMAINDER" ] && [ "$REMAINDER" -ne 0 ]; then - ITER_COUNT=$((ITER_PER_WORKER + 1)) -else - ITER_COUNT=$ITER_PER_WORKER -fi - -echo "Worker $WORKER_ID running $ITER_COUNT of $TOTAL_ITER iterations" -if [ -n "${BASH_ENV:-}" ]; then - echo "export FLAKE_SHAKE_ITERATIONS=$ITER_COUNT" >> "$BASH_ENV" - echo "export FLAKE_SHAKE_WORKER_ID=$WORKER_ID" >> "$BASH_ENV" -fi - -exit 0 diff --git a/op-acceptance-tests/scripts/ci_flake_shake_generate_summary.sh b/op-acceptance-tests/scripts/ci_flake_shake_generate_summary.sh deleted file mode 100644 index 43e12c8b45c..00000000000 --- a/op-acceptance-tests/scripts/ci_flake_shake_generate_summary.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ci_flake_shake_generate_summary.sh -# -# Purpose: -# Used in CI by the flake-shake report job to transform the aggregated -# flake-shake report (JSON) into two derivative artifacts: -# 1) daily-summary.json – compact daily snapshot for downstream tooling -# 2) promotion-ready.json – list of tests with 100% pass rate (promotion candidates) -# -# Usage: ci_flake_shake_generate_summary.sh [REPORT_JSON] [OUT_DIR] -# $1 REPORT_JSON (optional) – path to flake-shake aggregated report JSON -# Default: final-report/flake-shake-report.json -# $2 OUT_DIR (optional) – directory where outputs will be written -# Default: final-report -# -# Side-effects (env): -# Exports UNSTABLE_COUNT into $BASH_ENV for later CI steps (if available). -# -# Requirements: -# - jq must be available in PATH -# -# Notes: -# - This script is intentionally simple and idempotent; it does not mutate -# the input report and only writes to OUT_DIR. - -REPORT_JSON=${1:-final-report/flake-shake-report.json} -OUT_DIR=${2:-final-report} - -if [ ! -f "$REPORT_JSON" ]; then - echo "ERROR: Report not found at $REPORT_JSON" >&2 - exit 1 -fi - -mkdir -p "$OUT_DIR" - -# Print a short human-readable summary to the job logs -echo "=== Flake-Shake Results ===" -STABLE=$(jq '[(.tests // [])[] | select(.recommendation == "STABLE")] | length' "$REPORT_JSON") -UNSTABLE=$(jq '[(.tests // [])[] | select(.recommendation == "UNSTABLE")] | length' "$REPORT_JSON") -echo "✅ STABLE: $STABLE tests" -echo "⚠️ UNSTABLE: $UNSTABLE tests" -if [ "$UNSTABLE" -gt 0 ]; then - echo "Unstable tests:" - jq -r '(.tests // [])[] | select(.recommendation == "UNSTABLE") | " - \(.test_name) (\(.pass_rate)%)"' "$REPORT_JSON" -fi - -# Write daily summary JSON (compact per-day snapshot) -jq '{date, gate, total_runs, iterations, - totals: { - stable: ([(.tests // [])[] | select(.recommendation=="STABLE")] | length), - unstable: ([(.tests // [])[] | select(.recommendation=="UNSTABLE")] | length) - }, - stable_tests: [ - (.tests // [])[] | select(.recommendation=="STABLE") | - {test_name, package, total_runs, pass_rate} - ], - unstable_tests: [ - (.tests // [])[] | select(.recommendation=="UNSTABLE") | - {test_name, package, total_runs, passes, failures, pass_rate} - ] - }' "$REPORT_JSON" > "$OUT_DIR/daily-summary.json" - -# Write promotion readiness (100% pass) JSON -jq '{ready: [(.tests // [])[] | select(.recommendation=="STABLE") | {test_name, package, total_runs, pass_rate, avg_duration, min_duration, max_duration}]}' "$REPORT_JSON" > "$OUT_DIR/promotion-ready.json" - -# Export UNSTABLE_COUNT for later CI steps (if BASH_ENV is present) -if [ -n "${BASH_ENV:-}" ]; then - echo "export UNSTABLE_COUNT=$UNSTABLE" >> "$BASH_ENV" -fi - -echo "Wrote: $OUT_DIR/daily-summary.json, $OUT_DIR/promotion-ready.json" diff --git a/op-acceptance-tests/scripts/ci_flake_shake_prepare_slack.sh b/op-acceptance-tests/scripts/ci_flake_shake_prepare_slack.sh deleted file mode 100644 index a3d28e66ed6..00000000000 --- a/op-acceptance-tests/scripts/ci_flake_shake_prepare_slack.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ci_flake_shake_prepare_slack.sh -# -# Purpose: -# Used in CI by the flake-shake promote job to parse the promoter output -# (`promotion-ready.json`) and prepare environment variables consumed by the -# Slack orb step. -# -# Inputs (positional): -# $1 PROMO_JSON – path to the promoter output `promotion-ready.json`. -# Default: ./final-promotion/promotion-ready.json -# -# Outputs (env): -# Exports into $BASH_ENV (for subsequent steps): -# - SLACK_BLOCKS_PAYLOAD: compact JSON array of Slack Block Kit blocks for the message body -# -# Requirements: -# - jq must be available in PATH -# -# Block count constraints: -# Slack allows max 50 blocks per message. Our layout uses: -# - 3 header/link/divider blocks -# - 4 blocks per candidate (2 sections + 1 context + 1 divider) -# - +1 optional overflow notice block -# This caps candidates at 11; if more, we add a final notice linking to the job. - -PROMO_JSON=${1:-./final-promotion/promotion-ready.json} - -SLACK_BLOCKS="[]" -if [ -f "$PROMO_JSON" ]; then - # Determine URL to the flake-shake report job (artifacts live there), - # falling back to the current job URL if not resolvable. - REPORT_JOB_URL="${CIRCLE_BUILD_URL:-}" - if [ -n "${CIRCLE_WORKFLOW_ID:-}" ] && [ -n "${CIRCLE_API_TOKEN:-}" ]; then - JOBS_JSON=$(curl -sfL -H "Circle-Token: ${CIRCLE_API_TOKEN}" "https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/jobs?limit=100" || true) - if [ -n "${JOBS_JSON:-}" ]; then - # Prefer web_url if available; otherwise construct URL from job_number - REPORT_WEB_URL=$(printf '%s' "$JOBS_JSON" | jq -r '.items[] | select(.name=="op-acceptance-tests-flake-shake-report") | .web_url // empty' | head -n1) - if [ -n "$REPORT_WEB_URL" ] && [ "$REPORT_WEB_URL" != "null" ]; then - REPORT_JOB_URL="$REPORT_WEB_URL" - else - REPORT_JOB_NUM=$(printf '%s' "$JOBS_JSON" | jq -r '.items[] | select(.name=="op-acceptance-tests-flake-shake-report") | .job_number // empty' | head -n1) - if [ -n "$REPORT_JOB_NUM" ] && [ "$REPORT_JOB_NUM" != "null" ] && [ -n "${CIRCLE_PROJECT_USERNAME:-}" ] && [ -n "${CIRCLE_PROJECT_REPONAME:-}" ]; then - REPORT_JOB_URL="https://circleci.com/gh/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${REPORT_JOB_NUM}" - fi - fi - fi - fi - - # Build Block Kit blocks (header + link + divider + per-candidate sections) - # See: https://docs.slack.dev/block-kit - SLACK_BLOCKS=$(jq -c \ - --arg url "${REPORT_JOB_URL}" \ - --arg job "${REPORT_JOB_URL}" \ - --slurpfile meta "${PROMO_JSON%/*}/metadata.json" ' - def name_or_pkg(t): (if ((t.test_name|tostring)|length) == 0 then "(package)" else t.test_name end); - def owner_or_unknown(t): (if ((t.owner|tostring)|length) == 0 then "unknown" else t.owner end); - def pkg_link(t): ( - (t.package|tostring) as $p | - (if ($p|test("^github\\.com/ethereum-optimism/optimism/")) then - ("https://github.com/ethereum-optimism/optimism/tree/develop/" + ($p | sub("^github\\.com/ethereum-optimism/optimism/"; ""))) - else "" end) as $u | - (if $u != "" then ("<" + $u + "|" + $p + ">") else $p end) - ); - def testblocks(t): [ - {"type":"section","fields":[ - {"type":"mrkdwn","text":"*Test:*\n\(name_or_pkg(t))"}, - {"type":"mrkdwn","text":"*Owner:*\n\(owner_or_unknown(t))"} - ]}, - {"type":"section","fields":[ - {"type":"mrkdwn","text":"*Runs:*\n\(t.total_runs)"}, - {"type":"mrkdwn","text":"*Pass Rate:*\n\((t.pass_rate|tostring))%"} - ]}, - {"type":"context","elements":[{"type":"mrkdwn","text": pkg_link(t) }]}, - {"type":"divider"} - ]; - . as $root | - ($meta | if length>0 then .[0] else {} end) as $meta | - ($meta.date // "") as $date | - ($meta.gate // "flake-shake") as $gate | - ($meta.pr_url // "") as $pr_url | - ( if (($meta|length) > 0 and ($meta.flake_gate_tests // 0) == 0) then - [] - elif ($root.candidates|length) == 0 then - [ - {"type":"header","text":{"type":"plain_text","text":":partywizard: Acceptance Tests: No Flake-Shake Promotion Candidates — \(if $date != "" then $date else (now|strftime("%Y-%m-%d")) end)"}}, - {"type":"section","text":{"type":"mrkdwn","text":"No promotions today. Artifacts: <\($job)|CircleCI Job>"}} - ] - else - ( - [ - {"type":"header","text":{"type":"plain_text","text":":partywizard: Acceptance Tests: Flake-Shake Promotion Candidates (\($root.candidates|length)) — \(if $date != "" then $date else (now|strftime("%Y-%m-%d")) end)"}}, - {"type":"section","text":{"type":"mrkdwn","text": (if $pr_url != "" then "<\($pr_url)|Pull Request> • <\($url)|CircleCI Job>" else "Artifacts: <\($url)|CircleCI Job>" end) }}, - {"type":"divider"} - ] - ) - + ( ($root.candidates[:11] | map(testblocks(.)) | add) ) - + ( if ($root.candidates|length) > 11 then - [ {"type":"section","text":{"type":"mrkdwn","text":"Too many tests; see the report: <\($url)|CircleCI Job>"}} ] - else [] end ) - end ) - ' "$PROMO_JSON") -fi - -echo "export SLACK_BLOCKS_PAYLOAD='$SLACK_BLOCKS'" >> "$BASH_ENV" - -echo "Prepared Slack env: blocks generated" - -echo "[debug] SLACK_BLOCKS: $SLACK_BLOCKS" diff --git a/op-acceptance-tests/tests/base/advance_test.go b/op-acceptance-tests/tests/base/advance_test.go index 7852774b0e0..2c37e4bd4ad 100644 --- a/op-acceptance-tests/tests/base/advance_test.go +++ b/op-acceptance-tests/tests/base/advance_test.go @@ -10,7 +10,7 @@ import ( ) func TestCLAdvance(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) tracer := t.Tracer() ctx := t.Ctx() diff --git a/op-acceptance-tests/tests/base/chain/chain_test.go b/op-acceptance-tests/tests/base/chain/chain_test.go index 244ead5f8d5..d2b17b109c8 100644 --- a/op-acceptance-tests/tests/base/chain/chain_test.go +++ b/op-acceptance-tests/tests/base/chain/chain_test.go @@ -14,7 +14,7 @@ import ( // TestChainFork checks that the chain does not fork (all nodes have the same block hash for a fixed block number). func TestChainFork(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) t.Logger().Info("Started chain fork test") diff --git a/op-acceptance-tests/tests/base/conductor/leadership_transfer_test.go b/op-acceptance-tests/tests/base/conductor/leadership_transfer_test.go index af1ab12a22f..9f396a6e3d0 100644 --- a/op-acceptance-tests/tests/base/conductor/leadership_transfer_test.go +++ b/op-acceptance-tests/tests/base/conductor/leadership_transfer_test.go @@ -22,7 +22,7 @@ type conductorWithInfo struct { // TestConductorLeadershipTransfer checks if the leadership transfer works correctly on the conductors func TestConductorLeadershipTransfer(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) logger := testlog.Logger(t, log.LevelInfo).With("Test", "TestConductorLeadershipTransfer") sys := presets.NewMinimalWithConductors(t) diff --git a/op-acceptance-tests/tests/base/deposit/deposit_test.go b/op-acceptance-tests/tests/base/deposit/deposit_test.go index c26f065d8af..0196654d6a9 100644 --- a/op-acceptance-tests/tests/base/deposit/deposit_test.go +++ b/op-acceptance-tests/tests/base/deposit/deposit_test.go @@ -17,7 +17,7 @@ import ( func TestL1ToL2Deposit(gt *testing.T) { // Create a test environment using op-devstack - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) // Skip this test if CGT is enabled diff --git a/op-acceptance-tests/tests/base/faucet_test.go b/op-acceptance-tests/tests/base/faucet_test.go index c13791a9b66..402a80b71ec 100644 --- a/op-acceptance-tests/tests/base/faucet_test.go +++ b/op-acceptance-tests/tests/base/faucet_test.go @@ -9,7 +9,7 @@ import ( ) func TestFaucetFund(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) tracer := t.Tracer() ctx := t.Ctx() diff --git a/op-acceptance-tests/tests/base/public_rpc_advance_test.go b/op-acceptance-tests/tests/base/public_rpc_advance_test.go index ce30ca15e75..fd3ba51bf7a 100644 --- a/op-acceptance-tests/tests/base/public_rpc_advance_test.go +++ b/op-acceptance-tests/tests/base/public_rpc_advance_test.go @@ -9,7 +9,7 @@ import ( ) func TestPublicRpcAdvance(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) sys.L2Chain.PublicRPC().Advanced(eth.Unsafe, 5) diff --git a/op-acceptance-tests/tests/base/rpc_connectivity_test.go b/op-acceptance-tests/tests/base/rpc_connectivity_test.go index 160ad10d259..4e3e82bbbdc 100644 --- a/op-acceptance-tests/tests/base/rpc_connectivity_test.go +++ b/op-acceptance-tests/tests/base/rpc_connectivity_test.go @@ -17,7 +17,7 @@ import ( // TestRPCConnectivity checks we can connect to L2 execution layer RPC endpoints func TestRPCConnectivity(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) logger := testlog.Logger(t, log.LevelInfo).With("Test", "TestRPCConnectivity") tracer := t.Tracer() diff --git a/op-acceptance-tests/tests/base/transfer_test.go b/op-acceptance-tests/tests/base/transfer_test.go index d4d647846d6..73092ef9244 100644 --- a/op-acceptance-tests/tests/base/transfer_test.go +++ b/op-acceptance-tests/tests/base/transfer_test.go @@ -11,7 +11,7 @@ import ( func TestTransfer(gt *testing.T) { // Create a test environment using op-devstack - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) // Create two L2 wallets diff --git a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test_helper.go b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test_helper.go index 6ff415cc717..b67151c8af6 100644 --- a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test_helper.go +++ b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test_helper.go @@ -38,7 +38,7 @@ func newSystem(t devtest.T, gameType gameTypes.GameType) *presets.Minimal { } func TestWithdrawal(gt *testing.T, gameType gameTypes.GameType) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSystem(t, gameType) require := sys.T.Require() diff --git a/op-acceptance-tests/tests/batcher/batcher_test.go b/op-acceptance-tests/tests/batcher/batcher_test.go index ae72cb4db94..c845491b1e7 100644 --- a/op-acceptance-tests/tests/batcher/batcher_test.go +++ b/op-acceptance-tests/tests/batcher/batcher_test.go @@ -21,7 +21,9 @@ import ( ) func TestBatcherFullChannelsAfterDowntime(gt *testing.T) { - t := devtest.SerialT(gt) + gt.Skip("Skipping test until we fix nonce too high error: tx: 177 state: 176") + + t := devtest.ParallelT(gt) opts := presets.Combine( // Keep verifier EL-sync behavior and no-discovery from the old package-level TestMain. presets.WithGlobalL2CLOption(sysgo.L2CLOptionFn(func(_ devtest.T, _ sysgo.ComponentTarget, cfg *sysgo.L2CLConfig) { diff --git a/op-acceptance-tests/tests/batcher/throttling/throttling_test.go b/op-acceptance-tests/tests/batcher/throttling/throttling_test.go index 880b65be05a..a0565289710 100644 --- a/op-acceptance-tests/tests/batcher/throttling/throttling_test.go +++ b/op-acceptance-tests/tests/batcher/throttling/throttling_test.go @@ -26,7 +26,7 @@ const blockSizeLimit = 5_000 // miner_setMaxDASize. It spams transactions to saturate block space and asserts that blocks are // filled to near capacity without exceeding the limit. func TestDABlockThrottling(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t, presets.WithBatcherOption(func(_ sysgo.ComponentTarget, cfg *bss.CLIConfig) { // Enable throttling with step controller for predictable behavior. cfg.ThrottleConfig.LowerThreshold = 99 // > 0 enables the throttling loop. diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go index 9afbb76406d..f8821d809fc 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go @@ -9,7 +9,7 @@ import ( // TestCGT_IntrospectionViaL1Block verifies that the L2 L1Block predeploy reports // that CGT mode is enabled and exposes non-empty token metadata (name, symbol). func TestCGT_IntrospectionViaL1Block(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) name, symbol := ensureCGTOrSkip(t, sys) diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go index bc8838e8248..fbe3aa88173 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go @@ -16,7 +16,7 @@ import ( // TestCGT_L1PortalIntrospection checks that the L1 OptimismPortal exposes // a valid SystemConfig address via its systemConfig() view. func TestCGT_L1PortalIntrospection(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) // Skip if this devnet is not CGT-enabled (uses your existing gate). diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go index 33d8d5f04f1..b31abb5c23c 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go @@ -11,7 +11,7 @@ import ( // value transfer charges gas in the native ERC-20, and balances reflect // recipient +amount and sender > amount decrease (amount + gas). func TestCGT_ValueTransferPaysGasInToken(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) ensureCGTOrSkip(t, sys) diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go index 6969273f1cd..96be20a4055 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go @@ -13,7 +13,7 @@ import ( // TestCGT_PortalReceiveReverts asserts that sending ETH to the L1 OptimismPortal // (receive() -> depositTransaction) reverts under CGT, preventing ETH from getting stuck. func TestCGT_PortalReceiveReverts(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) ensureCGTOrSkip(t, sys) diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go index e17be86d8cf..0118d0a4c3f 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go @@ -16,7 +16,7 @@ import ( // TestCGT_MessengerRejectsValue ensures that sending native value to the // L2CrossDomainMessenger reverts under CGT (non-payable path). func TestCGT_MessengerRejectsValue(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) ensureCGTOrSkip(t, sys) @@ -38,7 +38,7 @@ func TestCGT_MessengerRejectsValue(gt *testing.T) { // TestCGT_L2StandardBridge_LegacyWithdrawReverts verifies that the legacy // ETH-specific withdraw path on L2StandardBridge reverts under CGT. func TestCGT_L2StandardBridge_LegacyWithdrawReverts(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) ensureCGTOrSkip(t, sys) diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go index bbc11de26dc..aac3e4592e2 100644 --- a/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go @@ -16,7 +16,7 @@ import ( // TestCGT_SystemConfigFlagOnL1 checks that the L1 SystemConfig contract reports // CGT=true via isCustomGasToken(). Skips if the devnet does not wire this flag. func TestCGT_SystemConfigFlagOnL1(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) ensureCGTOrSkip(t, sys) @@ -61,7 +61,7 @@ func TestCGT_SystemConfigFlagOnL1(gt *testing.T) { // TestCGT_SystemConfigFeatureFlag re-validates the CGT flag on SystemConfig, // using locally encoded calls (mirrors the previous test structure). Skips on devnets without the flag. func TestCGT_SystemConfigFeatureFlag(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newCGTMinimal(t) // Skip if not in CGT mode (uses L2 L1Block.isCustomGasToken()). diff --git a/op-acceptance-tests/tests/depreqres/common/common.go b/op-acceptance-tests/tests/depreqres/common/common.go index 56a3531abda..6b4b54d0521 100644 --- a/op-acceptance-tests/tests/depreqres/common/common.go +++ b/op-acceptance-tests/tests/depreqres/common/common.go @@ -70,24 +70,7 @@ func SyncModeReqRespSyncOpts(syncMode sync.Mode) []presets.Option { } } -// stableSyncStatus returns the sync status of node after any in-flight gossip messages -// have been drained. DisconnectPeer closes the libp2p connection but a buffered gossip -// payload can still arrive and be processed via AddUnsafePayload (SyncModeReqResp=true -// routes CL gossip through the CLSync path even in ELSync mode). Polling until the -// head is stable ensures the snapshot reflects a quiesced state. -func stableSyncStatus(require *testreq.Assertions, node *dsl.L2CLNode) *eth.SyncStatus { - ss := node.SyncStatus() - require.Eventually(func() bool { - next := node.SyncStatus() - stable := next.UnsafeL2.Number == ss.UnsafeL2.Number - ss = next - return stable - }, 5*time.Second, 200*time.Millisecond, "L2CLB head should stabilize after disconnect") - return ss -} - -func UnsafeChainNotStalling_Disconnect(gt *testing.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { - t := devtest.SerialT(gt) +func UnsafeChainNotStalling_DisconnectT(t devtest.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { sys := presets.NewSingleChainMultiNodeWithoutCheck(t, opts...) require := t.Require() l := t.Logger().With("syncmode", syncMode) @@ -104,6 +87,7 @@ func UnsafeChainNotStalling_Disconnect(gt *testing.T, syncMode sync.Mode, sleep sys.L2CL.DisconnectPeer(sys.L2CLB) ssA_before := sys.L2CL.SyncStatus() + sys.L2CLB.WaitForStall(types.LocalUnsafe) ssB_before := sys.L2CLB.SyncStatus() l.Info("L2CL status before delay", "unsafeL2", ssA_before.UnsafeL2.ID(), "safeL2", ssA_before.SafeL2.ID()) @@ -129,8 +113,12 @@ func UnsafeChainNotStalling_Disconnect(gt *testing.T, syncMode sync.Mode, sleep sys.L2ELB.Reached(eth.Unsafe, ssA_after.UnsafeL2.Number, 30) } -func UnsafeChainNotStalling_RestartOpNode(gt *testing.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { - t := devtest.SerialT(gt) +func UnsafeChainNotStalling_Disconnect(gt *testing.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { + t := devtest.ParallelT(gt) + UnsafeChainNotStalling_DisconnectT(t, syncMode, sleep, opts...) +} + +func UnsafeChainNotStalling_RestartOpNodeT(t devtest.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { sys := presets.NewSingleChainMultiNodeWithoutCheck(t, opts...) require := t.Require() l := t.Logger().With("syncmode", syncMode) @@ -147,6 +135,7 @@ func UnsafeChainNotStalling_RestartOpNode(gt *testing.T, syncMode sync.Mode, sle sys.L2CL.DisconnectPeer(sys.L2CLB) ssA_before := sys.L2CL.SyncStatus() + sys.L2CLB.WaitForStall(types.LocalUnsafe) ssB_before := sys.L2CLB.SyncStatus() l.Info("L2CL status before delay", "unsafeL2", ssA_before.UnsafeL2.ID(), "safeL2", ssA_before.SafeL2.ID()) @@ -175,3 +164,8 @@ func UnsafeChainNotStalling_RestartOpNode(gt *testing.T, syncMode sync.Mode, sle sys.L2CLB.Reached(types.LocalUnsafe, ssA_after.UnsafeL2.Number, 30) sys.L2ELB.Reached(eth.Unsafe, ssA_after.UnsafeL2.Number, 30) } + +func UnsafeChainNotStalling_RestartOpNode(gt *testing.T, syncMode sync.Mode, sleep time.Duration, opts ...presets.Option) { + t := devtest.ParallelT(gt) + UnsafeChainNotStalling_RestartOpNodeT(t, syncMode, sleep, opts...) +} diff --git a/op-acceptance-tests/tests/depreqres/reqressyncdisabled/depreqres_test.go b/op-acceptance-tests/tests/depreqres/reqressyncdisabled/depreqres_test.go index ae004f0d637..f6c441985c0 100644 --- a/op-acceptance-tests/tests/depreqres/reqressyncdisabled/depreqres_test.go +++ b/op-acceptance-tests/tests/depreqres/reqressyncdisabled/depreqres_test.go @@ -13,8 +13,11 @@ import ( "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) +const disabledReqRespSyncFlakyReason = "known flaky in the default acceptance run" + func TestUnsafeChainNotStalling_DisabledReqRespSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) + t.MarkFlaky(disabledReqRespSyncFlakyReason) sys := presets.NewSingleChainMultiNodeWithoutCheck(t, common.ReqRespSyncDisabledOpts(sync.ELSync)...) // We don't want the safe head to move, as this can also progress the unsafe head sys.L2Batcher.Stop() diff --git a/op-acceptance-tests/tests/depreqres/reqressyncdisabled/divergence/divergence_test.go b/op-acceptance-tests/tests/depreqres/reqressyncdisabled/divergence/divergence_test.go index ce60a83251c..26f35f7bb4d 100644 --- a/op-acceptance-tests/tests/depreqres/reqressyncdisabled/divergence/divergence_test.go +++ b/op-acceptance-tests/tests/depreqres/reqressyncdisabled/divergence/divergence_test.go @@ -15,7 +15,7 @@ import ( // TestCLELDivergence tests that the CL and EL diverge when the CL advances the unsafe head, due to accepting SYNCING response from the EL, but the EL cannot validate the block (yet), does not canonicalize it, and doesn't serve it. func TestCLELDivergence(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSingleChainMultiNodeWithoutP2PWithoutCheck(t, common.ReqRespSyncDisabledOpts(sync.ELSync)...) require := t.Require() l := t.Logger() diff --git a/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync/elsync_test.go b/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync/elsync_test.go index 90a18183034..0e86d6a56c6 100644 --- a/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync/elsync_test.go +++ b/op-acceptance-tests/tests/depreqres/syncmodereqressync/elsync/elsync_test.go @@ -5,17 +5,26 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/depreqres/common" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-node/rollup/sync" ) +const syncModeReqRespSyncFlakyReason = "known flaky in the default acceptance run" + func TestUnsafeChainNotStalling_ELSync_Short(gt *testing.T) { - common.UnsafeChainNotStalling_Disconnect(gt, sync.ELSync, 20*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) + t := devtest.ParallelT(gt) + t.MarkFlaky(syncModeReqRespSyncFlakyReason) + common.UnsafeChainNotStalling_DisconnectT(t, sync.ELSync, 20*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) } func TestUnsafeChainNotStalling_ELSync_Long(gt *testing.T) { - common.UnsafeChainNotStalling_Disconnect(gt, sync.ELSync, 95*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) + t := devtest.ParallelT(gt) + t.MarkFlaky(syncModeReqRespSyncFlakyReason) + common.UnsafeChainNotStalling_DisconnectT(t, sync.ELSync, 95*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) } func TestUnsafeChainNotStalling_ELSync_RestartOpNode_Long(gt *testing.T) { - common.UnsafeChainNotStalling_RestartOpNode(gt, sync.ELSync, 95*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) + t := devtest.ParallelT(gt) + t.MarkFlaky(syncModeReqRespSyncFlakyReason) + common.UnsafeChainNotStalling_RestartOpNodeT(t, sync.ELSync, 95*time.Second, common.SyncModeReqRespSyncOpts(sync.ELSync)...) } diff --git a/op-acceptance-tests/tests/ecotone/fees_test.go b/op-acceptance-tests/tests/ecotone/fees_test.go index 88e09b990a0..57d4f861958 100644 --- a/op-acceptance-tests/tests/ecotone/fees_test.go +++ b/op-acceptance-tests/tests/ecotone/fees_test.go @@ -12,7 +12,7 @@ import ( ) func TestFees(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() diff --git a/op-acceptance-tests/tests/fjord/check_scripts_test.go b/op-acceptance-tests/tests/fjord/check_scripts_test.go index ef38ab30f3f..f3c11df0204 100644 --- a/op-acceptance-tests/tests/fjord/check_scripts_test.go +++ b/op-acceptance-tests/tests/fjord/check_scripts_test.go @@ -29,7 +29,7 @@ var ( ) func TestCheckFjordScript(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() ctx := t.Ctx() diff --git a/op-acceptance-tests/tests/fjord/fees_test.go b/op-acceptance-tests/tests/fjord/fees_test.go index c3bacca93db..6c410bf2102 100644 --- a/op-acceptance-tests/tests/fjord/fees_test.go +++ b/op-acceptance-tests/tests/fjord/fees_test.go @@ -14,7 +14,7 @@ import ( ) func TestFees(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() ctx := 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 365a82e87b5..e13bc230309 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go @@ -19,14 +19,11 @@ import ( "github.com/stretchr/testify/require" ) -var ( - flashblocksStreamRate = os.Getenv("FLASHBLOCKS_STREAM_RATE_MS") - maxExpectedFlashblocks = 20 -) +const maxExpectedFlashblocks = 20 // TestFlashblocksStream checks we can connect to the flashblocks stream across multiple CL backends. func TestFlashblocksStream(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) logger := t.Logger() sys := presets.NewSingleChainWithFlashblocks(t) filterHandler, ok := logmods.FindHandler[logfilter.FilterHandler](logger.Handler()) @@ -45,6 +42,7 @@ func TestFlashblocksStream(gt *testing.T) { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() + flashblocksStreamRate := os.Getenv("FLASHBLOCKS_STREAM_RATE_MS") if flashblocksStreamRate == "" { logger.Warn("FLASHBLOCKS_STREAM_RATE_MS is not set, using default of 250ms") flashblocksStreamRate = "250" diff --git a/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go b/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go index 80cf2e3c8f6..71a2a690d5e 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go @@ -31,7 +31,7 @@ type timedMessage struct { // - That Flashblock's time (in seconds) must be less than or equal to the Transaction's block time (in seconds). (Can't check the block time beyond the granularity of seconds) // - That Flashblock's time in nanoseconds must be before the approximated transaction confirmation time recorded previously. func TestFlashblocksTransfer(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) logger := t.Logger() tracer := t.Tracer() ctx := t.Ctx() diff --git a/op-acceptance-tests/tests/fusaka/fusaka_test.go b/op-acceptance-tests/tests/fusaka/fusaka_test.go index 42fb21c179d..b8d0c641708 100644 --- a/op-acceptance-tests/tests/fusaka/fusaka_test.go +++ b/op-acceptance-tests/tests/fusaka/fusaka_test.go @@ -26,7 +26,7 @@ import ( ) func TestSafeHeadAdvancesAfterOsaka(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newMinimalFusaka(t) l1Config := sys.L1Network.Escape().ChainConfig() t.Log("Waiting for Osaka to activate") @@ -50,7 +50,7 @@ func TestSafeHeadAdvancesAfterOsaka(gt *testing.T) { } func TestBlobBaseFeeIsCorrectAfterBPOFork(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newMinimalFusaka(t) t.Log("Waiting for BPO1 to activate") t.Require().NotNil(sys.L1Network.Escape().ChainConfig().BPO1Time) diff --git a/op-acceptance-tests/tests/fusaka/helpers.go b/op-acceptance-tests/tests/fusaka/helpers.go index b0c65789482..eff581ba40d 100644 --- a/op-acceptance-tests/tests/fusaka/helpers.go +++ b/op-acceptance-tests/tests/fusaka/helpers.go @@ -3,44 +3,21 @@ package fusaka import ( "bytes" "fmt" - "os" "os/exec" "strings" - "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-devstack/presets" ) -// ConfigureDevstackEnvVars sets the appropriate env vars to use a mise-installed geth binary for -// the L1 EL. This is useful in Osaka acceptance tests since op-geth does not include full Osaka -// support. This is meant to run before constructing a devstack target in the test. It will log to -// stdout. ResetDevstackEnvVars should be used to reset the environment variables when the test -// exits. -// -// Note that this is a no-op if either [sysgo.DevstackL1ELKindVar] or [sysgo.GethExecPathEnvVar] -// are set. -// -// The returned callback resets any modified environment variables. -func ConfigureDevstackEnvVars() func() { - if _, ok := os.LookupEnv(sysgo.DevstackL1ELKindEnvVar); ok { - return func() {} - } - if _, ok := os.LookupEnv(sysgo.GethExecPathEnvVar); ok { - return func() {} - } - +// L1GethOption resolves a mise-installed geth binary for the L1 EL. This avoids mutating +// process-global env vars, which would otherwise block package-wide test parallelism. +func L1GethOption() presets.Option { cmd := exec.Command("mise", "which", "geth") buf := bytes.NewBuffer([]byte{}) cmd.Stdout = buf if err := cmd.Run(); err != nil { - fmt.Printf("Failed to find mise-installed geth: %v\n", err) - return func() {} + panic(fmt.Sprintf("failed to find mise-installed geth: %v", err)) } execPath := strings.TrimSpace(buf.String()) - fmt.Println("Found mise-installed geth:", execPath) - _ = os.Setenv(sysgo.GethExecPathEnvVar, execPath) - _ = os.Setenv(sysgo.DevstackL1ELKindEnvVar, "geth") - return func() { - _ = os.Unsetenv(sysgo.GethExecPathEnvVar) - _ = os.Unsetenv(sysgo.DevstackL1ELKindEnvVar) - } + return presets.WithL1Geth(execPath) } diff --git a/op-acceptance-tests/tests/fusaka/setup_test.go b/op-acceptance-tests/tests/fusaka/setup_test.go index 16df5a29ee9..36d2e0b0d36 100644 --- a/op-acceptance-tests/tests/fusaka/setup_test.go +++ b/op-acceptance-tests/tests/fusaka/setup_test.go @@ -10,10 +10,8 @@ import ( ) func newMinimalFusaka(t devtest.T) *presets.Minimal { - resetEnvVars := ConfigureDevstackEnvVars() - t.Cleanup(resetEnvVars) - return presets.NewMinimal(t, + L1GethOption(), presets.WithDeployerOptions( sysgo.WithDefaultBPOBlobSchedule, // Make the BPO fork happen after Osaka so we can easily use geth's eip4844.CalcBlobFee diff --git a/op-acceptance-tests/tests/interop/boilerplate_test.go b/op-acceptance-tests/tests/interop/boilerplate_test.go deleted file mode 100644 index d8074a5e240..00000000000 --- a/op-acceptance-tests/tests/interop/boilerplate_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package interop - -import ( - "os" - - oplog "github.com/ethereum-optimism/optimism/op-service/log" - "github.com/ethereum/go-ethereum/log" -) - -func init() { - oplog.SetGlobalLogHandler(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)) -} diff --git a/op-acceptance-tests/tests/interop/contract/interop_contract_test.go b/op-acceptance-tests/tests/interop/contract/interop_contract_test.go index ca5b6193b0b..557aaa75460 100644 --- a/op-acceptance-tests/tests/interop/contract/interop_contract_test.go +++ b/op-acceptance-tests/tests/interop/contract/interop_contract_test.go @@ -17,7 +17,7 @@ import ( // TestRegularMessage checks that messages can be sent and relayed via L2ToL2CrossDomainMessenger func TestRegularMessage(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/interop/loadtest/interop_load_test.go b/op-acceptance-tests/tests/interop/loadtest/interop_load_test.go index e7de2a5fb9e..0feb1cd73e2 100644 --- a/op-acceptance-tests/tests/interop/loadtest/interop_load_test.go +++ b/op-acceptance-tests/tests/interop/loadtest/interop_load_test.go @@ -96,7 +96,7 @@ func setupLoadTest(gt *testing.T) (devtest.T, *L2, *L2) { if testing.Short() { gt.Skip("skipping load test in short mode") } - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) ctx, cancel := context.WithTimeout(t.Ctx(), 3*time.Minute) if timeoutStr, exists := os.LookupEnv("NAT_INTEROP_LOADTEST_TIMEOUT"); exists { diff --git a/op-acceptance-tests/tests/interop/message/interop_happy_tx_test.go b/op-acceptance-tests/tests/interop/message/interop_happy_tx_test.go index e16ab6951a7..9162ce4330c 100644 --- a/op-acceptance-tests/tests/interop/message/interop_happy_tx_test.go +++ b/op-acceptance-tests/tests/interop/message/interop_happy_tx_test.go @@ -18,7 +18,7 @@ import ( // the block number where the messages were included func TestInteropHappyTx(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) // two EOAs for triggering the init and exec interop txs diff --git a/op-acceptance-tests/tests/interop/message/interop_mon_test.go b/op-acceptance-tests/tests/interop/message/interop_mon_test.go index e92b9034a78..05d08607567 100644 --- a/op-acceptance-tests/tests/interop/message/interop_mon_test.go +++ b/op-acceptance-tests/tests/interop/message/interop_mon_test.go @@ -18,7 +18,7 @@ import ( // TestInteropMon is testing that the op-interop-mon metrics are correctly collected func TestInteropMon(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) clients := map[eth.ChainID]*sources.EthClient{ diff --git a/op-acceptance-tests/tests/interop/message/interop_msg_test.go b/op-acceptance-tests/tests/interop/message/interop_msg_test.go index c5b2e2d3cd8..994efef5fb4 100644 --- a/op-acceptance-tests/tests/interop/message/interop_msg_test.go +++ b/op-acceptance-tests/tests/interop/message/interop_msg_test.go @@ -31,7 +31,7 @@ import ( // TestInitExecMsg tests basic interop messaging func TestInitExecMsg(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) rng := rand.New(rand.NewSource(1234)) alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) @@ -49,7 +49,7 @@ func TestInitExecMsg(gt *testing.T) { // TestInitExecMsgWithDSL tests basic interop messaging with contract DSL func TestInitExecMsgWithDSL(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) rng := rand.New(rand.NewSource(1234)) alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) @@ -125,7 +125,7 @@ func TestInitExecMsgWithDSL(gt *testing.T) { // Construct random directed graph of messages. func TestRandomDirectedGraph(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) logger := sys.Log.With("Test", "TestRandomDirectedGraph") @@ -248,7 +248,7 @@ func TestRandomDirectedGraph(gt *testing.T) { // Transaction initiates and executes multiple messages of self func TestInitExecMultipleMsg(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() @@ -294,7 +294,7 @@ func TestInitExecMultipleMsg(gt *testing.T) { // Transaction that executes the same message twice. func TestExecSameMsgTwice(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() @@ -339,7 +339,7 @@ func TestExecSameMsgTwice(gt *testing.T) { // Execute message that links with initiating message with: 0, 1, 2, 3, or 4 topics in it func TestExecDifferentTopicCount(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() @@ -390,7 +390,7 @@ func TestExecDifferentTopicCount(gt *testing.T) { // Execute message that links with initiating message with: 0, 10KB of opaque event data in it func TestExecMsgOpaqueData(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() @@ -441,7 +441,7 @@ func TestExecMsgOpaqueData(gt *testing.T) { // Execute message that links with initiating message with: first, random or last event of a tx. func TestExecMsgDifferEventIndexInSingleTx(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() @@ -560,7 +560,7 @@ func executeIndexedFault( // Execute message, but with one or more invalid attributes inside identifiers func TestExecMessageInvalidAttributes(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := sys.T.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/interop/prep/prep_test.go b/op-acceptance-tests/tests/interop/prep/prep_test.go index 38f758dfa21..5bcc79e94a4 100644 --- a/op-acceptance-tests/tests/interop/prep/prep_test.go +++ b/op-acceptance-tests/tests/interop/prep/prep_test.go @@ -17,7 +17,7 @@ import ( // And then confirms we can finalize the chains. func TestUnscheduledInterop(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t, presets.WithDeployerOptions( func(p devtest.T, keys devkeys.Keys, builder intentbuilder.Builder) { for _, l2 := range builder.L2s() { diff --git a/op-acceptance-tests/tests/interop/proofs/fpp/fpp_test.go b/op-acceptance-tests/tests/interop/proofs/fpp/fpp_test.go index 386b9e3982e..5d3c65ad3ea 100644 --- a/op-acceptance-tests/tests/interop/proofs/fpp/fpp_test.go +++ b/op-acceptance-tests/tests/interop/proofs/fpp/fpp_test.go @@ -9,7 +9,7 @@ import ( ) func TestFPP(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled()) startTimestamp := max(sys.L2ChainA.Escape().RollupConfig().TimestampForBlock(1), sys.L2ChainB.Escape().RollupConfig().TimestampForBlock(1)) @@ -21,7 +21,7 @@ func TestFPP(gt *testing.T) { } func TestNextSuperRootNotFound(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) // TODO(#19180): Unskip this once supernode is updated. t.Skip("Supernode does not yet return optimistic blocks until blocks are fully validated") sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled()) @@ -47,13 +47,26 @@ func TestNextSuperRootNotFound(gt *testing.T) { startTimestamp := chainBLastBlock.Time endTimestamp := startTimestamp + blockTime + sys.SuperRoots.AwaitValidatedTimestamp(endTimestamp - 1) + t.Log("Validated timestamp", endTimestamp-1) + // Verify we have a super root at the last block timestamp resp := sys.SuperRoots.SuperRootAtTimestamp(startTimestamp) t.Require().NotNil(resp.Data) + // We have 1 second block times so we _should_ have a super root one second after startTimestamp + // and it should be the same as the super root at startTimestamp because there are no new blocks yet. + resp2 := sys.SuperRoots.SuperRootAtTimestamp(endTimestamp - 1) + t.Require().NotNil(resp2.Data) + t.Require().Len(resp2.OptimisticAtTimestamp, 2) + t.Log("Super root at", endTimestamp-1, "is", resp2.Data.SuperRoot) + // But not at the next block resp = sys.SuperRoots.SuperRootAtTimestamp(endTimestamp) t.Require().Nil(resp.Data) + t.Require().Len(resp.OptimisticAtTimestamp, 1) // Second chain is missing because it's not safe yet. + t.Require().Contains(resp.OptimisticAtTimestamp, sys.L2ChainA.ChainID()) + t.Require().NotContains(resp.OptimisticAtTimestamp, sys.L2ChainB.ChainID()) // Run FPP from timestamp of safe head on second chain, to 2 seconds later. dgf := sys.DisputeGameFactory() diff --git a/op-acceptance-tests/tests/interop/proofs/serial/interop_fault_proofs_test.go b/op-acceptance-tests/tests/interop/proofs/serial/interop_fault_proofs_test.go new file mode 100644 index 00000000000..7d9cdf66019 --- /dev/null +++ b/op-acceptance-tests/tests/interop/proofs/serial/interop_fault_proofs_test.go @@ -0,0 +1,63 @@ +package serial + +import ( + "testing" + + sfp "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/superfaultproofs" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func TestInteropFaultProofs(gt *testing.T) { + t := devtest.ParallelT(gt) + // TODO(#19180): Unskip this once supernode is updated. + t.Skip("Supernode does not yet return optimistic blocks until blocks are fully validated") + sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled()) + sfp.RunSuperFaultProofTest(t, sys) +} + +func TestInteropFaultProofs_ConsolidateValidCrossChainMessage(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled()) + sfp.RunConsolidateValidCrossChainMessageTest(t, sys) +} + +func TestInteropFaultProofs_VariedBlockTimes(gt *testing.T) { + t := devtest.SerialT(gt) + // TODO(#19010): Unskip once varied block time fault proofs are stable. + t.Skip("Skipping flaky varied block time fault proof test") + sys := presets.NewSimpleInteropSupernodeProofs( + t, + presets.WithChallengerCannonKonaEnabled(), + presets.WithL2BlockTimes(map[eth.ChainID]uint64{ + sysgo.DefaultL2AID: 1, + sysgo.DefaultL2BID: 2, + }), + ) + sfp.RunVariedBlockTimesTest(t, sys) +} + +func TestInteropFaultProofs_VariedBlockTimes_FasterChainB(gt *testing.T) { + t := devtest.SerialT(gt) + // TODO(#19010): Unskip once varied block time fault proofs are stable. + t.Skip("Skipping flaky varied block time fault proof test") + sys := presets.NewSimpleInteropSupernodeProofs( + t, + presets.WithChallengerCannonKonaEnabled(), + presets.WithL2BlockTimes(map[eth.ChainID]uint64{ + sysgo.DefaultL2AID: 2, + sysgo.DefaultL2BID: 1, + }), + ) + sfp.RunVariedBlockTimesTest(t, sys) +} + +func TestInteropFaultProofs_InvalidBlock(gt *testing.T) { + t := devtest.SerialT(gt) + // TODO(#19411): Unskip once supernode removes invalid transactions + t.Skip("Supernode does not yet remove invalid transactions from blocks") + sys := presets.NewSimpleInteropSupernodeProofs(t) + sfp.RunInvalidBlockTest(t, sys) +} diff --git a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go index 711e6091e65..a365e5773f8 100644 --- a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go +++ b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go @@ -10,7 +10,7 @@ import ( ) func TestSuperRootWithdrawal(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInteropSuperProofs(t, presets.WithTimeTravelEnabled()) sys.L1Network.WaitForOnline() diff --git a/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go b/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go index f7cfb53818a..0eaab29070c 100644 --- a/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go +++ b/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go @@ -23,7 +23,7 @@ import ( func TestReorgInitExecMsg(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) ctx := t.Ctx() sys := presets.NewSimpleInterop(t) diff --git a/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go b/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go index 33255b84056..29c90035441 100644 --- a/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go +++ b/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go @@ -47,7 +47,7 @@ func TestReorgInvalidExecMsgs(gt *testing.T) { } func testReorgInvalidExecMsg(gt *testing.T, txModifierFn func(msg *suptypes.Message)) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) ctx := t.Ctx() sys := presets.NewSimpleInterop(t) diff --git a/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go b/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go index 51e7e5ed0c6..da35684ad56 100644 --- a/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go +++ b/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go @@ -58,7 +58,7 @@ func TestL2ReorgAfterL1Reorg(gt *testing.T) { // op-e2e/e2eutils/geth/geth.go when initialising FakePoS) // pre- and post-checks are sanity checks to ensure that the blocks we expected to be reorged were indeed reorged or not func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksFunc) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) ctx := t.Ctx() sys := presets.NewSimpleInterop(t) diff --git a/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go b/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go index 26f12f4c97a..5272fa599de 100644 --- a/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go +++ b/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go @@ -16,7 +16,7 @@ import ( // TestReorgUnsafeHead starts an interop chain with an op-test-sequencer, which takes control over sequencing the L2 chain and introduces a reorg on the unsafe head func TestReorgUnsafeHead(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) ctx := t.Ctx() sys := presets.NewSimpleInterop(t) diff --git a/op-acceptance-tests/tests/interop/seqwindow/expiry_test.go b/op-acceptance-tests/tests/interop/seqwindow/expiry_test.go index e50f77c846f..0e79ecf37a6 100644 --- a/op-acceptance-tests/tests/interop/seqwindow/expiry_test.go +++ b/op-acceptance-tests/tests/interop/seqwindow/expiry_test.go @@ -22,7 +22,7 @@ import ( // This test can take 3 minutes to run. func TestSequencingWindowExpiry(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t, presets.WithDeployerOptions(sysgo.WithSequencingWindow(10)), diff --git a/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go index 881f834f428..33466c9b518 100644 --- a/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go +++ b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go @@ -14,14 +14,14 @@ import ( func TestInteropSystemNoop(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) _ = presets.NewMinimal(t) t.Log("noop") } func TestSmokeTest(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() ctx := t.Ctx() @@ -57,7 +57,7 @@ func TestSmokeTest(gt *testing.T) { func TestSmokeTestFailure(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() ctx := t.Ctx() diff --git a/op-acceptance-tests/tests/interop/smoke/smoke_test.go b/op-acceptance-tests/tests/interop/smoke/smoke_test.go index 749d66cd2b0..d3389304f34 100644 --- a/op-acceptance-tests/tests/interop/smoke/smoke_test.go +++ b/op-acceptance-tests/tests/interop/smoke/smoke_test.go @@ -19,7 +19,7 @@ import ( // This demonstrates the usage of DSL for contract bindings func TestWrapETH(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) require := t.Require() sys := presets.NewMinimal(t) diff --git a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go index 9e8d8af79ab..a7f402caeb3 100644 --- a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go @@ -16,7 +16,7 @@ import ( // L2CL ahead of supervisor, aka supervisor needs to reset the L2CL, to reproduce old data. Currently supervisor has only indexing mode implemented, so the supervisor will ask the L2CL to reset back. func TestL2CLAheadOfSupervisor(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMultiSupervisorInterop(t) logger := sys.Log.With("Test", "TestL2CLAheadOfSupervisor") @@ -136,7 +136,7 @@ func TestL2CLAheadOfSupervisor(gt *testing.T) { func TestUnsafeChainKnownToL2CL(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMultiSupervisorInterop(t) logger := sys.Log.With("Test", "TestUnsafeChainKnownToL2CL") @@ -207,7 +207,7 @@ func TestUnsafeChainKnownToL2CL(gt *testing.T) { func TestUnsafeChainUnknownToL2CL(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMultiSupervisorInterop(t) logger := sys.Log.With("Test", "TestUnsafeChainUnknownToL2CL") @@ -248,7 +248,7 @@ func TestUnsafeChainUnknownToL2CL(gt *testing.T) { // Tests started/restarted L2CL advances unsafe head via P2P connection. func TestL2CLSyncP2P(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMultiSupervisorInterop(t) logger := sys.Log.With("Test", "TestL2CLSyncP2P") diff --git a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go index 6e71100daaf..8c5ddd0321b 100644 --- a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go @@ -15,7 +15,7 @@ import ( // Resync is only possible when supervisor and L2CL reconnects. func TestL2CLResync(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) logger := sys.Log.With("Test", "TestL2CLResync") diff --git a/op-acceptance-tests/tests/interop/upgrade-no-supervisor/interop_contracts_test.go b/op-acceptance-tests/tests/interop/upgrade-no-supervisor/interop_contracts_test.go new file mode 100644 index 00000000000..4dcb491e98d --- /dev/null +++ b/op-acceptance-tests/tests/interop/upgrade-no-supervisor/interop_contracts_test.go @@ -0,0 +1,84 @@ +package upgrade + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + "github.com/ethereum-optimism/optimism/op-core/forks" + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum/go-ethereum/common" +) + +// TestInteropContractsDeployed verifies that interop contracts are deployed at genesis +// without any supervisor running. +// Note: CrossL2Inbox is only deployed when there are 2+ chains in the dependency set. +// This test uses a single-chain setup, so only L2ToL2CrossDomainMessenger is deployed. +func TestInteropContractsDeployed(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewMinimalInteropNoSupervisor(t) + require := t.Require() + logger := t.Logger() + + // Wait for interop activation - for interop at genesis this is block 0, + // but the upgrade transactions run in the first block + activationBlock := sys.L2Chain.AwaitActivation(t, forks.Interop) + logger.Info("interop activated", "block", activationBlock.Number, "hash", activationBlock.Hash) + + client := sys.L2EL.Escape().L2EthClient() + + // Verify L2ToL2CrossDomainMessenger is deployed + // (CrossL2Inbox is only deployed when there are 2+ chains in dependency set) + implAddrBytes, err := client.GetStorageAt(t.Ctx(), predeploys.L2toL2CrossDomainMessengerAddr, + genesis.ImplementationSlot, activationBlock.Hash.String()) + require.NoError(err) + implAddr := common.BytesToAddress(implAddrBytes[:]) + require.NotEqual(common.Address{}, implAddr, "L2ToL2CrossDomainMessenger should have implementation at %s", + predeploys.L2toL2CrossDomainMessengerAddr) + + // Verify the implementation has code + code, err := client.CodeAtHash(t.Ctx(), implAddr, activationBlock.Hash) + require.NoError(err) + require.NotEmpty(code, "L2ToL2CrossDomainMessenger implementation should have code") + + logger.Info("interop contracts deployed successfully without supervisor") +} + +// TestLocalFinalityWithoutSupervisor verifies that local finality and promotion work +// correctly when interop is active but no supervisor is running. +func TestLocalFinalityWithoutSupervisor(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewMinimalInteropNoSupervisor(t) + require := t.Require() + logger := t.Logger() + + targetBlocks := uint64(5) + + for i := 0; i < 30; i++ { + time.Sleep(time.Second * 2) + + status := sys.L2CL.SyncStatus() + require.NotNil(status) + + logger.Info("chain status", + "unsafe", status.UnsafeL2.Number, + "safe", status.SafeL2.Number, + "finalized", status.FinalizedL2.Number, + ) + + // Without supervisor, local promotion should work: + // - UnsafeL2 should advance (sequencer producing blocks) + // - SafeL2 should advance (after batches submitted to L1) + + if status.UnsafeL2.Number >= targetBlocks && + status.SafeL2.Number >= targetBlocks-2 { + logger.Info("local finality working without supervisor!") + return + } + } + + gt.Errorf("Expected unsafe >= %d and safe >= %d", targetBlocks, targetBlocks-2) + gt.FailNow() +} diff --git a/op-acceptance-tests/tests/interop/upgrade/post_test.go b/op-acceptance-tests/tests/interop/upgrade/post_test.go index 3a7e056bb98..43538addb2f 100644 --- a/op-acceptance-tests/tests/interop/upgrade/post_test.go +++ b/op-acceptance-tests/tests/interop/upgrade/post_test.go @@ -41,7 +41,7 @@ func TestPostInbox(gt *testing.T) { func TestPostInteropUpgradeComprehensive(gt *testing.T) { gt.Skip("Skipping Interop Acceptance Test") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSimpleInterop(t) require := t.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/isthmus/erc20_bridge/erc20_bridge_test.go b/op-acceptance-tests/tests/isthmus/erc20_bridge/erc20_bridge_test.go index 1dd7c0047ed..7fb8f8138a6 100644 --- a/op-acceptance-tests/tests/isthmus/erc20_bridge/erc20_bridge_test.go +++ b/op-acceptance-tests/tests/isthmus/erc20_bridge/erc20_bridge_test.go @@ -14,7 +14,7 @@ import ( ) func TestERC20Bridge(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() diff --git a/op-acceptance-tests/tests/isthmus/operator_fee/operator_fee_test.go b/op-acceptance-tests/tests/isthmus/operator_fee/operator_fee_test.go index ba4d10aeae7..cfc38b1dd01 100644 --- a/op-acceptance-tests/tests/isthmus/operator_fee/operator_fee_test.go +++ b/op-acceptance-tests/tests/isthmus/operator_fee/operator_fee_test.go @@ -10,7 +10,7 @@ import ( ) func TestOperatorFee(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) t.Require().True(sys.L2Chain.IsForkActive(forks.Isthmus), "Isthmus fork must be active for this test") dsl.RunOperatorFeeTest(t, sys.L2Chain, sys.L1EL, sys.FunderL1, sys.FunderL2) diff --git a/op-acceptance-tests/tests/isthmus/pectra/pectra_features_test.go b/op-acceptance-tests/tests/isthmus/pectra/pectra_features_test.go index ac2403a7102..c7a77c104cf 100644 --- a/op-acceptance-tests/tests/isthmus/pectra/pectra_features_test.go +++ b/op-acceptance-tests/tests/isthmus/pectra/pectra_features_test.go @@ -49,7 +49,7 @@ func newSystem(t devtest.T) *testSystem { } func TestPectra(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSystem(t) alice := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) diff --git a/op-acceptance-tests/tests/isthmus/preinterop-singlechain/interop_fault_proofs_test.go b/op-acceptance-tests/tests/isthmus/preinterop-singlechain/interop_fault_proofs_test.go index 50cc8d0f89e..12219be1fdd 100644 --- a/op-acceptance-tests/tests/isthmus/preinterop-singlechain/interop_fault_proofs_test.go +++ b/op-acceptance-tests/tests/isthmus/preinterop-singlechain/interop_fault_proofs_test.go @@ -9,7 +9,7 @@ import ( ) func TestPreinteropSingleChainFaultProofs(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSingleChainInteropIsthmusSuper( t, presets.WithChallengerCannonKonaEnabled(), diff --git a/op-acceptance-tests/tests/isthmus/preinterop/interop_fault_proofs_test.go b/op-acceptance-tests/tests/isthmus/preinterop/interop_fault_proofs_test.go index f1f82060736..d5394f0a756 100644 --- a/op-acceptance-tests/tests/isthmus/preinterop/interop_fault_proofs_test.go +++ b/op-acceptance-tests/tests/isthmus/preinterop/interop_fault_proofs_test.go @@ -8,19 +8,19 @@ import ( ) func TestPreinteropFaultProofs(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSimpleInteropPreinterop(t) sfp.RunSuperFaultProofTest(t, sys) } func TestPreinteropFaultProofs_TraceExtensionActivation(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSimpleInteropPreinterop(t) sfp.RunTraceExtensionActivationTest(t, sys) } func TestPreinteropFaultProofs_UnsafeProposal(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSimpleInteropPreinterop(t) sfp.RunUnsafeProposalTest(t, sys) } diff --git a/op-acceptance-tests/tests/isthmus/preinterop/proposer_test.go b/op-acceptance-tests/tests/isthmus/preinterop/proposer_test.go index 91310295cef..a61b94c2495 100644 --- a/op-acceptance-tests/tests/isthmus/preinterop/proposer_test.go +++ b/op-acceptance-tests/tests/isthmus/preinterop/proposer_test.go @@ -7,8 +7,7 @@ import ( ) func TestProposer(gt *testing.T) { - gt.Skip("TODO(#16166): Re-enable once the supervisor endpoint supports super roots before interop") - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSimpleInteropPreinterop(t) dgf := sys.DisputeGameFactory() @@ -16,7 +15,5 @@ func TestProposer(gt *testing.T) { newGame := dgf.WaitForGame() rootClaim := newGame.RootClaim().Value() l2SequenceNumber := newGame.L2SequenceNumber() - - superRoot := sys.Supervisor.FetchSuperRootAtTimestamp(l2SequenceNumber) - t.Require().Equal(superRoot.SuperRoot[:], rootClaim[:]) + sys.SuperRoots.AssertSuperRootAtTimestamp(l2SequenceNumber, rootClaim) } diff --git a/op-acceptance-tests/tests/isthmus/withdrawal_root/withdrawals_root_test.go b/op-acceptance-tests/tests/isthmus/withdrawal_root/withdrawals_root_test.go index a639c944405..f50f3ed41b1 100644 --- a/op-acceptance-tests/tests/isthmus/withdrawal_root/withdrawals_root_test.go +++ b/op-acceptance-tests/tests/isthmus/withdrawal_root/withdrawals_root_test.go @@ -12,7 +12,7 @@ import ( ) func TestWithdrawalRoot(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) // Skip this test if CGT is enabled diff --git a/op-acceptance-tests/tests/jovian/bpo2/bpo2_test.go b/op-acceptance-tests/tests/jovian/bpo2/bpo2_test.go index c4349254b9c..6f232b7a191 100644 --- a/op-acceptance-tests/tests/jovian/bpo2/bpo2_test.go +++ b/op-acceptance-tests/tests/jovian/bpo2/bpo2_test.go @@ -12,9 +12,8 @@ import ( ) func setupBPO2(t devtest.T) *presets.Minimal { - resetEnvVars := fusaka.ConfigureDevstackEnvVars() - t.Cleanup(resetEnvVars) return presets.NewMinimal(t, + fusaka.L1GethOption(), presets.WithDeployerOptions( sysgo.WithJovianAtGenesis, sysgo.WithDefaultBPOBlobSchedule, diff --git a/op-acceptance-tests/tests/jovian/bpo2/joviantest/cases.go b/op-acceptance-tests/tests/jovian/bpo2/joviantest/cases.go index d5e3bcf7914..03f6ed3146d 100644 --- a/op-acceptance-tests/tests/jovian/bpo2/joviantest/cases.go +++ b/op-acceptance-tests/tests/jovian/bpo2/joviantest/cases.go @@ -218,7 +218,7 @@ func (mbf *minBaseFeeEnv) waitForMinBaseFeeConfigChangeOnL2(t devtest.T, expecte } func RunDAFootprint(gt *testing.T, setup SetupFn) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := setup(t) require := t.Require() @@ -354,7 +354,7 @@ func RunDAFootprint(gt *testing.T, setup SetupFn) { } func RunMinBaseFee(gt *testing.T, setup SetupFn) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := setup(t) require := t.Require() @@ -393,7 +393,7 @@ func RunMinBaseFee(gt *testing.T, setup SetupFn) { } func RunOperatorFee(gt *testing.T, setup SetupFn) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := setup(t) t.Require().True(sys.L2Chain.IsForkActive(opforks.Jovian), "Jovian fork must be active for this test") dsl.RunOperatorFeeTest(t, sys.L2Chain, sys.L1EL, sys.FunderL1, sys.FunderL2) diff --git a/op-acceptance-tests/tests/jovian/da_footprint.go b/op-acceptance-tests/tests/jovian/da_footprint.go index 448ca65b837..e94c7017da4 100644 --- a/op-acceptance-tests/tests/jovian/da_footprint.go +++ b/op-acceptance-tests/tests/jovian/da_footprint.go @@ -117,7 +117,7 @@ func (env *daFootprintEnv) expectL1BlockDAFootprintGasScalar(t devtest.T, expect } func TestDAFootprint(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() diff --git a/op-acceptance-tests/tests/jovian/min_base_fee.go b/op-acceptance-tests/tests/jovian/min_base_fee.go index c6a636365a8..2084925d83d 100644 --- a/op-acceptance-tests/tests/jovian/min_base_fee.go +++ b/op-acceptance-tests/tests/jovian/min_base_fee.go @@ -118,7 +118,7 @@ func (mbf *minBaseFeeEnv) waitForMinBaseFeeConfigChangeOnL2(t devtest.T, expecte // TestMinBaseFee verifies configurable minimum base fee using devstack presets. func TestMinBaseFee(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) require := t.Require() diff --git a/op-acceptance-tests/tests/jovian/operator_fee.go b/op-acceptance-tests/tests/jovian/operator_fee.go index d3e5ebb0137..294228d336f 100644 --- a/op-acceptance-tests/tests/jovian/operator_fee.go +++ b/op-acceptance-tests/tests/jovian/operator_fee.go @@ -10,7 +10,7 @@ import ( ) func TestOperatorFee(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewMinimal(t) t.Require().True(sys.L2Chain.IsForkActive(forks.Jovian), "Jovian fork must be active for this test") dsl.RunOperatorFeeTest(t, sys.L2Chain, sys.L1EL, sys.FunderL1, sys.FunderL2) diff --git a/op-acceptance-tests/tests/proofs/cannon/smoke_test.go b/op-acceptance-tests/tests/proofs/cannon/smoke_test.go index 22d6517d01e..fbc7f21d974 100644 --- a/op-acceptance-tests/tests/proofs/cannon/smoke_test.go +++ b/op-acceptance-tests/tests/proofs/cannon/smoke_test.go @@ -9,7 +9,7 @@ import ( ) func TestSmoke(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSystem(t) require := t.Require() dgf := sys.DisputeGameFactory() diff --git a/op-acceptance-tests/tests/rules/init_test.go b/op-acceptance-tests/tests/rules/init_test.go deleted file mode 100644 index 2f2f5889ef0..00000000000 --- a/op-acceptance-tests/tests/rules/init_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package rules - -import ( - "os" - - "github.com/ethereum-optimism/optimism/op-devstack/devtest" - "github.com/ethereum-optimism/optimism/op-devstack/presets" - "github.com/ethereum/go-ethereum/common" -) - -const RULES_TEST_ENABLE_ENV = "OP_RBUILDER_RULES_TEST" - -func rulesEnabled() bool { - return os.Getenv(RULES_TEST_ENABLE_ENV) == "1" -} - -func newRulesSystem(t devtest.T) *presets.SingleChainWithFlashblocks { - return presets.NewSingleChainWithFlashblocks(t, presets.WithOPRBuilderRules(TestRulesYAML, TestRefreshInterval)) -} - -// BoostedRecipient is the well-known address that receives boosted transactions in tests. -// Transactions sent TO this address will be prioritized by the block builder when rules are enabled. -var BoostedRecipient = common.HexToAddress("0x1111111111111111111111111111111111111111") - -// HighPriorityRecipient receives transactions with the highest boost (weight: 5000) -var HighPriorityRecipient = common.HexToAddress("0x2222222222222222222222222222222222222222") - -// MediumPriorityRecipient receives transactions with medium boost (weight: 2000) -var MediumPriorityRecipient = common.HexToAddress("0x3333333333333333333333333333333333333333") - -// LowPriorityRecipient receives transactions with low boost (weight: 500) -var LowPriorityRecipient = common.HexToAddress("0x4444444444444444444444444444444444444444") - -const TestRefreshInterval = 5 - -// TestRulesYAML is the rules configuration used for rule ordering tests. -// It defines multiple boost levels to test priority ordering: -// - High priority (weight 5000): transactions TO 0x2222... -// - Medium priority (weight 2000): transactions TO 0x3333... -// - Low priority (weight 500): transactions TO 0x4444... -// - Legacy boost (weight 1000): transactions TO 0x1111... (BoostedRecipient) -const TestRulesYAML = `version: 1 - -aliases: - high_priority_recipients: - - "0x2222222222222222222222222222222222222222" - medium_priority_recipients: - - "0x3333333333333333333333333333333333333333" - low_priority_recipients: - - "0x4444444444444444444444444444444444444444" - boosted_recipients: - - "0x1111111111111111111111111111111111111111" - -rules: - boost: - - name: "High Priority Boost" - description: "Highest priority transactions" - type: to - aliases: - - "high_priority_recipients" - weight: 5000 - - name: "Medium Priority Boost" - description: "Medium priority transactions" - type: to - aliases: - - "medium_priority_recipients" - weight: 2000 - - name: "Low Priority Boost" - description: "Low priority transactions" - type: to - aliases: - - "low_priority_recipients" - weight: 500 - - name: "Legacy Boosted Recipient" - description: "Boost transactions to test recipient address" - type: to - aliases: - - "boosted_recipients" - weight: 1000 -` diff --git a/op-acceptance-tests/tests/rules/rules_test.go b/op-acceptance-tests/tests/rules/rules_test.go deleted file mode 100644 index 70de70471e1..00000000000 --- a/op-acceptance-tests/tests/rules/rules_test.go +++ /dev/null @@ -1,694 +0,0 @@ -package rules - -import ( - "context" - "fmt" - "math/big" - "math/rand" - "sort" - "sync" - "testing" - "time" - - "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/flashblocks" - "github.com/ethereum-optimism/optimism/op-devstack/devtest" - "github.com/ethereum-optimism/optimism/op-devstack/dsl" - "github.com/ethereum-optimism/optimism/op-service/bigs" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/retry" - "github.com/ethereum-optimism/optimism/op-service/txplan" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/require" -) - -// TestBoostPriorityOrdering validates that transactions to addresses with higher -// boost weights appear earlier in blocks than transactions to lower-weight addresses. -// -// Rules configuration (from init_test.go): -// - HighPriorityRecipient (0x2222...): weight 5000 -// - MediumPriorityRecipient (0x3333...): weight 2000 -// - LowPriorityRecipient (0x4444...): weight 500 -// - No boost for other addresses: weight 0 -// -// Expected ordering: High (5000) > Medium (2000) > Low (500) > Normal (0) -// We verify this by checking TransactionIndex in the block - lower index = earlier in block. -func TestBoostPriorityOrdering(gt *testing.T) { - t := devtest.SerialT(gt) - skipIfRulesNotEnabled(t) - - logger := t.Logger() - tracer := t.Tracer() - ctx := t.Ctx() - - sys := newRulesSystem(t) - - topLevelCtx, span := tracer.Start(ctx, "test boost priority ordering") - defer span.End() - - ctx, cancel := context.WithTimeout(topLevelCtx, 90*time.Second) - defer cancel() - - flashblocks.DriveViaTestSequencer(t, sys, 3) - - const maxRetries = 3 - err := retry.Do0(ctx, maxRetries, &retry.FixedStrategy{Dur: 0}, func() error { - fundAmount := eth.Ether(1) - senderHigh := sys.FunderL2.NewFundedEOA(fundAmount) - senderMedium := sys.FunderL2.NewFundedEOA(fundAmount) - senderLow := sys.FunderL2.NewFundedEOA(fundAmount) - senderNormal := sys.FunderL2.NewFundedEOA(fundAmount) - - normalRecipient := sys.Wallet.NewEOA(sys.L2EL) - normalRecipientAddr := normalRecipient.Address() - - sendAmount := eth.OneHundredthEther - var wg sync.WaitGroup - var txHigh, txMedium, txLow, txNormal *txplan.PlannedTx - - wg.Add(4) - go func() { - defer wg.Done() - txHigh = senderHigh.Transact( - senderHigh.Plan(), - txplan.WithTo(&HighPriorityRecipient), - txplan.WithValue(sendAmount), - ) - }() - go func() { - defer wg.Done() - txMedium = senderMedium.Transact( - senderMedium.Plan(), - txplan.WithTo(&MediumPriorityRecipient), - txplan.WithValue(sendAmount), - ) - }() - go func() { - defer wg.Done() - txLow = senderLow.Transact( - senderLow.Plan(), - txplan.WithTo(&LowPriorityRecipient), - txplan.WithValue(sendAmount), - ) - }() - go func() { - defer wg.Done() - txNormal = senderNormal.Transact( - senderNormal.Plan(), - txplan.WithTo(&normalRecipientAddr), - txplan.WithValue(sendAmount), - ) - }() - wg.Wait() - - receiptHigh, err := txHigh.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("high priority tx inclusion: %w", err) - } - receiptMedium, err := txMedium.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("medium priority tx inclusion: %w", err) - } - receiptLow, err := txLow.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("low priority tx inclusion: %w", err) - } - receiptNormal, err := txNormal.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("normal tx inclusion: %w", err) - } - - logger.Info("All transactions confirmed", - "high_block", receiptHigh.BlockNumber, "high_index", receiptHigh.TransactionIndex, - "medium_block", receiptMedium.BlockNumber, "medium_index", receiptMedium.TransactionIndex, - "low_block", receiptLow.BlockNumber, "low_index", receiptLow.TransactionIndex, - "normal_block", receiptNormal.BlockNumber, "normal_index", receiptNormal.TransactionIndex, - ) - - sameBlock := receiptHigh.BlockNumber.Cmp(receiptMedium.BlockNumber) == 0 && - receiptMedium.BlockNumber.Cmp(receiptLow.BlockNumber) == 0 && - receiptLow.BlockNumber.Cmp(receiptNormal.BlockNumber) == 0 - - if !sameBlock { - return fmt.Errorf("transactions landed in different blocks: high=%d, medium=%d, low=%d, normal=%d", - receiptHigh.BlockNumber, receiptMedium.BlockNumber, receiptLow.BlockNumber, receiptNormal.BlockNumber) - } - - require.Less(t, receiptHigh.TransactionIndex, receiptMedium.TransactionIndex, - "high priority (weight 5000) should have lower tx index than medium priority (weight 2000)") - require.Less(t, receiptMedium.TransactionIndex, receiptLow.TransactionIndex, - "medium priority (weight 2000) should have lower tx index than low priority (weight 500)") - require.Less(t, receiptLow.TransactionIndex, receiptNormal.TransactionIndex, - "low priority (weight 500) should have lower tx index than normal (no boost)") - - logger.Info("Boost priority ordering verified successfully", - "order", fmt.Sprintf("high(idx=%d) < medium(idx=%d) < low(idx=%d) < normal(idx=%d)", - receiptHigh.TransactionIndex, receiptMedium.TransactionIndex, - receiptLow.TransactionIndex, receiptNormal.TransactionIndex), - ) - return nil - }) - require.NoError(t, err, "boost priority ordering verification failed") -} - -// TestBoostedVsNonBoostedOrdering validates that a boosted transaction appears before -// a non-boosted transaction even when the non-boosted transaction has a MUCH HIGHER -// priority fee (gas tip). This proves that rule-based boost takes precedence over -// economic incentives (EIP-1559 priority fees). -// -// Test setup: -// - Boosted tx: to BoostedRecipient (weight 1000), LOW priority fee (1 gwei tip) -// - Normal tx: to normal recipient (no boost), HIGH priority fee (100 gwei tip) -// -// Expected: Despite 100x higher gas tip, the normal tx should come AFTER the boosted tx. -func TestBoostedVsNonBoostedOrdering(gt *testing.T) { - t := devtest.SerialT(gt) - skipIfRulesNotEnabled(t) - - logger := t.Logger() - tracer := t.Tracer() - ctx := t.Ctx() - - sys := newRulesSystem(t) - - topLevelCtx, span := tracer.Start(ctx, "test boosted vs non-boosted ordering") - defer span.End() - - ctx, cancel := context.WithTimeout(topLevelCtx, 90*time.Second) - defer cancel() - - flashblocks.DriveViaTestSequencer(t, sys, 2) - - lowGasTip := big.NewInt(1_000_000_000) - highGasTip := big.NewInt(100_000_000_000) - highGasFeeCap := big.NewInt(200_000_000_000) - - const maxRetries = 3 - err := retry.Do0(ctx, maxRetries, &retry.FixedStrategy{Dur: 0}, func() error { - fundAmount := eth.ThreeHundredthsEther - senderBoosted := sys.FunderL2.NewFundedEOA(fundAmount) - senderNormal := sys.FunderL2.NewFundedEOA(fundAmount) - - normalRecipient := sys.Wallet.NewEOA(sys.L2EL) - normalRecipientAddr := normalRecipient.Address() - - sendAmount := eth.OneHundredthEther - var wg sync.WaitGroup - var txBoosted, txNormal *txplan.PlannedTx - - wg.Add(2) - go func() { - defer wg.Done() - txBoosted = senderBoosted.Transact( - senderBoosted.Plan(), - txplan.WithTo(&BoostedRecipient), - txplan.WithValue(sendAmount), - txplan.WithGasTipCap(lowGasTip), - ) - }() - go func() { - defer wg.Done() - txNormal = senderNormal.Transact( - senderNormal.Plan(), - txplan.WithTo(&normalRecipientAddr), - txplan.WithValue(sendAmount), - txplan.WithGasTipCap(highGasTip), - txplan.WithGasFeeCap(highGasFeeCap), - ) - }() - wg.Wait() - - receiptBoosted, err := txBoosted.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("boosted tx inclusion: %w", err) - } - receiptNormal, err := txNormal.Included.Eval(ctx) - if err != nil { - return fmt.Errorf("normal tx inclusion: %w", err) - } - - logger.Info("Transactions confirmed", - "boosted_block", receiptBoosted.BlockNumber, - "boosted_index", receiptBoosted.TransactionIndex, - "normal_block", receiptNormal.BlockNumber, - "normal_index", receiptNormal.TransactionIndex, - ) - - if receiptBoosted.BlockNumber.Cmp(receiptNormal.BlockNumber) != 0 { - return fmt.Errorf("transactions landed in different blocks: boosted=%d, normal=%d", - receiptBoosted.BlockNumber, receiptNormal.BlockNumber) - } - - require.Less(t, receiptBoosted.TransactionIndex, receiptNormal.TransactionIndex, - "boosted transaction (weight 1000, 1 gwei tip) should have lower tx index than "+ - "normal transaction (no boost, 100 gwei tip) - proving rules > gas priority") - - logger.Info("Rule-based boost precedence over gas priority verified!", - "boosted_index", receiptBoosted.TransactionIndex, - "normal_index", receiptNormal.TransactionIndex, - ) - return nil - }) - require.NoError(t, err, "boosted vs non-boosted ordering verification failed") -} - -// TestSameSenderNonceOrdering verifies that transactions from the same sender -// maintain nonce ordering regardless of boost rules. -func TestSameSenderNonceOrdering(gt *testing.T) { - t := devtest.SerialT(gt) - skipIfRulesNotEnabled(t) - - logger := t.Logger() - tracer := t.Tracer() - ctx := t.Ctx() - - sys := newRulesSystem(t) - - topLevelCtx, span := tracer.Start(ctx, "test same sender nonce ordering") - defer span.End() - - ctx, cancel := context.WithTimeout(topLevelCtx, 60*time.Second) - defer cancel() - - // Drive initial blocks - flashblocks.DriveViaTestSequencer(t, sys, 2) - - // Create a single funded sender - sender := sys.FunderL2.NewFundedEOA(eth.Ether(1)) - - // Create normal recipient - normalRecipient := sys.Wallet.NewEOA(sys.L2EL) - normalRecipientAddr := normalRecipient.Address() - - logger.Info("Test sender created", "address", sender.Address().Hex()) - - sendAmount := eth.OneHundredthEther - - // Send 3 sequential transactions from same sender to different recipients - // TX0 -> Normal recipient (no boost) - // TX1 -> HighPriorityRecipient (weight 5000) - // TX2 -> Normal recipient (no boost) - // - // Even though TX1 has the highest boost, it must come after TX0 due to nonce ordering - - // TX0: to normal recipient - tx0 := sender.Transact( - sender.Plan(), - txplan.WithTo(&normalRecipientAddr), - txplan.WithValue(sendAmount), - ) - receipt0, err := tx0.Included.Eval(ctx) - require.NoError(t, err, "tx0 should be included") - - // TX1: to high priority recipient - tx1 := sender.Transact( - sender.Plan(), - txplan.WithTo(&HighPriorityRecipient), - txplan.WithValue(sendAmount), - ) - receipt1, err := tx1.Included.Eval(ctx) - require.NoError(t, err, "tx1 should be included") - - // TX2: to normal recipient - tx2 := sender.Transact( - sender.Plan(), - txplan.WithTo(&normalRecipientAddr), - txplan.WithValue(sendAmount), - ) - receipt2, err := tx2.Included.Eval(ctx) - require.NoError(t, err, "tx2 should be included") - - logger.Info("Sequential transactions confirmed", - "tx0_hash", receipt0.TxHash.Hex(), "tx0_block", receipt0.BlockNumber, "tx0_index", receipt0.TransactionIndex, - "tx1_hash", receipt1.TxHash.Hex(), "tx1_block", receipt1.BlockNumber, "tx1_index", receipt1.TransactionIndex, - "tx2_hash", receipt2.TxHash.Hex(), "tx2_block", receipt2.BlockNumber, "tx2_index", receipt2.TransactionIndex, - ) - - // Verify nonce ordering is preserved - // If transactions are in the same block, their indices must reflect nonce order - if receipt0.BlockNumber.Cmp(receipt1.BlockNumber) == 0 { - require.Less(t, receipt0.TransactionIndex, receipt1.TransactionIndex, - "tx0 (nonce N) must have lower index than tx1 (nonce N+1) despite tx1 having higher boost") - } else { - // If in different blocks, tx0's block must be <= tx1's block - require.LessOrEqual(t, - bigs.Uint64Strict(receipt0.BlockNumber), - bigs.Uint64Strict(receipt1.BlockNumber), - "tx0 must be in same or earlier block than tx1", - ) - } - - if receipt1.BlockNumber.Cmp(receipt2.BlockNumber) == 0 { - require.Less(t, receipt1.TransactionIndex, receipt2.TransactionIndex, - "tx1 (nonce N+1) must have lower index than tx2 (nonce N+2)") - } else { - require.LessOrEqual(t, - bigs.Uint64Strict(receipt1.BlockNumber), - bigs.Uint64Strict(receipt2.BlockNumber), - "tx1 must be in same or earlier block than tx2", - ) - } - - logger.Info("Nonce ordering verified - boost rules do not break same-sender ordering") -} - -// TestMultipleSendersWithMixedPriorities tests a realistic scenario with multiple -// senders sending to different priority recipients concurrently. -func TestMultipleSendersWithMixedPriorities(gt *testing.T) { - t := devtest.SerialT(gt) - skipIfRulesNotEnabled(t) - - logger := t.Logger() - tracer := t.Tracer() - ctx := t.Ctx() - - sys := newRulesSystem(t) - - topLevelCtx, span := tracer.Start(ctx, "test multiple senders mixed priorities") - defer span.End() - - ctx, cancel := context.WithTimeout(topLevelCtx, 120*time.Second) - defer cancel() - - flashblocks.DriveViaTestSequencer(t, sys, 2) - - type senderConfig struct { - eoa *dsl.EOA - priority string - recipient common.Address - weight int - } - - type txResult struct { - receipt *types.Receipt - priority string - weight int - } - - const maxRetries = 3 - err := retry.Do0(ctx, maxRetries, &retry.FixedStrategy{Dur: 0}, func() error { - fundAmount := eth.ThreeHundredthsEther - - normalRecipient := sys.Wallet.NewEOA(sys.L2EL) - normalRecipientAddr := normalRecipient.Address() - - configs := []struct { - priority string - recipient common.Address - weight int - }{ - {"high", HighPriorityRecipient, 5000}, - {"medium", MediumPriorityRecipient, 2000}, - {"low", LowPriorityRecipient, 500}, - {"normal", normalRecipientAddr, 0}, - {"high", HighPriorityRecipient, 5000}, - {"normal", normalRecipientAddr, 0}, - } - - senders := make([]senderConfig, len(configs)) - for i, cfg := range configs { - senders[i] = senderConfig{ - eoa: sys.FunderL2.NewFundedEOA(fundAmount), - priority: cfg.priority, - recipient: cfg.recipient, - weight: cfg.weight, - } - } - - sendAmount := eth.OneHundredthEther - var wg sync.WaitGroup - plannedTxs := make([]*txplan.PlannedTx, len(senders)) - - for i := range senders { - wg.Add(1) - idx := i - go func() { - defer wg.Done() - recipient := senders[idx].recipient - plannedTxs[idx] = senders[idx].eoa.Transact( - senders[idx].eoa.Plan(), - txplan.WithTo(&recipient), - txplan.WithValue(sendAmount), - ) - }() - } - wg.Wait() - - results := make([]txResult, len(senders)) - for i := range senders { - receipt, err := plannedTxs[i].Included.Eval(ctx) - if err != nil { - return fmt.Errorf("tx%d inclusion: %w", i, err) - } - results[i] = txResult{ - receipt: receipt, - priority: senders[i].priority, - weight: senders[i].weight, - } - logger.Info("Transaction confirmed", - "index", i, - "priority", senders[i].priority, - "weight", senders[i].weight, - "block", receipt.BlockNumber, - "tx_index", receipt.TransactionIndex, - ) - } - - blockGroups := make(map[uint64][]txResult) - for _, r := range results { - blockNum := bigs.Uint64Strict(r.receipt.BlockNumber) - blockGroups[blockNum] = append(blockGroups[blockNum], r) - } - - if len(blockGroups) != 1 { - blockNumbers := make([]uint64, 0, len(blockGroups)) - for blockNum := range blockGroups { - blockNumbers = append(blockNumbers, blockNum) - } - return fmt.Errorf("transactions landed in %d different blocks: %v", len(blockGroups), blockNumbers) - } - - for blockNum, txs := range blockGroups { - logger.Info("Verifying ordering in block", "block", blockNum, "tx_count", len(txs)) - - for i := 0; i < len(txs); i++ { - for j := i + 1; j < len(txs); j++ { - if txs[i].weight > txs[j].weight { - require.Less(t, txs[i].receipt.TransactionIndex, txs[j].receipt.TransactionIndex, - "tx with weight %d should have lower index than tx with weight %d in block %d", - txs[i].weight, txs[j].weight, blockNum) - } else if txs[i].weight < txs[j].weight { - require.Greater(t, txs[i].receipt.TransactionIndex, txs[j].receipt.TransactionIndex, - "tx with weight %d should have higher index than tx with weight %d in block %d", - txs[i].weight, txs[j].weight, blockNum) - } - } - } - } - - logger.Info("Multiple senders mixed priorities test completed successfully") - return nil - }) - require.NoError(t, err, "multiple senders mixed priorities verification failed") -} - -// TestSingleSenderRandomNonceOrderWithRandomScores sends 10 transactions from a single sender -// with explicit nonces, random gas tips, and random boost recipients. The transactions are -// submitted to the mempool in a shuffled nonce order to verify that op-rbuilder correctly -// sorts by transaction score while still respecting nonce ordering for the same sender. -// -// Setup: -// - 1 sender, 10 transactions (nonces base+0 through base+9) -// - Each tx gets a random gas tip (1-50 gwei) AND a random recipient from -// {HighPriority, MediumPriority, LowPriority, Normal} for varied boost weights -// - Transactions are shuffled before submission so the mempool receives them out-of-order -// -// Expected: All 10 transactions are included with strict nonce ordering preserved, -// i.e. within the same block tx with nonce N always has a lower TransactionIndex -// than tx with nonce N+1, and across blocks lower nonces are in earlier blocks. -func TestSingleSenderRandomNonceOrderWithRandomScores(gt *testing.T) { - t := devtest.SerialT(gt) - skipIfRulesNotEnabled(t) - - logger := t.Logger() - tracer := t.Tracer() - ctx := t.Ctx() - - sys := newRulesSystem(t) - - topLevelCtx, span := tracer.Start(ctx, "test single sender random nonce order with random scores") - defer span.End() - - ctx, cancel := context.WithTimeout(topLevelCtx, 120*time.Second) - defer cancel() - - flashblocks.DriveViaTestSequencer(t, sys, 2) - - const txCount = 10 - - type recipientInfo struct { - addr common.Address - weight int - } - normalRecipient := sys.Wallet.NewEOA(sys.L2EL) - normalRecipientAddr := normalRecipient.Address() - recipients := []recipientInfo{ - {HighPriorityRecipient, 5000}, - {MediumPriorityRecipient, 2000}, - {LowPriorityRecipient, 500}, - {normalRecipientAddr, 0}, - } - - const maxRetries = 3 - err := retry.Do0(ctx, maxRetries, &retry.FixedStrategy{Dur: 0}, func() error { - sender := sys.FunderL2.NewFundedEOA(eth.Ether(1)) - baseNonce := sender.PendingNonce() - - logger.Info("Test sender created", - "address", sender.Address().Hex(), - "baseNonce", baseNonce, - ) - - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - - type txConfig struct { - nonce uint64 - tipGwei int64 - recipient recipientInfo - } - configs := make([]txConfig, txCount) - for i := 0; i < txCount; i++ { - tipGwei := int64(1 + rng.Intn(50)) - recip := recipients[rng.Intn(len(recipients))] - configs[i] = txConfig{ - nonce: baseNonce + uint64(i), - tipGwei: tipGwei, - recipient: recip, - } - logger.Info("Transaction config", - "index", i, - "nonce", configs[i].nonce, - "tipGwei", tipGwei, - "recipient", recip.addr.Hex(), - "boostWeight", recip.weight, - ) - } - - submitOrder := rng.Perm(txCount) - logger.Info("Shuffled submission order", "order", submitOrder) - - sendAmount := eth.OneHundredthEther - highFeeCap := new(big.Int).Mul(big.NewInt(200), big.NewInt(1_000_000_000)) - - plannedTxs := make([]*txplan.PlannedTx, txCount) - var wg sync.WaitGroup - for _, idx := range submitOrder { - wg.Add(1) - go func(i int) { - defer wg.Done() - cfg := configs[i] - tip := new(big.Int).Mul(big.NewInt(cfg.tipGwei), big.NewInt(1_000_000_000)) - recipAddr := cfg.recipient.addr - plannedTxs[i] = sender.Transact( - sender.Plan(), - txplan.WithStaticNonce(cfg.nonce), - txplan.WithTo(&recipAddr), - txplan.WithValue(sendAmount), - txplan.WithGasTipCap(tip), - txplan.WithGasFeeCap(highFeeCap), - ) - }(idx) - } - wg.Wait() - - type txResult struct { - nonce uint64 - tipGwei int64 - weight int - receipt *types.Receipt - } - results := make([]txResult, txCount) - for i := 0; i < txCount; i++ { - receipt, err := plannedTxs[i].Included.Eval(ctx) - if err != nil { - return fmt.Errorf("tx%d (nonce %d) inclusion: %w", i, configs[i].nonce, err) - } - results[i] = txResult{ - nonce: configs[i].nonce, - tipGwei: configs[i].tipGwei, - weight: configs[i].recipient.weight, - receipt: receipt, - } - logger.Info("Transaction confirmed", - "index", i, - "nonce", configs[i].nonce, - "tipGwei", configs[i].tipGwei, - "boostWeight", configs[i].recipient.weight, - "block", receipt.BlockNumber, - "txIndex", receipt.TransactionIndex, - ) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].nonce < results[j].nonce - }) - - // Count how many transactions share a block with at least one other tx. - // If every tx lands in its own block, nonce ordering is trivially - // guaranteed by the protocol and the test does not exercise the builder's - // intra-block ordering logic. Treat that as a retryable condition so the - // next attempt (with a fresh sender/nonces) hopefully lands more txs in - // the same block. - blockCounts := make(map[uint64]int) - for _, r := range results { - blockCounts[bigs.Uint64Strict(r.receipt.BlockNumber)]++ - } - maxPerBlock := 0 - for _, c := range blockCounts { - if c > maxPerBlock { - maxPerBlock = c - } - } - if maxPerBlock < 2 { - return fmt.Errorf("all %d transactions landed in separate blocks (%d blocks); "+ - "need at least 2 in the same block to validate intra-block nonce ordering", - txCount, len(blockCounts)) - } - - for i := 0; i < len(results)-1; i++ { - cur := results[i] - next := results[i+1] - - if cur.receipt.BlockNumber.Cmp(next.receipt.BlockNumber) == 0 { - require.Less(t, cur.receipt.TransactionIndex, next.receipt.TransactionIndex, - "nonce %d (tip=%d gwei, boost=%d, txIdx=%d) must have lower tx index than nonce %d (tip=%d gwei, boost=%d, txIdx=%d) in block %d", - cur.nonce, cur.tipGwei, cur.weight, cur.receipt.TransactionIndex, - next.nonce, next.tipGwei, next.weight, next.receipt.TransactionIndex, - cur.receipt.BlockNumber) - } else { - require.Less( - t, - bigs.Uint64Strict(cur.receipt.BlockNumber), - bigs.Uint64Strict(next.receipt.BlockNumber), - "nonce %d must be in an earlier block than nonce %d (got blocks %d and %d)", - cur.nonce, next.nonce, cur.receipt.BlockNumber, next.receipt.BlockNumber, - ) - } - } - - logger.Info("Single sender random nonce order test passed - nonce ordering preserved despite random scores and shuffled submission", - "txCount", txCount, - "blocksUsed", len(blockCounts), - "maxTxsInOneBlock", maxPerBlock, - "nonceRange", fmt.Sprintf("%d-%d", results[0].nonce, results[len(results)-1].nonce), - ) - return nil - }) - require.NoError(t, err, "single sender random nonce order with random scores verification failed") -} - -func skipIfRulesNotEnabled(t devtest.T) { - if !rulesEnabled() { - t.Skip("Skipping rule ordering test") - } -} diff --git a/op-acceptance-tests/tests/safeheaddb_clsync/safeheaddb_test.go b/op-acceptance-tests/tests/safeheaddb_clsync/safeheaddb_test.go index 31a07ed6c7c..da441258a0e 100644 --- a/op-acceptance-tests/tests/safeheaddb_clsync/safeheaddb_test.go +++ b/op-acceptance-tests/tests/safeheaddb_clsync/safeheaddb_test.go @@ -12,7 +12,7 @@ import ( ) func TestPreserveDatabaseOnCLResync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSingleChainMultiNode(t, presets.WithGlobalL2CLOption(sysgo.L2CLOptionFn(func(p devtest.T, _ sysgo.ComponentTarget, cfg *sysgo.L2CLConfig) { cfg.SequencerSyncMode = sync.CLSync diff --git a/op-acceptance-tests/tests/safeheaddb_elsync/safeheaddb_test.go b/op-acceptance-tests/tests/safeheaddb_elsync/safeheaddb_test.go index e6ee101e26f..5ddfe16c5d0 100644 --- a/op-acceptance-tests/tests/safeheaddb_elsync/safeheaddb_test.go +++ b/op-acceptance-tests/tests/safeheaddb_elsync/safeheaddb_test.go @@ -21,7 +21,7 @@ func newSingleChainMultiNodeELSync(t devtest.T) *presets.SingleChainMultiNode { } func TestTruncateDatabaseOnELResync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSingleChainMultiNodeELSync(t) dsl.CheckAll(t, @@ -50,7 +50,7 @@ func TestTruncateDatabaseOnELResync(gt *testing.T) { } func TestNotTruncateDatabaseOnRestartWithExistingDatabase(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSingleChainMultiNodeELSync(t) dsl.CheckAll(t, diff --git a/op-acceptance-tests/tests/superfaultproofs/superfaultproofs.go b/op-acceptance-tests/tests/superfaultproofs/superfaultproofs.go index d2d8ef01a41..ea1958f65a6 100644 --- a/op-acceptance-tests/tests/superfaultproofs/superfaultproofs.go +++ b/op-acceptance-tests/tests/superfaultproofs/superfaultproofs.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/dsl" "github.com/ethereum-optimism/optimism/op-devstack/presets" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-program/client/interop" interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" "github.com/ethereum-optimism/optimism/op-service/apis" "github.com/ethereum-optimism/optimism/op-service/bigs" @@ -511,3 +512,166 @@ func RunConsolidateValidCrossChainMessageTest(t devtest.T, sys *presets.SimpleIn }) } } + +func RunInvalidBlockTest(t devtest.T, sys *presets.SimpleInterop) { + t.Require().NotNil(sys.SuperRoots, "supernode is required for this test") + rng := rand.New(rand.NewSource(1234)) + + chains := orderedChains(sys) + t.Require().Len(chains, 2, "expected exactly 2 interop chains") + + aliceA := sys.FunderA.NewFundedEOA(eth.OneEther) + aliceB := aliceA.AsEL(sys.L2ELB) + sys.FunderB.Fund(aliceB, eth.OneEther) + + l1BlockBeforeBatches := sys.L1EL.BlockRefByLabel(eth.Unsafe) + + eventLogger := aliceA.DeployEventLogger() + initMsg := aliceA.SendRandomInitMessage(rng, eventLogger, 2, 10) + execMsg := aliceB.SendInvalidExecMessage(initMsg) + + endTimestamp := sys.L2ChainB.TimestampForBlockNum(bigs.Uint64Strict(execMsg.BlockNumber())) + startTimestamp := endTimestamp - 1 + + sys.SuperRoots.AwaitValidatedTimestamp(endTimestamp) + sys.L2CLB.Reached(types.CrossSafe, bigs.Uint64Strict(execMsg.BlockNumber()), 10) + sys.L2ELB.AssertExecMessageNotInBlock(execMsg) + + l1HeadCurrent := latestRequiredL1(sys.SuperRoots.SuperRootAtTimestamp(endTimestamp)) + + start := superRootAtTimestamp(t, chains, startTimestamp) + crossSafeSuperRootEnd := superRootAtTimestamp(t, chains, endTimestamp) + + firstOptimistic := optimisticBlockAtTimestamp(t, chains[0], endTimestamp) + secondOptimistic := optimisticBlockAtTimestamp(t, chains[1], endTimestamp) + paddingStep := func(step uint64) []byte { + return marshalTransition(start, step, firstOptimistic, secondOptimistic) + } + + preReplacementSuperRoot := eth.NewSuperV1(endTimestamp, + eth.ChainIDAndOutput{ChainID: chains[0].ID, Output: firstOptimistic.OutputRoot}, + eth.ChainIDAndOutput{ChainID: chains[1].ID, Output: secondOptimistic.OutputRoot}) + + step1Expected := marshalTransition(start, 1, firstOptimistic) + step2Expected := marshalTransition(start, 2, firstOptimistic, secondOptimistic) + + tests := []*transitionTest{ + { + Name: "FirstChainOptimisticBlock", + AgreedClaim: start.Marshal(), + DisputedClaim: step1Expected, + DisputedTraceIndex: 0, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "SecondChainOptimisticBlock", + AgreedClaim: step1Expected, + DisputedClaim: step2Expected, + DisputedTraceIndex: 1, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "FirstPaddingStep", + AgreedClaim: step2Expected, + DisputedClaim: paddingStep(3), + DisputedTraceIndex: 2, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "SecondPaddingStep", + AgreedClaim: paddingStep(3), + DisputedClaim: paddingStep(4), + DisputedTraceIndex: 3, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "LastPaddingStep", + AgreedClaim: paddingStep(consolidateStep - 1), + DisputedClaim: paddingStep(consolidateStep), + DisputedTraceIndex: consolidateStep - 1, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "Consolidate-ExpectInvalidPendingBlock", + AgreedClaim: paddingStep(consolidateStep), + DisputedClaim: preReplacementSuperRoot.Marshal(), + DisputedTraceIndex: consolidateStep, + ExpectValid: false, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "Consolidate-ReplaceInvalidBlock", + AgreedClaim: paddingStep(consolidateStep), + DisputedClaim: crossSafeSuperRootEnd.Marshal(), + DisputedTraceIndex: consolidateStep, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + { + Name: "AlreadyAtClaimedTimestamp", + AgreedClaim: crossSafeSuperRootEnd.Marshal(), + DisputedClaim: crossSafeSuperRootEnd.Marshal(), + DisputedTraceIndex: 5000, + ExpectValid: true, + L1Head: l1HeadCurrent, + ClaimTimestamp: endTimestamp, + }, + + { + Name: "FirstChainReachesL1Head", + AgreedClaim: start.Marshal(), + DisputedClaim: interop.InvalidTransition, + DisputedTraceIndex: 0, + // The derivation reaches the L1 head before the next block can be created + L1Head: l1BlockBeforeBatches.ID(), + ExpectValid: true, + ClaimTimestamp: endTimestamp, + }, + { + Name: "SuperRootInvalidIfUnsupportedByL1Data", + AgreedClaim: start.Marshal(), + DisputedClaim: step1Expected, + DisputedTraceIndex: 0, + // The derivation reaches the L1 head before the next block can be created + L1Head: l1BlockBeforeBatches.ID(), + ExpectValid: false, + ClaimTimestamp: endTimestamp, + }, + { + Name: "FromInvalidTransitionHash", + AgreedClaim: interop.InvalidTransition, + DisputedClaim: interop.InvalidTransition, + DisputedTraceIndex: 2, + // The derivation reaches the L1 head before the next block can be created + L1Head: l1BlockBeforeBatches.ID(), + ExpectValid: true, + ClaimTimestamp: endTimestamp, + }, + } + + challengerCfg := sys.L2ChainA.Escape().L2Challengers()[0].Config() + gameDepth := sys.DisputeGameFactory().GameImpl(gameTypes.SuperCannonKonaGameType).SplitDepth() + for _, test := range tests { + t.Run(test.Name+"-fpp", func(t devtest.T) { + runKonaInteropProgram(t, challengerCfg.CannonKona, test.L1Head.Hash, + test.AgreedClaim, crypto.Keccak256Hash(test.DisputedClaim), + test.ClaimTimestamp, test.ExpectValid) + }) + + t.Run(test.Name+"-challenger", func(t devtest.T) { + runChallengerProviderTest(t, sys.SuperRoots.QueryAPI(), gameDepth, startTimestamp, test.ClaimTimestamp, test) + }) + } +} diff --git a/op-acceptance-tests/tests/supernode/interop/activation/activation_after_genesis_test.go b/op-acceptance-tests/tests/supernode/interop/activation/activation_after_genesis_test.go index 95f0cf68f4c..ad851589a95 100644 --- a/op-acceptance-tests/tests/supernode/interop/activation/activation_after_genesis_test.go +++ b/op-acceptance-tests/tests/supernode/interop/activation/activation_after_genesis_test.go @@ -12,12 +12,14 @@ import ( // InteropActivationDelay is the delay in seconds from genesis to interop activation. // This is set to 20 seconds to allow several blocks to be produced before interop kicks in. const InteropActivationDelay = uint64(20) +const activationAfterGenesisFlakyReason = "known flaky in the default acceptance run" // TestSupernodeInteropActivationAfterGenesis tests behavior when interop is activated // AFTER genesis. This verifies that VerifiedAt (via superroot_atTimestamp) returns // verified data for timestamps both before and after the activation boundary. func TestSupernodeInteropActivationAfterGenesis(gt *testing.T) { t := devtest.ParallelT(gt) + t.MarkFlaky(activationAfterGenesisFlakyReason) t.Skip("The TestMain setup code for this test is unstable") sys := presets.NewTwoL2SupernodeInterop(t, InteropActivationDelay) diff --git a/op-acceptance-tests/tests/supernode/interop/cross_message_test.go b/op-acceptance-tests/tests/supernode/interop/cross_message_test.go index 59e96f290ac..a47e38b5220 100644 --- a/op-acceptance-tests/tests/supernode/interop/cross_message_test.go +++ b/op-acceptance-tests/tests/supernode/interop/cross_message_test.go @@ -14,7 +14,7 @@ import ( // (A->B and B->A) to verify the supernode handles bidirectional interop correctly. // All messages are valid, and no interruptions to the chains are expected. func TestSupernodeInteropBidirectionalMessages(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSupernodeInteropWithTimeTravel(t, 0) // Create funded EOAs on both chains diff --git a/op-acceptance-tests/tests/supernode/interop/follow_l2/sync_test.go b/op-acceptance-tests/tests/supernode/interop/follow_l2/sync_test.go new file mode 100644 index 00000000000..919771062e9 --- /dev/null +++ b/op-acceptance-tests/tests/supernode/interop/follow_l2/sync_test.go @@ -0,0 +1,147 @@ +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-supervisor/supervisor/types" +) + +func TestFollowSource_HeadsDivergeThenConverge(gt *testing.T) { + t := devtest.ParallelT(gt) + require := t.Require() + sys := presets.NewTwoL2SupernodeFollowL2(t, 0) + + type chainPair struct { + name string + source *dsl.L2CLNode + follower *dsl.L2CLNode + } + + type headSnapshot struct { + sourceUnsafe uint64 + sourceLocalSafe uint64 + sourceCrossSafe uint64 + followerUnsafe uint64 + followerLocalSafe uint64 + followerCrossSafe uint64 + } + + chains := []chainPair{ + {name: "A", source: sys.L2ACL, follower: sys.L2AFollowCL}, + {name: "B", source: sys.L2BCL, follower: sys.L2BFollowCL}, + } + + // Initial sanity: followers are aligned with upstream on both local-safe and cross-safe. + initialChecks := make([]dsl.CheckFunc, 0, len(chains)*2) + for _, chain := range chains { + initialChecks = append(initialChecks, + chain.follower.MatchedFn(chain.source, types.LocalSafe, 20), + chain.follower.MatchedFn(chain.source, types.CrossSafe, 20), + ) + } + dsl.CheckAll(t, initialChecks...) + + pausedAt := sys.Supernode.EnsureInteropPaused(sys.L2ACL, sys.L2BCL, 10) + t.Logger().Info("interop paused", "timestamp", pausedAt) + + baselines := make(map[string]headSnapshot, len(chains)) + for _, chain := range chains { + sourceStatus := chain.source.SyncStatus() + followerStatus := chain.follower.SyncStatus() + baselines[chain.name] = headSnapshot{ + sourceUnsafe: sourceStatus.UnsafeL2.Number, + sourceLocalSafe: sourceStatus.LocalSafeL2.Number, + sourceCrossSafe: sourceStatus.SafeL2.Number, + followerUnsafe: followerStatus.UnsafeL2.Number, + followerLocalSafe: followerStatus.LocalSafeL2.Number, + followerCrossSafe: followerStatus.SafeL2.Number, + } + } + + // While interop is paused, unsafe should advance independently ahead of local-safe, and local-safe ahead of cross-safe. + require.Eventually(func() bool { + for _, chain := range chains { + baseline := baselines[chain.name] + sourceStatus := chain.source.SyncStatus() + followerStatus := chain.follower.SyncStatus() + + if sourceStatus.UnsafeL2.Number <= baseline.sourceUnsafe { + return false + } + if followerStatus.UnsafeL2.Number <= baseline.followerUnsafe { + return false + } + if sourceStatus.UnsafeL2.Number <= sourceStatus.LocalSafeL2.Number { + return false + } + if followerStatus.UnsafeL2.Number <= followerStatus.LocalSafeL2.Number { + return false + } + if sourceStatus.LocalSafeL2.Number <= sourceStatus.SafeL2.Number { + return false + } + if followerStatus.LocalSafeL2.Number <= followerStatus.SafeL2.Number { + return false + } + if sourceStatus.LocalSafeL2.Number <= baseline.sourceLocalSafe { + return false + } + if followerStatus.LocalSafeL2.Number <= baseline.followerLocalSafe { + return false + } + if sourceStatus.SafeL2.Number < baseline.sourceCrossSafe { + return false + } + if followerStatus.SafeL2.Number < baseline.followerCrossSafe { + return false + } + } + return true + }, 2*time.Minute, 2*time.Second, "expected unsafe > local-safe > cross-safe with unsafe advancing on source and follower") + + // Core follow-source checks: follower must match source unsafe, local-safe, and cross-safe independently. + divergenceChecks := make([]dsl.CheckFunc, 0, len(chains)*3) + for _, chain := range chains { + divergenceChecks = append(divergenceChecks, + chain.follower.MatchedFn(chain.source, types.LocalUnsafe, 20), + chain.follower.MatchedFn(chain.source, types.LocalSafe, 20), + chain.follower.MatchedFn(chain.source, types.CrossSafe, 20), + ) + } + dsl.CheckAll(t, divergenceChecks...) + + // Freeze new block production so interop can catch cross-safe up to local-safe. + sys.L2ACL.StopSequencer() + sys.L2BCL.StopSequencer() + t.Cleanup(func() { + sys.L2ACL.StartSequencer() + sys.L2BCL.StartSequencer() + }) + + sys.Supernode.ResumeInterop() + + require.Eventually(func() bool { + for _, chain := range chains { + status := chain.follower.SyncStatus() + if status.LocalSafeL2.Hash != status.SafeL2.Hash || status.LocalSafeL2.Number != status.SafeL2.Number { + return false + } + } + return true + }, 3*time.Minute, 2*time.Second, "expected local-safe and cross-safe to converge on followers") + + // Final sanity: follower and source converge to the same unsafe, local-safe, and cross-safe heads. + finalChecks := make([]dsl.CheckFunc, 0, len(chains)*3) + for _, chain := range chains { + finalChecks = append(finalChecks, + chain.follower.MatchedFn(chain.source, types.LocalUnsafe, 20), + chain.follower.MatchedFn(chain.source, types.LocalSafe, 20), + chain.follower.MatchedFn(chain.source, types.CrossSafe, 20), + ) + } + dsl.CheckAll(t, finalChecks...) +} diff --git a/op-acceptance-tests/tests/supernode/interop/head_progression_test.go b/op-acceptance-tests/tests/supernode/interop/head_progression_test.go index 2442db2f447..624dd681cda 100644 --- a/op-acceptance-tests/tests/supernode/interop/head_progression_test.go +++ b/op-acceptance-tests/tests/supernode/interop/head_progression_test.go @@ -25,7 +25,7 @@ import ( // - Finalized head eventually catches up to a snapshot of the safe head // - Finalized L2 blocks have sane L1 origins (behind the L1 finalized head) func TestSupernodeInterop_SafeHeadProgression(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSupernodeInteropWithTimeTravel(t, 0) attempts := 15 // each attempt is hardcoded with a 2s by the DSL. @@ -152,7 +152,7 @@ func TestSupernodeInterop_SafeHeadProgression(gt *testing.T) { // - Cross-safe head is gated by the slower chain // - Safe head advances after slower chain catches up func TestSupernodeInterop_SafeHeadWithUnevenProgress(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSupernodeInteropWithTimeTravel(t, 0) attempts := 15 diff --git a/op-acceptance-tests/tests/supernode/interop/reorg/invalid_message_reorg_test.go b/op-acceptance-tests/tests/supernode/interop/reorg/invalid_message_reorg_test.go index 153bf3c920e..f5322fa3c0d 100644 --- a/op-acceptance-tests/tests/supernode/interop/reorg/invalid_message_reorg_test.go +++ b/op-acceptance-tests/tests/supernode/interop/reorg/invalid_message_reorg_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" "github.com/ethereum-optimism/optimism/op-service/bigs" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -24,8 +25,9 @@ import ( // - A replacement block is built at the same height (deposits-only) // - The replacement block's timestamp eventually becomes verified func TestSupernodeInteropInvalidMessageReplacement(gt *testing.T) { - t := devtest.SerialT(gt) + // TODO(ethereum-optimism/optimism#19411): remove skip once op-reth safe head mismatch is fixed + sysgo.SkipOnOpReth(t, "panics due to safe head mismatch in EngineController") sys := presets.NewTwoL2SupernodeInterop(t, 0) ctx := t.Ctx() @@ -117,4 +119,14 @@ func TestSupernodeInteropInvalidMessageReplacement(gt *testing.T) { "invalid_block_number", invalidBlockNumber, "invalid_block_hash", invalidBlockHash, ) + + // We should still be able to include new transactions and have them be fully validated + bruce := sys.FunderB.NewFundedEOA(eth.OneEther) + tx := bruce.Transfer(alice.Address(), eth.OneHundredthEther) + sys.L2ELB.AssertTxInBlock(bigs.Uint64Strict(tx.Included.Value().BlockNumber), tx.Included.Value().TxHash) + + txTimestamp := sys.L2B.TimestampForBlockNum(bigs.Uint64Strict(tx.Included.Value().BlockNumber)) + sys.Supernode.AwaitValidatedTimestamp(txTimestamp) + // Should still have the tx in the block. + sys.L2ELB.AssertTxInBlock(bigs.Uint64Strict(tx.Included.Value().BlockNumber), tx.Included.Value().TxHash) } diff --git a/op-acceptance-tests/tests/supernode/interop/same_timestamp_invalid/same_timestamp_test.go b/op-acceptance-tests/tests/supernode/interop/same_timestamp_invalid/same_timestamp_test.go index 1ef8326dd37..f198e9c0c38 100644 --- a/op-acceptance-tests/tests/supernode/interop/same_timestamp_invalid/same_timestamp_test.go +++ b/op-acceptance-tests/tests/supernode/interop/same_timestamp_invalid/same_timestamp_test.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" "github.com/ethereum-optimism/optimism/op-service/txplan" ) @@ -27,6 +28,8 @@ func TestSupernodeSameTimestampExecMessage(gt *testing.T) { // TestSupernodeSameTimestampInvalidTransitive: Bad log index causes transitive invalidation func TestSupernodeSameTimestampInvalidTransitive(gt *testing.T) { t := devtest.SerialT(gt) + // TODO(ethereum-optimism/optimism#19411): remove skip once op-reth safe head mismatch is fixed + sysgo.SkipOnOpReth(t, "panics due to safe head mismatch in EngineController") sys := presets.NewTwoL2SupernodeInterop(t, 0).ForSameTimestampTesting(t) rng := rand.New(rand.NewSource(77777)) @@ -43,6 +46,8 @@ func TestSupernodeSameTimestampInvalidTransitive(gt *testing.T) { // TestSupernodeSameTimestampCycle: Mutual exec messages create cycle - both replaced func TestSupernodeSameTimestampCycle(gt *testing.T) { t := devtest.SerialT(gt) + // TODO(ethereum-optimism/optimism#19411): remove skip once op-reth safe head mismatch is fixed + sysgo.SkipOnOpReth(t, "panics due to safe head mismatch in EngineController") sys := presets.NewTwoL2SupernodeInterop(t, 0).ForSameTimestampTesting(t) rng := rand.New(rand.NewSource(55555)) diff --git a/op-acceptance-tests/tests/supernode/interop/timestamp_progression_test.go b/op-acceptance-tests/tests/supernode/interop/timestamp_progression_test.go index 772c6868bbf..dce7a6fef57 100644 --- a/op-acceptance-tests/tests/supernode/interop/timestamp_progression_test.go +++ b/op-acceptance-tests/tests/supernode/interop/timestamp_progression_test.go @@ -7,6 +7,8 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/devtest" ) +const supernodeInteropFlakyReason = "known flaky in the default acceptance run" + // TestSupernodeInteropVerifiedAt tests that the VerifiedAt endpoint returns // correct data after the interop activity has processed timestamps. func TestSupernodeInteropVerifiedAt(gt *testing.T) { @@ -53,7 +55,8 @@ func TestSupernodeInteropVerifiedAt(gt *testing.T) { // // This proves the supernode waits for all chains' local safe heads before verifying. func TestSupernodeInteropChainLag(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) + t.MarkFlaky(supernodeInteropFlakyReason) sys := newSupernodeInteropWithTimeTravel(t, 0) blockTime := sys.L2A.Escape().RollupConfig().BlockTime diff --git a/op-acceptance-tests/tests/sync/clsync/gap_clp2p/sync_test.go b/op-acceptance-tests/tests/sync/clsync/gap_clp2p/sync_test.go index c00d71a53f9..60709b1670d 100644 --- a/op-acceptance-tests/tests/sync/clsync/gap_clp2p/sync_test.go +++ b/op-acceptance-tests/tests/sync/clsync/gap_clp2p/sync_test.go @@ -10,7 +10,7 @@ import ( // TestSyncAfterInitialELSync tests that blocks received out of order would be processed in order when running in CL sync mode. Note that this is not going to happen when running in EL sync mode, which relies on healthy ELP2P, something that is disabled in this test. func TestSyncAfterInitialELSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapCLP2PSystem(t) require := t.Require() diff --git a/op-acceptance-tests/tests/sync/elsync/gap_clp2p/sync_test.go b/op-acceptance-tests/tests/sync/elsync/gap_clp2p/sync_test.go index 349544b548b..2dd083f7cee 100644 --- a/op-acceptance-tests/tests/sync/elsync/gap_clp2p/sync_test.go +++ b/op-acceptance-tests/tests/sync/elsync/gap_clp2p/sync_test.go @@ -10,7 +10,7 @@ import ( ) func TestReachUnsafeTipByAppendingUnsafePayload(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapCLP2PSystem(t) logger := t.Logger() @@ -45,7 +45,7 @@ func TestReachUnsafeTipByAppendingUnsafePayload(gt *testing.T) { // not cause the CL's unsafe head to regress, preserving the last known valid head // while maintaining correct Engine API semantics. func TestCLUnsafeNotRewoundOnInvalidDuringELSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapCLP2PSystem(t) logger := t.Logger() require := t.Require() diff --git a/op-acceptance-tests/tests/sync/elsync/gap_elp2p/sync_test.go b/op-acceptance-tests/tests/sync/elsync/gap_elp2p/sync_test.go index 63d9d8a70fa..ae2a15255e7 100644 --- a/op-acceptance-tests/tests/sync/elsync/gap_elp2p/sync_test.go +++ b/op-acceptance-tests/tests/sync/elsync/gap_elp2p/sync_test.go @@ -54,7 +54,7 @@ import ( // assemble a non canonical chain later. // - With ELP2P enabled, repeated FCU attempts eventually validate and advance the canonical chain. func TestL2ELP2PCanonicalChainAdvancedByFCU(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapELP2PSystem(t) require := t.Require() logger := t.Logger() @@ -249,7 +249,7 @@ func TestL2ELP2PCanonicalChainAdvancedByFCU(gt *testing.T) { // forkchoice targets by consistently reporting SYNCING for each FCU attempt // and by avoiding advancement of the chain on invalid data. func TestELP2PFCUUnavailableHash(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapELP2PSystem(t) logger := t.Logger() genesis := sys.L2ELB.BlockRefByNumber(0) @@ -305,7 +305,7 @@ func TestELP2PFCUUnavailableHash(gt *testing.T) { // This validates that safe head updates are contingent on the unsafe target passing // appendability/sync checks first, per Engine API behavior. func TestSafeDoesNotAdvanceWhenUnsafeIsSyncing_NoELP2P(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapELP2PSystem(t) logger := t.Logger() @@ -392,7 +392,7 @@ func TestSafeDoesNotAdvanceWhenUnsafeIsSyncing_NoELP2P(gt *testing.T) { // In all scenarios, both CL and EL remain at the same head height, confirming that // invalid payloads—whether rejected at the CL or EL—do not advance the chain. func TestInvalidPayloadThroughCLP2P(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newGapELP2PSystem(t) logger := t.Logger() require := t.Require() diff --git a/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go b/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go index eb154a4a462..01935ea8057 100644 --- a/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go +++ b/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go @@ -17,7 +17,7 @@ import ( // 3. Verifier restarts, and consolidation drops the verifier previously-unsafe blocks. // 4. CLP2P is restored, the verifier backfills and the unsafe gap is closed. func TestUnsafeGapFillAfterSafeReorg(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newReorgSystem(t) require := t.Require() logger := t.Logger() @@ -147,7 +147,7 @@ func TestUnsafeGapFillAfterSafeReorg(gt *testing.T) { // 3. Verifier restarts and detects the L1 reorg, triggering its own unsafe reorg, // 4. Verifier then backfills and closes the unsafe gap once reconnected via CLP2P. func TestUnsafeGapFillAfterUnsafeReorg_RestartL2CL(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newReorgSystem(t) require := t.Require() logger := t.Logger() @@ -269,7 +269,7 @@ func TestUnsafeGapFillAfterUnsafeReorg_RestartL2CL(gt *testing.T) { // 3. Verifier detects the L1 reorg, triggering its own unsafe reorg. // 4. CLP2P is restored Verifier, the verifier backfills and the unsafe gap is closed. func TestUnsafeGapFillAfterUnsafeReorg_RestartCLP2P(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newReorgSystem(t) require := t.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/sync/follow_l2/sync_test.go b/op-acceptance-tests/tests/sync/follow_l2/sync_test.go index db5589644bc..f3e33c476d9 100644 --- a/op-acceptance-tests/tests/sync/follow_l2/sync_test.go +++ b/op-acceptance-tests/tests/sync/follow_l2/sync_test.go @@ -13,7 +13,7 @@ import ( ) func TestFollowL2_Safe_Finalized_CurrentL1(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSingleChainTwoVerifiersFollowL2(t) logger := t.Logger() @@ -55,7 +55,7 @@ func TestFollowL2_Safe_Finalized_CurrentL1(gt *testing.T) { } func TestFollowL2_ReorgRecovery(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSingleChainTwoVerifiersFollowL2(t) require := t.Require() logger := t.Logger() @@ -108,10 +108,18 @@ func TestFollowL2_ReorgRecovery(gt *testing.T) { // 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) + // L2ELB detects L2 reorg and replaces the original block. The replacement + // block at this height may also come from a different parent chain, so only + // assert that the original block is replaced before checking convergence. + var l2BlockAfterReorg eth.L2BlockRef + require.Eventually(func() bool { + l2BlockAfterReorg = sys.L2ELB.BlockRefByNumber(l2BlockBeforeReorg.Number) + if l2BlockAfterReorg.Hash == l2BlockBeforeReorg.Hash { + logger.Info("Waiting for L2 reorg", "before", l2BlockBeforeReorg, "current", l2BlockAfterReorg) + return false + } + return true + }, 60*time.Second, 2*time.Second) logger.Info("Triggered L2 reorg", "l2", l2BlockAfterReorg) attempts := 30 @@ -124,7 +132,7 @@ func TestFollowL2_ReorgRecovery(gt *testing.T) { } func TestFollowL2_WithoutCLP2P(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := newSingleChainTwoVerifiersFollowL2(t) require := t.Require() logger := t.Logger() @@ -136,9 +144,11 @@ func TestFollowL2_WithoutCLP2P(gt *testing.T) { 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 + // There is often a gap between safe and unsafe before disconnect, but the + // follow-source verifier may also catch up before we observe it. The actual + // property this test cares about is the post-disconnect behavior below. status := sys.L2CLC.SyncStatus() - require.NotEqual(status.LocalSafeL2, status.UnsafeL2) + logger.Info("Initial follow-source sync status", "safe", status.LocalSafeL2, "unsafe", status.UnsafeL2) logger.Info("Disconnect CLP2P") // L2CLC is the verifier with follow source, derivation disabled diff --git a/op-acceptance-tests/tests/sync/manual/sync_test.go b/op-acceptance-tests/tests/sync/manual/sync_test.go index d86b7a4b082..7b0bfdb9bf3 100644 --- a/op-acceptance-tests/tests/sync/manual/sync_test.go +++ b/op-acceptance-tests/tests/sync/manual/sync_test.go @@ -13,7 +13,7 @@ import ( ) func TestVerifierManualSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) // Disable ELP2P and Batcher sys := presets.NewSingleChainMultiNodeWithoutP2PWithoutCheck(t, diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_e2e/sync_tester_e2e_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_e2e/sync_tester_e2e_test.go index b7239f9007a..954a6aece6f 100644 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_e2e/sync_tester_e2e_test.go +++ b/op-acceptance-tests/tests/sync_tester/sync_tester_e2e/sync_tester_e2e_test.go @@ -11,7 +11,7 @@ import ( ) func TestSyncTesterE2E(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) // This test uses DefaultSimpleSystemWithSyncTester which includes: // - Minimal setup with L1EL, L1CL, L2EL, L2CL (sequencer) // - Additional L2CL2 (verifier) that connects to SyncTester instead of L2EL diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_elsync/elsync_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_elsync/elsync_test.go index 6b74436707d..7e337d6e9a7 100644 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_elsync/elsync_test.go +++ b/op-acceptance-tests/tests/sync_tester/sync_tester_elsync/elsync_test.go @@ -24,7 +24,7 @@ func simpleWithSyncTesterOpts() []presets.Option { } func TestSyncTesterELSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleWithSyncTester(t, simpleWithSyncTesterOpts()...) require := t.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_elsync_multi/sync_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_elsync_multi/sync_test.go index a8e54fa2568..8e9127f7467 100644 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_elsync_multi/sync_test.go +++ b/op-acceptance-tests/tests/sync_tester/sync_tester_elsync_multi/sync_test.go @@ -28,7 +28,7 @@ func simpleWithSyncTesterOpts() []presets.Option { } func TestMultiELSync(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleWithSyncTester(t, simpleWithSyncTesterOpts()...) require := t.Require() diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_hfs/sync_tester_hfs_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_hfs/sync_tester_hfs_test.go index 187dda2388c..2264100c46c 100644 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_hfs/sync_tester_hfs_test.go +++ b/op-acceptance-tests/tests/sync_tester/sync_tester_hfs/sync_tester_hfs_test.go @@ -34,7 +34,7 @@ func simpleWithSyncTesterOpts() []presets.Option { } func TestSyncTesterHardforks(gt *testing.T) { - t := devtest.SerialT(gt) + t := devtest.ParallelT(gt) sys := presets.NewSimpleWithSyncTester(t, simpleWithSyncTesterOpts()...) require := t.Require() diff --git a/op-deployer/pkg/deployer/apply.go b/op-deployer/pkg/deployer/apply.go index af88d5940b1..43d0e2c4484 100644 --- a/op-deployer/pkg/deployer/apply.go +++ b/op-deployer/pkg/deployer/apply.go @@ -23,8 +23,6 @@ import ( "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum-optimism/optimism/op-service/prestate" - "github.com/ethereum-optimism/optimism/op-validator/pkg/service" - "github.com/ethereum-optimism/optimism/op-validator/pkg/validations" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" diff --git a/op-devstack/devtest/testing.go b/op-devstack/devtest/testing.go index 6f4c0e0dcc1..a0b5b581331 100644 --- a/op-devstack/devtest/testing.go +++ b/op-devstack/devtest/testing.go @@ -22,6 +22,7 @@ import ( ) const ExpectPreconditionsMet = "DEVNET_EXPECT_PRECONDITIONS_MET" +const FailFlakyTests = "DEVNET_FAIL_FLAKY_TESTS" var ( // RootContext is the context that is used for the root of the test suite. @@ -70,6 +71,10 @@ type T interface { // Gate provides everything that Require does, but skips instead of fails the test upon error. Gate() *testreq.Assertions + // MarkFlaky downgrades future test failures to skips unless DEVNET_FAIL_FLAKY_TESTS is set. + // Call this near the top of the test, before storing any Require()-derived helper. + MarkFlaky(reason string) + // Deadline reports the time at which the test binary will have // exceeded the timeout specified by the -timeout flag. // @@ -92,6 +97,8 @@ type testingT struct { ctx context.Context req *testreq.Assertions gate *testreq.Assertions + flaky bool + reason string } func mustNotSkip() bool { @@ -100,8 +107,31 @@ func mustNotSkip() bool { return out } +func mustFailFlakyTests() bool { + v := os.Getenv(FailFlakyTests) + out, _ := strconv.ParseBool(v) // default to false + return out +} + +func (t *testingT) shouldSkipFlakyFailure() bool { + return t.flaky && !mustFailFlakyTests() +} + +func (t *testingT) skipFlakyFailure(msg string) { + if t.reason != "" { + t.logger.Warn("Ignoring failure in flaky test", "reason", t.reason, "failure", msg) + } else { + t.logger.Warn("Ignoring failure in flaky test", "failure", msg) + } + t.t.SkipNow() +} + func (t *testingT) Error(args ...any) { t.t.Helper() + if t.shouldSkipFlakyFailure() { + t.skipFlakyFailure(fmt.Sprintln(args...)) + return + } // Note: the test-logger catches panics when the test is logged to after test-end. // Note: we do not use t.Error directly, to keep the log-formatting more consistent. t.logger.Error(fmt.Sprintln(args...)) @@ -110,6 +140,10 @@ func (t *testingT) Error(args ...any) { func (t *testingT) Errorf(format string, args ...any) { t.t.Helper() + if t.shouldSkipFlakyFailure() { + t.skipFlakyFailure(fmt.Sprintf(format, args...)) + return + } // Note: the test-logger catches panics when the test is logged to after test-end. // Note: we do not use t.Errorf directly, to keep the log-formatting more consistent. t.logger.Error(fmt.Sprintf(format, args...)) @@ -118,6 +152,10 @@ func (t *testingT) Errorf(format string, args ...any) { func (t *testingT) Fail() { t.t.Helper() + if t.shouldSkipFlakyFailure() { + t.skipFlakyFailure("test called Fail") + return + } // if we already closed and failed, then this error is stale if t.ctx.Err() != nil && t.t.Failed() { return @@ -127,6 +165,10 @@ func (t *testingT) Fail() { func (t *testingT) FailNow() { t.t.Helper() + if t.shouldSkipFlakyFailure() { + t.skipFlakyFailure("test called FailNow") + return + } // If we already closed and failed the test-scope, then there is nothing to do. // This happens on e.g. a go-routine spawned by require.Eventually, when the time runs out, // the ctx is closed, a shared resource fails to do a lookup because of the ctx-timeout, @@ -193,6 +235,8 @@ func (t *testingT) WithCtx(ctx context.Context) T { logger: logger, tracer: t.tracer, ctx: ctx, + flaky: t.flaky, + reason: t.reason, } out.req = testreq.New(out) out.gate = testreq.New(&gateAdapter{out}) @@ -203,6 +247,13 @@ func (t *testingT) Require() *testreq.Assertions { return t.req } +func (t *testingT) MarkFlaky(reason string) { + t.flaky = true + if reason != "" { + t.reason = reason + } +} + func (t *testingT) Run(name string, fn func(T)) { baseName := t.Name() t.t.Run(name, func(subGoT *testing.T) { @@ -224,6 +275,8 @@ func (t *testingT) Run(name string, fn func(T)) { logger: logger, tracer: tracer, ctx: ctx, + flaky: t.flaky, + reason: t.reason, } subT.req = testreq.New(subT) subT.gate = testreq.New(&gateAdapter{subT}) diff --git a/op-devstack/devtest/testing_test.go b/op-devstack/devtest/testing_test.go new file mode 100644 index 00000000000..ee97766944a --- /dev/null +++ b/op-devstack/devtest/testing_test.go @@ -0,0 +1,44 @@ +package devtest + +import "testing" + +func TestMarkFlakySkipsRequireFailure(t *testing.T) { + t.Setenv(FailFlakyTests, "false") + ok := t.Run("flaky", func(gt *testing.T) { + dt := SerialT(gt) + dt.MarkFlaky("tracked as flaky") + dt.Require().Equal("expected", "actual") + gt.Fatal("flaky assertion should skip before reaching this line") + }) + if !ok { + t.Fatal("expected flaky subtest to be skipped instead of failed") + } +} + +func TestMarkFlakySkipsExplicitFailNow(t *testing.T) { + t.Setenv(FailFlakyTests, "false") + ok := t.Run("flaky", func(gt *testing.T) { + dt := SerialT(gt) + dt.MarkFlaky("tracked as flaky") + dt.FailNow() + gt.Fatal("flaky FailNow should skip before reaching this line") + }) + if !ok { + t.Fatal("expected flaky subtest to be skipped instead of failed") + } +} + +func TestMarkFlakyPropagatesToSubtests(t *testing.T) { + t.Setenv(FailFlakyTests, "false") + ok := t.Run("flaky-parent", func(gt *testing.T) { + dt := SerialT(gt) + dt.MarkFlaky("tracked as flaky") + dt.Run("child", func(dt T) { + dt.Require().Equal(1, 2) + gt.Fatal("flaky child assertion should skip before reaching this line") + }) + }) + if !ok { + t.Fatal("expected flaky child subtest to be skipped instead of failed") + } +} diff --git a/op-devstack/dsl/contract/call.go b/op-devstack/dsl/contract/call.go new file mode 100644 index 00000000000..a8bfbc8c181 --- /dev/null +++ b/op-devstack/dsl/contract/call.go @@ -0,0 +1,77 @@ +package contract + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/errutil" + "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum-optimism/optimism/op-service/txintent/contractio" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum/go-ethereum/core/types" +) + +const ( + // readTimeout bounds individual contract read calls to prevent tests from + // hanging when an in-memory geth node stalls under CI resource contention. + readTimeout = 60 * time.Second + // writeTimeout bounds contract write calls (transaction submission + mining). + writeTimeout = 5 * time.Minute +) + +// TestCallView is used in devstack for wrapping errors +type TestCallView[O any] interface { + Test() bindings.BaseTest +} + +// checkTestable checks whether the TypedCall can be used as a DSL using the testing context +func checkTestable[O any](call bindings.TypedCall[O]) { + callTest, ok := any(call).(TestCallView[O]) + if !ok || callTest.Test() == nil { + panic(fmt.Sprintf("call of type %T does not support testing", call)) + } +} + +// Read executes a new message call without creating a transaction on the blockchain. +// Each call is bounded by readTimeout to prevent hangs under CI resource contention. +func Read[O any](call bindings.TypedCall[O], opts ...txplan.Option) O { + checkTestable(call) + ctx, cancel := context.WithTimeout(call.Test().Ctx(), readTimeout) + defer cancel() + o, err := contractio.Read(call, ctx, opts...) + call.Test().Require().NoError(err) + return o +} + +// ReadArray retrieves all data from an array in batches. +// Each call is bounded by readTimeout to prevent hangs under CI resource contention. +func ReadArray[T any](countCall bindings.TypedCall[*big.Int], elemCall func(i *big.Int) bindings.TypedCall[T]) []T { + checkTestable(countCall) + test := countCall.Test() + ctx, cancel := context.WithTimeout(countCall.Test().Ctx(), readTimeout) + defer cancel() + + caller := countCall.Client().NewMultiCaller(batching.DefaultBatchSize) + + o, err := contractio.ReadArray(ctx, caller, countCall, elemCall) + test.Require().NoError(err) + return o +} + +// Write makes a user to write a tx by using the planned contract bindings. +// Each call is bounded by writeTimeout to prevent hangs under CI resource contention. +func Write[O any](user *dsl.EOA, call bindings.TypedCall[O], opts ...txplan.Option) *types.Receipt { + checkTestable(call) + ctx, cancel := context.WithTimeout(call.Test().Ctx(), writeTimeout) + defer cancel() + finalOpts := txplan.Combine(user.Plan(), txplan.Combine(opts...)) + o, err := contractio.Write(call, ctx, finalOpts) + call.Test().Require().NoError(err, "contract write failed: %v", errutil.TryAddRevertReason(err)) + return o +} + +var _ TestCallView[any] = (*bindings.TypedCall[any])(nil) diff --git a/op-devstack/dsl/eoa.go b/op-devstack/dsl/eoa.go index d23499e89fe..a2cc5d6e65d 100644 --- a/op-devstack/dsl/eoa.go +++ b/op-devstack/dsl/eoa.go @@ -496,33 +496,30 @@ func (u *EOA) PrepareSameTimestampInit( } } -// SubmitInit submits the init message without waiting for inclusion. -// Returns the planned tx which can be used to wait for inclusion later. +// SubmitInit returns a planned init transaction for same-timestamp testing. +// The test harness assigns deterministic nonces and includes the signed tx directly. func (p *SameTimestampPair) SubmitInit() *txplan.PlannedTx { tx := txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](p.eoa.Plan()) tx.Content.Set(p.Trigger) - _, err := tx.PlannedTx.Submitted.Eval(p.eoa.ctx) - p.eoa.require.NoError(err, "failed to submit init message") return tx.PlannedTx } -// SubmitExecTo submits an exec message to the given EOA's chain, referencing this init. -// The exec is submitted without waiting for inclusion. -// Returns the planned tx which can be used to wait for inclusion later. +// SubmitExecTo returns a planned exec transaction to the given EOA's chain, +// referencing this init. The test harness assigns deterministic nonces and +// includes the signed tx directly. func (p *SameTimestampPair) SubmitExecTo(executor *EOA) *txplan.PlannedTx { tx := txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](executor.Plan()) tx.Content.Set(&txintent.ExecTrigger{ Executor: predeploys.CrossL2InboxAddr, Msg: p.Message, }) - _, err := tx.PlannedTx.Submitted.Eval(executor.ctx) - executor.require.NoError(err, "failed to submit exec message") return tx.PlannedTx } // SubmitInvalidExecTo submits an exec message with an invalid log index. // This creates an exec that references a non-existent log, which should be detected as invalid. -// Returns the planned tx which can be used to wait for inclusion later. +// Returns the planned tx; the test harness assigns deterministic nonces and +// includes the signed tx directly. func (p *SameTimestampPair) SubmitInvalidExecTo(executor *EOA) *txplan.PlannedTx { invalidMsg := MakeInvalidLogIndex(p.Message) @@ -531,7 +528,5 @@ func (p *SameTimestampPair) SubmitInvalidExecTo(executor *EOA) *txplan.PlannedTx Executor: predeploys.CrossL2InboxAddr, Msg: invalidMsg, }) - _, err := tx.PlannedTx.Submitted.Eval(executor.ctx) - executor.require.NoError(err, "failed to submit invalid exec message") return tx.PlannedTx } diff --git a/op-devstack/dsl/l2_el.go b/op-devstack/dsl/l2_el.go index dedbfc9a105..41452a72ce7 100644 --- a/op-devstack/dsl/l2_el.go +++ b/op-devstack/dsl/l2_el.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-devstack/sysgo" "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/bigs" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/retry" suptypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" @@ -445,6 +446,10 @@ func (el *L2ELNode) FinalizedHead() *BlockRefResult { return &BlockRefResult{T: el.t, BlockRef: el.BlockRefByLabel(eth.Finalized)} } +func (el *L2ELNode) AssertExecMessageNotInBlock(execMessage *ExecMessage) { + el.AssertTxNotInBlock(bigs.Uint64Strict(execMessage.BlockNumber()), execMessage.TxHash()) +} + // AssertTxNotInBlock asserts that a transaction with the given hash does not exist in the block at the given number. func (el *L2ELNode) AssertTxNotInBlock(blockNumber uint64, txHash common.Hash) { ctx, cancel := context.WithTimeout(el.ctx, DefaultTimeout) @@ -453,13 +458,27 @@ func (el *L2ELNode) AssertTxNotInBlock(blockNumber uint64, txHash common.Hash) { _, txs, err := el.inner.EthClient().InfoAndTxsByNumber(ctx, blockNumber) el.require.NoError(err, "failed to fetch block %d", blockNumber) + for _, tx := range txs { + el.require.NotEqualf(tx.Hash(), txHash, "transaction should not exist in block", "Found tx %v in block %v", tx.Hash(), blockNumber) + } + el.log.Info("confirmed transaction not in block", "blockNumber", blockNumber, "txHash", txHash) +} + +// AssertTxNotInBlock asserts that a transaction with the given hash does not exist in the block at the given number. +func (el *L2ELNode) AssertTxInBlock(blockNumber uint64, txHash common.Hash) { + ctx, cancel := context.WithTimeout(el.ctx, DefaultTimeout) + defer cancel() + + _, txs, err := el.inner.EthClient().InfoAndTxsByNumber(ctx, blockNumber) + el.require.NoError(err, "failed to fetch block %d", blockNumber) + for _, tx := range txs { if tx.Hash() == txHash { - el.require.Failf("transaction should not exist in block", - "tx_hash=%s found in block %d", txHash, blockNumber) + el.log.Info("confirmed transaction in block", "blockNumber", blockNumber, "txHash", txHash) + return } } - el.log.Info("confirmed transaction not in block", "blockNumber", blockNumber, "txHash", txHash) + el.require.Fail("transaction should exist in block", "blockNumber", blockNumber, "txHash", txHash) } type BlockRefResult struct { diff --git a/op-devstack/dsl/l2_op_rbuilder.go b/op-devstack/dsl/l2_op_rbuilder.go new file mode 100644 index 00000000000..5e9aa6c2d32 --- /dev/null +++ b/op-devstack/dsl/l2_op_rbuilder.go @@ -0,0 +1,61 @@ +package dsl + +import ( + opclient "github.com/ethereum-optimism/optimism/op-service/client" + + "github.com/ethereum-optimism/optimism/op-devstack/stack" +) + +type OPRBuilderNodeSet []*OPRBuilderNode + +func NewOPRBuilderNodeSet(inner []stack.OPRBuilderNode) OPRBuilderNodeSet { + oprbuilders := make([]*OPRBuilderNode, len(inner)) + for i, c := range inner { + oprbuilders[i] = NewOPRBuilderNode(c) + } + return oprbuilders +} + +type OPRBuilderNode struct { + commonImpl + inner stack.OPRBuilderNode + wsClient *opclient.WSClient +} + +func NewOPRBuilderNode(inner stack.OPRBuilderNode) *OPRBuilderNode { + return &OPRBuilderNode{ + commonImpl: commonFromT(inner.T()), + inner: inner, + wsClient: inner.FlashblocksClient(), + } +} + +func (c *OPRBuilderNode) String() string { + return c.inner.Name() +} + +func (c *OPRBuilderNode) Escape() stack.OPRBuilderNode { + return c.inner +} + +func (c *OPRBuilderNode) FlashblocksClient() *opclient.WSClient { + return c.wsClient +} + +func (el *OPRBuilderNode) Stop() { + el.log.Info("Stopping", "name", el.inner.Name()) + lifecycle, ok := el.inner.(stack.Lifecycle) + el.require.Truef(ok, "op-rbuilder node %s is not lifecycle-controllable", el.inner.Name()) + lifecycle.Stop() +} + +func (el *OPRBuilderNode) Start() { + lifecycle, ok := el.inner.(stack.Lifecycle) + el.require.Truef(ok, "op-rbuilder node %s is not lifecycle-controllable", el.inner.Name()) + lifecycle.Start() +} + +func (el *OPRBuilderNode) UpdateRuleSet(rulesYaml string) { + el.log.Info("Updating rule", "content", rulesYaml) + el.require.NoError(el.inner.UpdateRuleSet(rulesYaml), "failed to update rule: %s", rulesYaml) +} diff --git a/op-devstack/dsl/proofs/dispute_game_factory.go b/op-devstack/dsl/proofs/dispute_game_factory.go new file mode 100644 index 00000000000..3ff363bffde --- /dev/null +++ b/op-devstack/dsl/proofs/dispute_game_factory.go @@ -0,0 +1,567 @@ +package proofs + +import ( + "context" + "encoding/binary" + "math/big" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "time" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + challengerConfig "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/prestates" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-challenger/metrics" + "github.com/ethereum-optimism/optimism/op-service/eth" + safetyTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" + + challengerTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/dsl/contract" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/bigs" + "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum-optimism/optimism/op-service/txplan" +) + +type DisputeGameFactory struct { + t devtest.T + require *require.Assertions + log log.Logger + l1Network *dsl.L1Network + ethClient apis.EthClient + dgf *bindings.DisputeGameFactory + addr common.Address + l2CL *dsl.L2CLNode + l2EL *dsl.L2ELNode + superNode *dsl.Supernode + gameHelper *GameHelper + challengerCfg *challengerConfig.Config + + honestTraces map[common.Address]challengerTypes.TraceAccessor +} + +func NewDisputeGameFactory( + t devtest.T, + l1Network *dsl.L1Network, + ethClient apis.EthClient, + dgfAddr common.Address, + l2CL *dsl.L2CLNode, + l2EL *dsl.L2ELNode, + superNode *dsl.Supernode, + challengerCfg *challengerConfig.Config, +) *DisputeGameFactory { + dgf := bindings.NewDisputeGameFactory(bindings.WithClient(ethClient), bindings.WithTo(dgfAddr), bindings.WithTest(t)) + + return &DisputeGameFactory{ + t: t, + require: require.New(t), + log: t.Logger(), + l1Network: l1Network, + dgf: dgf, + addr: dgfAddr, + l2CL: l2CL, + l2EL: l2EL, + superNode: superNode, + ethClient: ethClient, + challengerCfg: challengerCfg, + + honestTraces: make(map[common.Address]challengerTypes.TraceAccessor), + } +} + +type GameCfg struct { + allowFuture bool + allowUnsafe bool + l2SequenceNumber uint64 + l2SequenceNumberSet bool + rootClaimSet bool + rootClaim common.Hash + superOutputRoots []eth.Bytes32 +} +type GameOpt interface { + Apply(cfg *GameCfg) +} +type gameOptFn func(c *GameCfg) + +func (g gameOptFn) Apply(cfg *GameCfg) { + g(cfg) +} + +func WithUnsafeProposal() GameOpt { + return gameOptFn(func(c *GameCfg) { + c.allowUnsafe = true + }) +} + +func WithFutureProposal() GameOpt { + return gameOptFn(func(c *GameCfg) { + c.allowFuture = true + }) +} + +func WithRootClaim(claim common.Hash) GameOpt { + return gameOptFn(func(c *GameCfg) { + c.rootClaim = claim + c.rootClaimSet = true + }) +} + +func WithL2SequenceNumber(seqNum uint64) GameOpt { + return gameOptFn(func(c *GameCfg) { + c.l2SequenceNumber = seqNum + c.l2SequenceNumberSet = true + }) +} + +// WithSuperRootFrom sets the output roots to use in a super root game. +// The length of outputRoots must match the number of chains in the super root. +func WithSuperRootFrom(outputRoots ...eth.Bytes32) GameOpt { + return gameOptFn(func(c *GameCfg) { + c.superOutputRoots = outputRoots + }) +} + +func NewGameCfg(opts ...GameOpt) *GameCfg { + cfg := &GameCfg{} + for _, opt := range opts { + opt.Apply(cfg) + } + return cfg +} + +func (f *DisputeGameFactory) Address() common.Address { + return f.addr +} + +func (f *DisputeGameFactory) getGameHelper(eoa *dsl.EOA) *GameHelper { + if f.gameHelper != nil { + return f.gameHelper + } + gs := DeployGameHelper(f.t, eoa, f.honestTraceForGame) + f.gameHelper = gs + return gs +} + +func (f *DisputeGameFactory) GameCount() int64 { + return contract.Read(f.dgf.GameCount()).Int64() +} + +func (f *DisputeGameFactory) GameAtIndex(idx int64) *FaultDisputeGame { + gameInfo := contract.Read(f.dgf.GameAtIndex(big.NewInt(idx))) + game := bindings.NewFaultDisputeGame(bindings.WithClient(f.ethClient), bindings.WithTo(gameInfo.Proxy), bindings.WithTest(f.t)) + return NewFaultDisputeGame(f.t, f.require, gameInfo.Proxy, f.getGameHelper, f.honestTraceForGame, game) +} + +func (f *DisputeGameFactory) GameImpl(gameType gameTypes.GameType) *FaultDisputeGame { + implAddr := contract.Read(f.dgf.GameImpls(uint32(gameType))) + game := bindings.NewFaultDisputeGame(bindings.WithClient(f.ethClient), bindings.WithTo(implAddr), bindings.WithTest(f.t)) + return NewFaultDisputeGame(f.t, f.require, implAddr, f.getGameHelper, f.honestTraceForGame, game) +} + +func (f *DisputeGameFactory) GameArgs(gameType gameTypes.GameType) []byte { + return contract.Read(f.dgf.GameArgs(uint32(gameType))) +} + +func (f *DisputeGameFactory) WaitForGame() *FaultDisputeGame { + initialCount := f.GameCount() + f.t.Require().Eventually(func() bool { + gameCount := f.GameCount() + check := gameCount > initialCount + f.t.Logf("waiting for new game. current=%d new=%d", initialCount, gameCount) + return check + }, time.Minute*10, time.Second*5) + + return f.GameAtIndex(initialCount) +} + +func (f *DisputeGameFactory) StartSuperCannonKonaGame(eoa *dsl.EOA, opts ...GameOpt) *SuperFaultDisputeGame { + f.require.NotNil(f.superNode, "super node is required to start super games") + + return f.startSuperGameOfType(eoa, gameTypes.SuperCannonKonaGameType, opts...) +} + +func (f *DisputeGameFactory) startSuperGameOfType(eoa *dsl.EOA, gameType gameTypes.GameType, opts ...GameOpt) *SuperFaultDisputeGame { + cfg := NewGameCfg(opts...) + if len(cfg.superOutputRoots) != 0 && cfg.rootClaimSet { + f.t.Error("cannot set both super output roots and root claim in super game") + f.t.FailNow() + } + timestamp := cfg.l2SequenceNumber + if !cfg.l2SequenceNumberSet { + timestamp = f.safeTimestamp() + } + extraData := f.createSuperGameExtraData(timestamp, cfg) + rootClaim := cfg.rootClaim + if !cfg.rootClaimSet { + rootClaim = crypto.Keccak256Hash(extraData) + } + game, addr := f.createNewGame(eoa, gameType, rootClaim, extraData) + + return NewSuperFaultDisputeGame(f.t, f.require, addr, f.getGameHelper, f.honestTraceForGame, game) +} + +func (f *DisputeGameFactory) createSuperGameExtraData(timestamp uint64, cfg *GameCfg) []byte { + f.require.NotNil(f.superNode, "super node is required create super games") + if !cfg.allowFuture { + f.awaitMinVerifiedTimestamp(timestamp) + } + resp, err := f.superNode.QueryAPI().SuperRootAtTimestamp(f.t.Ctx(), timestamp) + f.require.NoError(err, "Failed to fetch super root at timestamp") + f.require.NotNil(resp.Data, "Super root data must be present at timestamp %v", timestamp) + superV1, ok := resp.Data.Super.(*eth.SuperV1) + f.require.Truef(ok, "unsupported super type %T", resp.Data.Super) + if len(cfg.superOutputRoots) != 0 { + f.require.Len(cfg.superOutputRoots, len(superV1.Chains), "Super output roots length mismatch") + for i := range superV1.Chains { + superV1.Chains[i].Output = cfg.superOutputRoots[i] + } + } + extraData := superV1.Marshal() + return extraData +} + +func (f *DisputeGameFactory) awaitMinVerifiedTimestamp(timestamp uint64) { + f.t.Require().Eventually(func() bool { + resp, err := f.superNode.QueryAPI().SuperRootAtTimestamp(f.t.Ctx(), timestamp) + f.require.NoError(err, "Failed to fetch supernode status (superroot_atTimestamp)") + return resp.Data != nil + }, 2*time.Minute, 1*time.Second) +} + +func (f *DisputeGameFactory) StartCannonGame(eoa *dsl.EOA, opts ...GameOpt) *FaultDisputeGame { + return f.startOutputRootGameOfType(eoa, gameTypes.CannonGameType, f.honestTraceForGame, opts...) +} + +func (f *DisputeGameFactory) StartCannonKonaGame(eoa *dsl.EOA, opts ...GameOpt) *FaultDisputeGame { + return f.startOutputRootGameOfType(eoa, gameTypes.CannonKonaGameType, f.honestTraceForGame, opts...) +} + +func (f *DisputeGameFactory) honestTraceForGame(game *FaultDisputeGame) challengerTypes.TraceAccessor { + if existing, ok := f.honestTraces[game.Address]; ok { + return existing + } + f.require.NotNil(f.challengerCfg, "Challenger config is required to create honest trace") + switch game.GameType() { + case gameTypes.CannonGameType: + return f.honestOutputCannonTrace( + game, + f.challengerCfg.CannonAbsolutePreStateBaseURL, + f.challengerCfg.CannonAbsolutePreState, + f.challengerCfg.Cannon, + vm.NewOpProgramServerExecutor(f.log), + ) + case gameTypes.CannonKonaGameType: + return f.honestOutputCannonTrace( + game, + f.challengerCfg.CannonKonaAbsolutePreStateBaseURL, + f.challengerCfg.CannonKonaAbsolutePreState, + f.challengerCfg.CannonKona, + vm.NewKonaExecutor(), + ) + case gameTypes.SuperCannonKonaGameType: + return f.honestSuperCannonTrace( + game, + f.challengerCfg.CannonKonaAbsolutePreStateBaseURL, + f.challengerCfg.CannonKonaAbsolutePreState, + f.challengerCfg.CannonKona, + vm.NewKonaSuperExecutor(), + ) + default: + f.require.Truef(false, "Honest trace not supported for game type %v", game.GameType()) + return nil + } +} + +func (f *DisputeGameFactory) honestOutputCannonTrace( + game *FaultDisputeGame, + prestateBaseUrl *url.URL, + prestateFile string, + vmConfig vm.Config, + serverExecutor vm.OracleServerExecutor, +) challengerTypes.TraceAccessor { + logger := f.t.Logger().New("role", "honestTrace") + prestateBlock := game.StartingL2SequenceNumber() + rollupClient := f.l2CL.Escape().RollupAPI() + prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) + l1HeadHash := game.L1Head() + l1Head, err := f.ethClient.BlockRefByHash(f.t.Ctx(), l1HeadHash) + f.require.NoError(err, "Failed to fetch L1 Head") + + prestateSource := prestates.NewPrestateSource( + prestateBaseUrl, + prestateFile, + path.Join(f.challengerCfg.Datadir, "test-prestates"), + cannon.NewStateConverter(vmConfig), + ) + prestatePath, err := prestateSource.PrestatePath(f.t.Ctx(), game.absolutePrestate()) + f.require.NoError(err, "Failed to get prestate path") + l2ElClient := f.l2EL.Escape().L2EthClient() + accessor, err := outputs.NewOutputCannonTraceAccessor( + logger, + metrics.NoopMetrics, + vmConfig, + serverExecutor, + ðClientHeaderProvider{client: l2ElClient}, + prestateProvider, + prestatePath, + rollupClient, + f.t.TempDir(), + l1Head.ID(), + game.SplitDepth(), + prestateBlock, + game.L2SequenceNumber(), + ) + f.require.NoError(err, "Failed to create trace accessor") + f.honestTraces[game.Address] = accessor + return accessor +} + +func (f *DisputeGameFactory) honestSuperCannonTrace( + game *FaultDisputeGame, + prestateBaseUrl *url.URL, + prestateFile string, + vmConfig vm.Config, + serverExecutor vm.OracleServerExecutor, +) challengerTypes.TraceAccessor { + logger := f.t.Logger().New("role", "honestSuperTrace") + f.require.NotNil(f.superNode, "SuperNode is required to create honest super trace") + + prestateTimestamp := game.StartingL2SequenceNumber() + poststateTimestamp := game.L2SequenceNumber() + + l1HeadHash := game.L1Head() + l1Head, err := f.ethClient.BlockRefByHash(f.t.Ctx(), l1HeadHash) + f.require.NoError(err, "Failed to fetch L1 Head") + + prestateProvider := super.NewSuperNodePrestateProvider(f.superNode.QueryAPI(), prestateTimestamp) + + vmPrestateSource := prestates.NewPrestateSource( + prestateBaseUrl, + prestateFile, + path.Join(f.challengerCfg.Datadir, "test-prestates"), + cannon.NewStateConverter(vmConfig), + ) + vmPrestatePath, err := vmPrestateSource.PrestatePath(f.t.Ctx(), game.absolutePrestate()) + f.require.NoError(err, "Failed to get prestate path") + + accessor, err := super.NewSuperCannonTraceAccessor( + logger, + metrics.NoopMetrics, + vmConfig, + serverExecutor, + prestateProvider, + nil, // supervisor client + f.superNode.QueryAPI(), + true, + vmPrestatePath, + path.Join(f.challengerCfg.Datadir, "test-prestates"), + l1Head.ID(), + game.SplitDepth(), + prestateTimestamp, + poststateTimestamp, + ) + f.require.NoError(err, "Failed to create super cannon trace accessor") + + f.honestTraces[game.Address] = accessor + return accessor +} + +func (f *DisputeGameFactory) startOutputRootGameOfType( + eoa *dsl.EOA, + gameType gameTypes.GameType, + honestTraceProvider func(game *FaultDisputeGame) challengerTypes.TraceAccessor, + opts ...GameOpt) *FaultDisputeGame { + cfg := NewGameCfg(opts...) + blockNum := cfg.l2SequenceNumber + if !cfg.l2SequenceNumberSet { + blockNum = f.l2CL.SafeL2BlockRef().Number + } + extraData := f.createOutputGameExtraData(blockNum, cfg) + rootClaim := cfg.rootClaim + if !cfg.rootClaimSet { + // Default to correct root claim + response, err := f.l2CL.Escape().RollupAPI().OutputAtBlock(f.t.Ctx(), blockNum) + f.require.NoErrorf(err, "Failed to get output root at block %v", blockNum) + rootClaim = common.Hash(response.OutputRoot) + } + game, addr := f.createNewGame(eoa, gameType, rootClaim, extraData) + return NewFaultDisputeGame(f.t, f.require, addr, f.getGameHelper, honestTraceProvider, game) +} + +func (f *DisputeGameFactory) createOutputGameExtraData(blockNum uint64, cfg *GameCfg) []byte { + f.require.NotNil(f.l2CL, "L2 CL is required create output games") + if !cfg.allowFuture { + f.l2CL.Reached(safetyTypes.LocalSafe, blockNum, 30) + } + extraData := make([]byte, 32) + binary.BigEndian.PutUint64(extraData[24:], blockNum) + return extraData +} + +func (f *DisputeGameFactory) createNewGame(eoa *dsl.EOA, gameType gameTypes.GameType, claim common.Hash, extraData []byte) (*bindings.FaultDisputeGame, common.Address) { + f.log.Info("Creating dispute game", "gameType", gameType, "claim", claim.Hex(), "extradata", common.Bytes2Hex(extraData)) + + // Pull some metadata we need to construct a new game + requiredBonds := f.initBond(gameType) + + receipt := contract.Write(eoa, f.dgf.Create(uint32(gameType), claim, extraData), txplan.WithValue(requiredBonds), txplan.WithGasRatio(2)) + f.require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + + // Extract logs from receipt + f.require.Equal(2, len(receipt.Logs)) + createdLog, err := f.dgf.ParseDisputeGameCreated(receipt.Logs[1]) + f.require.NoError(err) + + gameAddr := createdLog.DisputeProxy + log.Info("Dispute game created", "address", gameAddr.Hex()) + return bindings.NewFaultDisputeGame(bindings.WithClient(f.ethClient), bindings.WithTo(gameAddr), bindings.WithTest(f.t)), gameAddr +} + +func (f *DisputeGameFactory) initBond(gameType gameTypes.GameType) eth.ETH { + return eth.WeiBig(contract.Read(f.dgf.InitBonds(uint32(gameType)))) +} + +func (f *DisputeGameFactory) CreateHelperEOA(eoa *dsl.EOA) *GameHelperEOA { + helper := f.getGameHelper(eoa) + eoaHelper := helper.AuthEOA(eoa) + return &GameHelperEOA{ + helper: eoaHelper, + EOA: eoa, + } +} + +// safeTimestamp retrieves the current safe timestamp from the supernode. +func (f *DisputeGameFactory) safeTimestamp() uint64 { + now := uint64(time.Now().Unix()) + resp, err := f.superNode.QueryAPI().SuperRootAtTimestamp(f.t.Ctx(), now) + f.require.NoError(err, "Failed to fetch super root at timestamp") + return resp.CurrentSafeTimestamp +} + +// RunFPP runs the fault proof program between the two supplied timestamps. Currently only supports kona-interop. +func (f *DisputeGameFactory) RunFPP(startTimestamp uint64, endTimestamp uint64) { + f.require.NotNil(f.superNode, "super node is required to run FPP") + f.require.NotNil(f.challengerCfg, "challenger config is required to run FPP") + + splitDepth := f.GameImpl(gameTypes.SuperCannonKonaGameType).SplitDepth() + + // Use the current L1 head that the super node has processed. Otherwise the trace provider will fail because the node is not sufficiently up to date. + superRootResp, err := f.superNode.QueryAPI().SuperRootAtTimestamp(f.t.Ctx(), endTimestamp) + f.require.NoError(err, "Failed to fetch super root at timestamp") + l1Head := superRootResp.CurrentL1 + + prestateProvider := super.NewSuperNodePrestateProvider(f.superNode.QueryAPI(), startTimestamp) + traceProvider := super.NewSuperNodeTraceProvider( + f.log.New("role", "fpp-trace"), + prestateProvider, + f.superNode.QueryAPI(), + eth.BlockID{Hash: l1Head.Hash, Number: l1Head.Number}, + splitDepth, + startTimestamp, + endTimestamp, + ) + + tmpDir := f.t.TempDir() + + // Starting prestate is the aboslutePrestate + absolutePrestate, err := prestateProvider.AbsolutePreState(f.t.Ctx()) + f.require.NoError(err, "Failed to get absolute prestate") + agreedPrestate := absolutePrestate.Marshal() + + // Iterate through valid claims at splitDepth (the leaves of the top game) to get a few steps past the endTimestamp + for i := uint64(0); i < (endTimestamp-startTimestamp)*super.StepsPerTimestamp+3; i++ { + pos := challengerTypes.NewPosition(splitDepth, new(big.Int).SetUint64(i)) + + timestamp, step, err := traceProvider.ComputeStep(pos) + f.require.NoError(err, "Failed to compute step") + + // Create LocalGameInputs using the previous claim (or anchor state) as agreed and current as disputed + f.log.Info("Getting preimage bytes at position", "position", pos, "timestamp", timestamp, "step", step, "i", i) + claimedPreimage, err := traceProvider.GetPreimageBytes(f.t.Ctx(), pos) + f.require.NoError(err, "Failed to get claim at position %v", pos) + inputs := utils.LocalGameInputs{ + L1Head: l1Head.Hash, + AgreedPreState: agreedPrestate, + L2Claim: crypto.Keccak256Hash(claimedPreimage), + L2SequenceNumber: new(big.Int).SetUint64(endTimestamp), + } + + f.log.Info("Created LocalGameInputs for FPP", + "index", pos.IndexAtDepth(), + "l1Head", inputs.L1Head, + "l2Claim", inputs.L2Claim, + "startTimestamp", startTimestamp, + "endTimestamp", endTimestamp, + "timestamp", timestamp, + "step", step, + "invalidTransition", super.InvalidTransition, + "invalidTransitionHash", super.InvalidTransitionHash, + ) + + runFPPForStep(f, tmpDir, inputs) + + // This claim becomes the agreed prestate for the next iteration + agreedPrestate = claimedPreimage + } +} + +// runFPPForStep executes the native kona interop client using the LocalGameInputs and requires the claim to be successfully validated. +func runFPPForStep(f *DisputeGameFactory, tmpDir string, inputs utils.LocalGameInputs) { + executor := vm.NewNativeKonaSuperExecutor() + oracleCommand, err := executor.OracleCommand(f.challengerCfg.CannonKona, tmpDir, inputs) + f.require.NoError(err, "Failed to create command") + f.log.Info("Executing FPP", "command", oracleCommand) + exePath, err := filepath.Abs(oracleCommand[0]) + f.require.NoError(err, "Failed to get absolute path to executable") + cmd := exec.Command(exePath, oracleCommand[1:]...) + cmd.Dir = tmpDir + log := f.log.New("role", "fpp-trace") + cmd.Stdout = &mipsevm.LoggingWriter{Log: log} + cmd.Stderr = &mipsevm.LoggingWriter{Log: log} + cmd.Env = append(append(cmd.Env, os.Environ()...), "NO_COLOR=1") + err = cmd.Run() + f.require.NoError(err, "Failed to execute game") +} + +type GameHelperEOA struct { + helper *GameHelper + EOA *dsl.EOA +} + +func (a *GameHelperEOA) PerformMoves(game *FaultDisputeGame, moves ...GameHelperMove) []*Claim { + return a.helper.PerformMoves(a.EOA, game, moves) +} + +func (a *GameHelperEOA) Address() common.Address { + return a.EOA.Address() +} + +// ethClientHeaderProvider is an adapter for the L1Client interface used in op-node and devstack to +// the HeaderProvider interface used in challenger +type ethClientHeaderProvider struct { + client apis.EthClient +} + +func (p *ethClientHeaderProvider) HeaderByNumber(ctx context.Context, blockNum *big.Int) (*types.Header, error) { + info, err := p.client.InfoByNumber(ctx, bigs.Uint64Strict(blockNum)) + if err != nil { + return nil, err + } + return info.Header(), nil +} diff --git a/op-devstack/presets/flashblocks.go b/op-devstack/presets/flashblocks.go new file mode 100644 index 00000000000..b16d067cdd1 --- /dev/null +++ b/op-devstack/presets/flashblocks.go @@ -0,0 +1,176 @@ +package presets + +import ( + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/dsl/proofs" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-faucet/faucet" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type SingleChainWithFlashblocks struct { + *Minimal + + L2OPRBuilder *dsl.OPRBuilderNode + L2RollupBoost *dsl.RollupBoostNode + TestSequencer *dsl.TestSequencer +} + +func (m *SingleChainWithFlashblocks) L2Networks() []*dsl.L2Network { + return []*dsl.L2Network{ + m.L2Chain, + } +} + +func (m *SingleChainWithFlashblocks) StandardBridge() *dsl.StandardBridge { + return dsl.NewStandardBridge(m.T, m.L2Chain, m.L1EL) +} + +func (m *SingleChainWithFlashblocks) DisputeGameFactory() *proofs.DisputeGameFactory { + return proofs.NewDisputeGameFactory(m.T, m.L1Network, m.L1EL.EthClient(), m.L2Chain.DisputeGameFactoryProxyAddr(), m.L2CL, m.L2EL, nil, m.challengerConfig) +} + +func (m *SingleChainWithFlashblocks) AdvanceTime(amount time.Duration) { + m.Minimal.AdvanceTime(amount) +} + +func NewSingleChainWithFlashblocks(t devtest.T, opts ...Option) *SingleChainWithFlashblocks { + presetCfg, _ := collectSupportedPresetConfig(t, "NewSingleChainWithFlashblocks", opts, singleChainWithFlashblocksPresetSupportedOptionKinds) + runtime := sysgo.NewFlashblocksRuntimeWithConfig(t, presetCfg) + return singleChainWithFlashblocksFromRuntime(t, runtime) +} + +func singleChainWithFlashblocksFromRuntime(t devtest.T, runtime *sysgo.SingleChainRuntime) *SingleChainWithFlashblocks { + t.Require().NotNil(runtime.Flashblocks, "missing flashblocks support") + l1ChainID := runtime.L1Network.ChainID() + l2ChainID := runtime.L2Network.ChainID() + + l1Network := newPresetL1Network(t, "l1", runtime.L1Network.ChainConfig()) + l1EL := newL1ELFrontend(t, "l1", l1ChainID, runtime.L1EL.UserRPC()) + l1CL := newL1CLFrontend(t, "l1", l1ChainID, runtime.L1CL.BeaconHTTPAddr(), runtime.L1CL.FakePoS()) + l1Network.AddL1ELNode(l1EL) + l1Network.AddL1CLNode(l1CL) + + l2Chain := newPresetL2Network( + t, + "l2a", + runtime.L2Network.ChainConfig(), + runtime.L2Network.RollupConfig(), + runtime.L2Network.Deployment(), + newKeyring(runtime.Keys, t.Require()), + l1Network, + ) + + l2EL := newL2ELFrontend( + t, + "sequencer", + l2ChainID, + runtime.L2EL.UserRPC(), + runtime.L2EL.EngineRPC(), + runtime.L2EL.JWTPath(), + runtime.L2Network.RollupConfig(), + ) + l2CL := newL2CLFrontend( + t, + "sequencer", + l2ChainID, + runtime.L2CL.UserRPC(), + runtime.L2CL, + ) + + l2OPRBuilder := newOPRBuilderFrontend( + t, + "sequencer-builder", + l2ChainID, + runtime.Flashblocks.Builder.UserRPC(), + runtime.Flashblocks.Builder.FlashblocksWSURL(), + runtime.Flashblocks.Builder.UpdateRuleSet, + runtime.L2Network.RollupConfig(), + runtime.Flashblocks.Builder, + ) + l2RollupBoost := newRollupBoostFrontend( + t, + "rollup-boost", + l2ChainID, + runtime.Flashblocks.RollupBoost.UserRPC(), + runtime.Flashblocks.RollupBoost.FlashblocksWSURL(), + runtime.L2Network.RollupConfig(), + runtime.Flashblocks.RollupBoost, + ) + testSequencer := newTestSequencerFrontend( + t, + runtime.TestSequencer.Name, + runtime.TestSequencer.AdminRPC, + runtime.TestSequencer.ControlRPC, + runtime.TestSequencer.JWTSecret, + ) + + l2Chain.AddL2ELNode(l2EL) + l2Chain.AddL2CLNode(l2CL) + l2Chain.AddOPRBuilderNode(l2OPRBuilder) + l2Chain.AddRollupBoostNode(l2RollupBoost) + l2CL.attachEL(l2EL) + l2CL.attachOPRBuilderNode(l2OPRBuilder) + l2CL.attachRollupBoostNode(l2RollupBoost) + + faucetL1Frontend := newFaucetFrontendForChain(t, runtime.FaucetService, l1ChainID) + faucetL2Frontend := newFaucetFrontendForChain(t, runtime.FaucetService, l2ChainID) + l1Network.AddFaucet(faucetL1Frontend) + l2Chain.AddFaucet(faucetL2Frontend) + faucetL1 := dsl.NewFaucet(faucetL1Frontend) + faucetL2 := dsl.NewFaucet(faucetL2Frontend) + + l1ELDSL := dsl.NewL1ELNode(l1EL) + l1CLDSL := dsl.NewL1CLNode(l1CL) + l2ELDSL := dsl.NewL2ELNode(l2EL) + l2CLDSL := dsl.NewL2CLNode(l2CL) + + minimal := &Minimal{ + Log: t.Logger(), + T: t, + L1Network: dsl.NewL1Network(l1Network, l1ELDSL, l1CLDSL), + L1EL: l1ELDSL, + L1CL: l1CLDSL, + L2Chain: dsl.NewL2Network(l2Chain, l2ELDSL, l2CLDSL, l1ELDSL, nil, nil), + L2EL: l2ELDSL, + L2CL: l2CLDSL, + Wallet: dsl.NewRandomHDWallet(t, 30), // Random for test isolation + FaucetL1: faucetL1, + FaucetL2: faucetL2, + } + minimal.FunderL1 = dsl.NewFunder(minimal.Wallet, minimal.FaucetL1, minimal.L1EL) + minimal.FunderL2 = dsl.NewFunder(minimal.Wallet, minimal.FaucetL2, minimal.L2EL) + + return &SingleChainWithFlashblocks{ + L2OPRBuilder: dsl.NewOPRBuilderNode(l2OPRBuilder), + L2RollupBoost: dsl.NewRollupBoostNode(l2RollupBoost), + Minimal: minimal, + TestSequencer: dsl.NewTestSequencer(testSequencer), + } +} + +func newFaucetFrontendForChain(t devtest.T, faucetService *faucet.Service, chainID eth.ChainID) *faucetFrontend { + faucetName, faucetRPC, ok := defaultFaucetForChain(faucetService, chainID) + t.Require().Truef(ok, "missing default faucet for chain %s", chainID) + + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), faucetRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + + return newPresetFaucet(t, faucetName, chainID, rpcCl) +} + +func defaultFaucetForChain(faucetService *faucet.Service, chainID eth.ChainID) (string, string, bool) { + if faucetService == nil { + return "", "", false + } + faucetID, ok := faucetService.Defaults()[chainID] + if !ok { + return "", "", false + } + return faucetID.String(), faucetService.FaucetEndpoint(faucetID), true +} diff --git a/op-devstack/presets/option_validation.go b/op-devstack/presets/option_validation.go new file mode 100644 index 00000000000..793229dcd55 --- /dev/null +++ b/op-devstack/presets/option_validation.go @@ -0,0 +1,161 @@ +package presets + +import ( + "fmt" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +type optionKinds uint64 + +const ( + optionKindDeployer optionKinds = 1 << iota + optionKindBatcher + optionKindProposer + optionKindOPRBuilder + optionKindGlobalL2CL + optionKindGlobalSyncTesterEL + optionKindL1EL + optionKindAddedGameType + optionKindRespectedGameType + optionKindChallengerCannonKona + optionKindTimeTravel + optionKindMaxSequencingWindow + optionKindRequireInteropNotAtGen + optionKindAfterBuild + optionKindProofValidation +) + +const allOptionKinds = optionKindDeployer | + optionKindBatcher | + optionKindProposer | + optionKindOPRBuilder | + optionKindGlobalL2CL | + optionKindGlobalSyncTesterEL | + optionKindL1EL | + optionKindAddedGameType | + optionKindRespectedGameType | + optionKindChallengerCannonKona | + optionKindTimeTravel | + optionKindMaxSequencingWindow | + optionKindRequireInteropNotAtGen | + optionKindAfterBuild | + optionKindProofValidation + +var optionKindLabels = []struct { + kind optionKinds + label string +}{ + {kind: optionKindDeployer, label: "deployer options"}, + {kind: optionKindBatcher, label: "batcher options"}, + {kind: optionKindProposer, label: "proposer options"}, + {kind: optionKindOPRBuilder, label: "builder options"}, + {kind: optionKindGlobalL2CL, label: "L2 CL options"}, + {kind: optionKindGlobalSyncTesterEL, label: "sync tester EL options"}, + {kind: optionKindL1EL, label: "L1 EL options"}, + {kind: optionKindAddedGameType, label: "added game types"}, + {kind: optionKindRespectedGameType, label: "respected game types"}, + {kind: optionKindChallengerCannonKona, label: "challenger cannon-kona"}, + {kind: optionKindTimeTravel, label: "time travel"}, + {kind: optionKindMaxSequencingWindow, label: "max sequencing window"}, + {kind: optionKindRequireInteropNotAtGen, label: "interop-not-at-genesis"}, + {kind: optionKindAfterBuild, label: "after-build hooks"}, + {kind: optionKindProofValidation, label: "proof-validation hooks"}, +} + +func (k optionKinds) String() string { + if k == 0 { + return "none" + } + + names := make([]string, 0, len(optionKindLabels)) + for _, label := range optionKindLabels { + if k&label.kind == 0 { + continue + } + names = append(names, label.label) + } + if unknown := k &^ allOptionKinds; unknown != 0 { + names = append(names, fmt.Sprintf("unknown(%#x)", uint64(unknown))) + } + return strings.Join(names, ", ") +} + +func unsupportedPresetOptionKinds(opts Option, supported optionKinds) optionKinds { + if opts == nil { + return 0 + } + return opts.optionKinds() &^ supported +} + +func collectSupportedPresetConfig(t devtest.T, presetName string, opts []Option, supported optionKinds) (sysgo.PresetConfig, CombinedOption) { + cfg, combined := collectPresetConfig(opts) + if unsupported := unsupportedPresetOptionKinds(combined, supported); unsupported != 0 { + t.Require().FailNowf("%s does not support preset options: %s", presetName, unsupported) + } + return cfg, combined +} + +const minimalPresetSupportedOptionKinds = optionKindDeployer | + optionKindBatcher | + optionKindProposer | + optionKindGlobalL2CL | + optionKindL1EL | + optionKindAddedGameType | + optionKindRespectedGameType | + optionKindChallengerCannonKona | + optionKindTimeTravel | + optionKindAfterBuild | + optionKindProofValidation + +const minimalWithConductorsPresetSupportedOptionKinds = optionKindDeployer | + optionKindBatcher | + optionKindProposer | + optionKindGlobalL2CL | + optionKindL1EL | + optionKindAddedGameType | + optionKindRespectedGameType | + optionKindTimeTravel | + optionKindAfterBuild | + optionKindProofValidation + +const simpleWithSyncTesterPresetSupportedOptionKinds = minimalPresetSupportedOptionKinds | + optionKindGlobalSyncTesterEL + +const singleChainInteropPresetSupportedOptionKinds = optionKindDeployer | + optionKindBatcher | + optionKindProposer | + optionKindGlobalL2CL | + optionKindL1EL | + optionKindAddedGameType | + optionKindRespectedGameType | + optionKindTimeTravel | + optionKindMaxSequencingWindow | + optionKindRequireInteropNotAtGen | + optionKindAfterBuild | + optionKindProofValidation + +const simpleInteropSuperProofsPresetSupportedOptionKinds = optionKindDeployer | + optionKindBatcher | + optionKindProposer | + optionKindGlobalL2CL | + optionKindL1EL | + optionKindChallengerCannonKona | + optionKindTimeTravel | + optionKindMaxSequencingWindow | + optionKindRequireInteropNotAtGen + +const supernodeProofsPresetSupportedOptionKinds = optionKindChallengerCannonKona | + optionKindL1EL + +const twoL2SupernodePresetSupportedOptionKinds = optionKindDeployer | + optionKindL1EL + +const twoL2SupernodeInteropPresetSupportedOptionKinds = optionKindDeployer | + optionKindTimeTravel | + optionKindL1EL + +const singleChainWithFlashblocksPresetSupportedOptionKinds = optionKindDeployer | + optionKindOPRBuilder diff --git a/op-devstack/presets/options.go b/op-devstack/presets/options.go new file mode 100644 index 00000000000..37dceaaacac --- /dev/null +++ b/op-devstack/presets/options.go @@ -0,0 +1,283 @@ +package presets + +import ( + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type Option interface { + applyConfig(cfg *sysgo.PresetConfig) + applyPreset(target any) + optionKinds() optionKinds +} + +type option struct { + applyFn func(cfg *sysgo.PresetConfig) + applyPresetFn func(target any) + kinds optionKinds +} + +func (o option) applyConfig(cfg *sysgo.PresetConfig) { + if o.applyFn == nil { + return + } + o.applyFn(cfg) +} + +func (o option) applyPreset(target any) { + if o.applyPresetFn != nil { + o.applyPresetFn(target) + } +} + +func (o option) optionKinds() optionKinds { + return o.kinds +} + +type CombinedOption []Option + +func Combine(opts ...Option) CombinedOption { + return CombinedOption(opts) +} + +func (c CombinedOption) applyConfig(cfg *sysgo.PresetConfig) { + for _, opt := range c { + if opt == nil { + continue + } + opt.applyConfig(cfg) + } +} + +func (c CombinedOption) applyPreset(target any) { + for _, opt := range c { + if opt == nil { + continue + } + opt.applyPreset(target) + } +} + +func (c CombinedOption) optionKinds() optionKinds { + var kinds optionKinds + for _, opt := range c { + if opt == nil { + continue + } + kinds |= opt.optionKinds() + } + return kinds +} + +func AfterBuild(fn func(target any)) Option { + var kinds optionKinds + if fn != nil { + kinds = optionKindAfterBuild + } + return option{applyPresetFn: fn, kinds: kinds} +} + +func collectPresetConfig(opts []Option) (sysgo.PresetConfig, CombinedOption) { + cfg := sysgo.NewPresetConfig() + combined := Combine(opts...) + combined.applyConfig(&cfg) + return cfg, combined +} + +func WithDeployerOptions(opts ...sysgo.DeployerOption) Option { + var kinds optionKinds + for _, opt := range opts { + if opt != nil { + kinds = optionKindDeployer + break + } + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.DeployerOptions = append(cfg.DeployerOptions, opts...) + }, + } +} + +// WithLocalContractSourcesAt configures a preset to load local contracts-bedrock +// artifacts from the supplied directory instead of resolving them relative to +// the process working directory. +func WithLocalContractSourcesAt(path string) Option { + var kinds optionKinds + if path != "" { + kinds = optionKindDeployer + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if path == "" { + return + } + cfg.LocalContractArtifactsPath = path + }, + } +} + +func WithBatcherOption(opt sysgo.BatcherOption) Option { + var kinds optionKinds + if opt != nil { + kinds = optionKindBatcher + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if opt == nil { + return + } + cfg.BatcherOptions = append(cfg.BatcherOptions, opt) + }, + } +} + +func WithGlobalL2CLOption(opt sysgo.L2CLOption) Option { + var kinds optionKinds + if opt != nil { + kinds = optionKindGlobalL2CL + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if opt == nil { + return + } + cfg.GlobalL2CLOptions = append(cfg.GlobalL2CLOptions, opt) + }, + } +} + +func WithGlobalSyncTesterELOption(opt sysgo.SyncTesterELOption) Option { + var kinds optionKinds + if opt != nil { + kinds = optionKindGlobalSyncTesterEL + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if opt == nil { + return + } + cfg.GlobalSyncTesterELOptions = append(cfg.GlobalSyncTesterELOptions, opt) + }, + } +} + +func WithL1Geth(execPath string) Option { + return option{ + kinds: optionKindL1EL, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.L1ELKind = "geth" + cfg.L1GethExecPath = execPath + }, + } +} + +func WithProposerOption(opt sysgo.ProposerOption) Option { + var kinds optionKinds + if opt != nil { + kinds = optionKindProposer + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if opt == nil { + return + } + cfg.ProposerOptions = append(cfg.ProposerOptions, opt) + }, + } +} + +func WithOPRBuilderOption(opt sysgo.OPRBuilderNodeOption) Option { + var kinds optionKinds + if opt != nil { + kinds = optionKindOPRBuilder + } + return option{ + kinds: kinds, + applyFn: func(cfg *sysgo.PresetConfig) { + if opt == nil { + return + } + cfg.OPRBuilderOptions = append(cfg.OPRBuilderOptions, opt) + }, + } +} + +func WithGameTypeAdded(gameType gameTypes.GameType) Option { + return option{ + kinds: optionKindAddedGameType, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.AddedGameTypes = append(cfg.AddedGameTypes, gameType) + }, + } +} + +func WithRespectedGameTypeOverride(gameType gameTypes.GameType) Option { + return option{ + kinds: optionKindRespectedGameType, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.RespectedGameTypes = append(cfg.RespectedGameTypes, gameType) + }, + } +} + +func WithCannonKonaGameTypeAdded() Option { + return option{ + kinds: optionKindAddedGameType | optionKindChallengerCannonKona, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.EnableCannonKonaForChall = true + cfg.AddedGameTypes = append(cfg.AddedGameTypes, gameTypes.CannonKonaGameType) + }, + } +} + +func WithChallengerCannonKonaEnabled() Option { + return option{ + kinds: optionKindChallengerCannonKona, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.EnableCannonKonaForChall = true + }, + } +} + +func WithTimeTravelEnabled() Option { + return option{ + kinds: optionKindTimeTravel, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.EnableTimeTravel = true + }, + } +} + +func WithMaxSequencingWindow(max uint64) Option { + return option{ + kinds: optionKindMaxSequencingWindow, + applyFn: func(cfg *sysgo.PresetConfig) { + v := max + cfg.MaxSequencingWindow = &v + }, + } +} + +func WithRequireInteropNotAtGenesis() Option { + return option{ + kinds: optionKindRequireInteropNotAtGen, + applyFn: func(cfg *sysgo.PresetConfig) { + cfg.RequireInteropNotAtGen = true + }, + } +} + +// WithL2BlockTimes configures per-chain L2 block times via the deployer. +// The blockTimes map keys are L2 chain IDs and values are the desired block +// time in seconds for that chain. +func WithL2BlockTimes(blockTimes map[eth.ChainID]uint64) Option { + return WithDeployerOptions(sysgo.WithL2BlockTimes(blockTimes)) +} diff --git a/op-devstack/presets/options_test.go b/op-devstack/presets/options_test.go new file mode 100644 index 00000000000..9b0ab678159 --- /dev/null +++ b/op-devstack/presets/options_test.go @@ -0,0 +1,141 @@ +package presets + +import ( + "testing" + + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/stretchr/testify/require" +) + +func TestOptionKindsFromCompositeOptions(t *testing.T) { + t.Run("WithSequencingWindow", func(t *testing.T) { + require.Equal(t, + optionKindDeployer|optionKindMaxSequencingWindow, + WithSequencingWindow(12, 24).optionKinds(), + ) + }) + + t.Run("WithCannonKonaGameTypeAdded", func(t *testing.T) { + require.Equal(t, + optionKindAddedGameType|optionKindChallengerCannonKona, + WithCannonKonaGameTypeAdded().optionKinds(), + ) + }) + + t.Run("WithL1Geth", func(t *testing.T) { + require.Equal(t, + optionKindL1EL, + WithL1Geth("/tmp/geth").optionKinds(), + ) + }) + + t.Run("RequireGameTypePresent", func(t *testing.T) { + require.Equal(t, + optionKindAfterBuild|optionKindProofValidation, + RequireGameTypePresent(gameTypes.CannonGameType).optionKinds(), + ) + }) + + t.Run("nil adapters do not claim support kinds", func(t *testing.T) { + require.Zero(t, WithDeployerOptions(nil).optionKinds()) + require.Zero(t, WithLocalContractSourcesAt("").optionKinds()) + require.Zero(t, WithBatcherOption(nil).optionKinds()) + require.Zero(t, WithGlobalL2CLOption(nil).optionKinds()) + require.Zero(t, WithGlobalSyncTesterELOption(nil).optionKinds()) + require.Zero(t, WithProposerOption(nil).optionKinds()) + require.Zero(t, WithOPRBuilderOption(nil).optionKinds()) + require.Zero(t, AfterBuild(nil).optionKinds()) + }) +} + +func TestWithLocalContractSourcesAt(t *testing.T) { + cfg, _ := collectPresetConfig([]Option{WithLocalContractSourcesAt("/tmp/contracts-bedrock")}) + require.Equal(t, "/tmp/contracts-bedrock", cfg.LocalContractArtifactsPath) +} + +func TestUnsupportedPresetOptionKinds(t *testing.T) { + builderOpt := sysgo.OPRBuilderNodeOptionFn(func(devtest.CommonT, sysgo.ComponentTarget, *sysgo.OPRBuilderNodeConfig) {}) + + tests := []struct { + name string + supported optionKinds + opts Option + want optionKinds + }{ + { + name: "minimal allows proof validation hooks", + supported: minimalPresetSupportedOptionKinds, + opts: Combine( + WithTimeTravelEnabled(), + RequireGameTypePresent(gameTypes.CannonGameType), + ), + want: 0, + }, + { + name: "minimal allows l1 EL override", + supported: minimalPresetSupportedOptionKinds, + opts: WithL1Geth("/tmp/geth"), + want: 0, + }, + { + name: "minimal with conductors rejects challenger toggle", + supported: minimalWithConductorsPresetSupportedOptionKinds, + opts: WithChallengerCannonKonaEnabled(), + want: optionKindChallengerCannonKona, + }, + { + name: "flashblocks allows builder and deployer adapters", + supported: singleChainWithFlashblocksPresetSupportedOptionKinds, + opts: Combine( + WithLocalContractSourcesAt("/tmp/contracts-bedrock"), + WithOPRBuilderOption(builderOpt), + WithTimeTravelEnabled(), + ), + want: optionKindTimeTravel, + }, + { + name: "simple interop super proofs reject builder and proof hooks", + supported: simpleInteropSuperProofsPresetSupportedOptionKinds, + opts: Combine( + WithOPRBuilderOption(builderOpt), + RequireGameTypePresent(gameTypes.CannonGameType), + ), + want: optionKindOPRBuilder | optionKindAfterBuild | optionKindProofValidation, + }, + { + name: "supernode proofs only allow challenger toggle", + supported: supernodeProofsPresetSupportedOptionKinds, + opts: Combine( + WithChallengerCannonKonaEnabled(), + WithTimeTravelEnabled(), + ), + want: optionKindTimeTravel, + }, + { + name: "two l2 supernode rejects time travel", + supported: twoL2SupernodePresetSupportedOptionKinds, + opts: WithTimeTravelEnabled(), + want: optionKindTimeTravel, + }, + { + name: "two l2 supernode interop accepts time travel", + supported: twoL2SupernodeInteropPresetSupportedOptionKinds, + opts: WithTimeTravelEnabled(), + want: 0, + }, + { + name: "unsupported proof validation is called out separately from generic after build", + supported: optionKindAfterBuild, + opts: RequireGameTypePresent(gameTypes.CannonGameType), + want: optionKindProofValidation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, unsupportedPresetOptionKinds(tt.opts, tt.supported)) + }) + } +} diff --git a/op-devstack/presets/rpc_frontends.go b/op-devstack/presets/rpc_frontends.go new file mode 100644 index 00000000000..85d43b2eac5 --- /dev/null +++ b/op-devstack/presets/rpc_frontends.go @@ -0,0 +1,598 @@ +package presets + +import ( + "crypto/ecdsa" + "time" + + "github.com/ethereum/go-ethereum/common" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + challengerConfig "github.com/ethereum-optimism/optimism/op-challenger/config" + conductorRpc "github.com/ethereum-optimism/optimism/op-conductor/rpc" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/apis" + opclient "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/locks" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum-optimism/optimism/op-service/testreq" + "github.com/ethereum-optimism/optimism/op-sync-tester/synctester" +) + +type keyringImpl struct { + keys devkeys.Keys + require *testreq.Assertions +} + +var _ stack.Keys = (*keyringImpl)(nil) + +func newKeyring(keys devkeys.Keys, req *testreq.Assertions) *keyringImpl { + return &keyringImpl{ + keys: keys, + require: req, + } +} + +func (k *keyringImpl) Secret(key devkeys.Key) *ecdsa.PrivateKey { + pk, err := k.keys.Secret(key) + k.require.NoError(err) + return pk +} + +func (k *keyringImpl) Address(key devkeys.Key) common.Address { + addr, err := k.keys.Address(key) + k.require.NoError(err) + return addr +} + +type rpcELNode struct { + presetCommon + + client opclient.RPC + ethClient *sources.EthClient + chainID eth.ChainID + txTimeout time.Duration +} + +var _ stack.ELNode = (*rpcELNode)(nil) + +func newRPCELNode(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC, timeout time.Duration) rpcELNode { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + ethCl, err := sources.NewEthClient(rpcCl, t.Logger(), nil, sources.DefaultEthClientConfig(10)) + t.Require().NoError(err) + if timeout == 0 { + timeout = 30 * time.Second + } + return rpcELNode{ + presetCommon: newPresetCommon(t, name), + client: rpcCl, + ethClient: ethCl, + chainID: chainID, + txTimeout: timeout, + } +} + +func (r *rpcELNode) ChainID() eth.ChainID { + return r.chainID +} + +func (r *rpcELNode) EthClient() apis.EthClient { + return r.ethClient +} + +func (r *rpcELNode) TransactionTimeout() time.Duration { + return r.txTimeout +} + +type l1ELFrontend struct { + rpcELNode +} + +var _ stack.L1ELNode = (*l1ELFrontend)(nil) + +func newPresetL1ELNode(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC) *l1ELFrontend { + return &l1ELFrontend{ + rpcELNode: newRPCELNode(t, name, chainID, rpcCl, 0), + } +} + +type l1CLFrontend struct { + presetCommon + chainID eth.ChainID + client apis.BeaconClient + lifecycle stack.Lifecycle +} + +var _ stack.L1CLNode = (*l1CLFrontend)(nil) + +func newPresetL1CLNode(t devtest.T, name string, chainID eth.ChainID, httpCl opclient.HTTP) *l1CLFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &l1CLFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + client: sources.NewBeaconHTTPClient(httpCl), + } +} + +func (r *l1CLFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *l1CLFrontend) BeaconClient() apis.BeaconClient { + return r.client +} + +func (r *l1CLFrontend) Start() { + r.require().NotNil(r.lifecycle, "L1CL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *l1CLFrontend) Stop() { + r.require().NotNil(r.lifecycle, "L1CL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type l2ELFrontend struct { + rpcELNode + l2Client *sources.L2Client + l2EngineClient *sources.EngineClient + lifecycle stack.Lifecycle +} + +var _ stack.L2ELNode = (*l2ELFrontend)(nil) + +func newPresetL2ELNode(t devtest.T, name string, chainID eth.ChainID, userRPCCl opclient.RPC, engineRPCCl opclient.RPC, rollupCfg *rollup.Config) *l2ELFrontend { + t.Require().NotNil(rollupCfg, "rollup config must be configured") + l2Client, err := sources.NewL2Client(userRPCCl, t.Logger(), nil, sources.L2ClientSimpleConfig(rollupCfg, false, 10, 10)) + t.Require().NoError(err) + engineClientCfg := &sources.EngineClientConfig{ + L2ClientConfig: *sources.L2ClientSimpleConfig(rollupCfg, false, 10, 10), + } + engineClient, err := sources.NewEngineClient(engineRPCCl, t.Logger(), nil, engineClientCfg) + t.Require().NoError(err) + return &l2ELFrontend{ + rpcELNode: newRPCELNode(t, name, chainID, userRPCCl, 0), + l2Client: l2Client, + l2EngineClient: engineClient, + } +} + +func (r *l2ELFrontend) L2EthClient() apis.L2EthClient { + return r.l2Client +} + +func (r *l2ELFrontend) L2EngineClient() apis.EngineClient { + return r.l2EngineClient.EngineAPIClient +} + +func (r *l2ELFrontend) Start() { + r.require().NotNil(r.lifecycle, "L2EL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *l2ELFrontend) Stop() { + r.require().NotNil(r.lifecycle, "L2EL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type l2CLFrontend struct { + presetCommon + chainID eth.ChainID + client opclient.RPC + rollupClient apis.RollupClient + p2pClient apis.P2PClient + els locks.RWMap[string, *l2ELFrontend] + rollupBoostNodes locks.RWMap[string, *rollupBoostFrontend] + oprBuilderNodes locks.RWMap[string, *oprBuilderFrontend] + userRPC string + interopEndpoint string + interopJWTSecret eth.Bytes32 + lifecycle stack.Lifecycle +} + +var _ stack.L2CLNode = (*l2CLFrontend)(nil) + +func newPresetL2CLNode(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC, userRPC, interopEndpoint string, interopJWTSecret eth.Bytes32) *l2CLFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &l2CLFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + client: rpcCl, + rollupClient: sources.NewRollupClient(rpcCl), + p2pClient: sources.NewP2PClient(rpcCl), + userRPC: userRPC, + interopEndpoint: interopEndpoint, + interopJWTSecret: interopJWTSecret, + } +} + +func (r *l2CLFrontend) ClientRPC() opclient.RPC { + return r.client +} + +func (r *l2CLFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *l2CLFrontend) RollupAPI() apis.RollupClient { + return r.rollupClient +} + +func (r *l2CLFrontend) P2PAPI() apis.P2PClient { + return r.p2pClient +} + +func (r *l2CLFrontend) InteropRPC() (endpoint string, jwtSecret eth.Bytes32) { + return r.interopEndpoint, r.interopJWTSecret +} + +func (r *l2CLFrontend) UserRPC() string { + return r.userRPC +} + +func (r *l2CLFrontend) attachEL(el *l2ELFrontend) { + r.els.Set(el.Name(), el) +} + +func (r *l2CLFrontend) attachRollupBoostNode(node *rollupBoostFrontend) { + r.rollupBoostNodes.Set(node.Name(), node) +} + +func (r *l2CLFrontend) attachOPRBuilderNode(node *oprBuilderFrontend) { + r.oprBuilderNodes.Set(node.Name(), node) +} + +func (r *l2CLFrontend) ELs() []stack.L2ELNode { + return mapSlice(sortByNameFunc(r.els.Values()), func(v *l2ELFrontend) stack.L2ELNode { return v }) +} + +func (r *l2CLFrontend) RollupBoostNodes() []stack.RollupBoostNode { + return mapSlice(sortByNameFunc(r.rollupBoostNodes.Values()), func(v *rollupBoostFrontend) stack.RollupBoostNode { return v }) +} + +func (r *l2CLFrontend) OPRBuilderNodes() []stack.OPRBuilderNode { + return mapSlice(sortByNameFunc(r.oprBuilderNodes.Values()), func(v *oprBuilderFrontend) stack.OPRBuilderNode { return v }) +} + +func (r *l2CLFrontend) ELClient() apis.EthClient { + if els := sortByNameFunc(r.els.Values()); len(els) > 0 { + return els[0].EthClient() + } + if nodes := sortByNameFunc(r.rollupBoostNodes.Values()); len(nodes) > 0 { + return nodes[0].EthClient() + } + if nodes := sortByNameFunc(r.oprBuilderNodes.Values()); len(nodes) > 0 { + return nodes[0].EthClient() + } + return nil +} + +func (r *l2CLFrontend) Start() { + r.require().NotNil(r.lifecycle, "L2CL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *l2CLFrontend) Stop() { + r.require().NotNil(r.lifecycle, "L2CL node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type l2BatcherFrontend struct { + presetCommon + chainID eth.ChainID + client *sources.BatcherAdminClient +} + +var _ stack.L2Batcher = (*l2BatcherFrontend)(nil) + +func newPresetL2Batcher(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC) *l2BatcherFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &l2BatcherFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + client: sources.NewBatcherAdminClient(rpcCl), + } +} + +func (r *l2BatcherFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *l2BatcherFrontend) ActivityAPI() apis.BatcherActivity { + return r.client +} + +type l2ProposerFrontend struct { + presetCommon + chainID eth.ChainID +} + +var _ stack.L2Proposer = (*l2ProposerFrontend)(nil) + +func (r *l2ProposerFrontend) ChainID() eth.ChainID { + return r.chainID +} + +type l2ChallengerFrontend struct { + presetCommon + chainID eth.ChainID + config *challengerConfig.Config +} + +var _ stack.L2Challenger = (*l2ChallengerFrontend)(nil) + +func newPresetL2Challenger(t devtest.T, name string, chainID eth.ChainID, cfg *challengerConfig.Config) *l2ChallengerFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &l2ChallengerFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + config: cfg, + } +} + +func (r *l2ChallengerFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *l2ChallengerFrontend) Config() *challengerConfig.Config { + return r.config +} + +type oprBuilderFrontend struct { + rpcELNode + engineClient *sources.EngineClient + flashblocksClient *opclient.WSClient + lifecycle stack.Lifecycle + updateRuleSet func(rulesYaml string) error +} + +var _ stack.OPRBuilderNode = (*oprBuilderFrontend)(nil) + +func newPresetOPRBuilderNode(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC, rollupCfg *rollup.Config, flashblocksCl *opclient.WSClient, updateRuleSet func(string) error) *oprBuilderFrontend { + engineClient, err := sources.NewEngineClient(rpcCl, t.Logger(), nil, sources.EngineClientDefaultConfig(rollupCfg)) + t.Require().NoError(err) + return &oprBuilderFrontend{ + rpcELNode: newRPCELNode(t, name, chainID, rpcCl, 0), + engineClient: engineClient, + flashblocksClient: flashblocksCl, + updateRuleSet: updateRuleSet, + } +} + +func (r *oprBuilderFrontend) L2EthClient() apis.L2EthClient { + return r.engineClient.L2Client +} + +func (r *oprBuilderFrontend) L2EngineClient() apis.EngineClient { + return r.engineClient.EngineAPIClient +} + +func (r *oprBuilderFrontend) FlashblocksClient() *opclient.WSClient { + return r.flashblocksClient +} + +func (r *oprBuilderFrontend) UpdateRuleSet(rulesYaml string) error { + return r.updateRuleSet(rulesYaml) +} + +func (r *oprBuilderFrontend) Start() { + r.require().NotNil(r.lifecycle, "OPR builder node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *oprBuilderFrontend) Stop() { + r.require().NotNil(r.lifecycle, "OPR builder node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type rollupBoostFrontend struct { + rpcELNode + engineClient *sources.EngineClient + flashblocksClient *opclient.WSClient + lifecycle stack.Lifecycle +} + +var _ stack.RollupBoostNode = (*rollupBoostFrontend)(nil) + +func newPresetRollupBoostNode(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC, rollupCfg *rollup.Config, flashblocksCl *opclient.WSClient) *rollupBoostFrontend { + engineClient, err := sources.NewEngineClient(rpcCl, t.Logger(), nil, sources.EngineClientDefaultConfig(rollupCfg)) + t.Require().NoError(err) + return &rollupBoostFrontend{ + rpcELNode: newRPCELNode(t, name, chainID, rpcCl, 0), + engineClient: engineClient, + flashblocksClient: flashblocksCl, + } +} + +func (r *rollupBoostFrontend) L2EthClient() apis.L2EthClient { + return r.engineClient.L2Client +} + +func (r *rollupBoostFrontend) L2EngineClient() apis.EngineClient { + return r.engineClient.EngineAPIClient +} + +func (r *rollupBoostFrontend) FlashblocksClient() *opclient.WSClient { + return r.flashblocksClient +} + +func (r *rollupBoostFrontend) Start() { + r.require().NotNil(r.lifecycle, "rollup boost node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *rollupBoostFrontend) Stop() { + r.require().NotNil(r.lifecycle, "rollup boost node %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type supervisorFrontend struct { + presetCommon + api apis.SupervisorAPI + lifecycle stack.Lifecycle +} + +var _ stack.Supervisor = (*supervisorFrontend)(nil) + +func newPresetSupervisor(t devtest.T, name string, rpcCl opclient.RPC) *supervisorFrontend { + return &supervisorFrontend{ + presetCommon: newPresetCommon(t, name), + api: sources.NewSupervisorClient(rpcCl), + } +} + +func (r *supervisorFrontend) AdminAPI() apis.SupervisorAdminAPI { + return r.api +} + +func (r *supervisorFrontend) QueryAPI() apis.SupervisorQueryAPI { + return r.api +} + +func (r *supervisorFrontend) Start() { + r.require().NotNil(r.lifecycle, "supervisor %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Start() +} + +func (r *supervisorFrontend) Stop() { + r.require().NotNil(r.lifecycle, "supervisor %s is not lifecycle-controllable", r.Name()) + r.lifecycle.Stop() +} + +type supernodeFrontend struct { + presetCommon + api apis.SupernodeQueryAPI +} + +var _ stack.Supernode = (*supernodeFrontend)(nil) + +func newPresetSupernode(t devtest.T, name string, rpcCl opclient.RPC) *supernodeFrontend { + return &supernodeFrontend{ + presetCommon: newPresetCommon(t, name), + api: sources.NewSuperNodeClient(rpcCl), + } +} + +func (r *supernodeFrontend) QueryAPI() apis.SupernodeQueryAPI { + return r.api +} + +type conductorFrontend struct { + presetCommon + chainID eth.ChainID + api conductorRpc.API +} + +var _ stack.Conductor = (*conductorFrontend)(nil) + +func newPresetConductor(t devtest.T, name string, chainID eth.ChainID, rpcCl *gethrpc.Client) *conductorFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &conductorFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + api: conductorRpc.NewAPIClient(rpcCl), + } +} + +func (r *conductorFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *conductorFrontend) RpcAPI() conductorRpc.API { + return r.api +} + +type faucetFrontend struct { + presetCommon + chainID eth.ChainID + client *sources.FaucetClient +} + +var _ stack.Faucet = (*faucetFrontend)(nil) + +func newPresetFaucet(t devtest.T, name string, chainID eth.ChainID, rpcCl opclient.RPC) *faucetFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &faucetFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + client: sources.NewFaucetClient(rpcCl), + } +} + +func (r *faucetFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *faucetFrontend) API() apis.Faucet { + return r.client +} + +type testSequencerFrontend struct { + presetCommon + api apis.TestSequencerAPI + controls map[eth.ChainID]apis.TestSequencerControlAPI +} + +var _ stack.TestSequencer = (*testSequencerFrontend)(nil) + +func newPresetTestSequencer(t devtest.T, name string, adminRPCCl opclient.RPC, controlRPCs map[eth.ChainID]opclient.RPC) *testSequencerFrontend { + s := &testSequencerFrontend{ + presetCommon: newPresetCommon(t, name), + api: sources.NewBuilderClient(adminRPCCl), + controls: make(map[eth.ChainID]apis.TestSequencerControlAPI, len(controlRPCs)), + } + for chainID, rpcCl := range controlRPCs { + s.controls[chainID] = sources.NewControlClient(rpcCl) + } + return s +} + +func (r *testSequencerFrontend) AdminAPI() apis.TestSequencerAdminAPI { + return r.api +} + +func (r *testSequencerFrontend) BuildAPI() apis.TestSequencerBuildAPI { + return r.api +} + +func (r *testSequencerFrontend) ControlAPI(chainID eth.ChainID) apis.TestSequencerControlAPI { + return r.controls[chainID] +} + +type syncTesterFrontend struct { + presetCommon + chainID eth.ChainID + addr string + client *sources.SyncTesterClient +} + +var _ stack.SyncTester = (*syncTesterFrontend)(nil) + +func newPresetSyncTester(t devtest.T, name string, chainID eth.ChainID, addr string, rpcCl opclient.RPC) *syncTesterFrontend { + t = t.WithCtx(stack.ContextWithChainID(t.Ctx(), chainID)) + return &syncTesterFrontend{ + presetCommon: newPresetCommon(t, name), + chainID: chainID, + addr: addr, + client: sources.NewSyncTesterClient(rpcCl), + } +} + +func (r *syncTesterFrontend) ChainID() eth.ChainID { + return r.chainID +} + +func (r *syncTesterFrontend) API() apis.SyncTester { + return r.client +} + +func (r *syncTesterFrontend) APIWithSession(sessionID string) apis.SyncTester { + require := r.T().Require() + require.NoError(synctester.IsValidSessionID(sessionID)) + rpcCl, err := opclient.NewRPC(r.T().Ctx(), r.Logger(), r.addr+"/"+sessionID, opclient.WithLazyDial()) + require.NoError(err, "sync tester failed to initialize rpc per session") + return sources.NewSyncTesterClient(rpcCl) +} diff --git a/op-devstack/presets/sysgo_runtime.go b/op-devstack/presets/sysgo_runtime.go new file mode 100644 index 00000000000..9dee4db2fd7 --- /dev/null +++ b/op-devstack/presets/sysgo_runtime.go @@ -0,0 +1,175 @@ +package presets + +import ( + "os" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + gn "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func newL1ELFrontend(t devtest.T, name string, chainID eth.ChainID, userRPC string) *l1ELFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + return newPresetL1ELNode(t, name, chainID, rpcCl) +} + +func newL1CLFrontend(t devtest.T, name string, chainID eth.ChainID, beaconHTTPAddr string, lifecycle ...stack.Lifecycle) *l1CLFrontend { + beaconCl := client.NewBasicHTTPClient(beaconHTTPAddr, t.Logger()) + l1CL := newPresetL1CLNode(t, name, chainID, beaconCl) + if len(lifecycle) > 0 { + l1CL.lifecycle = lifecycle[0] + } + return l1CL +} + +func newL2ELFrontend(t devtest.T, name string, chainID eth.ChainID, userRPC string, engineRPC string, jwtPath string, rollupCfg *rollup.Config, lifecycle ...stack.Lifecycle) *l2ELFrontend { + userRPCCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(userRPCCl.Close) + jwtSecret := readJWTSecret(t, jwtPath) + engineRPCCl, err := client.NewRPC( + t.Ctx(), + t.Logger(), + engineRPC, + client.WithLazyDial(), + client.WithGethRPCOptions(rpc.WithHTTPAuth(gn.NewJWTAuth(jwtSecret))), + ) + t.Require().NoError(err) + t.Cleanup(engineRPCCl.Close) + l2EL := newPresetL2ELNode(t, name, chainID, userRPCCl, engineRPCCl, rollupCfg) + if len(lifecycle) > 0 { + l2EL.lifecycle = lifecycle[0] + } + return l2EL +} + +func readJWTSecret(t devtest.T, jwtPath string) [32]byte { + t.Require().NotEmpty(jwtPath, "missing jwt path") + content, err := os.ReadFile(jwtPath) + t.Require().NoError(err, "failed to read jwt path %s", jwtPath) + raw, err := hexutil.Decode(strings.TrimSpace(string(content))) + t.Require().NoError(err, "failed to decode jwt secret from %s", jwtPath) + t.Require().Len(raw, 32, "invalid jwt secret length from %s", jwtPath) + var secret [32]byte + copy(secret[:], raw) + return secret +} + +func newL2CLFrontend(t devtest.T, name string, chainID eth.ChainID, userRPC string, node sysgo.L2CLNode) *l2CLFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + interopEndpoint, interopJWT := node.InteropRPC() + l2CL := newPresetL2CLNode(t, name, chainID, rpcCl, userRPC, interopEndpoint, interopJWT) + if lifecycle, ok := any(node).(stack.Lifecycle); ok { + l2CL.lifecycle = lifecycle + } + return l2CL +} + +func newL2BatcherFrontend(t devtest.T, name string, chainID eth.ChainID, rpcEndpoint string) *l2BatcherFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), rpcEndpoint, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + return newPresetL2Batcher(t, name, chainID, rpcCl) +} + +func newOPRBuilderFrontend(t devtest.T, name string, chainID eth.ChainID, userRPC string, flashblocksWSURL string, updateRuleSet func(string) error, rollupCfg *rollup.Config, lifecycle ...stack.Lifecycle) *oprBuilderFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + + t.Require().NotEmpty(flashblocksWSURL, "missing flashblocks ws url for %s", name) + wsCl, err := client.DialWS(t.Ctx(), client.WSConfig{ + URL: flashblocksWSURL, + Log: t.Logger(), + }) + t.Require().NoError(err) + + oprb := newPresetOPRBuilderNode(t, name, chainID, rpcCl, rollupCfg, wsCl, updateRuleSet) + if len(lifecycle) > 0 { + oprb.lifecycle = lifecycle[0] + } + return oprb +} + +func newRollupBoostFrontend(t devtest.T, name string, chainID eth.ChainID, userRPC string, flashblocksWSURL string, rollupCfg *rollup.Config, lifecycle ...stack.Lifecycle) *rollupBoostFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + + t.Require().NotEmpty(flashblocksWSURL, "missing flashblocks ws url for %s", name) + wsCl, err := client.DialWS(t.Ctx(), client.WSConfig{ + URL: flashblocksWSURL, + Log: t.Logger(), + }) + t.Require().NoError(err) + + rollupBoost := newPresetRollupBoostNode(t, name, chainID, rpcCl, rollupCfg, wsCl) + if len(lifecycle) > 0 { + rollupBoost.lifecycle = lifecycle[0] + } + return rollupBoost +} + +func newSupervisorFrontend(t devtest.T, name string, userRPC string, lifecycle ...stack.Lifecycle) *supervisorFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + supervisor := newPresetSupervisor(t, name, rpcCl) + if len(lifecycle) > 0 { + supervisor.lifecycle = lifecycle[0] + } + return supervisor +} + +func newSupernodeFrontend(t devtest.T, name string, userRPC string) *supernodeFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), userRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + return newPresetSupernode(t, name, rpcCl) +} + +func newConductorFrontend(t devtest.T, name string, chainID eth.ChainID, rpcEndpoint string) *conductorFrontend { + rpcCl, err := rpc.DialContext(t.Ctx(), rpcEndpoint) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + return newPresetConductor(t, name, chainID, rpcCl) +} + +func newTestSequencerFrontend(t devtest.T, name string, adminRPC string, controlRPCs map[eth.ChainID]string, jwtSecret [32]byte) *testSequencerFrontend { + opts := []client.RPCOption{ + client.WithLazyDial(), + client.WithGethRPCOptions(rpc.WithHTTPAuth(gn.NewJWTAuth(jwtSecret))), + } + + adminRPCCl, err := client.NewRPC(t.Ctx(), t.Logger(), adminRPC, opts...) + t.Require().NoError(err) + t.Cleanup(adminRPCCl.Close) + + controlClients := make(map[eth.ChainID]client.RPC, len(controlRPCs)) + for chainID, endpoint := range controlRPCs { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), endpoint, opts...) + t.Require().NoErrorf(err, "failed to create control RPC client for chain %s", chainID) + t.Cleanup(rpcCl.Close) + controlClients[chainID] = rpcCl + } + return newPresetTestSequencer(t, name, adminRPCCl, controlClients) +} + +func newSyncTesterFrontend(t devtest.T, name string, chainID eth.ChainID, syncTesterRPC string) *syncTesterFrontend { + rpcCl, err := client.NewRPC(t.Ctx(), t.Logger(), syncTesterRPC, client.WithLazyDial()) + t.Require().NoError(err) + t.Cleanup(rpcCl.Close) + return newPresetSyncTester(t, name, chainID, syncTesterRPC, rpcCl) +} diff --git a/op-devstack/presets/twol2.go b/op-devstack/presets/twol2.go new file mode 100644 index 00000000000..f383289fa14 --- /dev/null +++ b/op-devstack/presets/twol2.go @@ -0,0 +1,303 @@ +package presets + +import ( + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/clock" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" +) + +// TwoL2 represents a two-L2 setup without interop considerations. +// It is useful for testing components which bridge multiple L2s without necessarily using interop. +type TwoL2 struct { + Log log.Logger + T devtest.T + + L1Network *dsl.L1Network + L1EL *dsl.L1ELNode + L1CL *dsl.L1CLNode + + L2A *dsl.L2Network + L2B *dsl.L2Network + L2ACL *dsl.L2CLNode + L2BCL *dsl.L2CLNode +} + +// NewTwoL2Supernode creates a fresh TwoL2 target backed by a shared supernode for the +// current test. +func NewTwoL2Supernode(t devtest.T, opts ...Option) *TwoL2 { + presetCfg, _ := collectSupportedPresetConfig(t, "NewTwoL2Supernode", opts, twoL2SupernodePresetSupportedOptionKinds) + return twoL2SupernodeFromRuntime(t, sysgo.NewTwoL2SupernodeRuntimeWithConfig(t, presetCfg)) +} + +// TwoL2SupernodeInterop represents a two-L2 setup with a shared supernode that has interop enabled. +// This allows testing of cross-chain message verification at each timestamp. +// Use delaySeconds=0 for interop at genesis, or a positive value to test the transition. +type TwoL2SupernodeInterop struct { + TwoL2 + + // Supernode provides access to the shared supernode for interop operations + Supernode *dsl.Supernode + + // TestSequencer provides deterministic block building on both L2 chains. + // Unlike the regular sequencer which uses wall-clock time, the TestSequencer + // builds blocks at parent.Time + blockTime, making it ideal for same-timestamp tests. + TestSequencer *dsl.TestSequencer + + // L2ELA and L2ELB provide access to the EL nodes for transaction submission + L2ELA *dsl.L2ELNode + L2ELB *dsl.L2ELNode + + // L2BatcherA and L2BatcherB provide access to the batchers for pausing/resuming + L2BatcherA *dsl.L2Batcher + L2BatcherB *dsl.L2Batcher + + // Faucets for funding test accounts + FaucetA *dsl.Faucet + FaucetB *dsl.Faucet + + // Wallet for test account management + Wallet *dsl.HDWallet + + // Funders for creating funded EOAs + FunderA *dsl.Funder + FunderB *dsl.Funder + + // GenesisTime is the genesis timestamp of the L2 chains + GenesisTime uint64 + + // InteropActivationTime is the timestamp when interop becomes active + InteropActivationTime uint64 + + // DelaySeconds is the delay from genesis to interop activation + DelaySeconds uint64 + + timeTravel *clock.AdvancingClock +} + +// AdvanceTime advances the time-travel clock if enabled. +func (s *TwoL2SupernodeInterop) AdvanceTime(amount time.Duration) { + s.T.Require().NotNil(s.timeTravel, "attempting to advance time on incompatible system") + s.timeTravel.AdvanceTime(amount) +} + +// SuperNodeClient returns an API for calling supernode-specific RPC methods +// like superroot_atTimestamp. +func (s *TwoL2SupernodeInterop) SuperNodeClient() apis.SupernodeQueryAPI { + return s.Supernode.QueryAPI() +} + +// NewTwoL2SupernodeInterop creates a fresh TwoL2SupernodeInterop target for the current +// test. +func NewTwoL2SupernodeInterop(t devtest.T, delaySeconds uint64, opts ...Option) *TwoL2SupernodeInterop { + presetCfg, _ := collectSupportedPresetConfig(t, "NewTwoL2SupernodeInterop", opts, twoL2SupernodeInteropPresetSupportedOptionKinds) + return twoL2SupernodeInteropFromRuntime(t, sysgo.NewTwoL2SupernodeInteropRuntimeWithConfig(t, delaySeconds, presetCfg)) +} + +// ============================================================================= +// Same-Timestamp Test Setup +// ============================================================================= + +// SameTimestampTestSetup provides a simplified setup for same-timestamp interop testing. +// It handles all the chain synchronization, sequencer control, and interop pausing +// needed to create blocks at the same timestamp on both chains. +type SameTimestampTestSetup struct { + *TwoL2SupernodeInterop + t devtest.T + + // Alice is a funded EOA on chain A + Alice *dsl.EOA + // Bob is a funded EOA on chain B + Bob *dsl.EOA + + // EventLoggerA is the EventLogger contract address on chain A + EventLoggerA common.Address + // EventLoggerB is the EventLogger contract address on chain B + EventLoggerB common.Address + + // NextTimestamp is the timestamp that will be used for the next blocks + NextTimestamp uint64 + // ExpectedBlockNumA is the expected block number on chain A + ExpectedBlockNumA uint64 + // ExpectedBlockNumB is the expected block number on chain B + ExpectedBlockNumB uint64 +} + +// ForSameTimestampTesting sets up the system for same-timestamp interop testing. +// It syncs the chains, pauses interop, stops sequencers, and calculates expected positions. +// After calling this, you can use PrepareInitA/B to create same-timestamp message pairs. +func (s *TwoL2SupernodeInterop) ForSameTimestampTesting(t devtest.T) *SameTimestampTestSetup { + // Create funded EOAs + alice := s.FunderA.NewFundedEOA(eth.OneEther) + bob := s.FunderB.NewFundedEOA(eth.OneEther) + + // Deploy event loggers + eventLoggerA := alice.DeployEventLogger() + eventLoggerB := bob.DeployEventLogger() + + // Sync chains and pause interop + s.L2B.CatchUpTo(s.L2A) + s.L2A.CatchUpTo(s.L2B) + s.Supernode.EnsureInteropPaused(s.L2ACL, s.L2BCL, 10) + + // Stop sequencers + s.L2ACL.StopSequencer() + s.L2BCL.StopSequencer() + + // Get current state and synchronize timestamps + unsafeA := s.L2ELA.BlockRefByLabel(eth.Unsafe) + unsafeB := s.L2ELB.BlockRefByLabel(eth.Unsafe) + unsafeA, unsafeB = synchronizeChainsToSameTimestamp(t, s, unsafeA, unsafeB) + + blockTime := s.L2A.Escape().RollupConfig().BlockTime + + return &SameTimestampTestSetup{ + TwoL2SupernodeInterop: s, + t: t, + Alice: alice, + Bob: bob, + EventLoggerA: eventLoggerA, + EventLoggerB: eventLoggerB, + NextTimestamp: unsafeA.Time + blockTime, + ExpectedBlockNumA: unsafeA.Number + 1, + ExpectedBlockNumB: unsafeB.Number + 1, + } +} + +// PrepareInitA creates a precomputed init message for chain A at the next timestamp. +func (s *SameTimestampTestSetup) PrepareInitA(rng *rand.Rand, logIdx uint32) *dsl.SameTimestampPair { + return s.Alice.PrepareSameTimestampInit(rng, s.EventLoggerA, s.ExpectedBlockNumA, logIdx, s.NextTimestamp) +} + +// PrepareInitB creates a precomputed init message for chain B at the next timestamp. +func (s *SameTimestampTestSetup) PrepareInitB(rng *rand.Rand, logIdx uint32) *dsl.SameTimestampPair { + return s.Bob.PrepareSameTimestampInit(rng, s.EventLoggerB, s.ExpectedBlockNumB, logIdx, s.NextTimestamp) +} + +// IncludeAndValidate builds blocks with deterministic timestamps using the TestSequencer, +// then validates interop and checks for expected reorgs. +// +// Unlike the regular sequencer which uses wall-clock time, the TestSequencer builds blocks +// at exactly parent.Time + blockTime, ensuring the blocks are at NextTimestamp. +func (s *SameTimestampTestSetup) IncludeAndValidate(txsA, txsB []*txplan.PlannedTx, expectReplacedA, expectReplacedB bool) { + ctx := s.t.Ctx() + + require.NotNil(s.t, s.TestSequencer, "TestSequencer is required for deterministic timestamp tests") + + // Assign nonces deterministically within each same-timestamp block. Relying on + // mempool-visible pending nonces is racy across clients, and op-reth is stricter + // about underpriced replacement transactions than op-geth. + baseNonceA := s.Alice.PendingNonce() + for i, ptx := range txsA { + txplan.WithStaticNonce(baseNonceA + uint64(i))(ptx) + } + baseNonceB := s.Bob.PendingNonce() + for i, ptx := range txsB { + txplan.WithStaticNonce(baseNonceB + uint64(i))(ptx) + } + + // Get parent blocks and chain IDs + parentA := s.L2ELA.BlockRefByLabel(eth.Unsafe) + parentB := s.L2ELB.BlockRefByLabel(eth.Unsafe) + chainIDA := s.L2A.Escape().ChainID() + chainIDB := s.L2B.Escape().ChainID() + + // Extract signed transaction bytes for chain A + var rawTxsA [][]byte + var txHashesA []common.Hash + for _, ptx := range txsA { + signedTx, err := ptx.Signed.Eval(ctx) + require.NoError(s.t, err, "failed to sign transaction for chain A") + rawBytes, err := signedTx.MarshalBinary() + require.NoError(s.t, err, "failed to marshal transaction for chain A") + rawTxsA = append(rawTxsA, rawBytes) + txHashesA = append(txHashesA, signedTx.Hash()) + } + + // Extract signed transaction bytes for chain B + var rawTxsB [][]byte + var txHashesB []common.Hash + for _, ptx := range txsB { + signedTx, err := ptx.Signed.Eval(ctx) + require.NoError(s.t, err, "failed to sign transaction for chain B") + rawBytes, err := signedTx.MarshalBinary() + require.NoError(s.t, err, "failed to marshal transaction for chain B") + rawTxsB = append(rawTxsB, rawBytes) + txHashesB = append(txHashesB, signedTx.Hash()) + } + + // Build blocks at deterministic timestamps using TestSequencer + // Block timestamp will be parent.Time + blockTime = NextTimestamp + s.TestSequencer.SequenceBlockWithTxs(s.t, chainIDA, parentA.Hash, rawTxsA) + s.TestSequencer.SequenceBlockWithTxs(s.t, chainIDB, parentB.Hash, rawTxsB) + + // Get block refs by looking up the tx receipts + var blockA, blockB eth.L2BlockRef + for _, txHash := range txHashesA { + receipt := s.L2ELA.WaitForReceipt(txHash) + blockA = s.L2ELA.BlockRefByHash(receipt.BlockHash) + } + for _, txHash := range txHashesB { + receipt := s.L2ELB.WaitForReceipt(txHash) + blockB = s.L2ELB.BlockRefByHash(receipt.BlockHash) + } + + // Verify same-timestamp property: both blocks at expected timestamp + require.Equal(s.t, s.NextTimestamp, blockA.Time, + "Chain A block must be at the precomputed NextTimestamp (init message identifier uses this)") + require.Equal(s.t, s.NextTimestamp, blockB.Time, + "Chain B block must be at the precomputed NextTimestamp (exec references init at this timestamp)") + require.Equal(s.t, blockA.Time, blockB.Time, "blocks must be at same timestamp") + + // Resume interop and wait for validation + s.Supernode.ResumeInterop() + s.Supernode.AwaitValidatedTimestamp(blockA.Time) + + // Check reorg expectations + currentA := s.L2ELA.BlockRefByNumber(blockA.Number) + currentB := s.L2ELB.BlockRefByNumber(blockB.Number) + + if expectReplacedA { + require.NotEqual(s.t, blockA.Hash, currentA.Hash, "Chain A should be replaced") + } else { + require.Equal(s.t, blockA.Hash, currentA.Hash, "Chain A should NOT be replaced") + } + + if expectReplacedB { + require.NotEqual(s.t, blockB.Hash, currentB.Hash, "Chain B should be replaced") + } else { + require.Equal(s.t, blockB.Hash, currentB.Hash, "Chain B should NOT be replaced") + } +} + +// synchronizeChainsToSameTimestamp ensures both chains are at the same timestamp. +func synchronizeChainsToSameTimestamp(t devtest.T, sys *TwoL2SupernodeInterop, unsafeA, unsafeB eth.L2BlockRef) (eth.L2BlockRef, eth.L2BlockRef) { + for i := 0; i < 10; i++ { + if unsafeA.Time == unsafeB.Time { + return unsafeA, unsafeB + } + if unsafeA.Time < unsafeB.Time { + sys.L2ACL.StartSequencer() + sys.L2ELA.WaitForTime(unsafeB.Time) + sys.L2ACL.StopSequencer() + unsafeA = sys.L2ELA.BlockRefByLabel(eth.Unsafe) + } else { + sys.L2BCL.StartSequencer() + sys.L2ELB.WaitForTime(unsafeA.Time) + sys.L2BCL.StopSequencer() + unsafeB = sys.L2ELB.BlockRefByLabel(eth.Unsafe) + } + } + require.Equal(t, unsafeA.Time, unsafeB.Time, "failed to synchronize chains") + return unsafeA, unsafeB +} diff --git a/op-devstack/stack/op_rbuilder.go b/op-devstack/stack/op_rbuilder.go new file mode 100644 index 00000000000..a063acab78a --- /dev/null +++ b/op-devstack/stack/op_rbuilder.go @@ -0,0 +1,16 @@ +package stack + +import ( + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" +) + +// OPRBuilderNode is a L2 ethereum execution-layer node +type OPRBuilderNode interface { + L2EthClient() apis.L2EthClient + L2EngineClient() apis.EngineClient + FlashblocksClient() *client.WSClient + UpdateRuleSet(rulesYaml string) error + + ELNode +} diff --git a/op-devstack/sysgo/l1_runtime.go b/op-devstack/sysgo/l1_runtime.go new file mode 100644 index 00000000000..f4f43c5efd0 --- /dev/null +++ b/op-devstack/sysgo/l1_runtime.go @@ -0,0 +1,245 @@ +package sysgo + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/blobstore" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/fakebeacon" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" + "github.com/ethereum-optimism/optimism/op-service/clock" + "github.com/ethereum-optimism/optimism/op-service/logpipe" + "github.com/ethereum-optimism/optimism/op-service/tasks" + "github.com/ethereum-optimism/optimism/op-service/testutils/tcpproxy" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" +) + +const DevstackL1ELKindEnvVar = "DEVSTACK_L1EL_KIND" + +const GethExecPathEnvVar = "SYSGO_GETH_EXEC_PATH" + +func writeJWTSecret(t devtest.T) (string, [32]byte) { + jwtPath := filepath.Join(t.TempDir(), "jwt_secret") + jwtSecret := [32]byte{123} + err := os.WriteFile(jwtPath, []byte(hexutil.Encode(jwtSecret[:])), 0o600) + t.Require().NoError(err, "failed to write jwt secret") + return jwtPath, jwtSecret +} + +func startInProcessL1(t devtest.T, l1Net *L1Network, jwtPath string) (*L1Geth, *L1CLNode) { + return startInProcessL1WithClock(t, l1Net, jwtPath, clock.SystemClock) +} + +func startInProcessL1WithClock(t devtest.T, l1Net *L1Network, jwtPath string, l1Clock clock.Clock) (*L1Geth, *L1CLNode) { + return startInProcessL1WithClockConfig(t, l1Net, jwtPath, l1Clock, PresetConfig{}) +} + +func startInProcessL1WithClockConfig(t devtest.T, l1Net *L1Network, jwtPath string, l1Clock clock.Clock, cfg PresetConfig) (*L1Geth, *L1CLNode) { + if useSubprocessL1Geth(cfg) { + return startSubprocessL1WithClock(t, l1Net, jwtPath, l1Clock, cfg) + } + + require := t.Require() + l1ChainID := l1Net.ChainID() + + blobPath := t.TempDir() + bcn := fakebeacon.NewBeacon(t.Logger().New("component", "l1cl"), blobstore.New(), l1Net.genesis.Timestamp, l1Net.blockTime) + t.Cleanup(func() { + _ = bcn.Close() + }) + require.NoError(bcn.Start("127.0.0.1:0")) + beaconAddr := bcn.BeaconAddr() + require.NotEmpty(beaconAddr, "beacon API listener must be up") + + l1Geth, fp, err := geth.InitL1( + l1Net.blockTime, + 20, + l1Net.genesis, + l1Clock, + filepath.Join(blobPath, "l1_el"), + bcn, + geth.WithAuth(jwtPath), + ) + require.NoError(err) + require.NoError(l1Geth.Node.Start()) + t.Cleanup(func() { + t.Logger().Info("Closing L1 geth") + _ = l1Geth.Close() + }) + + l1EL := &L1Geth{ + name: "l1", + chainID: l1ChainID, + userRPC: l1Geth.Node.HTTPEndpoint(), + authRPC: l1Geth.Node.HTTPAuthEndpoint(), + l1Geth: l1Geth, + blobPath: blobPath, + } + l1CL := &L1CLNode{ + name: "l1", + chainID: l1ChainID, + beaconHTTPAddr: beaconAddr, + beacon: bcn, + fakepos: &FakePoS{fakepos: fp, p: t}, + } + return l1EL, l1CL +} + +func startSubprocessL1WithClock(t devtest.T, l1Net *L1Network, jwtPath string, l1Clock clock.Clock, cfg PresetConfig) (*L1Geth, *L1CLNode) { + require := t.Require() + l1ChainID := l1Net.ChainID() + + execPath := cfg.L1GethExecPath + if execPath == "" { + var ok bool + execPath, ok = os.LookupEnv(GethExecPathEnvVar) + require.True(ok, "%s must be set when %s=geth", GethExecPathEnvVar, DevstackL1ELKindEnvVar) + } + _, err := os.Stat(execPath) + require.NotErrorIs(err, os.ErrNotExist, "geth executable must exist") + + tempDir := t.TempDir() + data, err := json.Marshal(l1Net.genesis) + require.NoError(err, "must json-encode genesis") + chainConfigPath := filepath.Join(tempDir, "genesis.json") + require.NoError(os.WriteFile(chainConfigPath, data, 0o644), "must write genesis file") + + dataDirPath := filepath.Join(tempDir, "data") + require.NoError(os.MkdirAll(dataDirPath, 0o755), "must create datadir") + + initCmd := exec.Command(execPath, "--datadir", dataDirPath, "init", chainConfigPath) + initCmd.Stdout = os.Stdout + initCmd.Stderr = os.Stderr + require.NoError(initCmd.Run(), "initialize geth datadir") + + userProxy := tcpproxy.New(t.Logger().New("component", "l1el-user-proxy")) + require.NoError(userProxy.Start()) + t.Cleanup(func() { + userProxy.Close() + }) + authProxy := tcpproxy.New(t.Logger().New("component", "l1el-auth-proxy")) + require.NoError(authProxy.Start()) + t.Cleanup(func() { + authProxy.Close() + }) + + userRPC := "ws://" + userProxy.Addr() + authRPC := "ws://" + authProxy.Addr() + userRPCUpstream := make(chan string, 1) + authRPCUpstream := make(chan string, 1) + onLogEntry := func(e logpipe.LogEntry) { + switch e.LogMessage() { + case "WebSocket enabled": + select { + case userRPCUpstream <- e.FieldValue("url").(string): + default: + } + case "HTTP server started": + if e.FieldValue("auth").(bool) { + select { + case authRPCUpstream <- "http://" + e.FieldValue("endpoint").(string): + default: + } + } + } + } + logOut := logpipe.ToLogger(t.Logger().New("component", "l1el", "src", "stdout")) + logErr := logpipe.ToLogger(t.Logger().New("component", "l1el", "src", "stderr")) + stdOutLogs := logpipe.LogCallback(func(line []byte) { + e := logpipe.ParseGoStructuredLogs(line) + logOut(e) + onLogEntry(e) + }) + stdErrLogs := logpipe.LogCallback(func(line []byte) { + e := logpipe.ParseGoStructuredLogs(line) + logErr(e) + onLogEntry(e) + }) + sub := NewSubProcess(t, stdOutLogs, stdErrLogs) + args := []string{ + "--log.format", "json", + "--datadir", dataDirPath, + "--ws", "--ws.addr", "127.0.0.1", "--ws.port", "0", "--ws.origins", "*", "--ws.api", "admin,debug,eth,net,txpool", + "--authrpc.addr", "127.0.0.1", "--authrpc.port", "0", "--authrpc.jwtsecret", jwtPath, + "--ipcdisable", + "--port", "0", + "--nodiscover", + "--verbosity", "5", + "--miner.recommit", "2s", + "--gcmode", "archive", + } + require.NoError(sub.Start(execPath, args, nil), "must start geth subprocess") + + var userRPCAddr string + var authRPCAddr string + require.NoError(tasks.Await(t.Ctx(), userRPCUpstream, &userRPCAddr), "need geth user RPC") + require.NoError(tasks.Await(t.Ctx(), authRPCUpstream, &authRPCAddr), "need geth auth RPC") + userProxy.SetUpstream(ProxyAddr(require, userRPCAddr)) + authProxy.SetUpstream(ProxyAddr(require, authRPCAddr)) + + backend, err := ethclient.DialContext(t.Ctx(), userRPC) + require.NoError(err, "failed to dial geth user RPC") + t.Cleanup(backend.Close) + + jwtSecret := readJWTSecret(t, jwtPath) + engineCl, err := dialEngine(t.Ctx(), authRPC, jwtSecret) + require.NoError(err, "failed to dial geth engine API") + t.Cleanup(func() { + engineCl.inner.Close() + }) + + bcn := fakebeacon.NewBeacon(t.Logger().New("component", "l1cl"), blobstore.New(), l1Net.genesis.Timestamp, l1Net.blockTime) + t.Cleanup(func() { + _ = bcn.Close() + }) + require.NoError(bcn.Start("127.0.0.1:0")) + beaconAddr := bcn.BeaconAddr() + require.NotEmpty(beaconAddr, "beacon API listener must be up") + + fp := &FakePoS{ + p: t, + fakepos: geth.NewFakePoS(backend, engineCl, l1Clock, t.Logger().New("component", "l1cl"), l1Net.blockTime, 20, bcn, l1Net.genesis.Config), + } + fp.Start() + t.Cleanup(fp.Stop) + + l1EL := &L1Geth{ + name: "l1", + chainID: l1ChainID, + userRPC: userRPC, + authRPC: authRPC, + blobPath: tempDir, + } + l1CL := &L1CLNode{ + name: "l1", + chainID: l1ChainID, + beaconHTTPAddr: beaconAddr, + beacon: bcn, + fakepos: fp, + } + return l1EL, l1CL +} + +func useSubprocessL1Geth(cfg PresetConfig) bool { + kind := cfg.L1ELKind + if kind == "" { + kind = os.Getenv(DevstackL1ELKindEnvVar) + } + return kind == "geth" +} + +func readJWTSecret(t devtest.T, jwtPath string) [32]byte { + data, err := os.ReadFile(jwtPath) + t.Require().NoError(err, "failed to read jwt secret file") + decoded, err := hexutil.Decode(strings.TrimSpace(string(data))) + t.Require().NoError(err, "failed to decode jwt secret file") + var jwtSecret [32]byte + copy(jwtSecret[:], decoded) + t.Require().Len(decoded, len(jwtSecret), "jwt secret must be 32 bytes") + return jwtSecret +} diff --git a/op-devstack/sysgo/mixed_runtime.go b/op-devstack/sysgo/mixed_runtime.go index 78a40d6f34c..3e096dab764 100644 --- a/op-devstack/sysgo/mixed_runtime.go +++ b/op-devstack/sysgo/mixed_runtime.go @@ -42,11 +42,21 @@ import ( type MixedL2ELKind string +const DevstackL2ELKindEnvVar = "DEVSTACK_L2EL_KIND" + const ( MixedL2ELOpGeth MixedL2ELKind = "op-geth" MixedL2ELOpReth MixedL2ELKind = "op-reth" ) +// SkipOnOpReth skips the test when the L2 execution layer is op-reth +// (i.e. DEVSTACK_L2EL_KIND is not "op-geth"). +func SkipOnOpReth(t devtest.T, reason string) { + if MixedL2ELKind(os.Getenv(DevstackL2ELKindEnvVar)) == MixedL2ELOpReth { + t.Skipf("skipping on op-reth: %s", reason) + } +} + type MixedL2CLKind string const ( diff --git a/op-devstack/sysgo/multichain_supernode_runtime.go b/op-devstack/sysgo/multichain_supernode_runtime.go index 9d1ae564b62..f886856122f 100644 --- a/op-devstack/sysgo/multichain_supernode_runtime.go +++ b/op-devstack/sysgo/multichain_supernode_runtime.go @@ -100,7 +100,7 @@ func NewTwoL2SupernodeRuntimeWithConfig(t devtest.T, cfg PresetConfig) *MultiCha // startSupernodeEL starts an L2 EL node for the supernode runtime. // It respects the DEVSTACK_L2EL_KIND env var: "op-geth" uses op-geth, otherwise op-reth is used. func startSupernodeEL(t devtest.T, l2Net *L2Network, jwtPath string, jwtSecret [32]byte) L2ELNode { - if os.Getenv("DEVSTACK_L2EL_KIND") == string(MixedL2ELOpGeth) { + if MixedL2ELKind(os.Getenv(DevstackL2ELKindEnvVar)) == MixedL2ELOpGeth { return startL2ELNode(t, l2Net, jwtPath, jwtSecret, "sequencer", NewELNodeIdentity(0)) } return startMixedOpRethNode(t, l2Net, "sequencer", jwtPath, jwtSecret, nil) @@ -122,7 +122,7 @@ func newSingleChainSupernodeRuntimeWithConfig(t devtest.T, interopAtGenesis bool timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) l1Clock = timeTravelClock } - l1EL, l1CL := startInProcessL1WithClock(t, l1Net, jwtPath, l1Clock) + l1EL, l1CL := startInProcessL1WithClockConfig(t, l1Net, jwtPath, l1Clock, cfg) l2EL := startSupernodeEL(t, l2Net, jwtPath, jwtSecret) var depSetStatic *depset.StaticConfigDependencySet @@ -174,7 +174,7 @@ func newTwoL2SupernodeRuntimeWithConfig(t devtest.T, enableInterop bool, delaySe timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) l1Clock = timeTravelClock } - l1EL, l1CL := startInProcessL1WithClock(t, l1Net, jwtPath, l1Clock) + l1EL, l1CL := startInProcessL1WithClockConfig(t, l1Net, jwtPath, l1Clock, cfg) l2AEL := startSupernodeEL(t, l2ANet, jwtPath, jwtSecret) l2BEL := startSupernodeEL(t, l2BNet, jwtPath, jwtSecret) diff --git a/op-devstack/sysgo/multichain_supervisor_runtime.go b/op-devstack/sysgo/multichain_supervisor_runtime.go index 84747e53bda..899e0c3d2d4 100644 --- a/op-devstack/sysgo/multichain_supervisor_runtime.go +++ b/op-devstack/sysgo/multichain_supervisor_runtime.go @@ -47,7 +47,7 @@ func NewSingleChainInteropRuntimeWithConfig(t devtest.T, cfg PresetConfig) *Mult timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) l1Clock = timeTravelClock } - l1EL, l1CL := startInProcessL1WithClock(t, l1Net, jwtPath, l1Clock) + l1EL, l1CL := startInProcessL1WithClockConfig(t, l1Net, jwtPath, l1Clock, cfg) supervisor := startSupervisor(t, "1-primary", l1EL, fullCfgSet, map[eth.ChainID]*rollup.Config{ l2Net.ChainID(): l2Net.rollupCfg, }) @@ -118,7 +118,7 @@ func NewSimpleInteropRuntimeWithConfig(t devtest.T, cfg PresetConfig) *MultiChai timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) l1Clock = timeTravelClock } - l1EL, l1CL := startInProcessL1WithClock(t, l1Net, jwtPath, l1Clock) + l1EL, l1CL := startInProcessL1WithClockConfig(t, l1Net, jwtPath, l1Clock, cfg) supervisor := startSupervisor(t, "1-primary", l1EL, fullCfgSet, map[eth.ChainID]*rollup.Config{ l2ANet.ChainID(): l2ANet.rollupCfg, l2BNet.ChainID(): l2BNet.rollupCfg, diff --git a/op-devstack/sysgo/op_rbuilder.go b/op-devstack/sysgo/op_rbuilder.go new file mode 100644 index 00000000000..0692f96743a --- /dev/null +++ b/op-devstack/sysgo/op_rbuilder.go @@ -0,0 +1,495 @@ +// Moved from fb_OPRbuilderNode_real.go +package sysgo + +import ( + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/log" + yaml "gopkg.in/yaml.v3" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/logpipe" + "github.com/ethereum-optimism/optimism/op-service/tasks" + "github.com/ethereum-optimism/optimism/op-service/testutils/tcpproxy" +) + +type OPRBuilderNode struct { + mu sync.Mutex + + name string + chainID eth.ChainID + rollupCfg *rollup.Config + + wsProxyURL string + wsProxy *tcpproxy.Proxy + + rpcProxyURL string + rpcProxy *tcpproxy.Proxy + + authProxyURL string + authProxy *tcpproxy.Proxy + + logger log.Logger + p devtest.CommonT + + sub *SubProcess + cfg *OPRBuilderNodeConfig //nolint:unused,structcheck // configuration retained for restarts and JWT lookups +} + +var _ stack.Lifecycle = (*OPRBuilderNode)(nil) +var _ L2ELNode = (*OPRBuilderNode)(nil) + +// OPRBuilderNodeConfig contains configuration used to generate the op-OPRbuilderNode CLI. +// Callers can modify the defaults via OPRbuilderNodeOption functions. +type OPRBuilderNodeConfig struct { + // Chain selector (defaults to "dev" to avoid mainnet imports during tests) + Chain string + + // DataDir for op-OPRbuilderNode. If empty, a temp dir is created and cleaned up. + DataDir string + + // Logging formats + LogStdoutFormat string // e.g. "json" + LogFileFormat string // e.g. "json" + + // Flashblocks websocket bind address (host) + FlashblocksAddr string + // Flashblocks websocket port. 0 means auto-allocate an available local port. + FlashblocksPort int + // EnableFlashblocks enables the flashblocks feature. + EnableFlashblocks bool + + // --http + EnableRPC bool + RPCAPI string + RPCAddr string + RPCPort int + RPCJWTPath string + + AuthRPCJWTPath string + AuthRPCAddr string + AuthRPCPort int + + // P2P + P2PPort int + P2PAddr string + P2PNodeKeyHex string + StaticPeers []string + TrustedPeers []string + + // Misc process toggles + WithUnusedPorts bool // choose unused ports for subsystems + DisableDiscovery bool // avoid discv5 UDP socket collisions + + Full bool + + RulesEnabled bool + RulesConfigPath string + + // ExtraArgs are appended to the generated CLI allowing callers to override defaults + // if the binary respects "last flag wins". + ExtraArgs []string + // Env is passed to the subprocess environment. + Env []string +} + +func DefaultOPRbuilderNodeConfig() *OPRBuilderNodeConfig { + return &OPRBuilderNodeConfig{ + EnableFlashblocks: true, + FlashblocksAddr: "127.0.0.1", + FlashblocksPort: 0, + EnableRPC: true, + RPCAPI: "admin,web3,debug,eth,txpool,net,miner", + RPCAddr: "127.0.0.1", + RPCPort: 0, + RPCJWTPath: "", + AuthRPCAddr: "127.0.0.1", + AuthRPCPort: 0, + AuthRPCJWTPath: "", + P2PAddr: "127.0.0.1", + P2PPort: 0, + P2PNodeKeyHex: "", + StaticPeers: nil, + TrustedPeers: nil, + Full: true, + LogStdoutFormat: "json", + LogFileFormat: "json", + Chain: "dev", + WithUnusedPorts: false, + DisableDiscovery: true, + DataDir: "", + RulesEnabled: false, + RulesConfigPath: "", + ExtraArgs: nil, + Env: nil, + } +} + +func (cfg *OPRBuilderNodeConfig) LaunchSpec(p devtest.CommonT) (args []string, env []string) { + p.Require().NotNil(cfg, "nil OPRbuilderNodeConfig") + + env = append([]string(nil), cfg.Env...) + args = make([]string, 0, len(cfg.ExtraArgs)+8) + + args = append(args, "node") + + if cfg.EnableFlashblocks { + if cfg.FlashblocksAddr == "" { + cfg.FlashblocksAddr = "127.0.0.1" + } + args = append(args, "--flashblocks.enabled") + args = append(args, "--flashblocks.addr="+cfg.FlashblocksAddr) + if cfg.FlashblocksPort > 0 { + // Use explicitly configured port + args = append(args, "--flashblocks.port="+strconv.Itoa(cfg.FlashblocksPort)) + } else { + // Use port 0 to let the OS assign a port atomically at bind time. + // The actual port will be discovered by parsing the process logs. + args = append(args, "--flashblocks.port=0") + } + } + + // P2P configuration: enforce deterministic identity and static peering to the sequencer EL. + if cfg.P2PNodeKeyHex != "" { + key := strings.TrimPrefix(cfg.P2PNodeKeyHex, "0x") + _, err := hex.DecodeString(key) + p.Require().NoError(err, "decode p2p node key") + keyPath := filepath.Join(p.TempDir(), "oprbuilder-nodekey") + p.Require().NoError(os.WriteFile(keyPath, []byte(key), 0o600), "write p2p node key") + args = append(args, "--p2p-secret-key", keyPath) + } + if cfg.P2PAddr != "" { + args = append(args, "--addr", cfg.P2PAddr) + } + if len(cfg.StaticPeers) > 0 { + args = append(args, "--bootnodes", strings.Join(cfg.StaticPeers, ",")) + } + if len(cfg.TrustedPeers) > 0 { + args = append(args, "--trusted-peers", strings.Join(cfg.TrustedPeers, ",")) + } + + if cfg.EnableRPC { + args = append(args, "--http") + args = append(args, "--http.addr="+cfg.RPCAddr) + if cfg.RPCPort > 0 { + // Use explicitly configured port + args = append(args, "--http.port="+strconv.Itoa(cfg.RPCPort)) + } else { + // Use port 0 to let the OS assign a port atomically at bind time. + // The actual port will be discovered by parsing the process logs. + args = append(args, "--http.port=0") + } + args = append(args, "--http.api="+cfg.RPCAPI) + } + + if cfg.AuthRPCAddr != "" { + args = append(args, "--authrpc.addr="+cfg.AuthRPCAddr) + } + if cfg.AuthRPCPort > 0 { + // Use explicitly configured port + args = append(args, "--authrpc.port="+strconv.Itoa(cfg.AuthRPCPort)) + } else { + // Use port 0 to let the OS assign a port atomically at bind time. + // The actual port will be discovered by parsing the process logs. + args = append(args, "--authrpc.port=0") + } + if cfg.AuthRPCJWTPath != "" { + args = append(args, "--authrpc.jwtsecret="+cfg.AuthRPCJWTPath) + } + + if cfg.Full { + args = append(args, "--full") + } + + if cfg.LogStdoutFormat != "" { + args = append(args, "--log.stdout.format="+cfg.LogStdoutFormat) + } + if cfg.LogFileFormat != "" { + args = append(args, "--log.file.format="+cfg.LogFileFormat) + } + if cfg.Chain != "" { + args = append(args, "--chain="+cfg.Chain) + } + if cfg.DisableDiscovery { + args = append(args, "--disable-discovery") + } + + if cfg.P2PPort > 0 { + // Use explicitly configured P2P port + args = append(args, "--port="+strconv.Itoa(cfg.P2PPort)) + } else { + // Use --with-unused-ports to let reth assign P2P port atomically at bind time. + args = append(args, "--with-unused-ports") + } + + if cfg.DataDir == "" { + tmpDir, err := os.MkdirTemp("", "op-OPRBuilderNode-datadir-*") + p.Require().NoError(err, "create temp datadir for op-OPRBuilderNode") + args = append(args, "--datadir="+tmpDir) + p.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + } else { + args = append(args, "--datadir="+cfg.DataDir) + } + + if cfg.RulesEnabled { + args = append(args, "--rules.enabled") + args = append(args, "--rules.config-path="+cfg.RulesConfigPath) + } + + args = append(args, cfg.ExtraArgs...) + + return args, env +} + +type OPRBuilderNodeOption interface { + Apply(p devtest.CommonT, target ComponentTarget, cfg *OPRBuilderNodeConfig) +} + +type OPRBuilderNodeOptionFn func(p devtest.CommonT, target ComponentTarget, cfg *OPRBuilderNodeConfig) + +var _ OPRBuilderNodeOption = OPRBuilderNodeOptionFn(nil) + +func (fn OPRBuilderNodeOptionFn) Apply(p devtest.CommonT, target ComponentTarget, cfg *OPRBuilderNodeConfig) { + fn(p, target, cfg) +} + +// OPRBuilderNodeOptionBundle applies multiple OPRBuilderNodeOptions in order. +type OPRBuilderNodeOptionBundle []OPRBuilderNodeOption + +var _ OPRBuilderNodeOption = OPRBuilderNodeOptionBundle(nil) + +func (b OPRBuilderNodeOptionBundle) Apply(p devtest.CommonT, target ComponentTarget, cfg *OPRBuilderNodeConfig) { + for _, opt := range b { + p.Require().NotNil(opt, "cannot Apply nil OPRBuilderNodeOption") + opt.Apply(p, target, cfg) + } +} + +// OPRBuilderWithP2PConfig sets deterministic P2P identity and static peers for the builder EL. +func OPRBuilderWithP2PConfig(addr string, port int, nodeKeyHex string, staticPeers, trustedPeers []string) OPRBuilderNodeOption { + return OPRBuilderNodeOptionFn(func(p devtest.CommonT, _ ComponentTarget, cfg *OPRBuilderNodeConfig) { + cfg.P2PAddr = addr + cfg.P2PPort = port + cfg.P2PNodeKeyHex = nodeKeyHex + cfg.StaticPeers = staticPeers + cfg.TrustedPeers = trustedPeers + }) +} + +// OPRBuilderWithNodeIdentity applies an ELNodeIdentity directly to the builder EL. +func OPRBuilderWithNodeIdentity(identity *ELNodeIdentity, addr string, staticPeers, trustedPeers []string) OPRBuilderNodeOption { + return OPRBuilderNodeOptionFn(func(p devtest.CommonT, _ ComponentTarget, cfg *OPRBuilderNodeConfig) { + cfg.P2PAddr = addr + cfg.P2PPort = identity.Port + cfg.P2PNodeKeyHex = identity.KeyHex() + cfg.StaticPeers = staticPeers + cfg.TrustedPeers = trustedPeers + }) +} + +func OPRBuilderNodeWithExtraArgs(args ...string) OPRBuilderNodeOption { + return OPRBuilderNodeOptionFn(func(p devtest.CommonT, _ ComponentTarget, cfg *OPRBuilderNodeConfig) { + cfg.ExtraArgs = append(cfg.ExtraArgs, args...) + }) +} + +func OPRBuilderNodeWithEnv(env ...string) OPRBuilderNodeOption { + return OPRBuilderNodeOptionFn(func(p devtest.CommonT, _ ComponentTarget, cfg *OPRBuilderNodeConfig) { + cfg.Env = append(cfg.Env, env...) + }) +} + +func (b *OPRBuilderNode) Start() { + b.mu.Lock() + defer b.mu.Unlock() + if b.sub != nil { + b.logger.Warn("OPRbuilderNode already started") + return + } + cfg := b.cfg + b.p.Require().NotNil(cfg, "OPRbuilderNode config not initialized") + + if b.wsProxy == nil { + b.wsProxy = tcpproxy.New(b.p.Logger()) + b.p.Require().NoError(b.wsProxy.Start()) + b.wsProxyURL = "ws://" + b.wsProxy.Addr() + b.p.Cleanup(func() { b.wsProxy.Close() }) + } + + if b.rpcProxy == nil { + b.rpcProxy = tcpproxy.New(b.p.Logger()) + b.p.Require().NoError(b.rpcProxy.Start()) + b.rpcProxyURL = "http://" + b.rpcProxy.Addr() + b.p.Cleanup(func() { b.rpcProxy.Close() }) + } + + if cfg.EnableRPC && b.authProxy == nil { + b.authProxy = tcpproxy.New(b.p.Logger()) + b.p.Require().NoError(b.authProxy.Start()) + b.authProxyURL = "http://" + b.authProxy.Addr() + b.p.Cleanup(func() { b.authProxy.Close() }) + } + + args, env := cfg.LaunchSpec(b.p) + + // Create channels for discovering ports from process logs. + // When using port 0, the OS assigns ports at bind time and the process logs them. + flashblocksWSChan := make(chan string, 1) + httpRPCChan := make(chan string, 1) + authRPCChan := make(chan string, 1) + defer close(flashblocksWSChan) + defer close(httpRPCChan) + defer close(authRPCChan) + + // Forward structured logs to Go logger and parse for port discovery + logOut := logpipe.ToLoggerWithMinLevel(b.logger.New("component", "op-OPRbuilderNode", "src", "stdout"), log.LevelWarn) + logErr := logpipe.ToLoggerWithMinLevel(b.logger.New("component", "op-OPRbuilderNode", "src", "stderr"), log.LevelWarn) + + // Log parsing callback to extract bound addresses from process output + onLogEntry := func(e logpipe.LogEntry) { + msg := e.LogMessage() + // Flashblocks WS - custom log message from wspub.rs + if strings.HasPrefix(msg, "Flashblocks WebSocketPublisher listening on ") { + addr := strings.TrimPrefix(msg, "Flashblocks WebSocketPublisher listening on ") + if validURL := parseAndValidateAddr(addr, "ws"); validURL != "" { + select { + case flashblocksWSChan <- validURL: + default: + } + } + } + // HTTP RPC - standard reth log message + if msg == "RPC HTTP server started" { + if addr, ok := e.FieldValue("url").(string); ok { + if validURL := parseAndValidateAddr(addr, "http"); validURL != "" { + select { + case httpRPCChan <- validURL: + default: + } + } + } + } + // Auth RPC - standard reth log message + if msg == "RPC auth server started" { + if addr, ok := e.FieldValue("url").(string); ok { + if validURL := parseAndValidateAddr(addr, "http"); validURL != "" { + select { + case authRPCChan <- validURL: + default: + } + } + } + } + } + + stdOut := logpipe.LogCallback(func(line []byte) { + e := logpipe.ParseRustStructuredLogs(line) + logOut(e) + onLogEntry(e) + }) + stdErr := logpipe.LogCallback(func(line []byte) { + logErr(logpipe.ParseRustStructuredLogs(line)) + }) + + b.sub = NewSubProcess(b.p, stdOut, stdErr) + + 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(execPath, args, env) + b.p.Require().NoError(err, "start OPRBuilderNode") + + // Wait for ports to be discovered from logs, then configure proxies + if cfg.EnableRPC { + var httpRPCAddr string + b.p.Require().NoError(tasks.Await(b.p.Ctx(), httpRPCChan, &httpRPCAddr), "need HTTP RPC address from logs") + b.logger.Info("OPRBuilderNode upstream RPC ready", "rpc", httpRPCAddr) + b.rpcProxy.SetUpstream(ProxyAddr(b.p.Require(), httpRPCAddr)) + b.logger.Info("OPRBuilderNode proxy RPC ready", "proxy_rpc", b.rpcProxyURL) + + var authRPCAddr string + b.p.Require().NoError(tasks.Await(b.p.Ctx(), authRPCChan, &authRPCAddr), "need Auth RPC address from logs") + b.logger.Info("OPRBuilderNode upstream auth RPC ready", "auth_rpc", authRPCAddr) + b.authProxy.SetUpstream(ProxyAddr(b.p.Require(), authRPCAddr)) + b.logger.Info("OPRBuilderNode proxy auth RPC ready", "proxy_auth_rpc", b.authProxyURL) + } + + if cfg.EnableFlashblocks { + var flashblocksAddr string + b.p.Require().NoError(tasks.Await(b.p.Ctx(), flashblocksWSChan, &flashblocksAddr), "need Flashblocks WS address from logs") + b.logger.Info("OPRBuilderNode upstream WS ready", "ws", flashblocksAddr) + b.wsProxy.SetUpstream(ProxyAddr(b.p.Require(), flashblocksAddr)) + b.logger.Info("OPRBuilderNode proxy WS ready", "proxy_ws", b.wsProxyURL) + } +} + +func (b *OPRBuilderNode) Stop() { + b.mu.Lock() + defer b.mu.Unlock() + if b.sub == nil { + b.logger.Warn("OPRbuilderNode already stopped") + return + } + b.p.Require().NoError(b.sub.Stop(true)) + b.sub = nil +} + +func (b *OPRBuilderNode) EngineRPC() string { + return b.authProxyURL +} + +func (b *OPRBuilderNode) JWTPath() string { + return b.cfg.AuthRPCJWTPath +} + +func (b *OPRBuilderNode) UserRPC() string { + return b.rpcProxyURL +} + +func (b *OPRBuilderNode) FlashblocksWSURL() string { + return b.wsProxyURL +} + +type RulesConfig struct { + File []struct { + Path string `yaml:"path"` + } `yaml:"file"` + RefreshInterval int `yaml:"refresh_interval"` +} + +func (b *OPRBuilderNode) UpdateRuleSet(rulesYaml string) error { + if b.cfg.RulesConfigPath == "" { + return fmt.Errorf("rules config path is not configured (rules not enabled?)") + } + rulesConfigContent, err := os.ReadFile(b.cfg.RulesConfigPath) + if err != nil { + return fmt.Errorf("failed to open rules config yaml: %w", err) + } + var rulesConfig RulesConfig + if err := yaml.Unmarshal(rulesConfigContent, &rulesConfig); err != nil { + return fmt.Errorf("failed to parse rules config yaml: %w", err) + } + if len(rulesConfig.File) == 0 { + return fmt.Errorf("no file entries found") + } + // Use only the first file entry for simplicity + rulesPath := rulesConfig.File[0].Path + if err := os.WriteFile(rulesPath, []byte(rulesYaml), 0644); err != nil { + return fmt.Errorf("failed to update rules file: %w", err) + } + return nil +} diff --git a/op-devstack/sysgo/preset_config.go b/op-devstack/sysgo/preset_config.go new file mode 100644 index 00000000000..19f91a11fac --- /dev/null +++ b/op-devstack/sysgo/preset_config.go @@ -0,0 +1,126 @@ +package sysgo + +import gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + +// PresetConfig captures preset constructor mutations. +// It is independent from orchestrator lifecycle hooks. +type PresetConfig struct { + LocalContractArtifactsPath string + DeployerOptions []DeployerOption + BatcherOptions []BatcherOption + ProposerOptions []ProposerOption + OPRBuilderOptions []OPRBuilderNodeOption + GlobalL2CLOptions []L2CLOption + GlobalSyncTesterELOptions []SyncTesterELOption + L1ELKind string + L1GethExecPath string + AddedGameTypes []gameTypes.GameType + RespectedGameTypes []gameTypes.GameType + EnableCannonKonaForChall bool + EnableTimeTravel bool + MaxSequencingWindow *uint64 + RequireInteropNotAtGen bool +} + +type PresetOption interface { + apply(cfg *PresetConfig) +} + +type presetOptionFn func(cfg *PresetConfig) + +func (fn presetOptionFn) apply(cfg *PresetConfig) { + fn(cfg) +} + +func NewPresetConfig(opts ...PresetOption) PresetConfig { + cfg := PresetConfig{} + for _, opt := range opts { + if opt == nil { + continue + } + opt.apply(&cfg) + } + return cfg +} + +func WithDeployerOptions(opts ...DeployerOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.DeployerOptions = append(cfg.DeployerOptions, opts...) + }) +} + +func WithBatcherOption(opt BatcherOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + if opt == nil { + return + } + cfg.BatcherOptions = append(cfg.BatcherOptions, opt) + }) +} + +func WithProposerOption(opt ProposerOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + if opt == nil { + return + } + cfg.ProposerOptions = append(cfg.ProposerOptions, opt) + }) +} + +func WithOPRBuilderOption(opt OPRBuilderNodeOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + if opt == nil { + return + } + cfg.OPRBuilderOptions = append(cfg.OPRBuilderOptions, opt) + }) +} + +func WithGlobalL2CLOption(opt L2CLOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + if opt == nil { + return + } + cfg.GlobalL2CLOptions = append(cfg.GlobalL2CLOptions, opt) + }) +} + +func WithGlobalSyncTesterELOption(opt SyncTesterELOption) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + if opt == nil { + return + } + cfg.GlobalSyncTesterELOptions = append(cfg.GlobalSyncTesterELOptions, opt) + }) +} + +func WithGameTypeAdded(gameType gameTypes.GameType) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.AddedGameTypes = append(cfg.AddedGameTypes, gameType) + }) +} + +func WithRespectedGameTypeOverride(gameType gameTypes.GameType) PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.RespectedGameTypes = append(cfg.RespectedGameTypes, gameType) + }) +} + +func WithCannonKonaGameTypeAdded() PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.EnableCannonKonaForChall = true + cfg.AddedGameTypes = append(cfg.AddedGameTypes, gameTypes.CannonKonaGameType) + }) +} + +func WithChallengerCannonKonaEnabled() PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.EnableCannonKonaForChall = true + }) +} + +func WithTimeTravelEnabled() PresetOption { + return presetOptionFn(func(cfg *PresetConfig) { + cfg.EnableTimeTravel = true + }) +} diff --git a/op-devstack/sysgo/singlechain_runtime.go b/op-devstack/sysgo/singlechain_runtime.go index 969fd90b68a..aa7ed075f96 100644 --- a/op-devstack/sysgo/singlechain_runtime.go +++ b/op-devstack/sysgo/singlechain_runtime.go @@ -101,7 +101,7 @@ func newSingleChainRuntimeWithConfig(t devtest.T, cfg PresetConfig, spec singleC timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) l1Clock = timeTravelClock } - l1EL, l1CL := startInProcessL1WithClock(t, world.L1Network, jwtPath, l1Clock) + l1EL, l1CL := startInProcessL1WithClockConfig(t, world.L1Network, jwtPath, l1Clock, cfg) primary := spec.StartPrimary(t, keys, world, l1EL, l1CL, jwtPath, jwtSecret, cfg) primaryNode := newSingleChainNodeRuntime("sequencer", true, primary.EL, primary.CL) diff --git a/op-up/main.go b/op-up/main.go index a9bbef2a91c..4dc406f66dd 100644 --- a/op-up/main.go +++ b/op-up/main.go @@ -445,6 +445,10 @@ func (t *testingT) Gate() *testreq.Assertions { return t.gate } +// MarkFlaky implements devtest.T. +func (t *testingT) MarkFlaky(string) { +} + // Helper implements devtest.T. func (t *testingT) Helper() { } diff --git a/rust/kona/tests/node/utils/package_scope.go b/rust/kona/tests/node/utils/package_scope.go index e08bacbfe88..1ceb94a463b 100644 --- a/rust/kona/tests/node/utils/package_scope.go +++ b/rust/kona/tests/node/utils/package_scope.go @@ -138,6 +138,9 @@ func (t *packageScopeT) Gate() *testreq.Assertions { return t.gate } +func (t *packageScopeT) MarkFlaky(string) { +} + func (t *packageScopeT) Deadline() (time.Time, bool) { return time.Time{}, false }