Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 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
1ff5dcf
Merge remote-tracking branch 'origin/forks/monad_nine' into eips/mona…
pdobacz Aug 31, 2026
12fa2f6
docs(skills): require a new fork to arrive as a replica of its parent
pdobacz Aug 31, 2026
44deaed
feat(tests): run the EIP-7928 state visibility tests on monad forks
pdobacz Aug 31, 2026
a793d6a
test(monad): exclude type 3 transactions by fork, not by skip
pdobacz Sep 4, 2026
404411d
feat(monad): reject blob transactions on the Monad forks
pdobacz Sep 4, 2026
41ed8e3
Merge remote-tracking branch 'origin/forks/monad_nine' into eips/mona…
pdobacz Oct 1, 2026
2153560
test(eip7981): pin the receipt to the gas limit on Monad
pdobacz Sep 22, 2026
c6f42a4
test(eip7981): size the gas regimes from the fork
pdobacz Sep 22, 2026
d20bef1
test(eip7954): pin the receipts to Monad's gas billing
pdobacz Sep 30, 2026
6f373d6
Merge from-upstream into eips/monad_next/eip-7708+7843+7981+7997+8024…
pdobacz Oct 13, 2026
7e22d08
chore(forks,tests): resolve the from-upstream merge conflicts
pdobacz Oct 14, 2026
e59c776
test(amsterdam): pin the new EIP-7708 and EIP-8246 cases to Monad's r…
pdobacz Oct 14, 2026
f05a76b
refactor(forks,ci): rename `monad_next` to `monad_eleven`
pdobacz Oct 14, 2026
022ff94
refactor(tests): rename `tests/monad_amsterdam` to `tests/monad_eleven`
pdobacz Oct 14, 2026
b647296
fix(monad): ban creates by the frame's recipient, not the code address
pdobacz Oct 14, 2026
a9ae470
test(monad): cover the create ban across the call shapes
pdobacz Oct 14, 2026
76e53a3
Merge remote-tracking branch 'origin/forks/monad_nine' into eips/mona…
pdobacz Oct 14, 2026
cdc38ca
refactor(monad_eleven): keep JUMPDEST analysis unchanged under EIP-8024
pdobacz Oct 14, 2026
c77f6d7
Merge remote-tracking branch 'origin/forks/monad_nine' into eips/mona…
pdobacz Oct 19, 2026
9b3982e
chore(skills): drop the merge skill's copy at its old path
pdobacz Oct 19, 2026
3c5a096
test(monad_eleven): expect the delegated factory transaction to fail
pdobacz Oct 19, 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
4 changes: 2 additions & 2 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
monad:
evm-type: eels
fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=143 -k "not invalid_header"
fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_ELEVEN --chain-id=143 -k "not invalid_header"

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"
fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_ELEVEN --chain-id=30143 --monad-runloop -k "not invalid_header"

monad_benchmark:
evm-type: eels
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/generate_build_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"MONAD_EIGHT",
"MONAD_NINE",
"MONAD_TEN",
"MONAD_NEXT",
"MONAD_ELEVEN",
]

FORK_INDEX = {name: i for i, name in enumerate(FORK_ORDER)}
Expand Down
2 changes: 1 addition & 1 deletion monad-runloop/rust-harness/eest-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ fn main() {
}

