Skip to content

Commit

Permalink
new(tests): EIP-2935: Serve historical block hashes from state (#564)
Browse files Browse the repository at this point in the history
* feat(forks): Add history contract to Prague

* new(tests): eip-2935: add file

* tests: eip-2935: new test

* fix(tests): EIP-2935: run HISTORY_SERVE_WINDOW tests

* chengelog

* refactor 1

* fix(fw): types: accept bool in `store_next`

* refactor 2
  • Loading branch information
marioevz authored May 24, 2024
1 parent 0470295 commit 3baf416
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Add tests for [EIP-6110: Supply validator deposits on chain](https://eips.ethereum.org/EIPS/eip-6110) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)).
- ✨ Add tests for [EIP-7002: Execution layer triggerable withdrawals](https://eips.ethereum.org/EIPS/eip-7002) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)).
- ✨ Add tests for [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)).
- ✨ Add tests for [EIP-2935: Serve historical block hashes from state
](https://eips.ethereum.org/EIPS/eip-2935) ([#564](https://github.com/ethereum/execution-spec-tests/pull/564)).

### 🛠️ Framework

Expand Down
16 changes: 14 additions & 2 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,8 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[int]:
@classmethod
def pre_allocation_blockchain(cls) -> Mapping:
"""
Prague requires pre-allocation of the beacon chain deposit contract for EIP-6110, and
the exits contract for EIP-7002.
Prague requires pre-allocation of the beacon chain deposit contract for EIP-6110,
the exits contract for EIP-7002, and the history storage contract for EIP-2935.
"""
new_allocation = {}

Expand Down Expand Up @@ -542,6 +542,18 @@ def pre_allocation_blockchain(cls) -> Mapping:
},
}
)

# Add the history storage contract
with open(CURRENT_FOLDER / "history_contract.bin", mode="rb") as f:
new_allocation.update(
{
0x25A219378DAD9B3503C8268C9CA836A52427A4FB: {
"nonce": 1,
"code": f.read(),
}
}
)

return new_allocation | super(Prague, cls).pre_allocation_blockchain()

@classmethod
Expand Down
Binary file added src/ethereum_test_forks/forks/history_contract.bin
Binary file not shown.
2 changes: 1 addition & 1 deletion src/ethereum_test_tools/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def keys(self) -> set[StorageKeyValueType]:
return set(self.root.keys())

def store_next(
self, value: StorageKeyValueTypeConvertible | StorageKeyValueType
self, value: StorageKeyValueTypeConvertible | StorageKeyValueType | bool
) -> StorageKeyValueType:
"""
Stores a value in the storage and returns the key where the value is stored.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Cross-client EIP-2935 Tests
"""
29 changes: 29 additions & 0 deletions tests/prague/eip2935_historical_block_hashes_from_state/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Defines EIP-2935 specification constants and functions.
"""
from dataclasses import dataclass


@dataclass(frozen=True)
class ReferenceSpec:
"""
Defines the reference spec version and git path.
"""

git_path: str
version: str


ref_spec_2935 = ReferenceSpec("EIPS/eip-2935.md", "3ab311ccd6029c080fb2a8b9615d493dfc093377")


@dataclass(frozen=True)
class Spec:
"""
Parameters from the EIP-2935 specifications as defined at
https://eips.ethereum.org/EIPS/eip-2935
"""

HISTORY_STORAGE_ADDRESS = 0x25A219378DAD9B3503C8268C9CA836A52427A4FB
HISTORY_SERVE_WINDOW = 8192
BLOCKHASH_OLD_WINDOW = 256
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""
abstract: Tests [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935)
Test [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935)
""" # noqa: E501

from itertools import count
from typing import Dict, List

import pytest

from ethereum_test_tools import Account, Address, Block, BlockchainTestFiller, Environment
from ethereum_test_tools import Opcodes as Op
from ethereum_test_tools import Storage, TestAddress, Transaction

from .spec import Spec, ref_spec_2935

REFERENCE_SPEC_GIT_PATH = ref_spec_2935.git_path
REFERENCE_SPEC_VERSION = ref_spec_2935.version

FORK_TIMESTAMP = 15_000


def generate_block_check_code(
block_number: int | None,
populated_blockhash: bool,
populated_contract: bool,
storage: Storage,
check_contract_first: bool = False,
) -> bytes:
"""
Generate EVM code to check that the blockhashes are correctly stored in the state.
Args:
block_number (int | None): The block number to check (or None to return empty code).
populated_blockhash (bool): Whether the blockhash should be populated.
populated_contract (bool): Whether the contract should be populated.
storage (Storage): The storage object to use.
check_contract_first (bool): Whether to check the contract first, for slot warming checks.
"""
if block_number is None:
# No block number to check
return b""

blockhash_key = storage.store_next(not populated_blockhash)
contract_key = storage.store_next(not populated_contract)

