Skip to content
Draft
4 changes: 4 additions & 0 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ monad_runloop:
evm-type: eels
# Like `monad`, but `--monad-runloop` and eestnet chain id `30143`
fill-params: --suppress-no-test-exit-code -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: --suppress-no-test-exit-code -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 @@ -376,7 +376,11 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self:
if fork.header_requests_required()
else None,
"block_access_list_hash": (
BlockAccessList().rlp_hash
(
BlockAccessList().rlp_hash
if fork.supports_block_access_lists()
else Hash(0)
)
if fork.header_bal_hash_required()
else None
),
Expand Down
12 changes: 12 additions & 0 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,18 @@ def header_bal_hash_required(cls) -> bool:
"""Return true if the header must contain block access list hash."""
pass

@classmethod
@abstractmethod
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 block access lists (e.g. Monad); the field is then fixed
at zero.
"""
pass

@classmethod
@abstractmethod
def empty_block_bal_item_count(cls) -> int:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def header_bal_hash_required(cls) -> bool:
"""
return True

@classmethod
def supports_block_access_lists(cls) -> bool:
"""
From EIP-7928, blocks build block access lists.
"""
return True

@classmethod
def gas_costs(cls) -> GasCosts:
"""
Expand Down
79 changes: 73 additions & 6 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,11 @@ def header_bal_hash_required(cls) -> bool:
"""At genesis, header must not contain block access list hash."""
return False

@classmethod
def supports_block_access_lists(cls) -> bool:
"""At genesis, no block access lists are built."""
return False

@classmethod
def empty_block_bal_item_count(cls) -> int:
"""Pre-Amsterdam forks have no block access list."""
Expand Down Expand Up @@ -1754,12 +1759,6 @@ def _calculate_sstore_gas_mip8(
return gas_cost


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

pass


class BPO1(
Osaka,
bpo_fork=True,
Expand Down Expand Up @@ -1847,3 +1846,71 @@ class Amsterdam(
# live on mainnet.

pass


class MONAD_NEXT(MONAD_TEN, Amsterdam, solc_name="cancun"): # noqa: N801
"""
MONAD_NEXT fork.

Amsterdam-based successor to MONAD_TEN. Only the EIP-7708, EIP-7843
and EIP-8024 changes are inherited from Amsterdam; every other
Amsterdam change is pinned back to the MONAD_TEN parent. The
EIP-7928 block access list hash header field is carried, but no
block access lists are built, so the field is always zero.
"""

@classmethod
def valid_opcodes(cls) -> List[Opcodes]:
"""
Inherit the Amsterdam opcode set: SLOTNUM (EIP-7843) and SWAPN,
DUPN, EXCHANGE (EIP-8024) on top of the MONAD_TEN opcodes.
"""
return Amsterdam.valid_opcodes()

@classmethod
def max_code_size(cls) -> int:
"""Return spec from explicit parent (skip EIP-7954)."""
return MONAD_TEN.max_code_size()

@classmethod
def calldata_gas_calculator(cls) -> CalldataGasCalculator:
"""Return spec from explicit parent (skip EIP-7976)."""
return MONAD_TEN.calldata_gas_calculator()

@classmethod
def transaction_data_floor_cost_calculator(
cls,
) -> TransactionDataFloorCostCalculator:
"""Return spec from explicit parent (skip EIP-7981)."""
return MONAD_TEN.transaction_data_floor_cost_calculator()

@classmethod
def transaction_intrinsic_cost_calculator(
cls,
) -> TransactionIntrinsicCostCalculator:
"""Return spec from explicit parent (skip EIP-7981)."""
return MONAD_TEN.transaction_intrinsic_cost_calculator()

@classmethod
def supports_block_access_lists(cls) -> bool:
"""Return spec from explicit parent (skip EIP-7928)."""
return MONAD_TEN.supports_block_access_lists()

@classmethod
def empty_block_bal_item_count(cls) -> int:
"""Return spec from explicit parent (skip EIP-7928)."""
return MONAD_TEN.empty_block_bal_item_count()

@classmethod
def engine_execution_payload_block_access_list(cls) -> bool:
"""Return spec from explicit parent (skip EIP-7928)."""
return MONAD_TEN.engine_execution_payload_block_access_list()


# MONAD_NEXT adopts EIP-7708, EIP-7843 and EIP-8024 from Amsterdam through the
# MRO rather than by inheriting their mixin classes: inheriting them would
# register MONAD_NEXT as a spurious `enabling_fork` (breaking
# `valid_at_transition_to`) and pull in EIP-7843's engine version bumps.
# Record the adopted EIP numbers on `_enabled_eips` directly so that
# `is_eip_enabled()` reports them without those side effects.
MONAD_NEXT._enabled_eips |= {7708, 7843, 8024}
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 @@ -910,17 +910,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 @@ -983,7 +989,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 @@ -42,6 +42,7 @@
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 @@ -294,6 +295,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 @@ -455,6 +457,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 @@ -1019,15 +1023,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 @@ -1039,7 +1060,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