Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/actions/merge-eip-branches/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Merge EIP Branches
description: Merges multiple EIP branches into a devnet branch, by pushing using --force-with-lease

inputs:
fork:
description: 'Fork name (e.g., amsterdam)'
required: true
devnet_name:
description: 'Devnet name (e.g., amsterdam/1, bal/2, eip-1234/3). Created branch will be `devnets/$devnet_name`'
required: true
eip_numbers:
description: 'Comma-separated list of EIP numbers (e.g., 1234,2345,3456)'
required: true

runs:
using: "composite"
steps:
- name: Configure Git
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Merge EIP branches
shell: bash
env:
FORK: ${{ inputs.fork }}
DEVNET_NAME: ${{ inputs.devnet_name }}
EIP_NUMBERS: ${{ inputs.eip_numbers }}
REMOTE: origin
run: |
set -euo pipefail

echo "FORK=$FORK"
echo "DEVNET_NAME=$DEVNET_NAME"
echo "EIP_NUMBERS=$EIP_NUMBERS"

FORK_BRANCH="forks/${FORK}"
DEVNET_BRANCH="devnets/${DEVNET_NAME}"

# Basic ref-format validation for user-provided pieces
# (git check-ref-format expects full refnames for some checks; --branch is easiest)
git check-ref-format --branch "${DEVNET_BRANCH}" >/dev/null

# Convert comma-separated list to array
IFS=',' read -ra RAW_EIPS <<< "$EIP_NUMBERS"

