Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(tests): EIP-2935: Serve historical block hashes from state #564

Merged
merged 8 commits into from
May 24, 2024
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
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,
)