diff --git a/.github/workflows/pos-e2e.yml b/.github/workflows/pos-e2e.yml index 6edf1f82..eef72b03 100644 --- a/.github/workflows/pos-e2e.yml +++ b/.github/workflows/pos-e2e.yml @@ -38,6 +38,22 @@ on: required: false type: string default: "feat/execution-specs" + ethereum-exec-specs-ref: + description: "Branch or tag of ethereum/execution-specs to use for remote execution tests." + required: false + type: string + default: "forks/amsterdam" + run-tests: + description: "Select which test jobs to run (bypasses release detection for the selected jobs)." + required: false + type: choice + default: "all" + options: + - all + - pos-exec-specs + - fork-trans + - matrix + - ethereum-exec-specs-remote force-run: description: "Run tests even if no new bor release is detected" required: false @@ -57,6 +73,7 @@ env: ENCLAVE_NAME: pos POLYCLI_VERSION: v0.1.103 KURTOSIS_POS_REF: ${{ inputs.kurtosis-pos-ref || 'feat/execution-specs' }} + EXEC_SPECS_REF: ${{ inputs.ethereum-exec-specs-ref || 'forks/amsterdam' }} jobs: # --------------------------------------------------------------------------- @@ -233,11 +250,10 @@ jobs: - name: Generate version compatibility matrix id: gen-compat-matrix run: | - COMPAT_CSV="${{ steps.resolve.outputs.compat-versions }}" COMPAT_YML="scripts/pos-version-matrix/compat-versions.yml" # --- Collect bor and erigon version lists --- - # YAML has separate bor_versions / erigon_versions sections. + # YAML has flat `versions:` list with `el_type: bor|erigon` discriminator. declare -a bor_images=() declare -a erigon_images=() OVERRIDE="${{ steps.resolve.outputs.compat-versions-override }}" @@ -247,15 +263,16 @@ jobs: IFS=',' read -ra bor_images <<< "$OVERRIDE" elif [[ -f "$COMPAT_YML" ]]; then echo "Reading versions from ${COMPAT_YML}..." - in_bor=false; in_erigon=false + pending_img="" while IFS= read -r line; do - [[ "$line" =~ ^bor_versions: ]] && { in_bor=true; in_erigon=false; continue; } - [[ "$line" =~ ^erigon_versions: ]] && { in_erigon=true; in_bor=false; continue; } - [[ "$line" =~ ^[a-z] ]] && { in_bor=false; in_erigon=false; continue; } - if [[ "$line" =~ image:\ *\"?([^\"]+)\"? ]]; then - img="${BASH_REMATCH[1]}" - $in_bor && bor_images+=("$img") - $in_erigon && erigon_images+=("$img") + if [[ "$line" =~ image:\ *\"?([^\"[:space:]]+)\"? ]]; then + [[ -n "$pending_img" ]] && echo "::warning::Skipped image without el_type: ${pending_img}" + pending_img="${BASH_REMATCH[1]}" + elif [[ -n "$pending_img" && "$line" =~ el_type:\ *(bor|erigon) ]]; then + el_type="${BASH_REMATCH[1]}" + [[ "$el_type" == "bor" ]] && bor_images+=("$pending_img") + [[ "$el_type" == "erigon" ]] && erigon_images+=("$pending_img") + pending_img="" fi done < "$COMPAT_YML" fi @@ -333,8 +350,10 @@ jobs: pos-execution-specs: needs: check-new-release if: >- - needs.check-new-release.outputs.should-run == 'true' && - needs.check-new-release.outputs.compat-pairs != '[]' + needs.check-new-release.outputs.compat-pairs != '[]' && ( + (github.event_name == 'schedule' && needs.check-new-release.outputs.should-run == 'true') || + (github.event_name == 'workflow_dispatch' && (inputs.run-tests == 'all' || inputs.run-tests == 'pos-exec-specs' || inputs.run-tests == 'matrix')) + ) runs-on: ubuntu-latest timeout-minutes: 60 strategy: @@ -342,7 +361,7 @@ jobs: max-parallel: 5 matrix: include: ${{ fromJson(needs.check-new-release.outputs.compat-pairs) }} - name: exec-specs (${{ matrix.pair_label }}) + name: pos-exec-specs (${{ matrix.pair_label }}) steps: - name: Checkout agglayer/e2e uses: actions/checkout@v6 @@ -535,7 +554,7 @@ jobs: - name: Upload logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: pos-e2e-dump-execspecs-${{ matrix.pair_label }}-${{ github.run_id }} path: ./dump @@ -552,8 +571,10 @@ jobs: pos-fork-transition: needs: check-new-release if: >- - needs.check-new-release.outputs.should-run == 'true' && - needs.check-new-release.outputs.compat-pairs != '[]' + needs.check-new-release.outputs.compat-pairs != '[]' && ( + (github.event_name == 'schedule' && needs.check-new-release.outputs.should-run == 'true') || + (github.event_name == 'workflow_dispatch' && (inputs.run-tests == 'all' || inputs.run-tests == 'fork-trans' || inputs.run-tests == 'matrix')) + ) runs-on: ubuntu-latest timeout-minutes: 60 strategy: @@ -868,7 +889,7 @@ jobs: - name: Upload logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: pos-e2e-dump-forktrans-${{ matrix.pair_label }}-${{ github.run_id }} path: ./dump @@ -879,18 +900,230 @@ jobs: kurtosis enclave stop "${{ env.ENCLAVE_NAME }}" || true kurtosis clean || true + # --------------------------------------------------------------------------- + # Job 3: Execution-specs remote — run ethereum/execution-specs against latest + # bor release via `execute remote`. Runs after all matrix jobs to maximise + # available runner resources. + # --------------------------------------------------------------------------- + ethereum-exec-specs-remote: + needs: [check-new-release, pos-execution-specs, pos-fork-transition] + if: >- + always() && ( + (github.event_name == 'schedule' && needs.check-new-release.outputs.should-run == 'true') || + (github.event_name == 'workflow_dispatch' && (inputs.run-tests == 'all' || inputs.run-tests == 'ethereum-exec-specs-remote')) + ) + runs-on: ubuntu-latest + timeout-minutes: 120 + name: ethereum-exec-specs-remote (against latest bor) + steps: + - name: Checkout agglayer/e2e + uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + + - name: Install kurtosis + run: | + echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list + sudo apt update + sudo apt install -y kurtosis-cli jq + kurtosis analytics disable + + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1.3.1 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Checkout kurtosis-pos + uses: actions/checkout@v6 + with: + repository: 0xPolygon/kurtosis-pos + ref: ${{ env.KURTOSIS_POS_REF }} + path: kurtosis-pos + + - name: Checkout execution-specs + uses: actions/checkout@v6 + with: + repository: ethereum/execution-specs + ref: ${{ env.EXEC_SPECS_REF }} + path: execution-specs + + - name: Install execution-specs dependencies + run: | + cd execution-specs + uv sync + + - name: Patch constants.star (all post-Rio forks at 256) + run: | + CONSTANTS="kurtosis-pos/src/config/constants.star" + for fork in madhugiri madhugiriPro dandeli lisovo lisovoPro giugliano; do + sed -i -E '/^\s+"'"${fork}"'":/s/[0-9]+/256/' "$CONSTANTS" + done + echo "--- Patched EL_HARD_FORK_BLOCKS ---" + grep -A 20 '^EL_HARD_FORK_BLOCKS' "$CONSTANTS" + + - name: Generate kurtosis params (single-version latest bor) + run: | + BOR_LATEST="${{ needs.check-new-release.outputs.bor-latest-image }}" + HV2_IMAGE="${{ needs.check-new-release.outputs.heimdall-v2-image }}" + + echo "Bor (latest): ${BOR_LATEST} | Heimdall-v2: ${HV2_IMAGE}" + + cat > /tmp/pos-params.yml </dev/null) || block=0 + [[ "$block" -ge "$TARGET_BLOCK" ]] && echo "Block ${block} — all forks active." && exit 0 + echo "[${elapsed}s] Block: ${block} / ${TARGET_BLOCK}" + sleep "$POLL_INTERVAL" + elapsed=$(( elapsed + POLL_INTERVAL )) + done + echo "::error::Chain stuck at block ${block}." + exit 1 + + - name: Run execution-specs remote tests + run: | + set -eo pipefail + L2_RPC_RAW="$(kurtosis port print "${{ env.ENCLAVE_NAME }}" l2-el-4-bor-heimdall-v2-rpc rpc)" + L2_RPC_RAW="${L2_RPC_RAW#http://}" ; L2_RPC_RAW="${L2_RPC_RAW#https://}" + L2_RPC_URL="http://${L2_RPC_RAW}" + L2_PRIVATE_KEY="0xd40311b5a5ca5eaeb48dfba5403bde4993ece8eccf4190e98e19fcd4754260ea" + CHAIN_ID="$(cast chain-id --rpc-url "${L2_RPC_URL}")" + + echo "RPC: ${L2_RPC_URL} | Chain ID: ${CHAIN_ID}" + + cd execution-specs + uv run execute remote \ + --fork=Prague \ + --rpc-endpoint="${L2_RPC_URL}" \ + --rpc-seed-key="${L2_PRIVATE_KEY}" \ + --chain-id="${CHAIN_ID}" \ + --default-max-fee-per-blob-gas=1 \ + --default-max-fee-per-gas=100000000000 \ + --default-max-priority-fee-per-gas=100000000000 \ + --default-gas-price=100000000000 \ + --sender-funding-txs-gas-price=100000000000 \ + --seed-account-sweep-amount=800000000000000000000 \ + --max-tx-per-batch=75 \ + -n 8 \ + -s \ + --tx-wait-timeout=60 \ + -k "not blob and not 4844 and not beacon_root and not mainnet and not genesis_hash_available and not test_selfdestruct_to_precompile and not test_selfdestruct_to_system_contract and not test_dynamic_create2_selfdestruct_collision_multi_tx and not test_extcodehash_of_empty and not test_call_memory_expands_on_early_revert and not type_3 and not address_0x000000000000000000000000000000000000000a and not address_0x000000000000000000000000000000000000000b and not address_0x000000000000000000000000000000000000000c and not address_0x000000000000000000000000000000000000000d and not address_0x000000000000000000000000000000000000000e and not address_0x000000000000000000000000000000000000000f and not address_0x0000000000000000000000000000000000000010 and not address_0x0000000000000000000000000000000000000011 and not test_create_nonce_overflow" \ + --ignore=tests/frontier/opcodes/test_calldatacopy.py \ + --ignore=tests/frontier/opcodes/test_dup.py \ + --ignore=tests/frontier/opcodes/test_all_opcodes.py \ + --ignore=tests/frontier/opcodes/test_blockhash.py \ + --ignore=tests/frontier/scenarios/test_scenarios.py \ + --ignore=tests/frontier/validation/test_transaction.py \ + --ignore=tests/frontier/create/test_create_one_byte.py \ + --ignore=tests/frontier/precompiles/test_precompile_absence.py \ + --ignore=tests/homestead/coverage/test_coverage.py \ + --ignore=tests/byzantium/eip197_ec_pairing/test_gas.py \ + --ignore=tests/byzantium/eip196_ec_add_mul/test_gas.py \ + --ignore=tests/london/eip1559_fee_market_change/test_tx_type.py \ + --ignore=tests/london/validation/test_header.py \ + --ignore=tests/cancun/eip4788_beacon_root \ + --ignore=tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py \ + --ignore=tests/cancun/eip6780_selfdestruct \ + --ignore=tests/cancun/create/test_create_oog_from_eoa_refunds.py \ + --ignore=tests/cancun/eip1153_tstore/test_basic_tload.py \ + --ignore=tests/cancun/eip1153_tstore/test_tload_calls.py \ + --ignore=tests/cancun/eip1153_tstore/test_tstore_reentrancy.py \ + --ignore=tests/cancun/eip1153_tstore/test_tload_reentrancy.py \ + --ignore=tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py \ + --ignore=tests/constantinople/eip1052_extcodehash/test_extcodehash.py \ + --ignore=tests/constantinople/eip145_bitwise_shift/test_shift_combinations.py \ + --ignore=tests/prague/eip6110_deposits \ + --ignore=tests/prague/eip7002_el_triggerable_withdrawals \ + --ignore=tests/prague/eip7251_consolidations \ + --ignore=tests/prague/eip7685_general_purpose_el_requests \ + --ignore=tests/prague/eip2537_bls_12_381_precompiles \ + --ignore=tests/prague/eip2935_historical_block_hashes_from_state \ + --ignore=tests/prague/eip7623_increase_calldata_cost \ + --ignore=tests/prague/eip7702_set_code_tx \ + --ignore=tests/shanghai/eip4895_withdrawals \ + --ignore=tests/istanbul/eip1344_chainid/test_chainid.py \ + tests/frontier/ \ + tests/homestead/ \ + tests/tangerine_whistle/ \ + tests/byzantium/ \ + tests/constantinople/ \ + tests/istanbul/ \ + tests/berlin/ \ + tests/london/ \ + tests/paris/ \ + tests/shanghai/ \ + tests/cancun/ \ + tests/prague/ + + - name: Dump enclave logs + if: failure() + run: kurtosis dump ./dump + + - name: Upload logs + if: failure() + uses: actions/upload-artifact@v6 + with: + name: pos-e2e-dump-execspecs-remote-${{ github.run_id }} + path: ./dump + + - name: Clean up enclave + if: always() + run: | + kurtosis enclave stop "${{ env.ENCLAVE_NAME }}" || true + kurtosis clean || true + # --------------------------------------------------------------------------- # Mark release as tested (write cache entry). # Only runs after all test phases succeed. # --------------------------------------------------------------------------- mark-tested: - needs: [check-new-release, pos-execution-specs, pos-fork-transition] + needs: [check-new-release, pos-execution-specs, pos-fork-transition, ethereum-exec-specs-remote] if: >- always() && needs.check-new-release.outputs.bor-tag != 'manual' && needs.check-new-release.outputs.bor-tag != '' && needs.pos-execution-specs.result == 'success' && - needs.pos-fork-transition.result == 'success' + needs.pos-fork-transition.result == 'success' && + needs.ethereum-exec-specs-remote.result == 'success' runs-on: ubuntu-latest steps: - name: Write cache marker @@ -900,7 +1133,7 @@ jobs: echo "Marked ${{ needs.check-new-release.outputs.bor-tag }} as tested." - name: Save cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: /tmp/pos-release-cache key: pos-e2e-bor-release-${{ needs.check-new-release.outputs.bor-tag }} diff --git a/.github/workflows/pos-update-compat-versions-matrix.yml b/.github/workflows/pos-update-compat-versions-matrix.yml index 9d07385e..1fc111c8 100644 --- a/.github/workflows/pos-update-compat-versions-matrix.yml +++ b/.github/workflows/pos-update-compat-versions-matrix.yml @@ -1,19 +1,21 @@ -name: Update PoS Compat Versions -run-name: Check for new bor/erigon releases +name: PoS Compat Versions Check on: - schedule: - # Check daily at 04:00 UTC (before the pos-e2e run at 06:00). - - cron: "0 4 * * *" - + pull_request: + branches: + - main + paths: + - "scripts/pos-version-matrix/**" + - "scenarios/pos/**" + - "tests/pos/**" + - ".github/workflows/pos-*.yml" workflow_dispatch: {} permissions: - contents: write - pull-requests: write + contents: read jobs: - update-compat-versions: + check-compat-versions: runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -21,52 +23,25 @@ jobs: uses: actions/checkout@v6 - name: Install dependencies - run: pip install -q requests PyYAML + run: pip install -q -r scripts/pos-version-matrix/requirements.txt - name: Check for new releases - id: check env: - GH_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }} run: | - output=$(python3 scripts/pos-version-matrix/update-compat-versions.py 2>&1) - echo "$output" + python3 scripts/pos-version-matrix/update-compat-versions.py if git diff --quiet scripts/pos-version-matrix/compat-versions.yml; then - echo "changed=false" >> "$GITHUB_OUTPUT" + echo "compat-versions.yml is up to date." else - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "--- Diff ---" + echo "New bor/erigon releases are available that are not in compat-versions.yml." + echo "" + echo "To update, run:" + echo " make update-pos-compat-versions" + echo "" + echo "then commit the result." + echo "" + echo "Diff:" git diff scripts/pos-version-matrix/compat-versions.yml - fi - - - name: Create pull request - if: steps.check.outputs.changed == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - BRANCH="auto/update-compat-versions" - - # Configure git for the github-actions bot. - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - # Create or update the branch. - git checkout -B "$BRANCH" - git add scripts/pos-version-matrix/compat-versions.yml - git commit -m "chore: update compat-versions.yml with new releases" - git push -f origin "$BRANCH" - - # Create PR if one doesn't already exist. - existing_pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") - if [[ -n "$existing_pr" ]]; then - echo "PR #${existing_pr} already exists, updated branch." - else - PR_BODY="New bor/erigon releases detected. This PR adds them to compat-versions.yml." - PR_BODY="${PR_BODY}\n\n**Review before merging** — remove any versions that should not be tested and verify the pair count is reasonable." - gh pr create \ - --title "chore: update PoS compat versions with new releases" \ - --body "$(echo -e "$PR_BODY")" \ - --head "$BRANCH" \ - --base main + exit 1 fi diff --git a/Makefile b/Makefile index 59220b60..13811e41 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,24 @@ -.PHONY: check-dependencies compile-contracts +.PHONY: check-dependencies compile-contracts update-tests-inventory update-pos-compat-versions check-dependencies: ./scripts/check-dependencies.sh compile-contracts: - find core/contracts/ -type f | grep -E '(yul|sol)' | while read contract; do echo "$$contract"; forge build -C "$$contract" ; done + find core/contracts/ -type f -name '*.sol' | while read contract; do echo "$$contract"; forge build -C "$$contract" ; done + # .yul files: use --root so forge picks up the per-directory foundry.toml which sets + # lint_on_build = false. Solar (forge's linter) does not support Yul syntax. + find core/contracts/ -type f -name '*.yul' | while read f; do dir=$$(dirname "$$f"); echo "$$f"; forge build --root "$$dir" ; done ./core/helpers/scripts/postprocess_contracts.sh +## Run before opening a PR that touches test files. +## Updates TESTSINVENTORY.md and the test_tags section of README.md. +update-tests-inventory: + ./scripts/update-tests-inventory.sh + +## Run before opening a PR that touches PoS-related files. +## Fetches the latest bor/erigon releases and updates scripts/pos-version-matrix/compat-versions.yml. +## Requires a GITHUB_TOKEN env var to avoid rate limits: GITHUB_TOKEN= make update-pos-compat-versions +update-pos-compat-versions: + pip install -q -r scripts/pos-version-matrix/requirements.txt + python3 scripts/pos-version-matrix/update-compat-versions.py + diff --git a/scripts/pos-version-matrix/compat-versions.yml b/scripts/pos-version-matrix/compat-versions.yml index c6018e70..b051f00f 100644 --- a/scripts/pos-version-matrix/compat-versions.yml +++ b/scripts/pos-version-matrix/compat-versions.yml @@ -1,31 +1,34 @@ -# PoS version compatibility matrix — curated version lists for compat testing. +# PoS version compatibility matrix — curated list of EL versions to test. # -# The pos-e2e workflow generates pairwise bor combinations and deploys -# mixed devnets with validators/RPCs split across bor versions, plus -# erigon RPCs as cross-client controls. +# The pos-e2e workflow reads this file to generate pairwise compat pairs. +# Only versions listed under `versions` are tested. Keep this list focused: +# - Versions actively running in production +# - Release candidates being validated for rollout +# - Remove versions once fully deprecated and no longer in use # -# Each devnet topology (per bor pair): -# - 2 validators on bor version_a -# - 1 validator on bor version_b -# - 1 RPC on bor version_a -# - 1 RPC on bor version_b -# - 1 RPC on erigon (latest from erigon_versions) +# Each entry must have: image, el_type (bor or erigon), reason. +# Pairwise pairs are generated across ALL entries (bor-bor, erigon-erigon, +# and bor-erigon cross-client combinations). # -# Tests run: fork-transition (staggered forks) + execution-specs (after all forks active). +# If this file is absent or `versions` is empty, the workflow falls back +# to auto-detecting the latest bor release from each major.minor line. -bor_versions: - - image: "0xpolygon/bor:2.7.0-beta3" - reason: "latest pre-release, validating for production" - - - image: "0xpolygon/bor:2.6.5" - reason: "latest 2.6.x stable, active in production" - - - image: "0xpolygon/bor:2.5.9" - reason: "latest 2.5.x stable, still active on some nodes" - -erigon_versions: - - image: "0xpolygon/erigon:v3.5.0-beta2" - reason: "latest pre-release, validating for production" - - - image: "0xpolygon/erigon:v3.4.0" - reason: "current stable, active in production" +versions: +- image: 0xpolygon/bor:2.7.0-beta5 + el_type: bor + reason: new pre-release (2.7 line), auto-detected +- image: 0xpolygon/bor:2.6.5 + el_type: bor + reason: latest stable release (2.6 line) +- image: 0xpolygon/bor:2.5.9 + el_type: bor + reason: latest stable release (2.5 line) +- image: 0xpolygon/erigon:v3.5.0-beta2 + el_type: erigon + reason: new pre-release (3.5 line), auto-detected +- image: 0xpolygon/erigon:v3.4.0 + el_type: erigon + reason: new stable release (3.4 line), auto-detected +- image: 0xpolygon/erigon:v3.3.7 + el_type: erigon + reason: new stable release (3.3 line), auto-detected diff --git a/scripts/pos-version-matrix/update-compat-versions.py b/scripts/pos-version-matrix/update-compat-versions.py index d9ebe4a8..dcf7557c 100644 --- a/scripts/pos-version-matrix/update-compat-versions.py +++ b/scripts/pos-version-matrix/update-compat-versions.py @@ -23,12 +23,14 @@ "el_type": "bor", "image_prefix": "0xpolygon/bor:", "strip_v": True, + "max_lines": 3, # Track 3 major.minor lines (e.g. 2.7, 2.6, 2.5) }, "erigon": { "repo": "0xPolygon/erigon", "el_type": "erigon", "image_prefix": "0xpolygon/erigon:", "strip_v": False, + "max_lines": 2, # Only latest lines — used as RPC endpoint, not pairwise tested }, } @@ -40,9 +42,9 @@ def get_headers() -> dict: return {} -def fetch_latest_releases(repo: str, max_results: int = 10) -> list: +def fetch_latest_releases(repo: str, max_results: int = 30) -> list: """Fetch recent non-draft releases with valid semver tags.""" - url = f"https://api.github.com/repos/{repo}/releases?per_page=30" + url = f"https://api.github.com/repos/{repo}/releases?per_page=50" resp = requests.get(url, timeout=15, headers=get_headers()) if resp.status_code != 200: print(f"Warning: failed to fetch releases for {repo} (HTTP {resp.status_code})") @@ -106,15 +108,19 @@ def update_compat_versions(compat_path: Path) -> list: if not releases: continue - # Group by major.minor line. + # Group by major.minor line (insertion order = newest first from API). lines: dict = {} for r in releases: mm = version_major_minor(r["tag"]) if mm not in lines: lines[mm] = r - # Check each major.minor line's latest release. - for mm, release in lines.items(): + # Limit to the N most recent major.minor lines. + max_lines = comp_config.get("max_lines", 3) + tracked_lines = dict(list(lines.items())[:max_lines]) + + # Check each tracked major.minor line's latest release. + for mm, release in tracked_lines.items(): image = make_image(release["tag"], comp_config) if image in current_images: continue