-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new(tests): EOF - EIP-7069: RETURNDATACOPY mem expansion and copy OOG (…
…#671) * new(tests): RETURNDATACOPY mem expansion and copy oog (EOF) * Address review comments --------- Co-authored-by: Andrei Maiboroda <andrei@ethereum.org>
- Loading branch information
Showing
1 changed file
with
271 additions
and
0 deletions.
There are no files selected for viewing
271 changes: 271 additions & 0 deletions
271
tests/prague/eip7692_eof_v1/eip7069_extcall/test_returndatacopy_memory_expansion.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,271 @@ | ||
""" | ||
Memory expansion tests for RETURNDATACOPY executing in EOF code | ||
""" | ||
|
||
from typing import Mapping, Tuple | ||
|
||
import pytest | ||
|
||
from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment | ||
from ethereum_test_tools import Opcodes as Op | ||
from ethereum_test_tools import StateTestFiller, Storage, Transaction, cost_memory_bytes | ||
from ethereum_test_tools.eof.v1 import Container | ||
|
||
from .. import EOF_FORK_NAME | ||
|
||
REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7069.md" | ||
REFERENCE_SPEC_VERSION = "e469fd6c8d736b2a3e1ce632263e3ad36fc8624d" | ||
|
||
pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) | ||
|
||
|
||
@pytest.fixture | ||
def callee_bytecode(dest: int, src: int, length: int) -> Container: | ||
""" | ||
Callee performs a single returndatacopy operation and then returns. | ||
""" | ||
bytecode = Bytecode() | ||
|
||
# Copy the initial memory | ||
bytecode += Op.CALLDATACOPY(0x00, 0x00, Op.CALLDATASIZE()) | ||
|
||
# Pushes for the return operation | ||
bytecode += Op.PUSH1(0x00) + Op.PUSH1(0x00) | ||
|
||
# Perform the returndatacopy operation | ||
bytecode += Op.RETURNDATACOPY(dest, src, length) | ||
|
||
bytecode += Op.RETURN | ||
|
||
return Container.Code(code=bytecode) | ||
|
||
|
||
@pytest.fixture | ||
def subcall_exact_cost( | ||
initial_memory: bytes, | ||
dest: int, | ||
length: int, | ||
) -> int: | ||
""" | ||
Returns the exact cost of the subcall, based on the initial memory and the length of the copy. | ||
""" | ||
returndatacopy_cost = 3 | ||
returndatacopy_cost += 3 * ((length + 31) // 32) | ||
if length > 0 and dest + length > len(initial_memory): | ||
returndatacopy_cost += cost_memory_bytes(dest + length, len(initial_memory)) | ||
|
||
calldatacopy_cost = 3 | ||
calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) | ||
calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) | ||
|
||
pushes_cost = 3 * 7 | ||
calldatasize_cost = 2 | ||
return returndatacopy_cost + calldatacopy_cost + pushes_cost + calldatasize_cost | ||
|
||
|
||
@pytest.fixture | ||
def bytecode_storage( | ||
subcall_exact_cost: int, | ||
successful: bool, | ||
memory_expansion_address: Address, | ||
) -> Tuple[Bytecode, Storage.StorageDictType]: | ||
""" | ||
Prepares the bytecode and storage for the test, based on the expected result of the subcall | ||
(whether it succeeds or fails depending on the length of the memory expansion). | ||
""" | ||
bytecode = Bytecode() | ||
storage = {} | ||
|
||
# Pass on the calldata | ||
bytecode += Op.CALLDATACOPY(0x00, 0x00, Op.CALLDATASIZE()) | ||
|
||
subcall_gas = subcall_exact_cost if successful else subcall_exact_cost - 1 | ||
|
||
# Perform the subcall and store a one in the result location | ||
bytecode += Op.SSTORE( | ||
Op.CALL(subcall_gas, memory_expansion_address, 0, 0, Op.CALLDATASIZE(), 0, 0), 1 | ||
) | ||
storage[int(successful)] = 1 | ||
|
||
return (bytecode, storage) | ||
|
||
|
||
@pytest.fixture | ||
def tx_max_fee_per_gas() -> int: # noqa: D103 | ||
return 7 | ||
|
||
|
||
@pytest.fixture | ||
def block_gas_limit() -> int: # noqa: D103 | ||
return 100_000_000 | ||
|
||
|
||
@pytest.fixture | ||
def tx_gas_limit( # noqa: D103 | ||
subcall_exact_cost: int, | ||
block_gas_limit: int, | ||
) -> int: | ||
return min(max(500_000, subcall_exact_cost * 2), block_gas_limit) | ||
|
||
|
||
@pytest.fixture | ||
def env( # noqa: D103 | ||
block_gas_limit: int, | ||
) -> Environment: | ||
return Environment(gas_limit=block_gas_limit) | ||
|
||
|
||
@pytest.fixture | ||
def caller_address( # noqa: D103 | ||
pre: Alloc, bytecode_storage: Tuple[bytes, Storage.StorageDictType] | ||
) -> Address: | ||
return pre.deploy_contract(code=bytecode_storage[0]) | ||
|
||
|
||
@pytest.fixture | ||
def memory_expansion_address(pre: Alloc, callee_bytecode: bytes) -> Address: # noqa: D103 | ||
return pre.deploy_contract(code=callee_bytecode) | ||
|
||
|
||
@pytest.fixture | ||
def sender(pre: Alloc, tx_max_fee_per_gas: int, tx_gas_limit: int) -> Address: # noqa: D103 | ||
return pre.fund_eoa(tx_max_fee_per_gas * tx_gas_limit) | ||
|
||
|
||
@pytest.fixture | ||
def tx( # noqa: D103 | ||
sender: Address, | ||
caller_address: Address, | ||
initial_memory: bytes, | ||
tx_max_fee_per_gas: int, | ||
tx_gas_limit: int, | ||
) -> Transaction: | ||
return Transaction( | ||
sender=sender, | ||
to=caller_address, | ||
data=initial_memory, | ||
gas_limit=tx_gas_limit, | ||
max_fee_per_gas=tx_max_fee_per_gas, | ||
max_priority_fee_per_gas=0, | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def post( # noqa: D103 | ||
caller_address: Address, bytecode_storage: Tuple[bytes, Storage.StorageDictType] | ||
) -> Mapping: | ||
return { | ||
caller_address: Account(storage=bytecode_storage[1]), | ||
} | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"dest,src,length", | ||
[ | ||
(0x00, 0x00, 0x01), | ||
(0x100, 0x00, 0x01), | ||
(0x1F, 0x00, 0x01), | ||
(0x20, 0x00, 0x01), | ||
(0x1000, 0x00, 0x01), | ||
(0x1000, 0x00, 0x40), | ||
(0x00, 0x00, 0x00), | ||
(2**256 - 1, 0x00, 0x00), | ||
(0x00, 2**256 - 1, 0x00), | ||
(2**256 - 1, 2**256 - 1, 0x00), | ||
], | ||
ids=[ | ||
"single_byte_expansion", | ||
"single_byte_expansion_2", | ||
"single_byte_expansion_word_boundary", | ||
"single_byte_expansion_word_boundary_2", | ||
"multi_word_expansion", | ||
"multi_word_expansion_2", | ||
"zero_length_expansion", | ||
"huge_dest_zero_length", | ||
"huge_src_zero_length", | ||
"huge_dest_huge_src_zero_length", | ||
], | ||
) | ||
@pytest.mark.parametrize("successful", [True, False]) | ||
@pytest.mark.parametrize( | ||
"initial_memory", | ||
[ | ||
bytes(range(0x00, 0x100)), | ||
bytes(), | ||
], | ||
ids=[ | ||
"from_existent_memory", | ||
"from_empty_memory", | ||
], | ||
) | ||
def test_returndatacopy_memory_expansion( | ||
state_test: StateTestFiller, | ||
env: Environment, | ||
pre: Alloc, | ||
post: Mapping[str, Account], | ||
tx: Transaction, | ||
): | ||
""" | ||
Perform RETURNDATACOPY operations that expand the memory, and verify the gas it costs to do so. | ||
""" | ||
state_test( | ||
env=env, | ||
pre=pre, | ||
post=post, | ||
tx=tx, | ||
) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"dest,src,length", | ||
[ | ||
(2**256 - 1, 0x00, 0x01), | ||
(2**256 - 2, 0x00, 0x01), | ||
(2**255 - 1, 0x00, 0x01), | ||
(0x00, 0x00, 2**256 - 1), | ||
(0x00, 0x00, 2**256 - 2), | ||
(0x00, 0x00, 2**255 - 1), | ||
], | ||
ids=[ | ||
"max_dest_single_byte_expansion", | ||
"max_dest_minus_one_single_byte_expansion", | ||
"half_max_dest_single_byte_expansion", | ||
"max_length_expansion", | ||
"max_length_minus_one_expansion", | ||
"half_max_length_expansion", | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"subcall_exact_cost", | ||
[2**128 - 1], | ||
ids=[""], | ||
) # Limit subcall gas, otherwise it would be impossibly large | ||
@pytest.mark.parametrize("successful", [False]) | ||
@pytest.mark.parametrize( | ||
"initial_memory", | ||
[ | ||
bytes(range(0x00, 0x100)), | ||
bytes(), | ||
], | ||
ids=[ | ||
"from_existent_memory", | ||
"from_empty_memory", | ||
], | ||
) | ||
def test_returndatacopy_huge_memory_expansion( | ||
state_test: StateTestFiller, | ||
env: Environment, | ||
pre: Mapping[str, Account], | ||
post: Mapping[str, Account], | ||
tx: Transaction, | ||
): | ||
""" | ||
Perform RETURNDATACOPY operations that expand the memory by huge amounts, and verify that it | ||
correctly runs out of gas. | ||
""" | ||
state_test( | ||
env=env, | ||
pre=pre, | ||
post=post, | ||
tx=tx, | ||
) |