Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b01952b
feat(monad_next): implement EIP-7708 ETH transfer/burn logs
pdobacz Jun 25, 2026
57b3fbc
feat(monad_next): implement EIP-7843 SLOTNUM opcode
pdobacz Jun 25, 2026
24e4bb8
feat(monad_next): implement EIP-8024 SWAPN, DUPN, EXCHANGE
pdobacz Jun 25, 2026
fe8abda
feat(forks): make MONAD_NEXT an Amsterdam fork inheriting 3 EIPs
pdobacz Jul 27, 2026
d4119a4
feat(ci): add monad_amsterdam fixture release config
pdobacz Jun 26, 2026
1466f6a
feat(ci): fill whole test suite for monad_amsterdam release
pdobacz Jul 27, 2026
a3d7fd8
feat(forks): carry zero EIP-7928 BAL hash slot in MONAD_NEXT headers
pdobacz Jul 30, 2026
8796fdd
refactor(forks): adopt Amsterdam EIPs by mixin, order by succession
pdobacz Aug 19, 2026
efe211d
test(amsterdam): keep the unadopted Amsterdam suites off the Monad forks
pdobacz Aug 19, 2026
e85abea
feat(monad_next): adopt EIP-8246 remove SELFDESTRUCT balance burn
pdobacz Aug 19, 2026
08ac088
test(selfdestruct): expect the Burn logs a fork without EIP-8246 emits
pdobacz Aug 19, 2026
ea4b01d
feat(monad_next): adopt EIP-7981 increase access list cost
pdobacz Aug 19, 2026
d90d9f4
feat(monad_next): adopt EIP-7997 deterministic factory predeploy
pdobacz Aug 19, 2026
bc45cc4
refactor(forks): declare fork succession as a trait, not a keyword
pdobacz Aug 25, 2026
4909497
refactor(forks): declare fork succession as a trait, not a keyword
pdobacz Aug 25, 2026
1faef38
refactor(forks): declare fork succession as a trait, not a keyword
pdobacz Aug 25, 2026
7916fb7
refactor(forks): declare fork succession as a trait, not a keyword
pdobacz Aug 25, 2026
cfa37f3
refactor(forks): derive block access list support from the EIP
pdobacz Aug 25, 2026
1a09d24
feat(ci): rehearse the monad fixture releases before they are cut
pdobacz Aug 25, 2026
16ff838
Merge branch 'monamsterdam' into merged
pdobacz Aug 27, 2026
8551370
Merge branch 'eips/monad_next/eip-7981' into merged
pdobacz Aug 27, 2026
193e5b8
Merge branch 'eips/monad_next/eip-7997' into merged
pdobacz Aug 27, 2026
e6e87da
Merge branch 'eips/monad_next/eip-8246' into merged
pdobacz Aug 27, 2026
74a642d
test(amsterdam): select the suites for the EIPs MONAD_NEXT now adopts
pdobacz Aug 27, 2026
d0a9612
test(selfdestruct): expect no Burn log now that EIP-8246 joins EIP-7708
pdobacz Aug 27, 2026
b39d290
feat(ci): release the merged MONAD_NEXT fixtures under the EIP bundle
pdobacz Aug 27, 2026
2ba48dd
docs(skills): add the EIP branch merge skill
pdobacz Aug 27, 2026
02880af
test(eip7997): select the transition tests by EIP, not by Amsterdam
pdobacz Aug 27, 2026
a1f2552
test(benchmark): drop the inert EIP-7928 exclusions
pdobacz Aug 27, 2026
2eaeb1d
refactor(specs): append the fixed block access list hash field
pdobacz Aug 27, 2026
b89a7f5
test(monad_next): pin the Amsterdam EIP interactions with Monad features
pdobacz Aug 28, 2026
6d0cadc
fix(ci): rehearse a fixture release once per push
pdobacz Aug 28, 2026
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
150 changes: 150 additions & 0 deletions .claude/commands/merge-eip-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Merge EIP Adoption Branches

Consolidate several single-EIP adoption branches into one fork that adopts
all of them. Run this skill before starting such a merge. Each source
branch was built by `/adopt-upstream-eip`, alone, against its own upstream
EIP branch, so no source branch knows what the combined fork does.

## Inputs