# Normalize: trim whitespace, drop empties
EIPS=()
for raw in "${RAW_EIPS[@]}"; do
eip="$(echo "$raw" | xargs)" # trims leading/trailing whitespace
if [[ -n "$eip" ]]; then
# Optional: ensure it looks numeric
if [[ ! "$eip" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid EIP number ${eip}"
exit 1
fi
EIPS+=("$eip")
fi
done

if [[ ${#EIPS[@]} -eq 0 ]]; then
echo "Error: No EIP numbers provided after parsing '${EIP_NUMBERS}'"
exit 1
fi

git fetch "${REMOTE}" --prune

# Build the list of expected remote branches and verify all exist,
# then checkout each branch and rebase it onto the fork branch automatically.
for eip in "${EIPS[@]}"; do
EIP_BRANCH="eips/${FORK}/eip-${eip}"
# Validate ref format for each constructed branch name
git check-ref-format --branch "${EIP_BRANCH}" >/dev/null || {
echo "Error: Computed branch name '${EIP_BRANCH}' is not a valid ref"
exit 1
}

if ! git show-ref --verify --quiet "refs/remotes/${REMOTE}/${EIP_BRANCH}"; then
echo "Error: Missing remote branch ${REMOTE}/${EIP_BRANCH}"
exit 1
fi

git checkout -B "${EIP_BRANCH}" "${REMOTE}/${EIP_BRANCH}"

if git merge-base --is-ancestor "${REMOTE}/${FORK_BRANCH}" "${EIP_BRANCH}"; then
echo "${EIP_BRANCH} is already based on ${FORK_BRANCH}, skipping rebase"
else
if ! git rebase "${REMOTE}/${FORK_BRANCH}"; then
echo "Error: Automatic rebase of ${EIP_BRANCH} onto ${FORK_BRANCH} failed"
git rebase --abort || true
exit 1
else
echo "Rebase of ${EIP_BRANCH} onto ${FORK_BRANCH} successful"
fi
fi
done

# Create (or reset) devnet branch from first EIP
FIRST_EIP="eips/${FORK}/eip-${EIPS[0]}"
echo "Creating branch ${DEVNET_BRANCH} from ${FIRST_EIP}"
git checkout -B "${DEVNET_BRANCH}" "${FIRST_EIP}"

# Merge remaining EIPs
for ((i=1; i<${#EIPS[@]}; i++)); do
EIP_BRANCH="eips/${FORK}/eip-${EIPS[$i]}"
echo "Merging ${EIP_BRANCH}..."

if ! git merge "${EIP_BRANCH}" -m "Merge ${EIP_BRANCH} into ${DEVNET_BRANCH}"; then
echo "Error: Merge conflict occurred while merging ${EIP_BRANCH}"
git merge --abort || true
exit 1
fi
done

echo "All EIP branches merged successfully"

- name: Push devnet branch
shell: bash
env:
DEVNET_NAME: ${{ inputs.devnet_name }}
REMOTE: origin
run: |
DEVNET_BRANCH="devnets/${DEVNET_NAME}"

echo "Force pushing ${DEVNET_BRANCH} to ${REMOTE}"
git push --force-with-lease "${REMOTE}" "${DEVNET_BRANCH}"
71 changes: 71 additions & 0 deletions .github/actions/rebase-eip-branch/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Rebase EIP Branch
description: Rebases an EIP branch onto a fork branch
inputs:
fork:
description: 'Fork name (e.g., amsterdam)'
required: true
eip_number:
description: 'EIP number'
required: true
runs:
using: "composite"
steps:
- name: Configure Git
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Rebase EIP branch (create if missing)
shell: bash
env:
FORK: ${{ inputs.fork }}
EIP_NUMBER: ${{ inputs.eip_number }}
REMOTE: origin
run: |
set -euo pipefail

echo "FORK=$FORK"
echo "EIP_NUMBER=$EIP_NUMBER"
EIP_BRANCH="eips/${FORK}/eip-${EIP_NUMBER}"
FORK_BRANCH="forks/${FORK}"

git fetch "${REMOTE}" --prune

# Sanity: fork base must exist
if ! git show-ref --verify --quiet "refs/remotes/${REMOTE}/${FORK_BRANCH}"; then
echo "Error: Base fork branch ${REMOTE}/${FORK_BRANCH} does not exist"
exit 1
fi

# Create local branch from remote if it exists, else create from fork base
if git show-ref --verify --quiet "refs/remotes/${REMOTE}/${EIP_BRANCH}"; then
echo "Checking out existing ${EIP_BRANCH} (tracking ${REMOTE})"
git checkout -B "${EIP_BRANCH}" "${REMOTE}/${EIP_BRANCH}"
else
echo "Branch ${EIP_BRANCH} does not exist on ${REMOTE}; creating from ${REMOTE}/${FORK_BRANCH}"
git checkout -B "${EIP_BRANCH}" "${REMOTE}/${FORK_BRANCH}"

# First push creates the remote branch (no force needed)
git push -u "${REMOTE}" "${EIP_BRANCH}"
fi

echo "Rebasing ${EIP_BRANCH} onto ${REMOTE}/${FORK_BRANCH}"
if ! git rebase "${REMOTE}/${FORK_BRANCH}"; then
echo "Error: Rebase conflict occurred while rebasing ${EIP_BRANCH} onto ${FORK_BRANCH}"
git rebase --abort || true
exit 1
fi

echo "Rebase successful"

- name: Push rebased branch
shell: bash
env:
FORK: ${{ inputs.fork }}
EIP_NUMBER: ${{ inputs.eip_number }}
REMOTE: origin
run: |
EIP_BRANCH="eips/${FORK}/eip-${EIP_NUMBER}"
echo "Force pushing ${EIP_BRANCH} to ${REMOTE}"
git push --force-with-lease "${REMOTE}" "${EIP_BRANCH}"
3 changes: 1 addition & 2 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,5 @@ benchmark_fast:

bal:
evm-type: develop
# TODO: Turn on block rlp limit tests after making filling them more flexible.
fill-params: --fork=Amsterdam -k "not eip7934" --fill-static-tests
fill-params: --fork=Amsterdam --fill-static-tests
feature_only: true
36 changes: 36 additions & 0 deletions .github/workflows/eip-rebase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: EIP Rebase Onto Fork Branch

on:
workflow_dispatch:
inputs:
fork:
description: 'Fork name (e.g., amsterdam)'
required: true
type: string
eip_number:
description: 'EIP number'
required: true
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.fork }}-${{ github.event.inputs.eip_number }}
cancel-in-progress: false

permissions:
contents: write

jobs:
rebase-eip:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Rebase EIP branch onto fork
uses: ./.github/actions/rebase-eip-branch
with:
fork: ${{ github.event.inputs.fork }}
eip_number: ${{ github.event.inputs.eip_number }}
44 changes: 44 additions & 0 deletions .github/workflows/update-devnet-branch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Update Devnet Branch

on:
workflow_dispatch:
inputs:
fork:
description: 'Fork name (e.g., amsterdam)'
required: true
type: string
default: 'amsterdam'
devnet_name:
description: 'Devnet name (e.g., amsterdam/1, bal/2, eip-1234/3)'
required: true
type: string
default: 'bal/2'
eip_numbers:
description: 'Comma-separated list of EIP numbers (e.g., 1234,2345,3456)'
required: true
type: string
default: '8024,7843,7708,7778'

concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.fork || 'amsterdam' }}-${{ github.event.inputs.devnet_name || github.ref }}
cancel-in-progress: false

permissions:
contents: write

jobs:
update-devnet-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Merge EIP branches into devnet
uses: ./.github/actions/merge-eip-branches
with:
fork: ${{ github.event.inputs.fork }}
devnet_name: ${{ github.event.inputs.devnet_name }}
eip_numbers: ${{ github.event.inputs.eip_numbers }}
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- 🔀 Relabel `@pytest.mark.repricing` markers in benchmark tests to reflect configurations requested for gas repricing analysis ([#1971](https://github.com/ethereum/execution-specs/pull/1971)).
- ✨ New EIP-7702 test cases added ([#1974](https://github.com/ethereum/execution-specs/pull/1974)).
- ✨ Add missing benchmark configurations / opcode to benchmark tests for repricing analysis([#2006](https://github.com/ethereum/execution-specs/pull/2006)).
- ✨ Port STATICCALL to CALL tests with zero and non-zero value transfer from `tests/static`, extending coverage with `pytest.mark.with_all_precompiles` ([#1960](https://github.com/ethereum/execution-specs/pull/1960)).

## [v5.4.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.4.0) - 2025-12-07

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1123,14 +1123,64 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
parametrize_fork(metafunc, pytest_params)


def get_param_level_min_valid_fork(metafunc: Metafunc) -> Fork | None:
"""
Extract the minimum valid fork from param-level valid_from markers.

Returns the earliest fork from any valid_from marker inside pytest.param,
or None if no such markers exist.
"""
min_fork: Fork | None = None

for marker in metafunc.definition.iter_markers("parametrize"):
if len(marker.args) < 2:
continue

for value in marker.args[1]:
if not isinstance(value, ParameterSet) or not value.marks:
continue

for mark in value.marks:
mark_obj = mark.mark if hasattr(mark, "mark") else mark
if mark_obj.name == "valid_from" and mark_obj.args:
fork_name = mark_obj.args[0]
try:
for fork in ALL_FORKS:
if fork.name() == fork_name:
if min_fork is None or fork < min_fork:
min_fork = fork
break
except (ValueError, InvalidForkError):
pass

return min_fork


def add_fork_covariant_parameters(
metafunc: Metafunc, fork_parametrizers: List[ForkParametrizer]
) -> None:
"""
Iterate over the fork covariant descriptors and add their values to the
test function.
"""
# Process all covariant decorators uniformly
# Check if any covariant markers are present
has_covariant_markers = any(
list(metafunc.definition.iter_markers(cd.marker_name))
for cd in fork_covariant_decorators
) or any(
marker.name == "parametrize_by_fork"
for marker in metafunc.definition.iter_markers()
)

# Filter forks before any param-level valid_from to avoid covariant
# assertion errors
if has_covariant_markers:
param_min_fork = get_param_level_min_valid_fork(metafunc)
if param_min_fork:
fork_parametrizers[:] = [
fp for fp in fork_parametrizers if fp.fork >= param_min_fork
]

for covariant_descriptor in fork_covariant_decorators:
if list(
metafunc.definition.iter_markers(covariant_descriptor.marker_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@
| `test_selfdestruct_created_in_same_tx_with_revert` | Ensure BAL tracks selfdestruct with revert correctly (pre-Amsterdam test with BAL) | Contract created and selfdestructed in same tx with nested revert | BAL **MUST** track storage reads and balance changes for selfdestruct even with reverts | ✅ Completed |
| `test_value_transfer_gas_calculation` | Ensure BAL correctly tracks OOG scenarios for CALL/CALLCODE/DELEGATECALL/STATICCALL (pre-Amsterdam test with BAL) | Nested calls with precise gas limits to test OOG behavior. For CALL with OOG: target account is read. For CALLCODE/DELEGATECALL/STATICCALL with OOG: target account **NOT** read (OOG before state access) | For CALL: target in BAL even with OOG. For CALLCODE/DELEGATECALL/STATICCALL: target **NOT** in BAL when OOG (state access deferred until after gas check) | ✅ Completed |
| `test_bal_call_with_value_in_static_context` | Ensure BAL does NOT include target when CALL with value fails in static context | `static_caller` uses `STATICCALL` to call `caller`. `caller` attempts `CALL(target, value=1)` which must fail due to static context. Target is an empty account. | BAL **MUST NOT** include target because static context check (`is_static && value > 0`) must happen BEFORE any account access or BAL tracking. BAL **MUST** include `static_caller` with `storage_changes` (STATICCALL succeeded), `caller` with empty changes. | ✅ Completed |
| `test_staticcall_reentrant_call_to_precompile` | Ensure BAL captures STATICCALL reentry with CALL to precompile | Contract STATICCALLs itself. On reentry (CALLVALUE=0), attempts CALL to precompile with parametrized value. File: `tests/byzantium/eip214_staticcall/test_staticcall.py`. | call_value=0: target with `storage_changes` (slot 0=1), precompile with empty changes. call_value>0: target with `storage_reads` (slot 0), precompile **NOT** in BAL (reverted before accessed). | ✅ Completed |
| `test_staticcall_call_to_precompile` | Ensure BAL captures STATICCALL → CALL to precompile chain | Contract A STATICCALLs contract B. B attempts CALL to precompile. File: `tests/byzantium/eip214_staticcall/test_staticcall.py`. | call_value=0: contract_a with markers, contract_b empty (STATICCALLed), precompile empty. call_value>0: contract_a with `storage_reads` for slot 1, precompile **NOT** in BAL. | ✅ Completed |
| `test_staticcall_nested_call_to_precompile` | Ensure BAL captures nested CALL → STATICCALL → CALL to precompile | Contract B CALLs A. A STATICCALLs C. C attempts CALL to precompile. File: `tests/byzantium/eip214_staticcall/test_staticcall.py`. | call_value=0: all contracts with markers/empty, precompile empty. call_value>0: contract_a with `storage_reads` for slot 1, precompile **NOT** in BAL. | ✅ Completed |
| `test_staticcall_call_to_precompile_from_contract_init` | Ensure BAL captures STATICCALL to precompile during CREATE init | Contract A CREATEs contract. Init code STATICCALLs B which CALLs precompile. File: `tests/byzantium/eip214_staticcall/test_staticcall.py`. | call_value=0: contract_a with markers/nonce, created_contract with markers/nonce, contract_b empty, precompile empty. call_value>0: created_contract with `storage_reads` for slot 1, precompile **NOT** in BAL. | ✅ Completed |
| `test_bal_4788_simple` | Ensure BAL captures beacon root storage writes during pre-execution system call | Block with 2 normal user transactions: Alice sends 10 wei to Charlie, Bob sends 10 wei to Charlie. At block start (pre-execution), `SYSTEM_ADDRESS` calls `BEACON_ROOTS_ADDRESS` to store parent beacon root | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with two `storage_changes` (timestamp slot and beacon root slot); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. At `block_access_index=1`: Alice with `nonce_changes`, Charlie with `balance_changes` (10 wei). At `block_access_index=2`: Bob with `nonce_changes`, Charlie with `balance_changes` (20 wei total). | ✅ Completed |
| `test_bal_4788_empty_block` | Ensure BAL captures beacon root storage writes in empty block | Block with no transactions. At block start (pre-execution), `SYSTEM_ADDRESS` calls `BEACON_ROOTS_ADDRESS` to store parent beacon root | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with two `storage_changes` (timestamp slot and beacon root slot); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. No transaction-related BAL entries. | ✅ Completed |
| `test_bal_4788_query` | Ensure BAL captures storage reads when querying beacon root (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 stores beacon root at timestamp 12. Block 2 queries with three timestamp scenarios (valid=12, invalid non-zero=42, invalid zero=0) and value (0 or 100 wei). Valid query (timestamp=12): reads both timestamp and root slots, writes returned value. If value > 0, beacon root contract receives balance. Invalid query with non-zero timestamp (timestamp=42): reads only timestamp slot before reverting, query contract has implicit SLOAD recorded (SSTORE reverts), no value transferred. Invalid query with zero timestamp (timestamp=0): reverts immediately without any storage access, query contract has implicit SLOAD recorded, no value transferred. | Block 1 BAL: System call writes. Block 2 BAL **MUST** include at `block_access_index=0`: System call writes for block 2. Valid case (timestamp=12) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with `storage_reads` [timestamp_slot, root_slot] and `balance_changes` if value > 0, query contract with `storage_changes`. Invalid non-zero case (timestamp=42) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with `storage_reads` [timestamp_slot only] and NO `balance_changes` (reverted), query contract with `storage_reads` [0] and NO `storage_changes`. Invalid zero case (timestamp=0) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, query contract with `storage_reads` [0] and NO `storage_changes`. | ✅ Completed |
Expand Down
7 changes: 7 additions & 0 deletions tests/byzantium/eip214_staticcall/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Test cases for EIP-214: STATICCALL opcode.

EIP-214 introduced the STATICCALL opcode which creates a read-only call
context. Any state-modifying operations (including CALL with non-zero
value) within a STATICCALL context will cause the call to fail.
"""
Loading
Loading