diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index a5f5d426a8a..404f7f02a65 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -989,13 +989,13 @@ def pre_allocation_blockchain(cls) -> Mapping: type tests. """ new_allocation = { - Address(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE): { - "nonce": 1, - "code": ( - "0x60203611603157600143035f35116029575f35612000014311602957612000" - "5f3506545f5260205ff35b5f5f5260205ff35b5f5ffd00" - ), - } + # Address(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE): { + # "nonce": 1, + # "code": ( + # "0x60203611603157600143035f35116029575f35612000014311602957612000" + # "5f3506545f5260205ff35b5f5f5260205ff35b5f5ffd00" + # ), + # } } # TODO: Utilize when testing for large init MPT # return VERKLE_PRE_ALLOCATION | super(Shanghai, cls).pre_allocation() diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index e8cdbbf275a..1a45c14b421 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -582,7 +582,7 @@ def generate_block_data( ) transition_tool_output.alloc = previous_alloc # TODO: hack for now, replace with actual witness output once available from t8n - if transition_tool_output.result.verkle_conversion_ended: + if transition_tool_output.result.verkle_conversion_ended and False: witness_parent_root = transition_tool_output.result.parent_state_root transition_tool_output.witness = Witness( verkle_proof=transition_tool_output.result.verkle_proof, diff --git a/tests/verkle/eip4762_verkle_gas_witness/test_balance.py b/tests/verkle/eip4762_verkle_gas_witness/test_balance.py index 91578c07a3b..48a6fd7510e 100644 --- a/tests/verkle/eip4762_verkle_gas_witness/test_balance.py +++ b/tests/verkle/eip4762_verkle_gas_witness/test_balance.py @@ -57,7 +57,7 @@ def test_balance(blockchain_test: BlockchainTestFiller, fork: Fork, target, warm ], ) @pytest.mark.parametrize( - " gas, exp_target_basic_data", + "gas, exp_target_basic_data", [ (21_203 + 2099, False), (21_203 + 2100, True), diff --git a/tests/verkle/eip7748/__init__.py b/tests/verkle/eip7748/__init__.py new file mode 100644 index 00000000000..2b7292f02eb --- /dev/null +++ b/tests/verkle/eip7748/__init__.py @@ -0,0 +1,6 @@ +""" +abstract: Tests [EIP-7748: State conversion to Verkle Tree] +(https://eips.ethereum.org/EIPS/eip-7748) + Tests for [EIP-7748: State conversion to Verkle Tree] + (https://eips.ethereum.org/EIPS/eip-7748). +""" diff --git a/tests/verkle/eip7748/modified_accounts.py b/tests/verkle/eip7748/modified_accounts.py new file mode 100644 index 00000000000..eafb82530b9 --- /dev/null +++ b/tests/verkle/eip7748/modified_accounts.py @@ -0,0 +1,239 @@ +import pytest + +from typing import Optional +from ethereum_test_tools import BlockchainTestFiller, Transaction, Account, TestAddress +from ethereum_test_tools.vm.opcode import Opcodes as Op +from .utils import stride, _state_conversion, accounts, ConversionTx + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7748.md" +REFERENCE_SPEC_VERSION = "TODO" + + +class StaleAccountTx: + """ + Class to represent a transaction that modifies an account to be converted, making it completely/partially stale. + It can be configured to: + - be in the same block as the conversion transaction or in a previous block + - revert or not revert + """ + + def __init__(self, same_block_as_conversion: bool, revert: bool): + self.same_block_as_conversion = same_block_as_conversion + self.revert = revert + + +@pytest.mark.valid_from("EIP6800Transition") +@pytest.mark.parametrize( + "storage_slot_write", + [ + None, + 0, + 1, + 300, + 301, + ], + ids=[ + "No storage slot modified", + "Stale storage slot in the header", + "New storage slot in the account header", + "Stale storage slot outside the header", + "New storage slot outside the header", + ], +) +@pytest.mark.parametrize( + "tx_send_value", + [True, False], +) +@pytest.mark.parametrize( + "tx_stale_account_config", + [ + StaleAccountTx(False, False), + StaleAccountTx(True, False), + StaleAccountTx(True, True), + ], + ids=[ + "Tx generating stale account in previous block", + "Tx generating stale account in the same block", + "Reverted tx generating stale account in the same block", + ], +) +def test_modified_contract( + blockchain_test: BlockchainTestFiller, + storage_slot_write: int, + tx_send_value: bool, + tx_stale_account_config: StaleAccountTx, +): + """ + Test converting a modified contract where a previous transaction writes to: + - Existing storage slots (i.e., storage slots that must not be converted (stale)) + - New storage slots (i.e., storage slots that must not be converted (not overriden with zeros)) + - Basic data (i.e., balance/nonce which must not be converted (stale)) + The conversion transaction can be in the same block or previous block as the conversion. + """ + _convert_modified_account( + blockchain_test, + tx_send_value, + ContractSetup(storage_slot_write), + tx_stale_account_config, + ) + + +@pytest.mark.valid_from("EIP6800Transition") +@pytest.mark.parametrize( + "tx_stale_account_config", + [ + StaleAccountTx(False, False), + StaleAccountTx(True, False), + ], + ids=[ + "Tx generating stale account in previous block", + "Tx generating stale account in the same block", + ], +) +def test_modified_eoa( + blockchain_test: BlockchainTestFiller, tx_stale_account_config: StaleAccountTx +): + """ + Test converting a modified EOA in the same block or previous block as the conversion. + """ + _convert_modified_account(blockchain_test, True, None, tx_stale_account_config) + + +class ContractSetup: + def __init__(self, storage_slot_write: Optional[int]): + self.storage_slot_write = storage_slot_write + + +def _convert_modified_account( + blockchain_test: BlockchainTestFiller, + tx_send_value: bool, + contract_setup: Optional[ContractSetup], + tx_stale_account_config: StaleAccountTx, +): + pre_state = {} + pre_state[TestAddress] = Account(balance=1000000000000000000000) + + expected_conversion_blocks = 1 + accounts_idx = 0 + if not tx_stale_account_config.same_block_as_conversion: + expected_conversion_blocks = 2 + # TODO(hack): today the testing-framework does not support us signaling that we want to + # put the `ConversionTx(tx, **0**)` at the first block after genesis. To simulate that, we have + # to do this here so we "shift" the target account to the second block in the fork. + # If this is ever supported, remove this. + for i in range(stride): + pre_state[accounts[accounts_idx]] = Account(balance=100 + 1000 * i) + accounts_idx += 1 + + target_account = accounts[accounts_idx] + if contract_setup is not None: + pre_state[target_account] = Account( + balance=1_000, + nonce=0, + code=( + ( + Op.SSTORE(contract_setup.storage_slot_write, 9999) + if contract_setup.storage_slot_write is not None + else Op.RETURN + ) + + Op.REVERT + if tx_stale_account_config.revert + else Op.RETURN + ), + storage={0: 100, 300: 200}, + ) + else: + if tx_stale_account_config.revert: + raise Exception("Invalid test case -- EOA transfers can't revert") + pre_state[target_account] = Account(balance=10_000, nonce=0) + + tx = Transaction( + ty=0x0, + chain_id=0x01, + to=target_account, + value=500 if tx_send_value else 0, + gas_limit=100_000, + gas_price=10, + ) + + _state_conversion( + blockchain_test, + pre_state, + stride, + expected_conversion_blocks, + [ConversionTx(tx, 0)], + ) + + +@pytest.mark.valid_from("EIP6800Transition") +def test_modified_eoa_conversion_units(blockchain_test: BlockchainTestFiller): + """ + Test stale EOA are properly counted as used conversion units. + """ + pre_state = {} + pre_state[TestAddress] = Account(balance=1000000000000000000000) + + # TODO(hack): today the testing-framework does not support us signaling that we want to + # put the `ConversionTx(tx, **0**)` at the first block after genesis. To simulate that, we have + # to do this here so we "shift" the target account to the second block in the fork. + # If this is ever supported, remove this. + for i in range(stride): + pre_state[accounts[i]] = Account(balance=100 + 1000 * i) + + txs = [] + # Add stride+3 extra EOAs, and invalidate the first stride ones. This is to check that + # the conversion units are properly counted, and the last 3 aren't converted in that block. + for i in range(stride + 3): + pre_state[accounts[stride + i]] = Account(balance=1_000, nonce=0) + if i < stride: + tx = Transaction( + ty=0x0, + chain_id=0x01, + nonce=i, + to=accounts[stride + i], + value=100, + gas_limit=100_000, + gas_price=10, + ) + txs.append(ConversionTx(tx, 0)) + + _state_conversion(blockchain_test, pre_state, stride, 3, txs) + + +@pytest.mark.valid_from("EIP6800Transition") +def test_modified_contract_conversion_units(blockchain_test: BlockchainTestFiller): + """ + Test stale contract storage slots are properly counted as used conversion units. + """ + pre_state = {} + pre_state[TestAddress] = Account(balance=1000000000000000000000) + + # TODO(hack): today the testing-framework does not support us signaling that we want to + # put the `ConversionTx(tx, **0**)` at the first block after genesis. To simulate that, we have + # to do this here so we "shift" the target account to the second block in the fork. + # If this is ever supported, remove this. + for i in range(stride): + pre_state[accounts[i]] = Account(balance=100 + 1000 * i) + + target_account = accounts[stride] + pre_state[target_account] = Account( + balance=1_000, + nonce=0, + code=sum([Op.SSTORE(ss, 10_000 + i) for i, ss in enumerate(range(stride))]), + storage={ss: 100 + i for i, ss in enumerate(range(stride))}, + ) + for i in range(3): + pre_state[accounts[stride + 1 + i]] = Account(balance=1_000 + i, nonce=0) + + # Send tx that writes all existing storage slots. + tx = Transaction( + ty=0x0, + chain_id=0x01, + nonce=0, + to=target_account, + value=100, + gas_limit=100_000, + gas_price=10, + ) + + _state_conversion(blockchain_test, pre_state, stride, 3, [ConversionTx(tx, 0)]) diff --git a/tests/verkle/eip7748/unmodified_accounts.py b/tests/verkle/eip7748/unmodified_accounts.py new file mode 100644 index 00000000000..9ca89e8673e --- /dev/null +++ b/tests/verkle/eip7748/unmodified_accounts.py @@ -0,0 +1,238 @@ +""" +abstract: Tests [EIP-7748: State conversion to Verkle Tree] +(https://eips.ethereum.org/EIPS/eip-7748) + Tests for [EIP-7748: State conversion to Verkle Tree] + (https://eips.ethereum.org/EIPS/eip-7748). +""" + +import pytest +import math +from typing import Optional + +from ethereum_test_tools import BlockchainTestFiller +from .utils import AccountConfig, stride, _generic_conversion + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7748.md" +REFERENCE_SPEC_VERSION = "TODO" + + +@pytest.mark.valid_from("EIP6800Transition") +@pytest.mark.parametrize( + "account_configs", + [ + [AccountConfig(0, 0)], + [AccountConfig(0, 0)] * 2, + [AccountConfig(0, 0)] * stride, + [AccountConfig(15, 2)], + [AccountConfig(31 * 2 + 1, 3)], # 3 code-chunks + 3 slots + account data = stride + [AccountConfig(0, 0), AccountConfig(15, 2)], + [AccountConfig(0, 0), AccountConfig(31 + 1, 3)], + [AccountConfig(15, 2), AccountConfig(0, 0)], + [AccountConfig(31 + 1, 3), AccountConfig(0, 0)], + [AccountConfig(5, 1), AccountConfig(8, 1)], + [AccountConfig(5, 2), AccountConfig(8, 1)], + [AccountConfig(0, stride - 1)], + ], + ids=[ + "EOA", + "EOAs under-fit", + "EOAs perfect-fit", + "Contract under-fit", + "Contract perfect-fit", + "EOA and Contract under-fit", + "EOA and Contract perfect-fit", + "Contract and EOA under-fit", + "Contract and EOA perfect-fit", + "Contract and Contract under-fit", + "Contract and Contract perfect-fit", + "Empty code with storage slots perfect-fit", + ], +) +@pytest.mark.parametrize( + "fill_first_block", + [True, False], +) +@pytest.mark.parametrize( + "fill_last_block", + [True, False], +) +def test_non_partial( + blockchain_test: BlockchainTestFiller, + account_configs: list[AccountConfig], + fill_first_block: bool, + fill_last_block: bool, +): + """ + Test non-partial account conversions. + """ + _generic_conversion(blockchain_test, account_configs, fill_first_block, fill_last_block) + + +@pytest.mark.valid_from("EIP6800Transition") +@pytest.mark.parametrize( + "storage_slots_overlow", + [ + 2, + 1, + 0, + ], + ids=[ + "Two storage slots overflowed to next block", + "One storage slot overflowed to next block", + "Storage slots fit in one block", + ], +) +@pytest.mark.parametrize( + "account_prefix", + [ + None, + AccountConfig(0, 0), + AccountConfig(10, 1), + ], + ids=[ + "No prefix", + "EOA prefix", + "Contract prefix", + ], +) +@pytest.mark.parametrize( + "empty_code", + [True, False], +) +@pytest.mark.parametrize( + "fill_first_block", + [True, False], +) +@pytest.mark.parametrize( + "fill_last_block", + [True, False], +) +def test_partial( + blockchain_test: BlockchainTestFiller, + storage_slots_overlow: int, + account_prefix: Optional[AccountConfig], + empty_code: bool, + fill_first_block: bool, + fill_last_block: bool, +): + """ + Test partial account conversions. + """ + conversion_unit_offset = 0 + account_configs = [] + if account_prefix is not None: + conversion_unit_offset += 1 # Account basic data + conversion_unit_offset += math.ceil(account_prefix.code_length / 31) # Code-chunks + conversion_unit_offset += account_prefix.storage_slots_count + account_configs.append(account_prefix) + + code_length = 0 if empty_code else 31 + # For the `stride` quota in this block, we already used `conversion_unit_offset` units from the accounts prefixes. + # For the remaining quota (i.e., `stride-conversion_unit_offset`), we should configure the contract having + # `(stride - conversion_unit_offset)+storage_slots_overlow` so we can overflow the desired storage slots to the + # next block. + num_storage_slots = stride - conversion_unit_offset + storage_slots_overlow + account_configs.append(AccountConfig(code_length, num_storage_slots)) + + _generic_conversion(blockchain_test, account_configs, fill_first_block, fill_last_block) + + +@pytest.mark.valid_from("EIP6800Transition") +@pytest.mark.parametrize( + "account_configs", + [ + [AccountConfig(31 * (stride + 10) + 1, 4)], + [AccountConfig(31 * (stride + 3), 1), AccountConfig(0, 0)], + ], + ids=[ + "Stride overflow", + "Stride overflow followed by EOA", + ], +) +@pytest.mark.parametrize( + "fill_first_block", + [False, True], +) +@pytest.mark.parametrize( + "fill_last_block", + [False, True], +) +def test_codechunks_stride_overflow( + blockchain_test: BlockchainTestFiller, + account_configs: list[AccountConfig], + fill_first_block: bool, + fill_last_block: bool, +): + """ + Test code-chunks stride overflow. + """ + _generic_conversion(blockchain_test, account_configs, fill_first_block, fill_last_block) + + +# @pytest.mark.skip("stride config not supported yet") +# @pytest.mark.valid_from("EIP6800Transition") +# @pytest.mark.parametrize( +# "fcb, stride, num_expected_blocks", +# [ +# (True, 1, 2), +# (True, 2, 1), +# (False, 1, 1), +# (False, 2, 1), +# ], +# ) +# def test_empty_account( +# blockchain_test: BlockchainTestFiller, +# fcb: bool, +# stride: int, +# num_expected_blocks: int, +# ): +# """ +# Test EIP-161 accounts. +# """ +# pre_state = {} +# if not fcb: +# pre_state[Account0] = Account(balance=1000) + +# # Empty account (EIP-161) +# pre_state[Account1] = Account( +# balance=0, +# nonce=0, +# storage={0: 0x1, 1: 0x2}, +# ) + +# pre_state[Account2] = Account(balance=1001) + +# _state_conversion(blockchain_test, pre_state, stride, num_expected_blocks) + + +# @pytest.mark.skip("stride config not supported yet") +# @pytest.mark.valid_from("EIP6800Transition") +# @pytest.mark.parametrize( +# "fcb, stride, num_expected_blocks", +# [ +# (True, 1, 2), +# (True, 2, 1), +# (False, 1, 1), +# ], +# ) +# def test_last_conversion_block( +# blockchain_test: BlockchainTestFiller, +# fcb: bool, +# stride: int, +# num_expected_blocks: int, +# ): +# """ +# Test last conversion block scenario. +# """ +# pre_state = {} +# if not fcb: +# pre_state[Account0] = Account(balance=1000) + +# # Empty account (EIP-161) +# pre_state[Account1] = Account( +# balance=0, +# nonce=0, +# storage={0: 0x1, 1: 0x2}, +# ) + +# _state_conversion(blockchain_test, pre_state, stride, num_expected_blocks) diff --git a/tests/verkle/eip7748/utils.py b/tests/verkle/eip7748/utils.py new file mode 100644 index 00000000000..8e6d81c86e9 --- /dev/null +++ b/tests/verkle/eip7748/utils.py @@ -0,0 +1,109 @@ +from ethereum_test_tools.vm.opcode import Opcodes as Op +import math +from ethereum_test_tools import ( + Account, + Address, + Block, + BlockchainTestFiller, + Transaction, + Environment, +) + +stride = 7 + +accounts = sorted([Address(i) for i in range(0, 100)], key=lambda x: x.keccak256()) + + +class AccountConfig: + def __init__(self, code_length: int, storage_slot_count: int): + self.code_length = code_length + self.storage_slots_count = storage_slot_count + + +class StaleBasicDataTx: + def __init__(self, account_config_idx: int, block_num: int): + self.account_config_idx = account_config_idx + self.block_num = block_num + + +def _generic_conversion( + blockchain_test: BlockchainTestFiller, + account_configs: list[AccountConfig], + fill_first_block: bool, + fill_last_block: bool, +): + conversion_units = 0 + pre_state = {} + account_idx = 0 + if fill_first_block: + for i in range(stride): + conversion_units += 1 + pre_state[accounts[account_idx]] = Account(balance=100 + 1000 * i) + account_idx += 1 + + target_accounts: list[Address] = [] + for i, account_config in enumerate(account_configs): + storage = {} + for j in range(account_config.storage_slots_count): + conversion_units += 1 + storage[j] = j + 1 + + pre_state[accounts[account_idx]] = Account( + balance=100 + 1000 * i, + nonce=i, + code=Op.JUMPDEST * account_config.code_length, + storage=storage, + ) + target_accounts.append(accounts[account_idx]) + account_idx += 1 + + conversion_units += 1 # Account basic data + num_code_chunks = math.ceil(account_config.code_length / 31) + # Code is always converted in one go, but it counts for stride quota usage + conversion_units += min(num_code_chunks, stride - conversion_units % stride) + + if fill_last_block: + for i in range((-conversion_units) % stride + stride): + conversion_units += 1 + pre_state[accounts[account_idx]] = Account(balance=100 + 1000 * i) + account_idx += 1 + + _state_conversion(blockchain_test, pre_state, stride, math.ceil(conversion_units / stride)) + + +class ConversionTx: + def __init__(self, tx: Transaction, block_num: int): + self.tx = tx + self.block_num = block_num + + +def _state_conversion( + blockchain_test: BlockchainTestFiller, + pre_state: dict[Address, Account], + stride: int, + num_blocks: int, + txs: list[ConversionTx] = [], +): + # TODO: test library should allow passing stride + env = Environment( + fee_recipient="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + difficulty=0x20000, + gas_limit=10000000000, + ) + + blocks: list[Block] = [] + for i in range(num_blocks): + blocks.append(Block(txs=[])) + + for tx in txs: + blocks[tx.block_num].txs.append(tx.tx) + + # TODO: witness assertion + # TODO: see if possible last block switch to finished conversion + + blockchain_test( + genesis_environment=env, + pre=pre_state, + post=pre_state.copy(), + blocks=blocks, + )