Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,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_amsterdam:
evm-type: eels
fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header"
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
41 changes: 38 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,25 @@ 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.
"""
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
# trait is reached through a cast, as elsewhere in this class.
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 +576,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 +1431,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
35 changes: 29 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,32 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool:
limit.
"""
return True


class MONAD_NEXT( # noqa: N801
eips.EIP7708,
eips.EIP7843,
eips.EIP8024,
MONAD_TEN,
):
"""
MONAD_NEXT fork.

Amsterdam-based successor to MONAD_TEN, adopting the EIP-7708,
EIP-7843 and EIP-8024 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
28 changes: 17 additions & 11 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,17 +986,23 @@ def generate_block_data(
int(env.slot_number) if env.slot_number is not None else 0
)

header_fields = transition_tool_output.result.model_dump(
exclude_none=True,
exclude={"blob_gas_used", "transactions_trie"},
) | env.model_dump(
exclude_none=True,
exclude={"blob_gas_used", "slot_number"},
)
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.
header_fields.setdefault("block_access_list_hash", Hash(0))

header = FixtureHeader(
**(
transition_tool_output.result.model_dump(
exclude_none=True,
exclude={"blob_gas_used", "transactions_trie"},
)
| env.model_dump(
exclude_none=True,
exclude={"blob_gas_used", "slot_number"},
)
),
**header_fields,
blob_gas_used=blob_gas_used,
transactions_trie=Transaction.list_root(txs),
extra_data=(
Expand Down Expand Up @@ -1059,7 +1065,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
8 changes: 6 additions & 2 deletions src/ethereum/forks/monad_next/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""
MONAD_NEXT fork is a placeholder for upcoming Monad changes and is
currently identical to MONAD_TEN.
MONAD_NEXT fork is a placeholder for upcoming Monad changes. It builds on
MONAD_TEN, adopting EIP-7708, EIP-7843 and EIP-8024 from Amsterdam
together with the Amsterdam block header layout; the [EIP-7928] block
access list hash header slot is carried but always zero.

[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
"""

from ethereum.fork_criteria import ByTimestamp, ForkCriteria
Expand Down
18 changes: 18 additions & 0 deletions src/ethereum/forks/monad_next/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ class Header:
[SHA2-256]: https://en.wikipedia.org/wiki/SHA-2
"""

block_access_list_hash: Hash32
"""
Header slot introduced by [EIP-7928] for the hash of the Block Access
List. Monad does not build block access lists, so this field is always
zero. See [`validate_header`][vh].

[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
[vh]: ref:ethereum.forks.monad_next.fork.validate_header
"""

slot_number: U64
"""
The slot number of this block as provided by the consensus layer.
Introduced in [EIP-7843].

[EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843
"""


@final
@slotted_freezable
Expand Down
32 changes: 28 additions & 4 deletions src/ethereum/forks/monad_next/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from ethereum.state import EMPTY_CODE_HASH, Address
from ethereum.state_paged import State, apply_changes_to_state
from ethereum.utils.byte import left_pad_zero_bytes

from . import vm
from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt
Expand Down Expand Up @@ -246,6 +247,7 @@ def state_transition(chain: BlockChain, block: Block) -> None:
prev_randao=block.header.prev_randao,
excess_blob_gas=block.header.excess_blob_gas,
parent_beacon_block_root=block.header.parent_beacon_block_root,
slot_number=block.header.slot_number,
)

block_output = apply_body(
Expand Down Expand Up @@ -402,6 +404,8 @@ def validate_header(chain: BlockChain, header: Header) -> None:
raise InvalidBlock
if header.ommers_hash != EMPTY_OMMER_HASH:
raise InvalidBlock
if header.block_access_list_hash != Hash32(b"\x00" * 32):
raise InvalidBlock

block_parent_hash = keccak256(rlp.encode(parent_header))
if header.parent_hash != block_parent_hash:
Expand Down Expand Up @@ -973,15 +977,32 @@ def process_transaction(
# transfer miner fees
create_ether(tx_state, block_env.coinbase, U256(transaction_fee))

for address in tx_output.accounts_to_delete:
destroy_account(tx_state, address)
# EIP-7708: Emit burn logs for balances held by accounts marked for
# deletion AFTER miner fee transfer.
finalization_logs: List[Log] = []
for address in sorted(tx_output.accounts_to_delete):
balance = get_account(tx_state, address).balance
if balance > U256(0):
padded_address = left_pad_zero_bytes(address, 32)
finalization_logs.append(
Log(
address=vm.SYSTEM_ADDRESS,
topics=(
vm.BURN_TOPIC,
Hash32(padded_address),
),
data=balance.to_be_bytes32(),
)
)

all_logs = tx_output.logs + tuple(finalization_logs)

# block_output.block_gas_used += tx_gas_used_after_refund
block_output.block_gas_used += tx.gas
block_output.blob_gas_used += tx_blob_gas_used

receipt = make_receipt(
tx, tx_output.error, block_output.block_gas_used, tx_output.logs
tx, tx_output.error, block_output.block_gas_used, all_logs
)

receipt_key = rlp.encode(Uint(index))
Expand All @@ -993,7 +1014,10 @@ def process_transaction(
receipt,
)

block_output.block_logs += tx_output.logs
block_output.block_logs += all_logs

for address in tx_output.accounts_to_delete:
destroy_account(tx_state, address)

incorporate_tx_into_block(tx_state)

Expand Down
Loading