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
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
from ethereum.crypto.hash import Hash32, keccak256
from ethereum.merkle_patricia_trie import (
EMPTY_TRIE_ROOT,
Trie,
bytes_to_nibble_list,
encode_internal_node,
patricialize,
root,
trie_set,
)
from ethereum.paged_storage_trie import (
PAGE_SIZE,
Expand Down Expand Up @@ -202,3 +205,46 @@ def test_single_page_root_matches_manual_reconstruction(
expected = keccak256(encoded) if len(encoded) < 32 else Hash32(root_node)

assert storage_root_paged(_storage(slot_values)) == expected


def _storage_trie(slot_values: Mapping[int, int]) -> Trie[Bytes32, U256]:
"""Build a secured storage trie holding `{slot: value}`."""
trie: Trie[Bytes32, U256] = Trie(secured=True, default=U256(0))
for slot, value in slot_values.items():
trie_set(trie, U256(slot).to_be_bytes32(), U256(value))
return trie


@pytest.mark.parametrize(
"slot_values",
[{0: 1}, {0: 1, 1: 2}, {0: 1, 128: 2}, {2**256 - 1: 1}],
)
def test_paged_root_differs_from_keccak_mpt_root(
slot_values: Dict[int, int],
) -> None:
"""The page commitment does not coincide with a keccak MPT over slots."""
assert storage_root_paged(_storage(slot_values)) != root(
_storage_trie(slot_values)
)


def test_fully_cleared_page_root_matches_never_written() -> None:
"""
Clearing every slot of a page restores the root the storage had
before that page was written.

Zeroing a slot drops it from the trie, so the emptied page has no
commitment entry at all — `page_commit` rejects an all-zero image.
"""
trie = _storage_trie({0: 1})
baseline = storage_root_paged(trie._data)

for offset in range(WORDS_PER_PAGE):
slot = U256(WORDS_PER_PAGE + offset).to_be_bytes32()
trie_set(trie, slot, U256(offset + 1))
assert storage_root_paged(trie._data) != baseline

for offset in range(WORDS_PER_PAGE):
slot = U256(WORDS_PER_PAGE + offset).to_be_bytes32()
trie_set(trie, slot, U256(0))
assert storage_root_paged(trie._data) == baseline
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
4. Building alloc B by `apply_diff`ing a diff onto alloc A produces a
post-state whose root matches an alloc independently constructed
to look like the post-state.
5. `state_root` follows the alloc's commitment scheme, so the same
accounts commit differently under MPT and under MIP-8 paging.
"""

from typing import Dict, Optional
Expand Down Expand Up @@ -287,3 +289,18 @@ def test_apply_diff_round_trip_matches_independent_post_state() -> None:
account_changes={}, storage_changes={}, code_changes={}
)
)


def test_state_root_follows_commitment_scheme() -> None:
"""
Identical accounts commit to different roots under MPT and paging.

Guards the `StateCommitment` dispatch in `Alloc._state_module`: a
misrouted scheme would silently keep producing pre-MIP-8 roots.
"""
mpt = _fixture_alloc()
paged = _fixture_alloc()
paged.migrate_state_commitment(StateCommitment.PAGED)

assert mpt.root == paged.root
assert mpt.state_root() != paged.state_root()
53 changes: 52 additions & 1 deletion tests/monad_ten/mip8_pageified_storage/test_fork_transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Storage,
Transaction,
)
from execution_testing.base_types.conversions import NumberConvertible
from execution_testing.forks import MONAD_NINE, MONAD_TEN
from execution_testing.forks.helpers import Fork

Expand All @@ -26,7 +27,7 @@
generous_gas,
simulate_sstore,
)
from .spec import ref_spec_8
from .spec import Spec, ref_spec_8

REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path
REFERENCE_SPEC_VERSION = ref_spec_8.version
Expand Down Expand Up @@ -495,3 +496,53 @@ def stable_i(i: int) -> Bytecode:
blocks=blocks,
post={contract_address: Account(storage=storage)},
)


@pytest.mark.parametrize("other_account_touched", [False, True])
@pytest.mark.valid_at_transition_to("MONAD_TEN")
def test_state_root_untouched_storage_at_fork(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
other_account_touched: bool,
) -> None:
"""
Storage untouched by the post-fork block still commits under the
MIP-8 page scheme.
"""
sender = pre.fund_eoa()

storage: dict[NumberConvertible, NumberConvertible] = {
0: 1,
1: 2**256 - 1,
Spec.SLOTS_PER_PAGE - 1: 0x2A,
Spec.SLOTS_PER_PAGE: 0x2B,
2 * Spec.SLOTS_PER_PAGE - 1: 0x2C,
2**256 - 1: 2**255,
}
contract_address = pre.deploy_contract(Op.STOP, storage=storage)

timestamps = [14_999, 15_000]
post: dict = {contract_address: Account(storage=storage)}

if other_account_touched:
target = pre.deploy_contract(Op.SSTORE(0, Op.TIMESTAMP))
post[target] = Account(storage={0: timestamps[-1]})
else:
target = pre.fund_eoa(amount=0)

blocks = [
Block(
timestamp=ts,
txs=[
Transaction(
to=target,
value=1,
sender=sender,
nonce=nonce,
),
],
)
for nonce, ts in enumerate(timestamps)
]

blockchain_test(pre=pre, blocks=blocks, post=post)
97 changes: 97 additions & 0 deletions tests/monad_ten/mip8_pageified_storage/test_sstore_gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,55 @@ def test_sstore_no_growth_after_clear(
)


@pytest.mark.parametrize("clears", [1, 3])
def test_sstore_negative_growth_floor(
state_test: StateTestFiller,
pre: Alloc,
fork: Fork,
clears: int,
) -> None:
"""
The per-page growth counter accumulates below zero, so refills stay
free until it climbs back above the peak it started at.
"""
page = TxPageState(slots=dict.fromkeys(range(clears), 1))

code = Bytecode()
for slot in range(clears):
code += Op.SSTORE(slot, 0)
simulate_sstore(page, slot, 0, fork)

overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork)
expected_storage: dict[int, int] = {}
for slot in range(clears + 1):
expected_storage[slot] = 1
expected_storage[slot_gas_measured + slot] = simulate_sstore(
page, slot, 1, fork
)
code += CodeGasMeasure(
code=Op.SSTORE(slot, 1),
overhead_cost=overhead,
extra_stack_items=0,
sstore_key=slot_gas_measured + slot,
)

contract_address = pre.deploy_contract(
code, storage=dict.fromkeys(range(clears), 1)
)

tx = Transaction(
gas_limit=generous_gas(fork),
to=contract_address,
sender=pre.fund_eoa(),
)

state_test(
pre=pre,
post={contract_address: Account(storage=expected_storage)},
tx=tx,
)


@pytest.mark.parametrize(
"warm_slot,measured_slot",
[
Expand Down Expand Up @@ -954,3 +1003,51 @@ def test_sstore_oog(
subject_storage=subject_storage,
expected_gas=expected_gas,
)


@pytest.mark.parametrize("surviving_pages", [1, 2])
def test_zeroed_page_omitted_from_root(
state_test: StateTestFiller,
pre: Alloc,
fork: Fork,
surviving_pages: int,
) -> None:
"""
A page whose every slot is cleared is omitted from the storage
commitment rather than committed as an all-zero page.
"""
cleared_page = 1
kept_pages = [0, 2][:surviving_pages]

kept_storage: dict[NumberConvertible, NumberConvertible] = {}
for page in kept_pages:
base = page * Spec.SLOTS_PER_PAGE
kept_storage[base] = 1
kept_storage[base + 1] = 2
kept_storage[base + Spec.SLOTS_PER_PAGE - 1] = 3

cleared_slots = range(
cleared_page * Spec.SLOTS_PER_PAGE,
(cleared_page + 1) * Spec.SLOTS_PER_PAGE,
)

code = Bytecode()
for slot in cleared_slots:
code += Op.SSTORE(slot, 0)

contract_address = pre.deploy_contract(
code,
storage={**kept_storage, **dict.fromkeys(cleared_slots, 0xFF)},
)

tx = Transaction(
gas_limit=generous_gas(fork) + full_page_sweep_gas(fork),
to=contract_address,
sender=pre.fund_eoa(),
)

state_test(
pre=pre,
post={contract_address: Account(storage=kept_storage)},
tx=tx,
)