Skip to content

Commit

Permalink
new(tests): EOF - EIP-7069: RETURNDATACOPY mem expansion and copy OOG (
Browse files Browse the repository at this point in the history
…#671)

* new(tests): RETURNDATACOPY mem expansion and copy oog (EOF)

* Address review comments

---------

Co-authored-by: Andrei Maiboroda <[email protected]>
  • Loading branch information
pdobacz and gumb0 authored Jul 11, 2024
1 parent a60adc2 commit a299911
Showing 1 changed file with 271 additions and 0 deletions.
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,
)

0 comments on commit a299911

Please sign in to comment.