check_blockhash = Op.SSTORE(blockhash_key, Op.ISZERO(Op.BLOCKHASH(block_number)))
check_contract = (
Op.MSTORE(0, block_number)
+ Op.POP(Op.CALL(Op.GAS, Spec.HISTORY_STORAGE_ADDRESS, 0, 0, 32, 0, 32))
+ Op.SSTORE(contract_key, Op.ISZERO(Op.MLOAD(0)))
)

if check_contract_first:
code = check_contract + check_blockhash
else:
code = check_blockhash + check_contract

if populated_contract and populated_blockhash:
# Both values must be equal
code += Op.SSTORE(storage.store_next(True), Op.EQ(Op.MLOAD(0), Op.BLOCKHASH(block_number)))

return code


@pytest.mark.parametrize(
"blocks_before_fork",
[
pytest.param(1, id="fork_at_1"),
pytest.param(Spec.BLOCKHASH_OLD_WINDOW, id="fork_at_BLOCKHASH_OLD_WINDOW"),
pytest.param(
Spec.BLOCKHASH_OLD_WINDOW + 1,
id="fork_at_BLOCKHASH_OLD_WINDOW_plus_1",
),
pytest.param(
Spec.BLOCKHASH_OLD_WINDOW + 2,
id="fork_at_BLOCKHASH_OLD_WINDOW_plus_2",
),
pytest.param(
Spec.HISTORY_SERVE_WINDOW + 1,
id="fork_at_HISTORY_SERVE_WINDOW_plus_1",
marks=pytest.mark.slow,
),
],
)
@pytest.mark.valid_at_transition_to("Prague")
def test_block_hashes_history_at_transition(
blockchain_test: BlockchainTestFiller,
blocks_before_fork: int,
):
"""
Test the fork transition and that the block hashes of previous blocks, even blocks
before the fork, are included in the state at the moment of the transition.
"""
# Fork happens at timestamp 15_000, and genesis counts as a block before fork.
blocks: List[Block] = []
assert blocks_before_fork >= 1 and blocks_before_fork < FORK_TIMESTAMP

pre = {TestAddress: Account(balance=10_000_000_000)}
post: Dict[Address, Account] = {}
tx_nonce = count(0)

current_code_address = 0x10000
for i in range(1, blocks_before_fork):
txs: List[Transaction] = []
if i == blocks_before_fork - 1:
# On the last block before the fork, BLOCKHASH must return values for the last 256
# blocks but not for the blocks before that.
# And HISTORY_STORAGE_ADDRESS should be empty.
code = b""
storage = Storage()

# Check the last block before the window
code += generate_block_check_code(
block_number=(
i - Spec.BLOCKHASH_OLD_WINDOW - 1
if i > Spec.BLOCKHASH_OLD_WINDOW
else None # Chain not long enough, no block to check
),
populated_blockhash=False,
populated_contract=False,
storage=storage,
)

# Check the first block inside the window
code += generate_block_check_code(
block_number=(
i - Spec.BLOCKHASH_OLD_WINDOW
if i > Spec.BLOCKHASH_OLD_WINDOW
else 0 # Entire chain is inside the window, check genesis
),
populated_blockhash=True,
populated_contract=False,
storage=storage,
)

txs.append(
Transaction(
to=current_code_address,
gas_limit=10_000_000,
nonce=next(tx_nonce),
)
)
pre[Address(current_code_address)] = Account(code=code, nonce=1)
post[Address(current_code_address)] = Account(storage=storage)
current_code_address += 0x100
blocks.append(Block(timestamp=i, txs=txs))

# Add the fork block
current_block_number = len(blocks) + 1
txs = []
# On the block after the fork, BLOCKHASH must return values for the last
# Spec.HISTORY_SERVE_WINDOW blocks.
# And HISTORY_STORAGE_ADDRESS should be also serve the same values.
code = b""
storage = Storage()

# Check the last block before the window
code += generate_block_check_code(
block_number=(
current_block_number - Spec.HISTORY_SERVE_WINDOW - 1
if current_block_number > Spec.HISTORY_SERVE_WINDOW
else None # Chain not long enough, no block to check
),
populated_blockhash=False,
populated_contract=False,
storage=storage,
)

# Check the first block inside the window
code += generate_block_check_code(
block_number=(
current_block_number - Spec.HISTORY_SERVE_WINDOW
if current_block_number > Spec.HISTORY_SERVE_WINDOW
else 0 # Entire chain is inside the window, check genesis
),
populated_blockhash=True,
populated_contract=True,
storage=storage,
)

txs.append(
Transaction(
to=current_code_address,
gas_limit=10_000_000,
nonce=next(tx_nonce),
)
)
pre[Address(current_code_address)] = Account(code=code, nonce=1)
post[Address(current_code_address)] = Account(storage=storage)
current_code_address += 0x100

blocks.append(Block(timestamp=FORK_TIMESTAMP, txs=txs))

blockchain_test(
genesis_environment=Environment(),
pre=pre,
blocks=blocks,
post=post,
)

0 comments on commit 3baf416

Please sign in to comment.