- The source branches, and `<MONAD_FORK>` they all target.
- `<MONAD_BRANCH>`, the branch they merge into. Example: `forks/monad_nine`.
- `<ETH_FORK>` and its upstream integration branch, which already has every
EIP composed. Example: `upstream/forks/amsterdam`.

## Read First

`/adopt-upstream-eip` defines the fork shape, the shared-file cost rules,
the step-12 cleanup list and the definition of done. This skill only covers
what the merge adds.

## 1. Prepare

```bash
git worktree add -b <merge_branch> <path> origin/<MONAD_BRANCH>
git branch --unset-upstream <merge_branch>
```

Merge into a fresh worktree, and never check out or rewrite a source
branch. Note which commits already landed on `<MONAD_BRANCH>` after the
source branches were cut: a branch carrying its own copy of one of them
conflicts, and `<MONAD_BRANCH>`'s version wins.

```bash
git log --oneline $(git merge-base origin/<MONAD_BRANCH> <branch>)..origin/<MONAD_BRANCH>
```

## 2. Merge

Merge the largest branch first, then the rest, with real merge commits.
Squashing or cherry-picking hides which branch each conflict came from.

```bash
git merge <branch> # once per source branch
```

## 3. Resolve the Conflicts Git Reports

Expect these, in rising order of thought needed:

- **`.github/configs/feature.yaml`** — every branch appends its own entry
at the same point. Keep all entries for now; step 5 collapses them.
- **`forks.py`, the `<MONAD_FORK>` declaration** — each branch declares the
class with one mixin. The merged class lists every mixin ahead of the
parent fork, in ascending EIP order. Drop any dead `pass` left after
`follows()`.
- **CI files already on `<MONAD_BRANCH>`** — keep the base branch's version.
- **`src/ethereum/forks/<monad_fork>/`** — two EIPs changing the same file.
Resolve against the upstream integration branch, not against either
branch alone.

What must merge silently: the `follows()` trait, the
`BaseForkMeta._is_subclass_of` walk, the `ValidAtTransitionTo` guard and the
exclusion conftests. They are byte-identical across branches by design.
Check afterwards that one copy of each survived.

## 4. Find the Silent Merges

This is the main risk. Two branches editing the same file at different
lines merge cleanly and produce behavior neither branch had. Git reports
nothing.

Find the candidates from the upstream spec changes:

```bash
for n in <eips>; do
base=$(git merge-base <upstream_integration> upstream/eips/<eth_fork>/eip-$n)
echo "### EIP-$n"
git diff --stat $base..upstream/eips/<eth_fork>/eip-$n -- src/ethereum/forks/<eth_fork>
done
```

Any file listed under two EIPs needs reading in full on the merge branch.
Where one EIP removes what another adds, the merged file must match the
upstream integration branch, which already has both.

Two shapes seen in practice:

- One EIP adds a log for a state change a later EIP removes. Git keeps
both: a log for something that no longer happens.
- Two branches add the same conftest hook at different offsets, one after
the imports and one appended. Git keeps both hook definitions in one
module.

Also read every file the merge touched outside `src/ethereum/forks/monad*`
and `tests/monad*`, whether or not git flagged it.

## 5. Clean Up the Scaffolding

Follow `/adopt-upstream-eip` step 12, driven by what the merged fork now
adopts:

- Delete the exclusion conftest for each adopted EIP's suite. Those suites
must now be selected and pass.
- Collapse the per-branch feature entries into one. When the branch name
follows `eips/<fork>/eip-<n>+<n>`, the feature name must carry the same
numbers in the same order: `check_release_matrix.py` matches the sequence
as a literal string. Verify it:

```bash
python3 .github/scripts/check_release_matrix.py "" <merge_branch_name>
```

- Revert expectation scaffolding written for an intermediate fork shape. A
test edit that only made a fork with EIP-A but without EIP-B green is
scaffolding once B joins.

To test whether an edit was scaffolding, revert it and diff against the
base branch:

```bash
git revert --no-commit <sha> && git diff origin/<MONAD_BRANCH> -- <file>
```

An empty diff means the edit existed only for the intermediate shape.
Reverting cleanly needs the source branch to have kept scaffolding in its
own commit; a commit mixing scaffolding with permanent change has to be
undone by hand.