// The canonical state root is the page-encoded secondary timeline's
// from MONAD_NEXT (MIP-8, revision 10) on, the slot-encoded
// from MONAD_ELEVEN (MIP-8, revision 10) on, the slot-encoded
// primary's before. The schedule is ascending, so the last entry is
// the revision the run ends in.
let final_revision = input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,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 @@ -1275,7 +1275,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 @@ -50,10 +50,10 @@
"MONAD_EIGHT": [(8, 0)],
"MONAD_NINE": [(9, 0)],
"MONAD_TEN": [(10, 0)],
"MONAD_NEXT": [(11, 0)],
"MONAD_ELEVEN": [(11, 0)],
"MONAD_EIGHTToMONAD_NINEAtTime15k": [(8, 0), (9, 15_000)],
"MONAD_NINEToMONAD_TENAtTime15k": [(9, 0), (10, 15_000)],
"MONAD_TENToMONAD_NEXTAtTime15k": [(10, 0), (11, 15_000)],
"MONAD_TENToMONAD_ELEVENAtTime15k": [(10, 0), (11, 15_000)],
}


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 @@ -397,7 +397,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
8 changes: 4 additions & 4 deletions packages/testing/src/execution_testing/forks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
BPO4,
BPO5,
MONAD_EIGHT,
MONAD_NEXT,
MONAD_ELEVEN,
MONAD_NINE,
MONAD_TEN,
Amsterdam,
Expand Down Expand Up @@ -52,7 +52,7 @@
CancunToPragueAtTime15k,
MONAD_EIGHTToMONAD_NINEAtTime15k,
MONAD_NINEToMONAD_TENAtTime15k,
MONAD_TENToMONAD_NEXTAtTime15k,
MONAD_TENToMONAD_ELEVENAtTime15k,
OsakaToBPO1AtTime15k,
ParisToShanghaiAtTime15k,
PragueToOsakaAtTime15k,
Expand Down Expand Up @@ -161,8 +161,8 @@
"MONAD_NINE",
"MONAD_NINEToMONAD_TENAtTime15k",
"MONAD_TEN",
"MONAD_TENToMONAD_NEXTAtTime15k",
"MONAD_NEXT",
"MONAD_TENToMONAD_ELEVENAtTime15k",
"MONAD_ELEVEN",
"BPO1",
"BPO1ToBPO2AtTime15k",
"BPO2",
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 @@ -335,12 +335,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 @@ -565,6 +583,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 @@ -1469,6 +1498,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 @@ -1838,12 +1838,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 @@ -1947,3 +1941,37 @@ class Bogota(
"""Bogota fork."""

pass


class MONAD_ELEVEN( # noqa: N801
eips.EIP7708,
eips.EIP7843,
eips.EIP7981,
eips.EIP7997,
eips.EIP8024,
eips.EIP8246,
MONAD_TEN,
deployed=False,
):
"""
MONAD_ELEVEN 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_ELEVEN
after Amsterdam through `follows`.
"""

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

@classmethod
def header_bal_hash_required(cls) -> bool:
"""
MONAD_ELEVEN headers carry the block access list hash field, fixed
at zero, without building block access lists (EIP-7928).
"""
return True
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
BPO3,
BPO4,
MONAD_EIGHT,
MONAD_NEXT,
MONAD_ELEVEN,
MONAD_NINE,
MONAD_TEN,
Amsterdam,
Expand Down Expand Up @@ -74,9 +74,11 @@ class MONAD_NINEToMONAD_TENAtTime15k(TransitionBaseClass): # noqa: N801
pass


@transition_fork(to_fork=MONAD_NEXT, from_fork=MONAD_TEN, at_timestamp=15_000)
class MONAD_TENToMONAD_NEXTAtTime15k(TransitionBaseClass): # noqa: N801
"""MONAD_TEN to MONAD_NEXT transition at Timestamp 15k."""
@transition_fork(
to_fork=MONAD_ELEVEN, from_fork=MONAD_TEN, at_timestamp=15_000
)
class MONAD_TENToMONAD_ELEVENAtTime15k(TransitionBaseClass): # noqa: N801
"""MONAD_TEN to MONAD_ELEVEN transition at Timestamp 15k."""

pass

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from execution_testing.vm import Opcodes

from ..base_fork import BaseFork, BaseForkMeta, SystemCallPhase
from ..forks.eips import EIP2780, EIP7708, EIP8024
from ..forks.eips.paris.eip_3675 import EIP3675
from ..forks.forks import (
BPO1,
Expand All @@ -18,6 +19,8 @@
BPO4,
BPO5,
MONAD_EIGHT,
MONAD_ELEVEN,
MONAD_TEN,
Amsterdam,
Berlin,
Bogota,
Expand Down Expand Up @@ -304,6 +307,25 @@ def test_fork_comparison() -> None:
assert fork == Berlin


def test_succession_does_not_imply_eip_membership() -> None:
"""
A fork placed after another by `follows` does not gain its EIPs.

Succession orders MONAD_ELEVEN after Amsterdam so the Amsterdam
suites can select it, while the EIPs it declines must still compare
as absent. An adopted EIP is reached through inheritance instead, so
it keeps comparing as present.
"""
assert MONAD_ELEVEN > Amsterdam
assert MONAD_ELEVEN > MONAD_TEN

assert MONAD_ELEVEN >= EIP7708
assert MONAD_ELEVEN.is_eip_enabled(7708)

assert not MONAD_ELEVEN >= EIP2780
assert not MONAD_ELEVEN.is_eip_enabled(2780)


def test_transition_fork_comparison() -> None:
"""
Test comparing to a transition fork.
Expand Down Expand Up @@ -390,6 +412,39 @@ class PreAllocTransitionFork(TransitionBaseClass):
pass


def test_monad_eleven_is_not_deployed() -> None:
"""
A fork under development must stay out of the deployed fork range.
"""
deployed_forks = get_deployed_forks()
assert MONAD_TEN in deployed_forks
assert MONAD_ELEVEN not in deployed_forks


def opcode_names(fork: Type[BaseFork]) -> set[str]:
"""Return the names of the opcodes a fork declares valid."""
return {str(opcode) for opcode in fork.valid_opcodes()}


def test_adopted_opcodes_depend_on_mixin_order() -> None:
"""
The adopted opcodes reach MONAD_ELEVEN through its mixin order.

MONAD_NINE returning Osaka's list directly is what ends the chain
there, which is what lets a mixin placed ahead of the Monad forks
contribute on the way out. Making that call cooperative, or
reordering MONAD_ELEVEN's bases, drops these opcodes silently.
"""
adopted = {"DUPN", "SWAPN", "EXCHANGE"}
assert adopted <= opcode_names(MONAD_ELEVEN)
assert not adopted & opcode_names(MONAD_TEN)

class MixinAfterFork(MONAD_TEN, EIP8024): # noqa: N801
"""Dummy fork ordering an EIP mixin after the Monad fork."""

assert not adopted & opcode_names(MixinAfterFork)


def test_pre_alloc() -> None: # noqa: D103
assert PrePreAllocFork.pre_allocation() == {"test": "test"}
assert PreAllocFork.pre_allocation() == {"test": "test", "test2": "test2"}
Expand Down
13 changes: 12 additions & 1 deletion packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,16 @@ def generate_block_data(
int(env.slot_number) if env.slot_number is not None else 0
)

# Prepare block_access_list_hash for header initialization
bal_hash_field: Dict[str, Any] = {}
if fork.header_bal_hash_required() and (
not fork.supports_block_access_lists()
):
# Fork requires the block access list hash header field but
# doesn't build block access lists (e.g. Monad): fix value at
# zero.
bal_hash_field["block_access_list_hash"] = Hash(0)

header = FixtureHeader(
**(
transition_tool_output.result.model_dump(
Expand All @@ -1105,6 +1115,7 @@ def generate_block_data(
exclude_none=True,
exclude={"blob_gas_used", "slot_number"},
)
| bal_hash_field
),
blob_gas_used=blob_gas_used,
transactions_trie=Transaction.list_root(txs),
Expand Down Expand Up @@ -1168,7 +1179,7 @@ def generate_block_data(
if t8n_bal_rlp is not None:
t8n_bal = BlockAccessList.from_rlp(t8n_bal_rlp)

if fork.header_bal_hash_required():
if fork.supports_block_access_lists():
assert t8n_bal is not None, (
"Block access list is required for this block but was not "
"provided by the transition tool"
Expand Down
Loading
Loading