From 05d4bbff2eb89b7f57316c8bdb24bf5b88e12c3d Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 20 Jun 2024 23:32:26 +0000 Subject: [PATCH] new(tests): EIP-7251 condolidation tests fix(tests): EIP-7251 new(tests): Add deposit+withdrawal+consolidation tests new(tests): Add different requests in same tx new(tests): Add invalid-order requests tests with consolidations Update tests/prague/eip7251_consolidations/helpers.py Co-authored-by: spencer Update tests/prague/eip7251_consolidations/spec.py Co-authored-by: spencer Update tests/prague/eip7251_consolidations/spec.py Co-authored-by: spencer tests: add consolidation test with same pubkey fix(tests): EIP-7002,EIP-7251: keep producing empty blocks to exhaust queues fix(tests): tox fix(tests): tox fix --- tests/prague/eip6110_deposits/helpers.py | 6 + .../conftest.py | 22 +- .../test_withdrawal_requests.py | 6 +- .../prague/eip7251_consolidations/__init__.py | 3 + .../prague/eip7251_consolidations/conftest.py | 93 +++ .../prague/eip7251_consolidations/helpers.py | 292 +++++++ tests/prague/eip7251_consolidations/spec.py | 83 ++ .../test_consolidations.py | 769 ++++++++++++++++++ .../conftest.py | 25 +- .../test_deposits_withdrawals.py | 402 --------- ...est_deposits_withdrawals_consolidations.py | 376 +++++++++ whitelist.txt | 2 + 12 files changed, 1664 insertions(+), 415 deletions(-) create mode 100644 tests/prague/eip7251_consolidations/__init__.py create mode 100644 tests/prague/eip7251_consolidations/conftest.py create mode 100644 tests/prague/eip7251_consolidations/helpers.py create mode 100644 tests/prague/eip7251_consolidations/spec.py create mode 100644 tests/prague/eip7251_consolidations/test_consolidations.py delete mode 100644 tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals.py create mode 100644 tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals_consolidations.py diff --git a/tests/prague/eip6110_deposits/helpers.py b/tests/prague/eip6110_deposits/helpers.py index 90a05b3c6a..7d00028aa9 100644 --- a/tests/prague/eip6110_deposits/helpers.py +++ b/tests/prague/eip6110_deposits/helpers.py @@ -91,6 +91,12 @@ def calldata(self) -> bytes: + self.signature ) + def with_source_address(self, source_address: Address) -> "DepositRequest": + """ + Return a copy. + """ + return self.copy() + @dataclass(kw_only=True) class DepositInteractionBase: diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py b/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py index 00f8a336c1..4c6cd812b9 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py @@ -1,6 +1,7 @@ """ Fixtures for the EIP-7002 deposit tests. """ +from itertools import zip_longest from typing import List import pytest @@ -57,6 +58,13 @@ def included_requests( excess_withdrawal_requests, len(current_block_requests), ) + while carry_over_requests: + # Keep adding blocks until all withdrawal requests are included + per_block_included_requests.append( + carry_over_requests[: Spec.MAX_WITHDRAWAL_REQUESTS_PER_BLOCK] + ) + carry_over_requests = carry_over_requests[Spec.MAX_WITHDRAWAL_REQUESTS_PER_BLOCK :] + return per_block_included_requests @@ -69,10 +77,16 @@ def blocks( """ Return the list of blocks that should be included in the test. """ - return [ + return [ # type: ignore Block( txs=sum((r.transactions() for r in block_requests), []), - header_verify=Header(requests_root=included_requests[i]), + header_verify=Header(requests_root=block_included_requests), + ) + for block_requests, block_included_requests in zip_longest( + blocks_withdrawal_requests, + included_requests, + fillvalue=[], ) - for i, block_requests in enumerate(blocks_withdrawal_requests) - ] + ] + [ + Block(header_verify=Header(requests_root=[])) + ] # Add an empty block at the end to verify that no more withdrawal requests are included diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py index c11652af79..23d1b3cf96 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py @@ -64,6 +64,7 @@ validator_pubkey=0x01, amount=0, fee=0, + valid=False, ) ], ), @@ -262,7 +263,6 @@ ), pytest.param( [ - # Block 1 [ WithdrawalRequestTransaction( requests=[ @@ -275,10 +275,6 @@ ] ) ], - # Block 2, no new withdrawal requests, but queued requests from previous block - [], - # Block 3, no new nor queued withdrawal requests - [], ], id="multiple_block_above_max_withdrawal_requests_from_eoa", ), diff --git a/tests/prague/eip7251_consolidations/__init__.py b/tests/prague/eip7251_consolidations/__init__.py new file mode 100644 index 0000000000..2c845d1193 --- /dev/null +++ b/tests/prague/eip7251_consolidations/__init__.py @@ -0,0 +1,3 @@ +""" +Cross-client EIP-7251 Tests +""" diff --git a/tests/prague/eip7251_consolidations/conftest.py b/tests/prague/eip7251_consolidations/conftest.py new file mode 100644 index 0000000000..ef5c3d87b9 --- /dev/null +++ b/tests/prague/eip7251_consolidations/conftest.py @@ -0,0 +1,93 @@ +""" +Fixtures for the EIP-7251 consolidations tests. +""" +from itertools import zip_longest +from typing import List + +import pytest + +from ethereum_test_tools import Alloc, Block, Header + +from .helpers import ConsolidationRequest, ConsolidationRequestInteractionBase +from .spec import Spec + + +@pytest.fixture +def update_pre( + pre: Alloc, + blocks_consolidation_requests: List[List[ConsolidationRequestInteractionBase]], +): + """ + Initial state of the accounts. Every deposit transaction defines their own pre-state + requirements, and this fixture aggregates them all. + """ + for requests in blocks_consolidation_requests: + for r in requests: + r.update_pre(pre) + + +@pytest.fixture +def included_requests( + update_pre: None, # Fixture is used for its side effects + blocks_consolidation_requests: List[List[ConsolidationRequestInteractionBase]], +) -> List[List[ConsolidationRequest]]: + """ + Return the list of consolidation requests that should be included in each block. + """ + excess_consolidation_requests = 0 + carry_over_requests: List[ConsolidationRequest] = [] + per_block_included_requests: List[List[ConsolidationRequest]] = [] + for block_consolidation_requests in blocks_consolidation_requests: + # Get fee for the current block + current_minimum_fee = Spec.get_fee(excess_consolidation_requests) + + # With the fee, get the valid consolidation requests for the current block + current_block_requests = [] + for w in block_consolidation_requests: + current_block_requests += w.valid_requests(current_minimum_fee) + + # Get the consolidation requests that should be included in the block + pending_requests = carry_over_requests + current_block_requests + per_block_included_requests.append( + pending_requests[: Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK] + ) + carry_over_requests = pending_requests[Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK :] + + # Update the excess consolidation requests + excess_consolidation_requests = Spec.get_excess_consolidation_requests( + excess_consolidation_requests, + len(current_block_requests), + ) + + while carry_over_requests: + # Keep adding blocks until all consolidation requests are included + per_block_included_requests.append( + carry_over_requests[: Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK] + ) + carry_over_requests = carry_over_requests[Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK :] + + return per_block_included_requests + + +@pytest.fixture +def blocks( + update_pre: None, # Fixture is used for its side effects + blocks_consolidation_requests: List[List[ConsolidationRequestInteractionBase]], + included_requests: List[List[ConsolidationRequest]], +) -> List[Block]: + """ + Return the list of blocks that should be included in the test. + """ + return [ # type: ignore + Block( + txs=sum((r.transactions() for r in block_requests), []), + header_verify=Header(requests_root=block_included_requests), + ) + for block_requests, block_included_requests in zip_longest( + blocks_consolidation_requests, + included_requests, + fillvalue=[], + ) + ] + [ + Block(header_verify=Header(requests_root=[])) + ] # Add an empty block at the end to verify that no more consolidation requests are included diff --git a/tests/prague/eip7251_consolidations/helpers.py b/tests/prague/eip7251_consolidations/helpers.py new file mode 100644 index 0000000000..8c896b5d7f --- /dev/null +++ b/tests/prague/eip7251_consolidations/helpers.py @@ -0,0 +1,292 @@ +""" +Helpers for the EIP-7251 consolidation tests. +""" +from dataclasses import dataclass, field +from functools import cached_property +from itertools import count +from typing import Callable, ClassVar, List + +from ethereum_test_tools import EOA, Address, Alloc, Bytecode +from ethereum_test_tools import ConsolidationRequest as ConsolidationRequestBase +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import Transaction + +from .spec import Spec + + +class ConsolidationRequest(ConsolidationRequestBase): + """ + Class used to describe a consolidation request in a test. + """ + + fee: int = 0 + """ + Fee to be paid for the consolidation request. + """ + valid: bool = True + """ + Whether the consolidation request is valid or not. + """ + gas_limit: int = 1_000_000 + """ + Gas limit for the call. + """ + calldata_modifier: Callable[[bytes], bytes] = lambda x: x + """ + Calldata modifier function. + """ + + interaction_contract_address: ClassVar[Address] = Address( + Spec.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS + ) + + @property + def value(self) -> int: + """ + Returns the value of the consolidation request. + """ + return self.fee + + @cached_property + def calldata(self) -> bytes: + """ + Returns the calldata needed to call the consolidation request contract and make the + consolidation. + """ + return self.calldata_modifier(self.source_pubkey + self.target_pubkey) + + def with_source_address(self, source_address: Address) -> "ConsolidationRequest": + """ + Return a new instance of the consolidation request with the source address set. + """ + return self.copy(source_address=source_address) + + +@dataclass(kw_only=True) +class ConsolidationRequestInteractionBase: + """ + Base class for all types of consolidation transactions we want to test. + """ + + sender_balance: int = 32_000_000_000_000_000_000 * 100 + """ + Balance of the account that sends the transaction. + """ + sender_account: EOA | None = None + """ + Account that will send the transaction. + """ + requests: List[ConsolidationRequest] + """ + Consolidation requests to be included in the block. + """ + + def transactions(self) -> List[Transaction]: + """Return a transaction for the consolidation request.""" + raise NotImplementedError + + def update_pre(self, pre: Alloc): + """Return the pre-state of the account.""" + raise NotImplementedError + + def valid_requests(self, current_minimum_fee: int) -> List[ConsolidationRequest]: + """Return the list of consolidation requests that should be valid in the block.""" + raise NotImplementedError + + +@dataclass(kw_only=True) +class ConsolidationRequestTransaction(ConsolidationRequestInteractionBase): + """ + Class used to describe a consolidation request originated from an externally owned account. + """ + + def transactions(self) -> List[Transaction]: + """Return a transaction for the consolidation request.""" + assert self.sender_account is not None, "Sender account not initialized" + return [ + Transaction( + gas_limit=request.gas_limit, + gas_price=0x07, + to=request.interaction_contract_address, + value=request.value, + data=request.calldata, + sender=self.sender_account, + ) + for request in self.requests + ] + + def update_pre(self, pre: Alloc): + """Return the pre-state of the account.""" + self.sender_account = pre.fund_eoa(self.sender_balance) + + def valid_requests(self, current_minimum_fee: int) -> List[ConsolidationRequest]: + """Return the list of consolidation requests that are valid.""" + assert self.sender_account is not None, "Sender account not initialized" + return [ + request.with_source_address(self.sender_account) + for request in self.requests + if request.valid and request.fee >= current_minimum_fee + ] + + +@dataclass(kw_only=True) +class ConsolidationRequestContract(ConsolidationRequestInteractionBase): + """Class used to describe a consolidation originated from a contract.""" + + tx_gas_limit: int = 10_000_000 + """ + Gas limit for the transaction. + """ + + contract_balance: int = 32_000_000_000_000_000_000 * 100 + """ + Balance of the contract that will make the call to the pre-deploy contract. + """ + contract_address: Address | None = None + """ + Address of the contract that will make the call to the pre-deploy contract. + """ + entry_address: Address | None = None + """ + Address to send the transaction to. + """ + + call_type: Op = field(default_factory=lambda: Op.CALL) + """ + Type of call to be used to make the consolidation request. + """ + call_depth: int = 2 + """ + Frame depth of the pre-deploy contract when it executes the call. + """ + extra_code: Bytecode = field(default_factory=Bytecode) + """ + Extra code to be added to the contract code. + """ + + @property + def contract_code(self) -> Bytecode: + """Contract code used by the relay contract.""" + code = Bytecode() + current_offset = 0 + for r in self.requests: + value_arg = [r.value] if self.call_type in (Op.CALL, Op.CALLCODE) else [] + code += Op.CALLDATACOPY(0, current_offset, len(r.calldata)) + Op.POP( + self.call_type( + Op.GAS if r.gas_limit == -1 else r.gas_limit, + r.interaction_contract_address, + *value_arg, + 0, + len(r.calldata), + 0, + 0, + ) + ) + current_offset += len(r.calldata) + return code + self.extra_code + + def transactions(self) -> List[Transaction]: + """Return a transaction for the consolidation request.""" + assert self.entry_address is not None, "Entry address not initialized" + return [ + Transaction( + gas_limit=self.tx_gas_limit, + gas_price=0x07, + to=self.entry_address, + value=0, + data=b"".join(r.calldata for r in self.requests), + sender=self.sender_account, + ) + ] + + def update_pre(self, pre: Alloc): + """Return the pre-state of the account.""" + self.sender_account = pre.fund_eoa(self.sender_balance) + self.contract_address = pre.deploy_contract( + code=self.contract_code, balance=self.contract_balance + ) + self.entry_address = self.contract_address + if self.call_depth > 2: + for _ in range(1, self.call_depth - 1): + self.entry_address = pre.deploy_contract( + code=Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.POP( + Op.CALL( + Op.GAS, + self.entry_address, + 0, + 0, + Op.CALLDATASIZE, + 0, + 0, + ) + ) + ) + + def valid_requests(self, current_minimum_fee: int) -> List[ConsolidationRequest]: + """Return the list of consolidation requests that are valid.""" + assert self.contract_address is not None, "Contract address not initialized" + return [ + r.with_source_address(self.contract_address) + for r in self.requests + if r.valid and r.value >= current_minimum_fee + ] + + +def get_n_fee_increments(n: int) -> List[int]: + """ + Get the first N excess consolidation requests that increase the fee. + """ + excess_consolidation_requests_counts = [] + last_fee = 1 + for i in count(0): + if Spec.get_fee(i) > last_fee: + excess_consolidation_requests_counts.append(i) + last_fee = Spec.get_fee(i) + if len(excess_consolidation_requests_counts) == n: + break + return excess_consolidation_requests_counts + + +def get_n_fee_increment_blocks(n: int) -> List[List[ConsolidationRequestContract]]: + """ + Return N blocks that should be included in the test such that each subsequent block has an + increasing fee for the consolidation requests. + + This is done by calculating the number of consolidations required to reach the next fee + increment and creating a block with that number of consolidation requests plus the number of + consolidations required to reach the target. + """ + blocks = [] + previous_excess = 0 + consolidation_index = 0 + previous_fee = 0 + for required_excess_consolidations in get_n_fee_increments(n): + consolidations_required = ( + required_excess_consolidations + + Spec.TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK + - previous_excess + ) + fee = Spec.get_fee(previous_excess) + assert fee > previous_fee + blocks.append( + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=fee, + ) + for i in range( + consolidation_index, consolidation_index + consolidations_required + ) + ], + ) + ], + ) + previous_fee = fee + consolidation_index += consolidations_required + previous_excess = required_excess_consolidations + + return blocks diff --git a/tests/prague/eip7251_consolidations/spec.py b/tests/prague/eip7251_consolidations/spec.py new file mode 100644 index 0000000000..27c22845cf --- /dev/null +++ b/tests/prague/eip7251_consolidations/spec.py @@ -0,0 +1,83 @@ +""" +Defines EIP-7251 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_7251 = ReferenceSpec("EIPS/eip-7251.md", "e5af719767e789c88c0e063406c6557c8f53cfba") + + +# Constants +@dataclass(frozen=True) +class Spec: + """ + Parameters from the EIP-7251 specifications as defined at + https://eips.ethereum.org/EIPS/eip-7251#execution-layer + """ + + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = 0x00B42DBF2194E931E80326D950320F7D9DBEAC02 + SYSTEM_ADDRESS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE + + EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT = 0 + CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT = 1 + CONSOLIDATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT = ( + 2 # Pointer to head of the consolidation request message queue + ) + CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT = ( + 3 # Pointer to the tail of the consolidation request message queue + ) + CONSOLIDATION_REQUEST_QUEUE_STORAGE_OFFSET = ( + 4 # The start memory slot of the in-state consolidation request message queue + ) + MAX_CONSOLIDATION_REQUESTS_PER_BLOCK = ( + 1 # Maximum number of consolidation requests that can be de-queued into a block + ) + TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK = 1 + MIN_CONSOLIDATION_REQUEST_FEE = 1 + CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION = 17 + EXCESS_INHIBITOR = 1181 + + @staticmethod + def fake_exponential(factor: int, numerator: int, denominator: int) -> int: + """ + Used to calculate the consolidation request fee. + """ + i = 1 + output = 0 + numerator_accumulator = factor * denominator + while numerator_accumulator > 0: + output += numerator_accumulator + numerator_accumulator = (numerator_accumulator * numerator) // (denominator * i) + i += 1 + return output // denominator + + @staticmethod + def get_fee(excess_consolidation_requests: int) -> int: + """ + Calculate the fee for the excess consolidation requests. + """ + return Spec.fake_exponential( + Spec.MIN_CONSOLIDATION_REQUEST_FEE, + excess_consolidation_requests, + Spec.CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION, + ) + + @staticmethod + def get_excess_consolidation_requests(previous_excess: int, count: int) -> int: + """ + Calculate the new excess consolidation requests. + """ + if previous_excess + count > Spec.TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK: + return previous_excess + count - Spec.TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK + return 0 diff --git a/tests/prague/eip7251_consolidations/test_consolidations.py b/tests/prague/eip7251_consolidations/test_consolidations.py new file mode 100644 index 0000000000..1ce2db47e6 --- /dev/null +++ b/tests/prague/eip7251_consolidations/test_consolidations.py @@ -0,0 +1,769 @@ +""" +abstract: Tests [EIP-7251: Increase the MAX_EFFECTIVE_BALANCE](https://eips.ethereum.org/EIPS/eip-7251) + Test execution layer triggered consolidations [EIP-7251: Increase the MAX_EFFECTIVE_BALANCE](https://eips.ethereum.org/EIPS/eip-7251) + +""" # noqa: E501 + +from typing import List + +import pytest + +from ethereum_test_tools import ( + Address, + Alloc, + Block, + BlockchainTestFiller, + BlockException, + Environment, + Header, + Macros, +) +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import TestAddress, TestAddress2 + +from .helpers import ( + ConsolidationRequest, + ConsolidationRequestContract, + ConsolidationRequestInteractionBase, + ConsolidationRequestTransaction, + get_n_fee_increment_blocks, +) +from .spec import Spec, ref_spec_7251 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7251.git_path +REFERENCE_SPEC_VERSION = ref_spec_7251.version + +pytestmark = pytest.mark.valid_from("Prague") + + +@pytest.mark.parametrize( + "blocks_consolidation_requests", + [ + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x01, + fee=Spec.get_fee(0), + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa_equal_pubkeys", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=-1, + target_pubkey=-2, + fee=Spec.get_fee(0), + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa_max_pubkeys", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=0, + valid=False, + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa_insufficient_fee", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + calldata_modifier=lambda x: x[:-1], + valid=False, + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa_input_too_short", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + calldata_modifier=lambda x: x + b"\x00", + valid=False, + ) + ], + ), + ], + ], + id="single_block_single_consolidation_request_from_eoa_input_too_long", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + ), + ], + ), + ], + ], + id="single_block_multiple_consolidation_request_from_same_eoa", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ) + ], + ), + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + ) + ], + ), + ], + ], + id="single_block_multiple_consolidation_request_from_different_eoa", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK) + ], + ) + ], + ], + marks=pytest.mark.skip( + reason="duplicate test due to MAX_CONSOLIDATION_REQUESTS_PER_BLOCK==1" + ), + id="single_block_max_consolidation_requests_from_eoa", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=0, + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + ), + ] + ), + ], + ], + id="single_block_multiple_consolidation_request_first_reverts", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=0, + ), + ] + ), + ], + ], + id="single_block_multiple_consolidation_request_last_reverts", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + # TODO: Value obtained from trace minus one + gas_limit=50_000 - 1, + valid=False, + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + ), + ] + ), + ], + ], + id="single_block_multiple_consolidation_request_first_oog", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + # TODO: Value obtained from trace minus one + gas_limit=80_047 - 1, + valid=False, + ), + ] + ), + ], + ], + id="single_block_multiple_consolidation_request_last_oog", + ), + pytest.param( + [ + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ] + ) + ], + ], + id="multiple_block_above_max_consolidation_requests_from_eoa", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ] + ), + ], + ], + id="single_block_single_consolidation_request_from_contract", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ], + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=0x00, + target_pubkey=0x01, + fee=0, + ) + ] + + [ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + ) + for i in range(1, Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ], + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_first_reverts", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ] + + [ + ConsolidationRequest( + source_pubkey=-1, + target_pubkey=-2, + fee=0, + ) + ], + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_last_reverts", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=-1, + target_pubkey=-2, + gas_limit=100, + fee=Spec.get_fee(0), + valid=False, + ) + ] + + [ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + gas_limit=1_000_000, + fee=Spec.get_fee(0), + valid=True, + ) + for i in range(1, Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ], + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_first_oog", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + gas_limit=1_000_000, + valid=True, + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ] + + [ + ConsolidationRequest( + source_pubkey=-1, + target_pubkey=-2, + gas_limit=100, + fee=Spec.get_fee(0), + valid=False, + ) + ], + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_last_oog", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + valid=False, + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ], + extra_code=Op.REVERT(0, 0), + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_caller_reverts", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=i * 2, + target_pubkey=i * 2 + 1, + fee=Spec.get_fee(0), + valid=False, + ) + for i in range(Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK * 5) + ], + extra_code=Macros.OOG(), + ), + ], + ], + id="single_block_multiple_consolidation_requests_from_contract_caller_oog", + ), + pytest.param( + # Test the first 50 fee increments + get_n_fee_increment_blocks(50), + id="multiple_block_fee_increments", + ), + pytest.param( + [ + [ + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + valid=False, + ) + ], + call_type=Op.DELEGATECALL, + ), + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + valid=False, + ) + ], + call_type=Op.STATICCALL, + ), + ConsolidationRequestContract( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + valid=False, + ) + ], + call_type=Op.CALLCODE, + ), + ], + ], + id="single_block_single_consolidation_request_delegatecall_staticcall_callcode", + ), + ], +) +def test_consolidation_requests( + blockchain_test: BlockchainTestFiller, + blocks: List[Block], + pre: Alloc, +): + """ + Test making a consolidation request to the beacon chain. + """ + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) + + +@pytest.mark.parametrize( + "requests,block_body_override_requests,exception", + [ + pytest.param( + [], + [ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + source_address=Address(0), + ), + ], + BlockException.INVALID_REQUESTS, + id="no_consolidations_non_empty_requests_list", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ] + ), + ], + [], + BlockException.INVALID_REQUESTS, + id="single_consolidation_request_empty_requests_list", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ] + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x00, + target_pubkey=0x02, + source_address=TestAddress, + ) + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_request_source_public_key_mismatch", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ] + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x00, + source_address=TestAddress, + ) + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_request_target_public_key_mismatch", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ] + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x02, + target_pubkey=0x01, + source_address=TestAddress, + ) + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_request_pubkeys_swapped", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ) + ], + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + source_address=TestAddress2, + ) + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_request_source_address_mismatch", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ), + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(0), + ), + ], + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + source_address=TestAddress, + ), + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + source_address=TestAddress, + ), + ], + BlockException.INVALID_REQUESTS, + id="two_consolidation_requests_out_of_order", + ), + pytest.param( + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(0), + ) + ], + ), + ], + [ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + source_address=TestAddress, + ), + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + source_address=TestAddress, + ), + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_requests_duplicate_in_requests_list", + ), + ], +) +def test_consolidation_requests_negative( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + requests: List[ConsolidationRequestInteractionBase], + block_body_override_requests: List[ConsolidationRequest], + exception: BlockException, +): + """ + Test blocks where the requests list and the actual consolidation requests that happened in the + block's transactions do not match. + """ + for d in requests: + d.update_pre(pre) + + # No previous block so fee is the base + fee = 1 + current_block_requests = [] + for w in requests: + current_block_requests += w.valid_requests(fee) + included_requests = current_block_requests[: Spec.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK] + + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=[ + Block( + txs=sum((r.transactions() for r in requests), []), + header_verify=Header( + requests_root=included_requests, + ), + requests=block_body_override_requests, + exception=exception, + ) + ], + ) diff --git a/tests/prague/eip7685_general_purpose_el_requests/conftest.py b/tests/prague/eip7685_general_purpose_el_requests/conftest.py index fe06e89b2e..4a4aa61e05 100644 --- a/tests/prague/eip7685_general_purpose_el_requests/conftest.py +++ b/tests/prague/eip7685_general_purpose_el_requests/conftest.py @@ -13,10 +13,16 @@ WithdrawalRequest, WithdrawalRequestInteractionBase, ) +from ..eip7251_consolidations.helpers import ( + ConsolidationRequest, + ConsolidationRequestInteractionBase, +) @pytest.fixture -def block_body_override_requests() -> List[DepositRequest] | None: +def block_body_override_requests() -> List[ + DepositRequest | WithdrawalRequest | ConsolidationRequest +] | None: """List of requests that overwrite the requests in the header. None by default.""" return None @@ -30,27 +36,38 @@ def exception() -> BlockException | None: @pytest.fixture def blocks( pre: Alloc, - requests: List[DepositInteractionBase | WithdrawalRequestInteractionBase], - block_body_override_requests: List[DepositRequest | WithdrawalRequest] | None, + requests: List[ + DepositInteractionBase + | WithdrawalRequestInteractionBase + | ConsolidationRequestInteractionBase + ], + block_body_override_requests: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] + | None, exception: BlockException | None, ) -> List[Block]: """List of blocks that comprise the test.""" included_deposit_requests = [] included_withdrawal_requests = [] + included_consolidation_requests = [] # Single block therefore base fee withdrawal_request_fee = 1 + consolidation_request_fee = 1 for r in requests: r.update_pre(pre) if isinstance(r, DepositInteractionBase): included_deposit_requests += r.valid_requests(10**18) elif isinstance(r, WithdrawalRequestInteractionBase): included_withdrawal_requests += r.valid_requests(withdrawal_request_fee) + elif isinstance(r, ConsolidationRequestInteractionBase): + included_consolidation_requests += r.valid_requests(consolidation_request_fee) return [ Block( txs=sum((r.transactions() for r in requests), []), header_verify=Header( - requests_root=included_deposit_requests + included_withdrawal_requests, + requests_root=included_deposit_requests + + included_withdrawal_requests + + included_consolidation_requests, ), requests=block_body_override_requests, exception=exception, diff --git a/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals.py b/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals.py deleted file mode 100644 index ca63e8b9a7..0000000000 --- a/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals.py +++ /dev/null @@ -1,402 +0,0 @@ -""" -abstract: Tests [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) - Cross testing for withdrawal and deposit request for [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) - -""" # noqa: E501 - -from typing import List - -import pytest - -from ethereum_test_tools import ( - Alloc, - Block, - BlockchainTestFiller, - BlockException, - Environment, - Header, -) -from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import TestAddress, Transaction - -from ..eip6110_deposits.helpers import DepositContract, DepositRequest, DepositTransaction -from ..eip6110_deposits.spec import Spec as Spec_EIP6110 -from ..eip7002_el_triggerable_withdrawals.helpers import ( - WithdrawalRequest, - WithdrawalRequestContract, - WithdrawalRequestTransaction, -) -from ..eip7002_el_triggerable_withdrawals.spec import Spec as Spec_EIP7002 -from .spec import ref_spec_7685 - -REFERENCE_SPEC_GIT_PATH = ref_spec_7685.git_path -REFERENCE_SPEC_VERSION = ref_spec_7685.version - -pytestmark = pytest.mark.valid_from("Prague") - - -@pytest.mark.parametrize( - "requests", - [ - pytest.param( - [ - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - ], - id="single_deposit_from_eoa_single_withdrawal_from_eoa", - ), - pytest.param( - [ - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - ], - id="single_withdrawal_from_eoa_single_deposit_from_eoa", - ), - pytest.param( - [ - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x1, - ) - ], - ), - ], - id="two_deposits_from_eoa_single_withdrawal_from_eoa", - ), - pytest.param( - [ - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=1, - fee=1, - ) - ], - ), - ], - id="two_withdrawals_from_eoa_single_deposit_from_eoa", - ), - pytest.param( - [ - DepositContract( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - WithdrawalRequestContract( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - ], - id="single_deposit_from_contract_single_withdrawal_from_contract", - ), - pytest.param( - [ - WithdrawalRequestContract( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - DepositContract( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - ], - id="single_withdrawal_from_contract_single_deposit_from_contract", - ), - # TODO: Deposit and withdrawal in the same transaction - ], -) -def test_valid_deposit_withdrawal_requests( - blockchain_test: BlockchainTestFiller, - pre: Alloc, - blocks: List[Block], -): - """ - Test making a deposit to the beacon chain deposit contract and a withdrawal in the same block. - """ - blockchain_test( - genesis_environment=Environment(), - pre=pre, - post={}, - blocks=blocks, - ) - - -@pytest.mark.parametrize( - "deposit_first", - [ - pytest.param(True, id="deposit_first"), - pytest.param(False, id="withdrawal_first"), - ], -) -def test_valid_deposit_withdrawal_request_from_same_tx( - blockchain_test: BlockchainTestFiller, - pre: Alloc, - deposit_first: bool, -): - """ - Test making a deposit to the beacon chain deposit contract and a withdrawal in the same tx. - """ - withdrawal_request_fee = 1 - deposit_request = DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - withdrawal_request = WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - ) - if deposit_first: - calldata = deposit_request.calldata + withdrawal_request.calldata - contract_code = ( - Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) - + Op.POP( - Op.CALL( - Op.GAS, - Spec_EIP6110.DEPOSIT_CONTRACT_ADDRESS, - deposit_request.value, - 0, - len(deposit_request.calldata), - 0, - 0, - ) - ) - + Op.POP( - Op.CALL( - Op.GAS, - Spec_EIP7002.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, - withdrawal_request_fee, - len(deposit_request.calldata), - len(withdrawal_request.calldata), - 0, - 0, - ) - ) - ) - else: - calldata = withdrawal_request.calldata + deposit_request.calldata - contract_code = ( - Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) - + Op.POP( - Op.CALL( - Op.GAS, - Spec_EIP7002.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, - withdrawal_request_fee, - 0, - len(withdrawal_request.calldata), - 0, - 0, - ) - ) - + Op.POP( - Op.CALL( - Op.GAS, - Spec_EIP6110.DEPOSIT_CONTRACT_ADDRESS, - deposit_request.value, - len(withdrawal_request.calldata), - len(deposit_request.calldata), - 0, - 0, - ) - ) - ) - - sender = pre.fund_eoa(10**18) - contract_address = pre.deploy_contract( - code=contract_code, - balance=deposit_request.value + withdrawal_request_fee, - ) - withdrawal_request = withdrawal_request.with_source_address(contract_address) - - tx = Transaction( - gas_limit=1_000_000, - gas_price=0x07, - to=contract_address, - value=0, - data=calldata, - sender=sender, - ) - - blockchain_test( - genesis_environment=Environment(), - pre=pre, - post={}, - blocks=[ - Block( - txs=[tx], - header_verify=Header( - requests_root=[deposit_request, withdrawal_request], - ), - ) - ], - ) - - -@pytest.mark.parametrize( - "requests,block_body_override_requests,exception", - [ - pytest.param( - [ - WithdrawalRequestTransaction( - requests=[ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - fee=1, - ) - ], - ), - DepositTransaction( - requests=[ - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ) - ], - ), - ], - [ - WithdrawalRequest( - validator_pubkey=0x01, - amount=0, - source_address=TestAddress, - ), - DepositRequest( - pubkey=0x01, - withdrawal_credentials=0x02, - amount=32_000_000_000, - signature=0x03, - index=0x0, - ), - ], - # TODO: on the Engine API, the issue should be detected as an invalid block hash - BlockException.INVALID_REQUESTS, - id="single_deposit_from_eoa_single_withdrawal_from_eoa_incorrect_order", - ), - ], -) -def test_invalid_deposit_withdrawal_requests( - blockchain_test: BlockchainTestFiller, - pre: Alloc, - blocks: List[Block], -): - """ - Negative testing for deposits and withdrawals in the same block. - """ - blockchain_test( - genesis_environment=Environment(), - pre=pre, - post={}, - blocks=blocks, - ) diff --git a/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals_consolidations.py b/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals_consolidations.py new file mode 100644 index 0000000000..1b8d12e415 --- /dev/null +++ b/tests/prague/eip7685_general_purpose_el_requests/test_deposits_withdrawals_consolidations.py @@ -0,0 +1,376 @@ +""" +abstract: Tests [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) + Cross testing for withdrawal and deposit request for [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) + +""" # noqa: E501 + +from itertools import permutations +from typing import Any, Generator, List + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + BlockException, + Bytecode, + Environment, + Header, +) +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import Storage, TestAddress, TestAddress2, Transaction + +from ..eip6110_deposits.helpers import DepositContract, DepositRequest, DepositTransaction +from ..eip6110_deposits.spec import Spec as Spec_EIP6110 +from ..eip7002_el_triggerable_withdrawals.helpers import ( + WithdrawalRequest, + WithdrawalRequestContract, + WithdrawalRequestTransaction, +) +from ..eip7002_el_triggerable_withdrawals.spec import Spec as Spec_EIP7002 +from ..eip7251_consolidations.helpers import ( + ConsolidationRequest, + ConsolidationRequestContract, + ConsolidationRequestTransaction, +) +from ..eip7251_consolidations.spec import Spec as Spec_EIP7251 +from .spec import ref_spec_7685 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7685.git_path +REFERENCE_SPEC_VERSION = ref_spec_7685.version + +pytestmark = pytest.mark.valid_from("Prague") + + +def single_deposit(i: int) -> DepositRequest: # noqa: D103 + return DepositRequest( + pubkey=(i * 3), + withdrawal_credentials=(i * 3) + 1, + amount=32_000_000_000, + signature=(i * 3) + 2, + index=i, + ) + + +def single_deposit_from_eoa(i: int) -> DepositTransaction: # noqa: D103 + return DepositTransaction(requests=[single_deposit(i)]) + + +def single_deposit_from_contract(i: int) -> DepositContract: # noqa: D103 + return DepositContract(requests=[single_deposit(i)]) + + +def single_withdrawal(i: int) -> WithdrawalRequest: # noqa: D103 + return WithdrawalRequest( + validator_pubkey=i + 1, + amount=0, + fee=1, + ) + + +def single_withdrawal_from_eoa(i: int) -> WithdrawalRequestTransaction: # noqa: D103 + return WithdrawalRequestTransaction(requests=[single_withdrawal(i)]) + + +def single_withdrawal_from_contract(i: int) -> WithdrawalRequestContract: # noqa: D103 + return WithdrawalRequestContract(requests=[single_withdrawal(i)]) + + +def single_consolidation(i: int) -> ConsolidationRequest: # noqa: D103 + return ConsolidationRequest( + source_pubkey=(i * 2), + target_pubkey=(i * 2) + 1, + fee=1, + ) + + +def single_consolidation_from_eoa(i: int) -> ConsolidationRequestTransaction: # noqa: D103 + return ConsolidationRequestTransaction(requests=[single_consolidation(i)]) + + +def single_consolidation_from_contract(i: int) -> ConsolidationRequestContract: # noqa: D103 + return ConsolidationRequestContract(requests=[single_consolidation(i)]) + + +def get_permutations(n: int = 3) -> Generator[Any, None, None]: + """ + Returns all possible permutations of the requests from an EOA. + """ + requests = [ + ( + "deposit", + single_deposit(0), + ), + ( + "withdrawal", + single_withdrawal(0), + ), + ( + "consolidation", + single_consolidation(0), + ), + ] + for perm in permutations(requests, n): + yield pytest.param([p[1] for p in perm], id="+".join([p[0] for p in perm])) + + +def get_eoa_permutations(n: int = 3) -> Generator[Any, None, None]: + """ + Returns all possible permutations of the requests from an EOA. + """ + requests = [ + ( + "deposit_from_eoa", + single_deposit_from_eoa(0), + ), + ( + "withdrawal_from_eoa", + single_withdrawal_from_eoa(0), + ), + ( + "consolidation_from_eoa", + single_consolidation_from_eoa(0), + ), + ] + for perm in permutations(requests, n): + yield pytest.param([p[1] for p in perm], id="+".join([p[0] for p in perm])) + + +def get_contract_permutations(n: int = 3) -> Generator[Any, None, None]: + """ + Returns all possible permutations of the requests from a contract. + """ + requests = [ + ( + "deposit_from_contract", + single_deposit_from_contract(0), + ), + ( + "withdrawal_from_contract", + single_withdrawal_from_contract(0), + ), + ( + "consolidation_from_contract", + single_consolidation_from_contract(0), + ), + ] + for perm in permutations(requests, n): + yield pytest.param([p[1] for p in perm], id="+".join([p[0] for p in perm])) + + +@pytest.mark.parametrize( + "requests", + [ + *get_eoa_permutations(), + *get_contract_permutations(), + pytest.param( + [ + single_deposit_from_eoa(0), + single_withdrawal_from_eoa(0), + single_deposit_from_contract(1), + ], + id="deposit_from_eoa+withdrawal_from_eoa+deposit_from_contract", + ), + pytest.param( + [ + single_withdrawal_from_eoa(0), + single_deposit_from_eoa(0), + single_withdrawal_from_contract(1), + ], + id="withdrawal_from_eoa+deposit_from_eoa+withdrawal_from_contract", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + single_consolidation_from_eoa(0), + single_deposit_from_contract(1), + ], + id="deposit_from_eoa+consolidation_from_eoa+deposit_from_contract", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + single_deposit_from_eoa(0), + single_consolidation_from_contract(1), + ], + marks=pytest.mark.skip("Only one consolidation request is allowed per block"), + id="consolidation_from_eoa+deposit_from_eoa+consolidation_from_contract", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + single_withdrawal_from_eoa(0), + single_consolidation_from_contract(1), + ], + marks=pytest.mark.skip("Only one consolidation request is allowed per block"), + id="consolidation_from_eoa+withdrawal_from_eoa+consolidation_from_contract", + ), + pytest.param( + [ + single_withdrawal_from_eoa(0), + single_consolidation_from_eoa(0), + single_withdrawal_from_contract(1), + ], + id="withdrawal_from_eoa+consolidation_from_eoa+withdrawal_from_contract", + ), + ], +) +def test_valid_deposit_withdrawal_consolidation_requests( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + blocks: List[Block], +): + """ + Test making a deposit to the beacon chain deposit contract and a withdrawal in the same block. + """ + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) + + +@pytest.mark.parametrize("requests", [*get_permutations()]) +def test_valid_deposit_withdrawal_consolidation_request_from_same_tx( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + requests: List[DepositRequest | WithdrawalRequest | ConsolidationRequest], +): + """ + Test making a deposit to the beacon chain deposit contract and a withdrawal in the same tx. + """ + withdrawal_request_fee = 1 + consolidation_request_fee = 1 + + calldata = b"" + contract_code = Bytecode() + total_value = 0 + storage = Storage() + + for request in requests: + calldata_start = len(calldata) + current_calldata = request.calldata + calldata += current_calldata + + contract_code += Op.CALLDATACOPY(0, calldata_start, len(current_calldata)) + + call_contract_address = 0 + value = 0 + if isinstance(request, DepositRequest): + call_contract_address = Spec_EIP6110.DEPOSIT_CONTRACT_ADDRESS + value = request.value + elif isinstance(request, WithdrawalRequest): + call_contract_address = Spec_EIP7002.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS + value = withdrawal_request_fee + elif isinstance(request, ConsolidationRequest): + call_contract_address = Spec_EIP7251.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS + value = consolidation_request_fee + + total_value += value + + contract_code += Op.SSTORE( + storage.store_next(1), + Op.CALL( + address=call_contract_address, + value=value, + args_offset=0, + args_size=len(current_calldata), + ), + ) + + sender = pre.fund_eoa() + contract_address = pre.deploy_contract( + code=contract_code, + ) + + tx = Transaction( + gas_limit=10_000_000, + to=contract_address, + value=total_value, + data=calldata, + sender=sender, + ) + + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={ + contract_address: Account( + storage=storage, + ) + }, + blocks=[ + Block( + txs=[tx], + header_verify=Header( + requests_root=[ + request.with_source_address(contract_address) + for request in sorted(requests, key=lambda r: r.type_byte()) + ] + ), + ) + ], + ) + + +@pytest.mark.parametrize( + "requests,block_body_override_requests,exception", + [ + pytest.param( + [ + single_withdrawal_from_eoa(0), + single_deposit_from_eoa(0), + ], + [ + single_withdrawal(0).with_source_address(TestAddress), + single_deposit(0), + ], + # TODO: on the Engine API, the issue should be detected as an invalid block hash + BlockException.INVALID_REQUESTS, + id="single_withdrawal_single_deposit_incorrect_order", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + single_deposit_from_eoa(0), + ], + [ + single_consolidation(0).with_source_address(TestAddress), + single_deposit(0), + ], + # TODO: on the Engine API, the issue should be detected as an invalid block hash + BlockException.INVALID_REQUESTS, + id="single_consolidation_single_deposit_incorrect_order", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + single_withdrawal_from_eoa(0), + ], + [ + single_consolidation(0).with_source_address(TestAddress), + single_withdrawal(0).with_source_address(TestAddress2), + ], + # TODO: on the Engine API, the issue should be detected as an invalid block hash + BlockException.INVALID_REQUESTS, + id="single_consolidation_single_withdrawal_incorrect_order", + ), + ], +) +def test_invalid_deposit_withdrawal_consolidation_requests( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + blocks: List[Block], +): + """ + Negative testing for deposits and withdrawals in the same block. + """ + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) diff --git a/whitelist.txt b/whitelist.txt index 40f8197aec..cb65a23331 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -107,6 +107,7 @@ eips EIPs eip6110 eip7002 +eip7251 el endianness EngineAPI @@ -134,6 +135,7 @@ extcodehash extcodesize F00 filesystem +fillvalue firstlineno fn fname