Keep gates on a capability (`fork.is_eip_enabled(<n>)`, `fork >=
MONAD_EIGHT`) that stay true on the merged fork. They are permanent.

## 6. Validate

Run `/adopt-upstream-eip`'s step-3 shape check, its step-5 `--collect-only`
selection check, then the full-suite fill for the fork. The full suite
matters: the merge changes which suites select the fork.

Measure any exclusion that survived the cleanup, one suite at a time, by
stripping its hook and filling that suite. State which ones you did not get
to measure.

## Definition of Done

- Every source branch merged, with the merge commits kept.
- Each file two EIPs touched read in full and matched against the upstream
integration branch.
- One copy of each shared addition; no duplicated hook or trait.
- Adopted suites selected and passing; their exclusions deleted.
- One feature entry, its name matching the branch's EIP sequence.
- Full-suite fill green, with any unmeasured exclusion named.
4 changes: 4 additions & 0 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ monad_runloop:
evm-type: eels
# Like `monad`, but `--monad-runloop` and eestnet chain id `30143`
fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header"

monad_eip7708+7843+7981+7997+8024+8246:
evm-type: eels
fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header"
21 changes: 5 additions & 16 deletions .github/workflows/check_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ run-name: ${{ github.event_name == 'workflow_dispatch' && format('Check Fixture
# breaks surfaces on the branch that broke it rather than on the next
# release attempt.
#
# Two tiers, both running on every pull request and on pushes to the
# branches that release in parallel with the fork branch,
# `from-upstream` and `eips/**`.
# Two tiers, both running on pushes to the branches that release in
# parallel with the fork branch, `from-upstream` and `eips/**`. A pull
# request adds no trigger of its own: its pushes already run here, and
# an event per push and per synchronize rehearsed each push twice.
#
# `collect` is cheap: it reads the features the branch releases out of
# `.github/configs/feature.yaml`, builds the release's job matrix
Expand All @@ -32,15 +33,6 @@ on:
- "whitelist.txt"
- "docs/**"
- "mkdocs.yml"
pull_request:
paths-ignore:
- "**.md"
- "LICENSE*"
- ".gitignore"
- ".vscode/**"
- "whitelist.txt"
- "docs/**"
- "mkdocs.yml"
workflow_dispatch:
inputs:
depth:
Expand Down Expand Up @@ -79,10 +71,7 @@ jobs:
shell: bash
env:
INPUT_FEATURES: ${{ inputs.features }}
# The head branch, so a pull request from an EIP branch
# rehearses that branch's feature; `ref_name` is the merge ref
# on a pull request and the branch everywhere else.
BRANCH: ${{ github.head_ref || github.ref_name }}
BRANCH: ${{ github.ref_name }}
run: |
# The feature selection and the per-feature release matrix live
# in (and are shared with the release workflow via)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ def build_genesis_header(
requests_hash=Requests()
if genesis_fork.header_requests_required()
else None,
block_access_list_hash=BlockAccessList().rlp_hash
block_access_list_hash=(
BlockAccessList().rlp_hash
if genesis_fork.supports_block_access_lists()
else Hash(0)
)
if genesis_fork.header_bal_hash_required()
else None,
slot_number=0 if genesis_fork.header_slot_number_required() else None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,9 @@ def _process_with_marker_args(
"Missing fork argument with 'valid_at_transition_to' marker."
)

if len(forks) > 1:
# A single EIP argument expands to one fork per enabling fork, so
# the limit is on the arguments rather than on the resolved forks.
if len(fork_args) > 1:
raise Exception(
"Too many forks specified to 'valid_at_transition_to' marker."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def _resolve_excess_blob_gas(
}
if fork.has_compute_requests_hash:
arguments["requests_hash"] = Hash32(b"\0" * 32)
if fork.has_hash_block_access_list:
if fork.has_block_access_list_hash_header:
arguments["block_access_list_hash"] = Hash32(b"\0" * 32)
if fork.has_slot_number:
arguments["slot_number"] = U64(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,13 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self:
if fork.header_requests_required():
extras["requests_hash"] = Requests()
if fork.header_bal_hash_required():
extras["block_access_list_hash"] = BlockAccessList().rlp_hash
# A fork can require the header field without building block
# access lists (e.g. Monad); the field is then fixed at zero.
extras["block_access_list_hash"] = (
BlockAccessList().rlp_hash
if fork.supports_block_access_lists()
else Hash(0)
)
if fork.header_slot_number_required():
extras["slot_number"] = (
int(env.slot_number) if env.slot_number is not None else 0
Expand Down
46 changes: 43 additions & 3 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,30 @@ def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta":
@staticmethod
def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool:
"""
Check if `a` is a subclass of `b`, taking fork transitions into
account.
Check if `a` is a subclass of `b`, taking fork transitions and
declared succession into account.

A fork can follow another fork it does not inherit from, which
places it after that fork (and after everything that fork comes
after) in the fork order without adopting its behavior. The
EIPs of a followed fork stay out of that ordering.
"""
a = BaseForkMeta._maybe_transitioned(a)
b = BaseForkMeta._maybe_transitioned(b)
return issubclass(a, b)
if issubclass(a, b):
return True
# The metaclass sees its instances as plain classes, so the
# traits are reached through a cast, as elsewhere in this class.
# Succession places a fork after another fork, not after that
# fork's EIPs; an adopted EIP is reached by inheritance above.
if cast(Type["BaseFork"], b).is_eip():
return False
followed = cast(Type["BaseFork"], a).follows()
while followed is not None:
if issubclass(followed, b):
return True
followed = followed.follows()
return False

def __gt__(cls, other: "BaseForkMeta") -> bool:
"""Compare if a fork is newer than some other fork (cls > other)."""
Expand Down Expand Up @@ -563,6 +581,17 @@ def header_bal_hash_required(cls) -> bool:
"""Return true if the header must contain block access list hash."""
pass

@classmethod
def supports_block_access_lists(cls) -> bool:
"""
Return true if the fork builds block access lists (EIP-7928).

A fork can require the block access list hash header field
without building the lists, and then fixes the field at zero, so
this follows the EIP rather than the header requirement.
"""
return cls.is_eip_enabled(7928)

@classmethod
@abstractmethod
def empty_block_bal_item_count(cls) -> int:
Expand Down Expand Up @@ -1407,6 +1436,17 @@ def enabling_forks(cls) -> Set[Type["BaseFork"]]:
raise Exception(f"Class {cls.__name__} is not an EIP.")
return cls._enabling_forks

@classmethod
def follows(cls) -> Type["BaseFork"] | None:
"""
Return the fork this one comes after without inheriting it.

A fork that reuses another lineage's ordering overrides this;
comparisons then place it after that fork, and after everything
that fork comes after, while its behavior stays its own.
"""
return None

@classmethod
def parent(cls) -> Type["BaseFork"] | None:
"""Return the parent fork."""
Expand Down
40 changes: 34 additions & 6 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,12 +1805,6 @@ def _calculate_sstore_gas_mip8(
return gas_cost


class MONAD_NEXT(MONAD_TEN): # noqa: N801
"""MONAD_NEXT fork, a placeholder identical to MONAD_TEN."""

pass


class BPO1(
Osaka,
bpo_fork=True,
Expand Down Expand Up @@ -1904,3 +1898,37 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool:
limit.
"""
return True


class MONAD_NEXT( # noqa: N801
eips.EIP7708,
eips.EIP7843,
eips.EIP7981,
eips.EIP7997,
eips.EIP8024,
eips.EIP8246,
MONAD_TEN,
deployed=False,
):
"""
MONAD_NEXT fork.

Amsterdam-based successor to MONAD_TEN, adopting the EIP-7708,
EIP-7843, EIP-7981, EIP-7997, EIP-8024 and EIP-8246 changes. The
Amsterdam changes it does not adopt stay out of the fork by not
being inherited at all; the fork order still places MONAD_NEXT
after Amsterdam through `follows`.
"""

@classmethod
def follows(cls) -> type[BaseFork] | None:
"""MONAD_NEXT comes after Amsterdam without inheriting it."""
return Amsterdam

@classmethod
def header_bal_hash_required(cls) -> bool:
"""
MONAD_NEXT headers carry the block access list hash field, fixed
at zero, without building block access lists (EIP-7928).
"""
return True
Loading