From 0f56ad485749f7c385cca89d6ee6b6e4d923cb7f Mon Sep 17 00:00:00 2001 From: winsvega Date: Mon, 28 Oct 2024 13:36:23 +0100 Subject: [PATCH 1/4] fix(ci): print fill output in coverage workflow on errors (#919) --- .github/workflows/coverage.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index e1874f6f56..93d0f0044d 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -179,8 +179,7 @@ jobs: mkdir -p fixtures/eof_tests echo "uv run fill $files --until=Cancun --evm-bin evmone-t8n >> filloutput.log 2>&1" - uv run fill $files --until=Cancun --evm-bin evmone-t8n >> filloutput.log 2>&1 - cat filloutput.log + uv run fill $files --until=Cancun --evm-bin evmone-t8n > >(tee -a filloutput.log) 2> >(tee -a filloutput.log >&2) if grep -q "FAILURES" filloutput.log; then echo "Error: failed to generate .py tests." @@ -235,8 +234,7 @@ jobs: if [ -n "$files_fixed" ]; then echo "uv run fill $files_fixed --until=Cancun --evm-bin evmone-t8n >> filloutput.log 2>&1" - uv run fill $files_fixed --until=Cancun --evm-bin evmone-t8n >> filloutput.log 2>&1 - cat filloutput.log + uv run fill $files_fixed --until=Cancun --evm-bin evmone-t8n > >(tee -a filloutput.log) 2> >(tee -a filloutput.log >&2) if grep -q "FAILURES" filloutput.log; then echo "Error: failed to generate .py tests from before the PR." From 358a26f1583efe6c98011b50996ebe473b052bdd Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:24:44 +0100 Subject: [PATCH 2/4] new(tests): Explicit test for EXTDELEGATECALL value cost (#911) --- .../eip7069_extcall/test_gas.py | 44 +++++++++++++++ tests/osaka/eip7692_eof_v1/gas_test.py | 53 +++++++++++-------- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py index 43196df560..b2417fb711 100644 --- a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py +++ b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py @@ -140,3 +140,47 @@ def test_ext_calls_gas( cold_gas=cold_gas + cost_memory_bytes(mem_expansion_bytes, 0), warm_gas=warm_gas + cost_memory_bytes(mem_expansion_bytes, 0), ) + + +@pytest.mark.parametrize("opcode", [Op.EXTCALL, Op.EXTDELEGATECALL, Op.EXTSTATICCALL]) +@pytest.mark.parametrize("value", [0, 1]) +def test_transfer_gas_is_cleared( + state_test: StateTestFiller, + pre: Alloc, + state_env: Environment, + opcode: Op, + value: int, +): + """ + Test that EXT*CALL call doesn't charge for value transfer, even if the outer call + transfered value. + + NOTE: This is particularly possible for EXTDELEGATECALL, which carries over the value sent + in the outer call, however, we extend the test to all 3 EXT*CALL opcodes for good measure. + """ + noop_callee_address = pre.deploy_contract(Container.Code(Op.STOP)) + + extdelegatecall_contract_address = pre.deploy_contract( + Container.Code(opcode(address=noop_callee_address) + Op.STOP) + ) + + push_gas = (4 if opcode == Op.EXTCALL else 3) * 3 + + gas_test( + state_test, + state_env, + pre, + setup_code=Op.PUSH1(value) + Op.PUSH0 * 2 + Op.PUSH20(extdelegatecall_contract_address), + subject_code=Op.EXTCALL, + subject_balance=5 * value, + tear_down_code=Op.STOP, + # NOTE: CALL_WITH_VALUE_GAS is charged only once on the outer EXTCALL, while the base + # call gas - twice. + cold_gas=2 * COLD_ACCOUNT_ACCESS_GAS + + (CALL_WITH_VALUE_GAS if value > 0 else 0) + + push_gas, + warm_gas=2 * WARM_ACCOUNT_ACCESS_GAS + + (CALL_WITH_VALUE_GAS if value > 0 else 0) + + push_gas, + out_of_gas_testing=False, + ) diff --git a/tests/osaka/eip7692_eof_v1/gas_test.py b/tests/osaka/eip7692_eof_v1/gas_test.py index 9e7d0bb602..c381db0a42 100644 --- a/tests/osaka/eip7692_eof_v1/gas_test.py +++ b/tests/osaka/eip7692_eof_v1/gas_test.py @@ -38,6 +38,7 @@ def gas_test( subject_address: Address | None = None, subject_balance: int = 0, oog_difference: int = 1, + out_of_gas_testing: bool = True, ): """ Creates a State Test to check the gas cost of a sequence of EOF code. @@ -104,27 +105,33 @@ def gas_test( + (Op.DUP3 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_warm_gas) + Op.SSTORE) # store cold gas: DUP2 is the gas of the baseline gas run + (Op.DUP2 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_cold_gas) + Op.SSTORE) - # oog gas run: - # - DUP7 is the gas of the baseline gas run, after other CALL args were pushed - # - subtract the gas charged by the harness - # - add warm gas charged by the subject - # - subtract `oog_difference` to cause OOG exception (1 by default) - + Op.SSTORE( - slot_oog_call_result, - Op.CALL( - gas=Op.ADD(warm_gas - gas_single_gas_run - oog_difference, Op.DUP7), - address=address_subject, - ), - ) - # sanity gas run: not subtracting 1 to see if enough gas makes the call succeed - + Op.SSTORE( - slot_sanity_call_result, - Op.CALL( - gas=Op.ADD(warm_gas - gas_single_gas_run, Op.DUP7), - address=address_subject, - ), + + ( + ( + # do an oog gas run, unless skipped with `out_of_gas_testing=False`: + # - DUP7 is the gas of the baseline gas run, after other CALL args were pushed + # - subtract the gas charged by the harness + # - add warm gas charged by the subject + # - subtract `oog_difference` to cause OOG exception (1 by default) + Op.SSTORE( + slot_oog_call_result, + Op.CALL( + gas=Op.ADD(warm_gas - gas_single_gas_run - oog_difference, Op.DUP7), + address=address_subject, + ), + ) + # sanity gas run: not subtracting 1 to see if enough gas makes the call succeed + + Op.SSTORE( + slot_sanity_call_result, + Op.CALL( + gas=Op.ADD(warm_gas - gas_single_gas_run, Op.DUP7), + address=address_subject, + ), + ) + + Op.STOP + ) + if out_of_gas_testing + else Op.STOP ) - + Op.STOP ), evm_code_type=EVMCodeType.LEGACY, # Needs to be legacy to use GAS opcode ) @@ -134,12 +141,14 @@ def gas_test( storage={ slot_warm_gas: warm_gas, slot_cold_gas: cold_gas, - slot_oog_call_result: LEGACY_CALL_FAILURE, - slot_sanity_call_result: LEGACY_CALL_SUCCESS, }, ), } + if out_of_gas_testing: + post[address_legacy_harness].storage[slot_oog_call_result] = LEGACY_CALL_FAILURE + post[address_legacy_harness].storage[slot_sanity_call_result] = LEGACY_CALL_SUCCESS + tx = Transaction(to=address_legacy_harness, gas_limit=env.gas_limit, sender=sender) state_test(env=env, pre=pre, tx=tx, post=post) From db603382507f50164c533e779335fc6b188d35d4 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 28 Oct 2024 12:25:28 -0600 Subject: [PATCH 3/4] feat(fw,tests): EIP-6110, EIP-7002, EIP-7251, EIP-7702: Pectra Devnet-4 updates (#832) * fix(forks): Update 7002, 7251 contracts * feat(forks): Add 7685 methods * fix(fw): Remove requests from block body, add as parameters for new payload * refactor(tests): Refactor requests usages, add more 7685 tests * feat(tests): EIP-7702, #8929 changes - Incomplete * Update tests/prague/eip7702_set_code_tx/spec.py Co-authored-by: Jochem Brouwer * feat(exceptions): Add invalid authorization format exception * new(tests): EIP 7702: chain id/nonce overflow tests * new(tests): EIP 7702: delegation clearing test * new(tests): EIP 7702: delegation clearing on failure test * new(tests): EIP 7702: fixup * new(tests): EIP 7702: test deployting a delegation-like contract * fix(tests): EIP 7702: remove `test_set_code_to_zero_address` * new(tests): EIP 7702: add `test_signature_s_out_of_range` * fix(forks): EIP-7002,7251 contracts * new(tests): EIP-7002: withdrawal request during fork * fix(tests): EIP-7002: fixup * fix(tests): EIP-7002: fixup * new(tests): EIP-7251: consolidation requests during fork * fix(tests): tox * github: Add devnet-4 configs * fix(github): feature devnet-4 * fix(tests): EIP-6110 conftest * fix(github): feature * fix(specs): Propagate `block.requests` to the Engine API params * fix(tests): Fix override requests comparison for empty list * fix(tox): whitelist * fix(tests): EIP-7702: note in `test_tx_validity_nonce` * new(tests): EIP-7702: Add invalid `v` (27, 28) for auth tuple test * chore(hive): update hive client config file in test summary * fix(plugins/execute): Requests * fix(rpc): Support `engine_getPayloadV4` * fix(plugins/execute): Support `engine_getPayloadV4` * fix(tests): EIP-7702: test id * fix(tests): EIP-7702: execute marks --------- Co-authored-by: Jochem Brouwer Co-authored-by: danceratopz --- .github/configs/evm.yaml | 6 +- .github/configs/feature.yaml | 6 +- src/ethereum_clis/types.py | 15 +- src/ethereum_test_exceptions/exceptions.py | 4 + src/ethereum_test_fixtures/blockchain.py | 96 +-- .../tests/test_blockchain.py | 399 ++++++----- src/ethereum_test_forks/base_fork.py | 16 + .../forks/contracts/consolidation_request.bin | Bin 328 -> 414 bytes .../forks/contracts/withdrawal_request.bin | Bin 306 -> 500 bytes src/ethereum_test_forks/forks/forks.py | 47 +- src/ethereum_test_rpc/types.py | 1 + src/ethereum_test_specs/blockchain.py | 124 +--- src/ethereum_test_tools/__init__.py | 2 + src/ethereum_test_types/tests/test_types.py | 52 +- src/ethereum_test_types/types.py | 186 ++---- .../consume/hive_simulators/conftest.py | 4 +- src/pytest_plugins/execute/rpc/hive.py | 17 +- tests/prague/eip6110_deposits/conftest.py | 16 +- .../conftest.py | 63 +- .../contract_deploy_tx.json | 16 + .../spec.py | 7 +- .../test_withdrawal_requests.py | 26 +- .../test_withdrawal_requests_during_fork.py | 125 ++++ .../prague/eip7251_consolidations/conftest.py | 63 +- .../contract_deploy_tx.json | 16 + tests/prague/eip7251_consolidations/spec.py | 7 +- .../test_consolidations.py | 18 +- .../test_consolidations_during_fork.py | 125 ++++ .../conftest.py | 63 +- ...est_deposits_withdrawals_consolidations.py | 235 +++++-- .../test_request_types.py | 114 ++++ tests/prague/eip7702_set_code_tx/spec.py | 5 +- .../eip7702_set_code_tx/test_set_code_txs.py | 620 ++++++++++++++---- whitelist.txt | 1 + 34 files changed, 1745 insertions(+), 750 deletions(-) create mode 100644 tests/prague/eip7002_el_triggerable_withdrawals/contract_deploy_tx.json create mode 100644 tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py create mode 100644 tests/prague/eip7251_consolidations/contract_deploy_tx.json create mode 100644 tests/prague/eip7251_consolidations/test_consolidations_during_fork.py create mode 100644 tests/prague/eip7685_general_purpose_el_requests/test_request_types.py diff --git a/.github/configs/evm.yaml b/.github/configs/evm.yaml index cc82c57b05..9ec6866ab8 100644 --- a/.github/configs/evm.yaml +++ b/.github/configs/evm.yaml @@ -17,4 +17,8 @@ eip7692-osaka: pectra-devnet-3: impl: ethjs repo: ethereumjs/ethereumjs-monorepo - ref: t8ntool \ No newline at end of file + ref: t8ntool +pectra-devnet-4: + impl: ethjs + repo: ethereumjs/ethereumjs-monorepo + ref: 7702-devnet-4-plus-t8ntool \ No newline at end of file diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index ce8057e0ea..2ae652d4c6 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -20,5 +20,9 @@ eip7692-osaka: solc: 0.8.21 pectra-devnet-3: evm-type: pectra-devnet-3 - fill-params: --fork=Prague -m "not slow and not 2537" ./tests/prague/ + fill-params: --fork=Prague -m "not slow" ./tests/prague/ + solc: 0.8.21 +pectra-devnet-4: + evm-type: pectra-devnet-4 + fill-params: --fork=Prague -m "not slow and not eip_version_check" ./tests/prague/ solc: 0.8.21 \ No newline at end of file diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index aba3f99752..51263b1f39 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -7,14 +7,7 @@ from pydantic import Field from ethereum_test_base_types import Address, Bloom, Bytes, CamelModel, Hash, HexNumber -from ethereum_test_types import ( - Alloc, - ConsolidationRequest, - DepositRequest, - Environment, - Transaction, - WithdrawalRequest, -) +from ethereum_test_types import Alloc, Environment, Transaction class TransactionLog(CamelModel): @@ -94,10 +87,8 @@ class Result(CamelModel): withdrawals_root: Hash | None = None excess_blob_gas: HexNumber | None = Field(None, alias="currentExcessBlobGas") blob_gas_used: HexNumber | None = None - requests_root: Hash | None = None - deposit_requests: List[DepositRequest] | None = None - withdrawal_requests: List[WithdrawalRequest] | None = None - consolidation_requests: List[ConsolidationRequest] | None = None + requests_hash: Hash | None = None + requests: List[Bytes] | None = None class TransitionToolInput(CamelModel): diff --git a/src/ethereum_test_exceptions/exceptions.py b/src/ethereum_test_exceptions/exceptions.py index 94e02c3b8e..7271430dc3 100644 --- a/src/ethereum_test_exceptions/exceptions.py +++ b/src/ethereum_test_exceptions/exceptions.py @@ -382,6 +382,10 @@ class TransactionException(ExceptionBase): """ Transaction is a type 4 transaction and has an empty `to`. """ + TYPE_4_INVALID_AUTHORIZATION_FORMAT = auto() + """ + Transaction is type 4, but contains an authorization that has an invalid format. + """ @unique diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 9b770ce986..27de996526 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -26,18 +26,11 @@ from ethereum_test_forks import Fork, Paris from ethereum_test_types.types import ( AuthorizationTupleGeneric, - ConsolidationRequest, - ConsolidationRequestGeneric, - DepositRequest, - DepositRequestGeneric, - Requests, Transaction, TransactionFixtureConverter, TransactionGeneric, Withdrawal, WithdrawalGeneric, - WithdrawalRequest, - WithdrawalRequestGeneric, ) from .base import BaseFixture @@ -117,7 +110,7 @@ class FixtureHeader(CamelModel): parent_beacon_block_root: Annotated[Hash, HeaderForkRequirement("beacon_root")] | None = Field( None ) - requests_root: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None) + requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None) fork: Fork | None = Field(None, exclude=True) @@ -210,9 +203,6 @@ class FixtureExecutionPayload(CamelModel): transactions: List[Bytes] withdrawals: List[Withdrawal] | None = None - deposit_requests: List[DepositRequest] | None = None - withdrawal_requests: List[WithdrawalRequest] | None = None - consolidation_requests: List[ConsolidationRequest] | None = None @classmethod def from_fixture_header( @@ -220,7 +210,6 @@ def from_fixture_header( header: FixtureHeader, transactions: List[Transaction], withdrawals: List[Withdrawal] | None, - requests: Requests | None, ) -> "FixtureExecutionPayload": """ Returns a FixtureExecutionPayload from a FixtureHeader, a list @@ -230,20 +219,20 @@ def from_fixture_header( **header.model_dump(exclude={"rlp"}, exclude_none=True), transactions=[tx.rlp for tx in transactions], withdrawals=withdrawals, - deposit_requests=requests.deposit_requests() if requests is not None else None, - withdrawal_requests=requests.withdrawal_requests() if requests is not None else None, - consolidation_requests=requests.consolidation_requests() - if requests is not None - else None, ) EngineNewPayloadV1Parameters = Tuple[FixtureExecutionPayload] EngineNewPayloadV3Parameters = Tuple[FixtureExecutionPayload, List[Hash], Hash] +EngineNewPayloadV4Parameters = Tuple[FixtureExecutionPayload, List[Hash], Hash, List[Bytes]] # Important: We check EngineNewPayloadV3Parameters first as it has more fields, and pydantic # has a weird behavior when the smaller tuple is checked first. -EngineNewPayloadParameters = Union[EngineNewPayloadV3Parameters, EngineNewPayloadV1Parameters] +EngineNewPayloadParameters = Union[ + EngineNewPayloadV4Parameters, + EngineNewPayloadV3Parameters, + EngineNewPayloadV1Parameters, +] class FixtureEngineNewPayload(CamelModel): @@ -280,7 +269,7 @@ def from_fixture_header( header: FixtureHeader, transactions: List[Transaction], withdrawals: List[Withdrawal] | None, - requests: Requests | None, + requests: List[Bytes] | None, **kwargs, ) -> "FixtureEngineNewPayload": """ @@ -296,10 +285,21 @@ def from_fixture_header( header=header, transactions=transactions, withdrawals=withdrawals, - requests=requests, ) - params: Tuple[FixtureExecutionPayload] | Tuple[FixtureExecutionPayload, List[Hash], Hash] - if fork.engine_new_payload_blob_hashes(header.number, header.timestamp): + params: EngineNewPayloadParameters + if ( + fork.engine_new_payload_requests(header.number, header.timestamp) + and requests is not None + ): + parent_beacon_block_root = header.parent_beacon_block_root + assert parent_beacon_block_root is not None + params = ( + execution_payload, + Transaction.list_blob_versioned_hashes(transactions), + parent_beacon_block_root, + requests, + ) + elif fork.engine_new_payload_blob_hashes(header.number, header.timestamp): parent_beacon_block_root = header.parent_beacon_block_root assert parent_beacon_block_root is not None params = ( @@ -366,50 +366,6 @@ def from_withdrawal(cls, w: WithdrawalGeneric) -> "FixtureWithdrawal": return cls(**w.model_dump()) -class FixtureDepositRequest(DepositRequestGeneric[ZeroPaddedHexNumber]): - """ - Structure to represent a single deposit request to be processed by the beacon - chain. - """ - - @classmethod - def from_deposit_request(cls, d: DepositRequestGeneric) -> "FixtureDepositRequest": - """ - Returns a FixtureDepositRequest from a DepositRequest. - """ - return cls(**d.model_dump()) - - -class FixtureWithdrawalRequest(WithdrawalRequestGeneric[ZeroPaddedHexNumber]): - """ - Structure to represent a single withdrawal request to be processed by the beacon - chain. - """ - - @classmethod - def from_withdrawal_request(cls, d: WithdrawalRequestGeneric) -> "FixtureWithdrawalRequest": - """ - Returns a FixtureWithdrawalRequest from a WithdrawalRequest. - """ - return cls(**d.model_dump()) - - -class FixtureConsolidationRequest(ConsolidationRequestGeneric[ZeroPaddedHexNumber]): - """ - Structure to represent a single consolidation request to be processed by the beacon - chain. - """ - - @classmethod - def from_consolidation_request( - cls, d: ConsolidationRequestGeneric - ) -> "FixtureConsolidationRequest": - """ - Returns a FixtureConsolidationRequest from a ConsolidationRequest. - """ - return cls(**d.model_dump()) - - class FixtureBlockBase(CamelModel): """Representation of an Ethereum block within a test Fixture without RLP bytes.""" @@ -417,9 +373,6 @@ class FixtureBlockBase(CamelModel): txs: List[FixtureTransaction] = Field(default_factory=list, alias="transactions") ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders") withdrawals: List[FixtureWithdrawal] | None = None - deposit_requests: List[FixtureDepositRequest] | None = None - withdrawal_requests: List[FixtureWithdrawalRequest] | None = None - consolidation_requests: List[FixtureConsolidationRequest] | None = None @computed_field(alias="blocknumber") # type: ignore[misc] @cached_property @@ -429,7 +382,7 @@ def block_number(self) -> Number: """ return Number(self.header.number) - def with_rlp(self, txs: List[Transaction], requests: Requests | None) -> "FixtureBlock": + def with_rlp(self, txs: List[Transaction]) -> "FixtureBlock": """ Returns a FixtureBlock with the RLP bytes set. """ @@ -442,9 +395,6 @@ def with_rlp(self, txs: List[Transaction], requests: Requests | None) -> "Fixtur if self.withdrawals is not None: block.append([w.to_serializable_list() for w in self.withdrawals]) - if requests is not None: - block.append(requests.to_serializable_list()) - return FixtureBlock( **self.model_dump(), rlp=eth_rlp.encode(block), diff --git a/src/ethereum_test_fixtures/tests/test_blockchain.py b/src/ethereum_test_fixtures/tests/test_blockchain.py index ab01f5f946..5276c1835b 100644 --- a/src/ethereum_test_fixtures/tests/test_blockchain.py +++ b/src/ethereum_test_fixtures/tests/test_blockchain.py @@ -601,7 +601,6 @@ ).with_signature_and_sender(), ], withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], - requests=None, ), { "parentHash": Hash(0).hex(), @@ -668,6 +667,7 @@ blob_gas_used=17, excess_blob_gas=18, parent_beacon_block_root=19, + requests_hash=20, ), transactions=[ Transaction( @@ -687,26 +687,25 @@ ], withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], requests=Requests( - [ - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ), - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ), - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ), - ] - ), + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list, validation_error=[ BlockException.INCORRECT_BLOCK_FORMAT, TransactionException.INTRINSIC_GAS_TOO_LOW, @@ -731,7 +730,7 @@ "blobGasUsed": hex(17), "excessBlobGas": hex(18), "blockHash": ( - "0x8eca4747db6a4b272018f2850e4208b863989ce9971bb1907467ae2204950695" + "0x93bd662d8a80a1f54bffc6d140b83d6cda233209998809f9540be51178b4d0b6" ), "transactions": [ Transaction( @@ -761,41 +760,35 @@ ) ) ], - "depositRequests": [ - to_json( - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ) - ), - ], - "withdrawalRequests": [ - to_json( - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ) - ), - ], - "consolidationRequests": [ - to_json( - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ) - ), - ], }, [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000001", + str(Hash(0)), + str(Hash(1)), ], str(Hash(19)), + [ + Bytes(r).hex() + for r in Requests( + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list + ], ], "forkchoiceUpdatedVersion": "3", "newPayloadVersion": "4", @@ -831,22 +824,7 @@ blob_gas_used=17, excess_blob_gas=18, parent_beacon_block_root=19, - requests_root=Requests( - [ - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ), - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ), - ] - ).trie_root, + requests_hash=20, ), transactions=[ Transaction( @@ -866,26 +844,25 @@ ], withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], requests=Requests( - [ - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ), - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ), - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ), - ] - ), + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list, validation_error=[ BlockException.INCORRECT_BLOCK_FORMAT, TransactionException.INTRINSIC_GAS_TOO_LOW, @@ -909,7 +886,7 @@ "blobGasUsed": hex(17), "excessBlobGas": hex(18), "blockHash": ( - "0x78a4bf2520248e0b403d343c32b6746a43da1ebcf3cc8de14b959bc9f461fe76" + "0x93bd662d8a80a1f54bffc6d140b83d6cda233209998809f9540be51178b4d0b6" ), "transactions": [ Transaction( @@ -939,41 +916,35 @@ ) ) ], - "depositRequests": [ - to_json( - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ) - ), - ], - "withdrawalRequests": [ - to_json( - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ) - ), - ], - "consolidationRequests": [ - to_json( - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ) - ), - ], }, [ "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000001", ], str(Hash(19)), + [ + Bytes(r).hex() + for r in Requests( + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list + ], ], "newPayloadVersion": "4", "forkchoiceUpdatedVersion": "3", @@ -1013,9 +984,10 @@ def test_json_deserialization( @pytest.mark.parametrize( - "adapter, type_instance, json_repr", + "can_be_deserialized, adapter, type_instance, json_repr", [ pytest.param( + True, EngineNewPayloadParametersAdapter, ( FixtureExecutionPayload.from_fixture_header( @@ -1057,7 +1029,6 @@ def test_json_deserialization( ).with_signature_and_sender(), ], withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], - requests=None, ), ), [ @@ -1104,6 +1075,7 @@ def test_json_deserialization( id="fixture_engine_new_payload_parameters_v1", ), pytest.param( + True, EngineNewPayloadParametersAdapter, ( FixtureExecutionPayload.from_fixture_header( @@ -1145,27 +1117,6 @@ def test_json_deserialization( ).with_signature_and_sender(), ], withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], - requests=Requests( - [ - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ), - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ), - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ), - ] - ), ), [Hash(1), Hash(2)], Hash(3), @@ -1209,40 +1160,146 @@ def test_json_deserialization( "withdrawals": [ to_json(Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)) ], - "depositRequests": [ - to_json( - DepositRequest( - pubkey=BLSPublicKey(0), - withdrawal_credentials=Hash(1), - amount=2, - signature=BLSSignature(3), - index=4, - ) - ), + }, + [Hash(1).hex(), Hash(2).hex()], + Hash(3).hex(), + ], + id="fixture_engine_new_payload_parameters_v3", + ), + pytest.param( + False, + EngineNewPayloadParametersAdapter, + ( + FixtureExecutionPayload.from_fixture_header( + header=FixtureHeader( + parent_hash=Hash(0), + ommers_hash=Hash(1), + fee_recipient=Address(2), + state_root=Hash(3), + transactions_trie=Hash(4), + receipts_root=Hash(5), + logs_bloom=Bloom(6), + difficulty=7, + number=8, + gas_limit=9, + gas_used=10, + timestamp=11, + extra_data=Bytes([12]), + prev_randao=Hash(13), + nonce=HeaderNonce(14), + base_fee_per_gas=15, + withdrawals_root=Hash(16), + blob_gas_used=17, + excess_blob_gas=18, + ), + transactions=[ + Transaction( + to=0x1234, + data=b"\x01\x00", + access_list=[ + AccessList( + address=0x1234, + storage_keys=[0, 1], + ) + ], + max_priority_fee_per_gas=10, + max_fee_per_gas=20, + max_fee_per_blob_gas=30, + blob_versioned_hashes=[0, 1], + ).with_signature_and_sender(), ], - "withdrawalRequests": [ - to_json( - WithdrawalRequest( - source_address=Address(0), - validator_pubkey=BLSPublicKey(1), - amount=2, - ) - ), + withdrawals=[Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)], + ), + [Hash(1), Hash(2)], + Hash(3), + Requests( + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list, + ), + [ + { + "parentHash": Hash(0).hex(), + "feeRecipient": Address(2).hex(), + "stateRoot": Hash(3).hex(), + "receiptsRoot": Hash(5).hex(), + "logsBloom": Bloom(6).hex(), + "blockNumber": hex(8), + "gasLimit": hex(9), + "gasUsed": hex(10), + "timestamp": hex(11), + "extraData": Bytes([12]).hex(), + "prevRandao": Hash(13).hex(), + "baseFeePerGas": hex(15), + "blobGasUsed": hex(17), + "excessBlobGas": hex(18), + "blockHash": "0xd90115b7fde329f64335763a446af1" + "50ab67e639281dccdb07a007d18bb80211", + "transactions": [ + Transaction( + to=0x1234, + data=b"\x01\x00", + access_list=[ + AccessList( + address=0x1234, + storage_keys=[0, 1], + ) + ], + max_priority_fee_per_gas=10, + max_fee_per_gas=20, + max_fee_per_blob_gas=30, + blob_versioned_hashes=[0, 1], + ) + .with_signature_and_sender() + .rlp.hex() ], - "consolidationRequests": [ - to_json( - ConsolidationRequest( - source_address=Address(0), - source_pubkey=BLSPublicKey(1), - target_pubkey=BLSPublicKey(2), - ) - ), + "withdrawals": [ + to_json(Withdrawal(index=0, validator_index=1, address=0x1234, amount=2)) ], }, [Hash(1).hex(), Hash(2).hex()], Hash(3).hex(), + [ + Bytes(r).hex() + for r in Requests( + DepositRequest( + pubkey=BLSPublicKey(0), + withdrawal_credentials=Hash(1), + amount=2, + signature=BLSSignature(3), + index=4, + ), + WithdrawalRequest( + source_address=Address(0), + validator_pubkey=BLSPublicKey(1), + amount=2, + ), + ConsolidationRequest( + source_address=Address(0), + source_pubkey=BLSPublicKey(1), + target_pubkey=BLSPublicKey(2), + ), + max_request_type=2, + ).requests_list + ], ], - id="fixture_engine_new_payload_parameters_v3", + id="fixture_engine_new_payload_parameters_v4", ), ], ) @@ -1253,6 +1310,7 @@ class TestPydanticAdaptersConversion: def test_json_serialization( self, + can_be_deserialized: bool, adapter: TypeAdapter, type_instance: Any, json_repr: str | Dict[str, Any], @@ -1272,6 +1330,7 @@ def test_json_serialization( def test_json_deserialization( self, + can_be_deserialized: bool, adapter: TypeAdapter, type_instance: Any, json_repr: str | Dict[str, Any], @@ -1279,4 +1338,6 @@ def test_json_deserialization( """ Test that to_json returns the expected JSON for the given object. """ + if not can_be_deserialized: + pytest.skip(reason="The model instance in this case can not be deserialized") assert adapter.validate_python(json_repr) == type_instance diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 53c161520f..1f468fba9c 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -261,6 +261,14 @@ def engine_new_payload_beacon_root(cls, block_number: int = 0, timestamp: int = """ pass + @classmethod + @abstractmethod + def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0) -> bool: + """ + Returns true if the engine api version requires new payload calls to include requests. + """ + pass + @classmethod @abstractmethod def engine_forkchoice_updated_version( @@ -320,6 +328,14 @@ def create_opcodes( """ pass + @classmethod + @abstractmethod + def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the max request type supported by the fork. + """ + pass + # Meta information about the fork @classmethod def name(cls) -> str: diff --git a/src/ethereum_test_forks/forks/contracts/consolidation_request.bin b/src/ethereum_test_forks/forks/contracts/consolidation_request.bin index 842fd006066c633a5bcd04427f632a122806ef37..d777e1c4a63f900f668ccdad22f3cbc625565179 100644 GIT binary patch literal 414 zcma)2yH3ME5M%il1?C+1b3~MM@i@B$8D=9s6k=_QiGJrp;3qzhk@D8yje(70vGBGjQkos zKku1IbLN!`V?#^=D8s9(K%V_DsGlnr(3>&RQ9D>s((i6}D!QpQ(Hvj+t delta 214 zcmWm8u}T9$6op~W%+|q$U9b@XX;P(-un2;coy{P#JA_-@Gw~2z7&1hqzPZ07P zK8%f63r@Prh5zt<+Aou?9^XUPwp<7G`oyLFN=G(r#6w`Mo|p}hRCKOe?!tt$H;27R zRVI)V?*~b5soGwaBEM`_Tj_cr5h_hry9dQ$!(}>$aP+(TH1nBu}3SIT@ACMkFyZ`_I diff --git a/src/ethereum_test_forks/forks/contracts/withdrawal_request.bin b/src/ethereum_test_forks/forks/contracts/withdrawal_request.bin index 426247e95129f85bab069f35320559d1080e11b1..200950fa01eb549fe39dbe7f5a662eb7cf4a5095 100644 GIT binary patch literal 500 zcma)(J5Iwu5Qdqxd6c$fBP3F!Oo@~SDJdvZSdN`7wwT`qC*S~CuQ|XPS_*E1I0J%; z10YfJm`y53!xX!pMl+*-{uy6?@5Wb#M?K4Pj5JqVV#pt6hIy7Z^sbR|W!Jk+zcbmU zl$-*2Htf z+WQm74ihSIG4Qyg7V|-iLt3^t6F3Mk64(zg6o>hVI3Xf(2Q&GXFAKj`E}nHXw1pIZ7LP)kVP#y{!yE`N*)06I&Nb?#TT414uB| AApigX delta 183 zcmWlSp$fuK7)JT81C!ujQ-eVaVls_kvWw+1wq4|g|J$3m&1Cwo%Qo==d;(u&&1%wM zbhZ=D;e2{`C6k?)I1I5goN|I0zio_l5EisD@Hqul3M6c~@Hx6yD|k X?i bool: + """ + At genesis, payloads do not have requests. + """ + return False + @classmethod def engine_forkchoice_updated_version( cls, block_number: int = 0, timestamp: int = 0 @@ -358,6 +365,13 @@ def create_opcodes( (Opcodes.CREATE, EVMCodeType.LEGACY), ] + @classmethod + def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + At genesis, no request type is supported, signaled by -1 + """ + return -1 + @classmethod def pre_allocation(cls) -> Mapping: """ @@ -842,11 +856,18 @@ def system_contracts(cls, block_number: int = 0, timestamp: int = 0) -> List[Add """ return [ Address(0x00000000219AB540356CBB839CBE05303D7705FA), - Address(0x00A3CA265EBCB825B45F985A16CEFB49958CE017), - Address(0x00B42DBF2194E931E80326D950320F7D9DBEAC02), + Address(0x09FC772D0857550724B07B850A4323F39112AAAA), + Address(0x01ABEA29659E5E97C95107F20BB753CD3E09BBBB), Address(0x0AAE40965E6800CD9B1F4B05FF21581047E3F91E), ] + super(Prague, cls).system_contracts(block_number, timestamp) + @classmethod + def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + At Prague, three request types are introduced, hence the max request type is 2 + """ + return 2 + @classmethod def pre_allocation_blockchain(cls) -> Mapping: """ @@ -878,7 +899,7 @@ def pre_allocation_blockchain(cls) -> Mapping: with open(CURRENT_FOLDER / "contracts" / "withdrawal_request.bin", mode="rb") as f: new_allocation.update( { - 0x00A3CA265EBCB825B45F985A16CEFB49958CE017: { + 0x09FC772D0857550724B07B850A4323F39112AAAA: { "nonce": 1, "code": f.read(), }, @@ -889,7 +910,7 @@ def pre_allocation_blockchain(cls) -> Mapping: with open(CURRENT_FOLDER / "contracts" / "consolidation_request.bin", mode="rb") as f: new_allocation.update( { - 0x00B42DBF2194E931E80326D950320F7D9DBEAC02: { + 0x01ABEA29659E5E97C95107F20BB753CD3E09BBBB: { "nonce": 1, "code": f.read(), }, @@ -912,8 +933,22 @@ def pre_allocation_blockchain(cls) -> Mapping: @classmethod def header_requests_required(cls, block_number: int, timestamp: int) -> bool: """ - Prague requires that the execution layer block contains the beacon - chain requests. + Prague requires that the execution layer header contains the beacon + chain requests hash. + """ + return True + + @classmethod + def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0) -> bool: + """ + Starting at Prague, new payloads include the requests hash as a parameter. + """ + return True + + @classmethod + def engine_new_payload_blob_hashes(cls, block_number: int = 0, timestamp: int = 0) -> bool: + """ + Starting at Prague, new payload directives must contain requests as parameter. """ return True diff --git a/src/ethereum_test_rpc/types.py b/src/ethereum_test_rpc/types.py index 66643a8c71..0cb0828d54 100644 --- a/src/ethereum_test_rpc/types.py +++ b/src/ethereum_test_rpc/types.py @@ -136,3 +136,4 @@ class GetPayloadResponse(CamelModel): execution_payload: FixtureExecutionPayload blobs_bundle: BlobsBundle | None = None + execution_requests: List[Bytes] | None = None diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 171c57e04a..64a0ccde74 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -34,27 +34,14 @@ Fixture, FixtureBlock, FixtureBlockBase, - FixtureConsolidationRequest, - FixtureDepositRequest, FixtureEngineNewPayload, FixtureHeader, FixtureTransaction, FixtureWithdrawal, - FixtureWithdrawalRequest, InvalidFixtureBlock, ) from ethereum_test_forks import Fork -from ethereum_test_types import ( - Alloc, - ConsolidationRequest, - DepositRequest, - Environment, - Removable, - Requests, - Transaction, - Withdrawal, - WithdrawalRequest, -) +from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal from .base import BaseTest, verify_result from .debugging import print_traces @@ -131,7 +118,7 @@ class Header(CamelModel): blob_gas_used: Removable | HexNumber | None = None excess_blob_gas: Removable | HexNumber | None = None parent_beacon_block_root: Removable | Hash | None = None - requests_root: Removable | Hash | None = None + requests_hash: Removable | Hash | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """ @@ -175,16 +162,6 @@ def validate_withdrawals_root(cls, value): return Withdrawal.list_root(value) return value - @field_validator("requests_root", mode="before") - @classmethod - def validate_requests_root(cls, value): - """ - Helper validator to convert a list of requests into the requests root hash. - """ - if isinstance(value, list): - return Requests(root=value).trie_root - return value - def apply(self, target: FixtureHeader) -> FixtureHeader: """ Produces a fixture header copy with the set values from the modifier. @@ -260,7 +237,7 @@ class Block(Header): """ List of withdrawals to perform for this block. """ - requests: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] | None = None + requests: List[Bytes] | None = None """ Custom list of requests to embed in this block. """ @@ -379,9 +356,9 @@ def make_genesis( Withdrawal.list_root(env.withdrawals) if env.withdrawals is not None else None ), parent_beacon_block_root=env.parent_beacon_block_root, - requests_root=( - Requests(root=[]).trie_root if fork.header_requests_required(0, 0) else None - ), + requests_hash=Requests(max_request_type=fork.max_request_type(0, 0)) + if fork.header_requests_required(0, 0) + else None, fork=fork, ) @@ -390,12 +367,7 @@ def make_genesis( FixtureBlockBase( header=genesis, withdrawals=None if env.withdrawals is None else [], - deposit_requests=[] if fork.header_requests_required(0, 0) else None, - withdrawal_requests=[] if fork.header_requests_required(0, 0) else None, - consolidation_requests=[] if fork.header_requests_required(0, 0) else None, - ).with_rlp( - txs=[], requests=Requests() if fork.header_requests_required(0, 0) else None - ), + ).with_rlp(txs=[]), ) def generate_block_data( @@ -406,7 +378,7 @@ def generate_block_data( previous_env: Environment, previous_alloc: Alloc, eips: Optional[List[int]] = None, - ) -> Tuple[FixtureHeader, List[Transaction], Requests | None, Alloc, Environment]: + ) -> Tuple[FixtureHeader, List[Transaction], List[Bytes] | None, Alloc, Environment]: """ Generate common block data for both make_fixture and make_hive_fixture. """ @@ -491,38 +463,36 @@ def generate_block_data( # Verify the header after transition tool processing. block.header_verify.verify(header) + requests_list: List[Bytes] | None = None + if fork.header_requests_required(header.number, header.timestamp): + assert ( + transition_tool_output.result.requests is not None + ), "Requests are required for this block" + requests = Requests(requests_lists=list(transition_tool_output.result.requests)) + + if Hash(requests) != header.requests_hash: + raise Exception( + "Requests root in header does not match the requests root in the transition " + "tool output: " + f"{header.requests_hash} != {Hash(requests)}" + ) + + requests_list = requests.requests_list + + if block.requests is not None: + header.requests_hash = Hash(Requests(requests_lists=list(block.requests))) + requests_list = block.requests + if block.rlp_modifier is not None: # Modify any parameter specified in the `rlp_modifier` after # transition tool processing. header = block.rlp_modifier.apply(header) header.fork = fork # Deleted during `apply` because `exclude=True` - requests = None - if fork.header_requests_required(header.number, header.timestamp): - requests_list: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] = [] - if transition_tool_output.result.deposit_requests is not None: - requests_list += transition_tool_output.result.deposit_requests - if transition_tool_output.result.withdrawal_requests is not None: - requests_list += transition_tool_output.result.withdrawal_requests - if transition_tool_output.result.consolidation_requests is not None: - requests_list += transition_tool_output.result.consolidation_requests - requests = Requests(root=requests_list) - - if requests is not None and requests.trie_root != header.requests_root: - raise Exception( - f"Requests root in header does not match the requests root in the transition tool " - "output: " - f"{header.requests_root} != {requests.trie_root}" - ) - - if block.requests is not None: - requests = Requests(root=block.requests) - header.requests_root = requests.trie_root - return ( header, txs, - requests, + requests_list, transition_tool_output.alloc, env, ) @@ -569,7 +539,7 @@ def make_fixture( # This is the most common case, the RLP needs to be constructed # based on the transactions to be included in the block. # Set the environment according to the block to execute. - header, txs, requests, new_alloc, new_env = self.generate_block_data( + header, txs, _, new_alloc, new_env = self.generate_block_data( t8n=t8n, fork=fork, block=block, @@ -581,37 +551,11 @@ def make_fixture( header=header, txs=[FixtureTransaction.from_transaction(tx) for tx in txs], ommers=[], - withdrawals=( - [FixtureWithdrawal.from_withdrawal(w) for w in new_env.withdrawals] - if new_env.withdrawals is not None - else None - ), - deposit_requests=( - [ - FixtureDepositRequest.from_deposit_request(d) - for d in requests.deposit_requests() - ] - if requests is not None - else None - ), - withdrawal_requests=( - [ - FixtureWithdrawalRequest.from_withdrawal_request(w) - for w in requests.withdrawal_requests() - ] - if requests is not None - else None - ), - consolidation_requests=( - [ - FixtureConsolidationRequest.from_consolidation_request(c) - for c in requests.consolidation_requests() - ] - if requests is not None - else None - ), + withdrawals=[FixtureWithdrawal.from_withdrawal(w) for w in new_env.withdrawals] + if new_env.withdrawals is not None + else None, fork=fork, - ).with_rlp(txs=txs, requests=requests) + ).with_rlp(txs=txs) if block.exception is None: fixture_blocks.append(fixture_block) # Update env, alloc and last block hash for the next block. diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index bcff5423ec..fa32d07980 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -45,6 +45,7 @@ DepositRequest, Environment, Removable, + Requests, Storage, TestParameterGroup, Transaction, @@ -128,6 +129,7 @@ "ReferenceSpec", "ReferenceSpecTypes", "Removable", + "Requests", "EOA", "StateTest", "StateTestFiller", diff --git a/src/ethereum_test_types/tests/test_types.py b/src/ethereum_test_types/tests/test_types.py index b7f916476a..68150fa9e4 100644 --- a/src/ethereum_test_types/tests/test_types.py +++ b/src/ethereum_test_types/tests/test_types.py @@ -5,22 +5,11 @@ from typing import Any, Dict, List import pytest -from pydantic import TypeAdapter from ethereum_test_base_types import Address, TestPrivateKey, to_json from ethereum_test_base_types.pydantic import CopyValidateModel -from ..types import ( - AccessList, - Account, - Alloc, - DepositRequest, - Environment, - Requests, - Storage, - Transaction, - Withdrawal, -) +from ..types import AccessList, Account, Alloc, Environment, Storage, Transaction, Withdrawal def test_storage(): @@ -833,45 +822,6 @@ def test_withdrawals_root(withdrawals: List[Withdrawal], expected_root: bytes): assert Withdrawal.list_root(withdrawals) == expected_root -@pytest.mark.parametrize( - ["json_str", "type_adapter", "expected"], - [ - pytest.param( - """ - [ - { - "type": "0x0", - "pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", - "withdrawalCredentials": "0x0000000000000000000000000000000000000000000000000000000000000002", - "amount": "0x1234", - "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003", - "index": "0x5678" - } - ] - """, # noqa: E501 - TypeAdapter(Requests), - Requests( - root=[ - DepositRequest( - pubkey=1, - withdrawal_credentials=2, - amount=0x1234, - signature=3, - index=0x5678, - ), - ] - ), - id="requests_1", - ), - ], -) -def test_parsing(json_str: str, type_adapter: TypeAdapter, expected: Any): - """ - Test that parsing the given JSON string returns the expected object. - """ - assert type_adapter.validate_json(json_str) == expected - - @pytest.mark.parametrize( "model", [ diff --git a/src/ethereum_test_types/types.py b/src/ethereum_test_types/types.py index d06a0883f5..e7f757523a 100644 --- a/src/ethereum_test_types/types.py +++ b/src/ethereum_test_types/types.py @@ -2,9 +2,10 @@ Useful types for generating Ethereum tests. """ +from abc import abstractmethod from dataclasses import dataclass from functools import cached_property -from typing import Any, ClassVar, Dict, Generic, List, Literal, Sequence, Tuple +from typing import Any, ClassVar, Dict, Generic, List, Literal, Sequence, SupportsBytes, Tuple from coincurve.keys import PrivateKey, PublicKey from ethereum import rlp as eth_rlp @@ -17,7 +18,6 @@ ConfigDict, Field, PrivateAttr, - RootModel, computed_field, model_serializer, model_validator, @@ -1124,173 +1124,129 @@ class RequestBase: Base class for requests. """ - @classmethod - def type_byte(cls) -> bytes: - """ - Returns the request type. - """ - raise NotImplementedError("request_type must be implemented in child classes") + type: ClassVar[int] - def to_serializable_list(self) -> List[Any]: + @abstractmethod + def __bytes__(self) -> bytes: """ - Returns the request's attributes as a list of serializable elements. + Returns the request's attributes as bytes. """ - raise NotImplementedError("to_serializable_list must be implemented in child classes") + ... -class DepositRequestGeneric(RequestBase, CamelModel, Generic[NumberBoundTypeVar]): +class DepositRequest(RequestBase, CamelModel): """ - Generic deposit type used as a parent for DepositRequest and FixtureDepositRequest. + Deposit Request type. """ pubkey: BLSPublicKey withdrawal_credentials: Hash - amount: NumberBoundTypeVar + amount: HexNumber signature: BLSSignature - index: NumberBoundTypeVar + index: HexNumber - @classmethod - def type_byte(cls) -> bytes: - """ - Returns the deposit request type. - """ - return b"\0" + type: ClassVar[int] = 0 - def to_serializable_list(self) -> List[Any]: + def __bytes__(self) -> bytes: """ - Returns the deposit's attributes as a list of serializable elements. + Returns the deposit's attributes as bytes. """ - return [ - self.pubkey, - self.withdrawal_credentials, - Uint(self.amount), - self.signature, - Uint(self.index), - ] - - -class DepositRequest(DepositRequestGeneric[HexNumber]): - """ - Deposit Request type - """ - - pass + return ( + bytes(self.pubkey) + + bytes(self.withdrawal_credentials) + + self.amount.to_bytes(8, "little") + + bytes(self.signature) + + self.index.to_bytes(8, "little") + ) -class WithdrawalRequestGeneric(RequestBase, CamelModel, Generic[NumberBoundTypeVar]): +class WithdrawalRequest(RequestBase, CamelModel): """ - Generic withdrawal request type used as a parent for WithdrawalRequest and - FixtureWithdrawalRequest. + Withdrawal Request type """ source_address: Address = Address(0) validator_pubkey: BLSPublicKey - amount: NumberBoundTypeVar + amount: HexNumber - @classmethod - def type_byte(cls) -> bytes: - """ - Returns the withdrawal request type. - """ - return b"\1" + type: ClassVar[int] = 1 - def to_serializable_list(self) -> List[Any]: + def __bytes__(self) -> bytes: """ - Returns the deposit's attributes as a list of serializable elements. + Returns the withdrawal's attributes as bytes. """ - return [ - self.source_address, - self.validator_pubkey, - Uint(self.amount), - ] - - -class WithdrawalRequest(WithdrawalRequestGeneric[HexNumber]): - """ - Withdrawal Request type - """ - - pass + return ( + bytes(self.source_address) + + bytes(self.validator_pubkey) + + self.amount.to_bytes(8, "little") + ) -class ConsolidationRequestGeneric(RequestBase, CamelModel, Generic[NumberBoundTypeVar]): +class ConsolidationRequest(RequestBase, CamelModel): """ - Generic consolidation request type used as a parent for ConsolidationRequest and - FixtureConsolidationRequest. + Consolidation Request type """ source_address: Address = Address(0) source_pubkey: BLSPublicKey target_pubkey: BLSPublicKey - @classmethod - def type_byte(cls) -> bytes: - """ - Returns the consolidation request type. - """ - return b"\2" + type: ClassVar[int] = 2 - def to_serializable_list(self) -> List[Any]: + def __bytes__(self) -> bytes: """ - Returns the consolidation's attributes as a list of serializable elements. + Returns the consolidation's attributes as bytes. """ - return [ - self.source_address, - self.source_pubkey, - self.target_pubkey, - ] + return bytes(self.source_address) + bytes(self.source_pubkey) + bytes(self.target_pubkey) -class ConsolidationRequest(ConsolidationRequestGeneric[HexNumber]): +def requests_list_to_bytes(requests_list: List[RequestBase] | Bytes | SupportsBytes) -> Bytes: """ - Consolidation Request type + Converts a list of requests to bytes. """ - - pass + if not isinstance(requests_list, list): + return Bytes(requests_list) + return Bytes(b"".join([bytes(r) for r in requests_list])) -class Requests(RootModel[List[DepositRequest | WithdrawalRequest | ConsolidationRequest]]): +class Requests: """ Requests for the transition tool. """ - root: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] = Field( - default_factory=list - ) + requests_list: List[Bytes] - def to_serializable_list(self) -> List[Any]: + def __init__( + self, + *requests: RequestBase, + max_request_type: int | None = None, + requests_lists: List[List[RequestBase] | Bytes] | None = None, + ): """ - Returns the requests as a list of serializable elements. + Initializes the requests object. """ - return [r.type_byte() + eth_rlp.encode(r.to_serializable_list()) for r in self.root] + if requests_lists is not None: + assert len(requests) == 0, "requests must be empty if list is provided" + self.requests_list = [] + for requests_list in requests_lists: + self.requests_list.append(requests_list_to_bytes(requests_list)) + return + else: - @cached_property - def trie_root(self) -> Hash: - """ - Returns the root hash of the requests. - """ - t = HexaryTrie(db={}) - for i, r in enumerate(self.root): - t.set( - eth_rlp.encode(Uint(i)), - r.type_byte() + eth_rlp.encode(r.to_serializable_list()), - ) - return Hash(t.root_hash) + assert max_request_type is not None, "max_request_type must be provided" - def deposit_requests(self) -> List[DepositRequest]: - """ - Returns the list of deposit requests. - """ - return [d for d in self.root if isinstance(d, DepositRequest)] + lists: List[List[RequestBase]] = [[] for _ in range(max_request_type + 1)] + for r in requests: + lists[r.type].append(r) - def withdrawal_requests(self) -> List[WithdrawalRequest]: - """ - Returns the list of withdrawal requests. - """ - return [w for w in self.root if isinstance(w, WithdrawalRequest)] + self.requests_list = [requests_list_to_bytes(requests_list) for requests_list in lists] - def consolidation_requests(self) -> List[ConsolidationRequest]: + def __bytes__(self) -> bytes: """ - Returns the list of consolidation requests. + Returns the requests hash. """ - return [c for c in self.root if isinstance(c, ConsolidationRequest)] + s: bytes = b"" + for i, r in enumerate(self.requests_list): + # Append the index of the request type to the request data before hashing + s = s + Bytes(bytes([i]) + r).sha256() + return Bytes(s).sha256() diff --git a/src/pytest_plugins/consume/hive_simulators/conftest.py b/src/pytest_plugins/consume/hive_simulators/conftest.py index 56f561824f..73b3b55f19 100644 --- a/src/pytest_plugins/consume/hive_simulators/conftest.py +++ b/src/pytest_plugins/consume/hive_simulators/conftest.py @@ -55,7 +55,7 @@ def hive_consume_command( """ return ( f"./hive --sim ethereum/{test_suite_name} " - f"--client-file configs/develop.yaml " + f"--client-file configs/prague.yaml " f"--client {client_type.name} " f'--sim.limit "{test_case.id}"' ) @@ -70,7 +70,7 @@ def eest_consume_commands( """ Commands to run the test within EEST using a hive dev back-end. """ - hive_dev = f"./hive --dev --client-file configs/develop.yaml --client {client_type.name}" + hive_dev = f"./hive --dev --client-file configs/prague.yaml --client {client_type.name}" consume = ( f'consume {test_suite_name.split("-")[-1]} -v --input latest-develop-release -k ' f'"{test_case.id}"' diff --git a/src/pytest_plugins/execute/rpc/hive.py b/src/pytest_plugins/execute/rpc/hive.py index 57f2f9cc3d..38995fc2ac 100644 --- a/src/pytest_plugins/execute/rpc/hive.py +++ b/src/pytest_plugins/execute/rpc/hive.py @@ -229,6 +229,8 @@ def base_pre_genesis( if empty_accounts := pre_alloc.empty_accounts(): raise Exception(f"Empty accounts in pre state: {empty_accounts}") state_root = pre_alloc.state_root() + block_number = 0 + timestamp = 1 genesis = FixtureHeader( parent_hash=0, ommers_hash=EmptyOmmersRoot, @@ -238,10 +240,10 @@ def base_pre_genesis( receipts_root=EmptyTrieRoot, logs_bloom=0, difficulty=0x20000 if env.difficulty is None else env.difficulty, - number=0, + number=block_number, gas_limit=env.gas_limit, gas_used=0, - timestamp=1, + timestamp=timestamp, extra_data=b"\x00", prev_randao=0, nonce=0, @@ -252,8 +254,13 @@ def base_pre_genesis( if env.withdrawals is not None else None, parent_beacon_block_root=env.parent_beacon_block_root, - requests_root=Requests(root=[]).trie_root - if base_fork.header_requests_required(0, 0) + requests_hash=Requests( + max_request_type=base_fork.max_request_type( + block_number=block_number, + timestamp=timestamp, + ), + ) + if base_fork.header_requests_required(block_number=block_number, timestamp=timestamp) else None, ) @@ -673,6 +680,8 @@ def generate_block(self: "EthRPC"): new_payload_args.append(new_payload.blobs_bundle.blob_versioned_hashes()) if parent_beacon_block_root is not None: new_payload_args.append(parent_beacon_block_root) + if new_payload.execution_requests is not None: + new_payload_args.append(new_payload.execution_requests) new_payload_version = self.fork.engine_new_payload_version() assert new_payload_version is not None, "Fork does not support engine new_payload" new_payload_response = self.engine_rpc.new_payload( diff --git a/tests/prague/eip6110_deposits/conftest.py b/tests/prague/eip6110_deposits/conftest.py index f99c3a3c64..b099b079ff 100644 --- a/tests/prague/eip6110_deposits/conftest.py +++ b/tests/prague/eip6110_deposits/conftest.py @@ -5,7 +5,8 @@ import pytest -from ethereum_test_tools import Alloc, Block, BlockException, Header, Transaction +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, BlockException, Header, Requests, Transaction from .helpers import DepositInteractionBase, DepositRequest @@ -61,6 +62,7 @@ def included_requests( @pytest.fixture def blocks( + fork: Fork, included_requests: List[DepositRequest], block_body_override_requests: List[DepositRequest] | None, txs: List[Transaction], @@ -71,9 +73,17 @@ def blocks( Block( txs=txs, header_verify=Header( - requests_root=included_requests, + requests_hash=Requests( + *included_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ), ), - requests=block_body_override_requests, + requests=Requests( + *block_body_override_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ).requests_list + if block_body_override_requests is not None + else None, exception=exception, ) ] diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py b/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py index 4c6cd812b9..41f0b59436 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/conftest.py @@ -6,7 +6,8 @@ import pytest -from ethereum_test_tools import Alloc, Block, Header +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, Header, Requests from .helpers import WithdrawalRequest, WithdrawalRequestInteractionBase from .spec import Spec @@ -68,25 +69,65 @@ def included_requests( return per_block_included_requests +@pytest.fixture +def timestamp() -> int: + """ + Return the timestamp for the first block. + """ + return 1 + + @pytest.fixture def blocks( + fork: Fork, update_pre: None, # Fixture is used for its side effects blocks_withdrawal_requests: List[List[WithdrawalRequestInteractionBase]], included_requests: List[List[WithdrawalRequest]], + timestamp: int, ) -> 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), + blocks: List[Block] = [] + + for block_requests, block_included_requests in zip_longest( # type: ignore + blocks_withdrawal_requests, + included_requests, + fillvalue=[], + ): + max_request_type = fork.max_request_type( + block_number=len(blocks) + 1, + timestamp=timestamp, + ) + header_verify: Header | None = None + if max_request_type > -1: + header_verify = Header( + requests_hash=Requests( + *block_included_requests, + max_request_type=max_request_type, + ) + ) + else: + assert not block_included_requests + blocks.append( + Block( + txs=sum((r.transactions() for r in block_requests), []), + header_verify=header_verify, + timestamp=timestamp, + ) ) - for block_requests, block_included_requests in zip_longest( - blocks_withdrawal_requests, - included_requests, - fillvalue=[], + timestamp += 1 + + return blocks + [ + Block( + header_verify=Header( + requests_hash=Requests( + max_request_type=fork.max_request_type( + block_number=len(blocks) + 1, + timestamp=timestamp, + ) + ) + ), + timestamp=timestamp, ) - ] + [ - 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/contract_deploy_tx.json b/tests/prague/eip7002_el_triggerable_withdrawals/contract_deploy_tx.json new file mode 100644 index 0000000000..936474332d --- /dev/null +++ b/tests/prague/eip7002_el_triggerable_withdrawals/contract_deploy_tx.json @@ -0,0 +1,16 @@ +{ + "type": "0x0", + "nonce": "0x0", + "to": null, + "gasLimit": "0x3d090", + "gasPrice": "0xe8d4a51000", + "maxPriorityFeePerGas": null, + "maxFeePerGas": null, + "value": "0x0", + "input": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f556101f480602d5f395ff33373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd", + "v": "0x1b", + "r": "0x539", + "s": "0x10e740537d4d36b9", + "hash": "0x1cd8bf929988b27b07ba1c7b898b396c08c484bb0db83fdeb992aa21b5cdf0ce", + "protected": false + } \ No newline at end of file diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/spec.py b/tests/prague/eip7002_el_triggerable_withdrawals/spec.py index f56381e437..1e5964e9d6 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/spec.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/spec.py @@ -5,6 +5,8 @@ from dataclasses import dataclass +from ethereum_test_tools import Address + @dataclass(frozen=True) class ReferenceSpec: @@ -16,7 +18,7 @@ class ReferenceSpec: version: str -ref_spec_7002 = ReferenceSpec("EIPS/eip-7002.md", "e5af719767e789c88c0e063406c6557c8f53cfba") +ref_spec_7002 = ReferenceSpec("EIPS/eip-7002.md", "9fe721f56f45bd5cf2d2958c0e6867aa81f82ebc") # Constants @@ -30,7 +32,8 @@ class Spec: out. """ - WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = 0x00A3CA265EBCB825B45F985A16CEFB49958CE017 + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = 0x09FC772D0857550724B07B850A4323F39112AAAA + WITHDRAWAL_REQUEST_PREDEPLOY_SENDER = Address(0x57B8C3C2766D0623EA0A499365A6F5A26AD38B47) SYSTEM_ADDRESS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE EXCESS_WITHDRAWAL_REQUESTS_STORAGE_SLOT = 0 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 23d1b3cf96..ce3c535a58 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py @@ -8,6 +8,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Address, Alloc, @@ -19,7 +20,7 @@ Macros, ) from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import TestAddress, TestAddress2 +from ethereum_test_tools import Requests, TestAddress, TestAddress2 from .helpers import ( WithdrawalRequest, @@ -350,9 +351,11 @@ + [ WithdrawalRequest( validator_pubkey=Spec.MAX_WITHDRAWAL_REQUESTS_PER_BLOCK, - amount=Spec.MAX_AMOUNT - 1 - if (Spec.MAX_WITHDRAWAL_REQUESTS_PER_BLOCK - 1) % 2 == 0 - else 0, + amount=( + Spec.MAX_AMOUNT - 1 + if (Spec.MAX_WITHDRAWAL_REQUESTS_PER_BLOCK - 1) % 2 == 0 + else 0 + ), fee=0, ) ], @@ -678,6 +681,7 @@ def test_withdrawal_requests( ) def test_withdrawal_requests_negative( pre: Alloc, + fork: Fork, blockchain_test: BlockchainTestFiller, requests: List[WithdrawalRequestInteractionBase], block_body_override_requests: List[WithdrawalRequest], @@ -705,9 +709,19 @@ def test_withdrawal_requests_negative( Block( txs=sum((r.transactions() for r in requests), []), header_verify=Header( - requests_root=included_requests, + requests_hash=Requests( + *included_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ), + ), + requests=( + Requests( + *block_body_override_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ).requests_list + if block_body_override_requests is not None + else None ), - requests=block_body_override_requests, exception=exception, ) ], diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py new file mode 100644 index 0000000000..44848e3a18 --- /dev/null +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py @@ -0,0 +1,125 @@ +""" +abstract: Tests [EIP-7002: Execution layer triggerable withdrawals](https://eips.ethereum.org/EIPS/eip-7002) + Test execution layer triggered exits [EIP-7002: Execution layer triggerable withdrawals](https://eips.ethereum.org/EIPS/eip-7002) + +""" # noqa: E501 + +from os.path import realpath +from pathlib import Path +from typing import List + +import pytest + +from ethereum_test_tools import ( + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + Environment, + Transaction, +) + +from .helpers import WithdrawalRequest, WithdrawalRequestTransaction +from .spec import Spec, ref_spec_7002 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7002.git_path +REFERENCE_SPEC_VERSION = ref_spec_7002.version + +pytestmark = pytest.mark.valid_at_transition_to("Prague") + +BLOCKS_BEFORE_FORK = 2 + + +@pytest.mark.parametrize( + "blocks_withdrawal_requests", + [ + pytest.param( + [ + [], # No withdrawal requests, but we deploy the contract + [ + WithdrawalRequestTransaction( + requests=[ + WithdrawalRequest( + validator_pubkey=0x01, + amount=0, + fee=Spec.get_fee(10), + # Pre-fork withdrawal request + valid=False, + ) + ], + ), + ], + [ + WithdrawalRequestTransaction( + requests=[ + WithdrawalRequest( + validator_pubkey=0x02, + amount=0, + fee=Spec.get_fee(10), + # First post-fork withdrawal request, will not be included + # because the inhibitor is cleared at the end of the block + valid=False, + ) + ], + ), + ], + [ + WithdrawalRequestTransaction( + requests=[ + WithdrawalRequest( + validator_pubkey=0x03, + amount=0, + fee=Spec.get_fee(0), + # First withdrawal that is valid + valid=True, + ) + ], + ), + ], + ], + id="one_valid_request_second_block_after_fork", + ), + ], +) +@pytest.mark.parametrize("timestamp", [15_000 - BLOCKS_BEFORE_FORK], ids=[""]) +def test_withdrawal_requests_during_fork( + blockchain_test: BlockchainTestFiller, + blocks: List[Block], + pre: Alloc, +): + """ + Test making a withdrawal request to the beacon chain at the time of the fork. + """ + # We need to delete the deployed contract that comes by default in the pre state. + pre[Spec.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS] = Account( + balance=0, + code=bytes(), + nonce=0, + storage={}, + ) + + with open(Path(realpath(__file__)).parent / "contract_deploy_tx.json", mode="r") as f: + deploy_tx = Transaction.model_validate_json( + f.read() + ).with_signature_and_sender() # type: ignore + + deployer_address = deploy_tx.sender + assert deployer_address is not None + assert Address(deployer_address) == Spec.WITHDRAWAL_REQUEST_PREDEPLOY_SENDER + + tx_gas_price = deploy_tx.gas_price + assert tx_gas_price is not None + deployer_required_balance = deploy_tx.gas_limit * tx_gas_price + + pre.fund_address(Spec.WITHDRAWAL_REQUEST_PREDEPLOY_SENDER, deployer_required_balance) + + # Append the deployment transaction to the first block + blocks[0].txs.append(deploy_tx) + + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) diff --git a/tests/prague/eip7251_consolidations/conftest.py b/tests/prague/eip7251_consolidations/conftest.py index ef5c3d87b9..8f57088f7b 100644 --- a/tests/prague/eip7251_consolidations/conftest.py +++ b/tests/prague/eip7251_consolidations/conftest.py @@ -6,7 +6,8 @@ import pytest -from ethereum_test_tools import Alloc, Block, Header +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, Header, Requests from .helpers import ConsolidationRequest, ConsolidationRequestInteractionBase from .spec import Spec @@ -69,25 +70,65 @@ def included_requests( return per_block_included_requests +@pytest.fixture +def timestamp() -> int: + """ + Return the timestamp for the first block. + """ + return 1 + + @pytest.fixture def blocks( + fork: Fork, update_pre: None, # Fixture is used for its side effects blocks_consolidation_requests: List[List[ConsolidationRequestInteractionBase]], included_requests: List[List[ConsolidationRequest]], + timestamp: int, ) -> 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), + blocks: List[Block] = [] + + for block_requests, block_included_requests in zip_longest( # type: ignore + blocks_consolidation_requests, + included_requests, + fillvalue=[], + ): + max_request_type = fork.max_request_type( + block_number=len(blocks) + 1, + timestamp=timestamp, + ) + header_verify: Header | None = None + if max_request_type > -1: + header_verify = Header( + requests_hash=Requests( + *block_included_requests, + max_request_type=max_request_type, + ) + ) + else: + assert not block_included_requests + blocks.append( + Block( + txs=sum((r.transactions() for r in block_requests), []), + header_verify=header_verify, + timestamp=timestamp, + ) ) - for block_requests, block_included_requests in zip_longest( - blocks_consolidation_requests, - included_requests, - fillvalue=[], + timestamp += 1 + + return blocks + [ + Block( + header_verify=Header( + requests_hash=Requests( + max_request_type=fork.max_request_type( + block_number=len(blocks) + 1, + timestamp=timestamp, + ) + ) + ), + timestamp=timestamp, ) - ] + [ - 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/contract_deploy_tx.json b/tests/prague/eip7251_consolidations/contract_deploy_tx.json new file mode 100644 index 0000000000..0b38ed57cf --- /dev/null +++ b/tests/prague/eip7251_consolidations/contract_deploy_tx.json @@ -0,0 +1,16 @@ +{ + "type": "0x0", + "nonce": "0x0", + "to": null, + "gasLimit": "0x3d090", + "gasPrice": "0xe8d4a51000", + "maxPriorityFeePerGas": null, + "maxFeePerGas": null, + "value": "0x0", + "input": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f5561019e80602d5f395ff33373fffffffffffffffffffffffffffffffffffffffe1460cf573615156028575f545f5260205ff35b366060141561019a5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f821115608057810190830284830290049160010191906065565b90939004341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060011160e3575060015b5f5b8181146101295780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160e5565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd", + "v": "0x1b", + "r": "0x539", + "s": "0x832fdd8c49a416f1", + "hash": "0x5e174f35e55bc53c898f3c5e315d81e054363363a0e95dfd6e43c23e8ebb9407", + "protected": false + } \ No newline at end of file diff --git a/tests/prague/eip7251_consolidations/spec.py b/tests/prague/eip7251_consolidations/spec.py index 27c22845cf..402773a0f0 100644 --- a/tests/prague/eip7251_consolidations/spec.py +++ b/tests/prague/eip7251_consolidations/spec.py @@ -4,6 +4,8 @@ from dataclasses import dataclass +from ethereum_test_tools import Address + @dataclass(frozen=True) class ReferenceSpec: @@ -15,7 +17,7 @@ class ReferenceSpec: version: str -ref_spec_7251 = ReferenceSpec("EIPS/eip-7251.md", "e5af719767e789c88c0e063406c6557c8f53cfba") +ref_spec_7251 = ReferenceSpec("EIPS/eip-7251.md", "18af57e74e4e862da5cbb8140aeb24128088f4e2") # Constants @@ -26,7 +28,8 @@ class Spec: https://eips.ethereum.org/EIPS/eip-7251#execution-layer """ - CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = 0x00B42DBF2194E931E80326D950320F7D9DBEAC02 + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = 0x01ABEA29659E5E97C95107F20BB753CD3E09BBBB + CONSOLIDATION_REQUEST_PREDEPLOY_SENDER = Address(0x81E9AFA909FE8B57AF2A6FD18862AE9DAE3163F4) SYSTEM_ADDRESS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT = 0 diff --git a/tests/prague/eip7251_consolidations/test_consolidations.py b/tests/prague/eip7251_consolidations/test_consolidations.py index 6cf97e57d0..ca2d18f87a 100644 --- a/tests/prague/eip7251_consolidations/test_consolidations.py +++ b/tests/prague/eip7251_consolidations/test_consolidations.py @@ -8,6 +8,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Address, Alloc, @@ -19,7 +20,7 @@ Macros, ) from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import TestAddress, TestAddress2 +from ethereum_test_tools import Requests, TestAddress, TestAddress2 from .helpers import ( ConsolidationRequest, @@ -731,6 +732,7 @@ def test_consolidation_requests( ) def test_consolidation_requests_negative( pre: Alloc, + fork: Fork, blockchain_test: BlockchainTestFiller, requests: List[ConsolidationRequestInteractionBase], block_body_override_requests: List[ConsolidationRequest], @@ -758,9 +760,19 @@ def test_consolidation_requests_negative( Block( txs=sum((r.transactions() for r in requests), []), header_verify=Header( - requests_root=included_requests, + requests_hash=Requests( + *included_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ), + ), + requests=( + Requests( + *block_body_override_requests, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ).requests_list + if block_body_override_requests is not None + else None ), - requests=block_body_override_requests, exception=exception, ) ], diff --git a/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py b/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py new file mode 100644 index 0000000000..8658dfa4f2 --- /dev/null +++ b/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py @@ -0,0 +1,125 @@ +""" +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 os.path import realpath +from pathlib import Path +from typing import List + +import pytest + +from ethereum_test_tools import ( + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + Environment, + Transaction, +) + +from .helpers import ConsolidationRequest, ConsolidationRequestTransaction +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_at_transition_to("Prague") + +BLOCKS_BEFORE_FORK = 2 + + +@pytest.mark.parametrize( + "blocks_consolidation_requests", + [ + pytest.param( + [ + [], # No consolidation requests, but we deploy the contract + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x01, + target_pubkey=0x02, + fee=Spec.get_fee(10), + # Pre-fork consolidation request + valid=False, + ) + ], + ), + ], + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x03, + target_pubkey=0x04, + fee=Spec.get_fee(10), + # First post-fork consolidation request, will not be included + # because the inhibitor is cleared at the end of the block + valid=False, + ) + ], + ), + ], + [ + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=0x05, + target_pubkey=0x06, + fee=Spec.get_fee(0), + # First consolidation that is valid + valid=True, + ) + ], + ), + ], + ], + id="one_valid_request_second_block_after_fork", + ), + ], +) +@pytest.mark.parametrize("timestamp", [15_000 - BLOCKS_BEFORE_FORK], ids=[""]) +def test_consolidation_requests_during_fork( + blockchain_test: BlockchainTestFiller, + blocks: List[Block], + pre: Alloc, +): + """ + Test making a consolidation request to the beacon chain at the time of the fork. + """ + # We need to delete the deployed contract that comes by default in the pre state. + pre[Spec.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS] = Account( + balance=0, + code=bytes(), + nonce=0, + storage={}, + ) + + with open(Path(realpath(__file__)).parent / "contract_deploy_tx.json", mode="r") as f: + deploy_tx = Transaction.model_validate_json( + f.read() + ).with_signature_and_sender() # type: ignore + + deployer_address = deploy_tx.sender + assert deployer_address is not None + assert Address(deployer_address) == Spec.CONSOLIDATION_REQUEST_PREDEPLOY_SENDER + + tx_gas_price = deploy_tx.gas_price + assert tx_gas_price is not None + deployer_required_balance = deploy_tx.gas_limit * tx_gas_price + + pre.fund_address(Spec.CONSOLIDATION_REQUEST_PREDEPLOY_SENDER, deployer_required_balance) + + # Append the deployment transaction to the first block + blocks[0].txs.append(deploy_tx) + + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) diff --git a/tests/prague/eip7685_general_purpose_el_requests/conftest.py b/tests/prague/eip7685_general_purpose_el_requests/conftest.py index 4a4aa61e05..34a1cd70d7 100644 --- a/tests/prague/eip7685_general_purpose_el_requests/conftest.py +++ b/tests/prague/eip7685_general_purpose_el_requests/conftest.py @@ -2,11 +2,12 @@ Fixtures for the EIP-7685 deposit tests. """ -from typing import List +from typing import List, SupportsBytes import pytest -from ethereum_test_tools import Alloc, Block, BlockException, Header +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, BlockException, Bytes, Header, Requests from ..eip6110_deposits.helpers import DepositInteractionBase, DepositRequest from ..eip7002_el_triggerable_withdrawals.helpers import ( @@ -20,13 +21,31 @@ @pytest.fixture -def block_body_override_requests() -> List[ - DepositRequest | WithdrawalRequest | ConsolidationRequest -] | None: +def block_body_override_requests( + request: pytest.FixtureRequest, +) -> List[DepositRequest | WithdrawalRequest | ConsolidationRequest] | None: """List of requests that overwrite the requests in the header. None by default.""" + if hasattr(request, "param"): + return request.param return None +@pytest.fixture +def block_body_extra_requests() -> List[SupportsBytes]: + """List of requests that overwrite the requests in the header. None by default.""" + return [] + + +@pytest.fixture +def correct_requests_hash_in_header() -> bool: + """ + Whether to include the correct requests hash in the header so the calculated + block hash is correct, even though the requests in the new payload parameters might + be wrong. + """ + return False + + @pytest.fixture def exception() -> BlockException | None: """Block exception expected by the tests. None by default.""" @@ -35,41 +54,53 @@ def exception() -> BlockException | None: @pytest.fixture def blocks( + fork: Fork, pre: Alloc, requests: List[ DepositInteractionBase | WithdrawalRequestInteractionBase | ConsolidationRequestInteractionBase ], - block_body_override_requests: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] - | None, + block_body_override_requests: (List[Bytes | SupportsBytes] | None), + block_body_extra_requests: List[SupportsBytes], + correct_requests_hash_in_header: bool, exception: BlockException | None, ) -> List[Block]: """List of blocks that comprise the test.""" - included_deposit_requests = [] - included_withdrawal_requests = [] - included_consolidation_requests = [] + valid_requests_list: List[DepositRequest | WithdrawalRequest | ConsolidationRequest] = [] # 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) + valid_requests_list += r.valid_requests(10**18) elif isinstance(r, WithdrawalRequestInteractionBase): - included_withdrawal_requests += r.valid_requests(withdrawal_request_fee) + valid_requests_list += r.valid_requests(withdrawal_request_fee) elif isinstance(r, ConsolidationRequestInteractionBase): - included_consolidation_requests += r.valid_requests(consolidation_request_fee) + valid_requests_list += r.valid_requests(consolidation_request_fee) + + valid_requests = Requests( + *valid_requests_list, + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ) + if block_body_override_requests is None and block_body_extra_requests is not None: + block_body_override_requests = valid_requests.requests_list + block_body_extra_requests + + rlp_modifier: Header | None = None + if correct_requests_hash_in_header: + rlp_modifier = Header( + requests_hash=valid_requests, + ) return [ Block( txs=sum((r.transactions() for r in requests), []), header_verify=Header( - requests_root=included_deposit_requests - + included_withdrawal_requests - + included_consolidation_requests, + requests_hash=valid_requests, ), requests=block_body_override_requests, exception=exception, + rlp_modifier=rlp_modifier, ) ] 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 index 1b8d12e415..92329cd05c 100644 --- 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 @@ -9,10 +9,12 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, Alloc, Block, + BlockchainTestEngineFiller, BlockchainTestFiller, BlockException, Bytecode, @@ -20,7 +22,7 @@ Header, ) from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import Storage, TestAddress, TestAddress2, Transaction +from ethereum_test_tools import Requests, Storage, TestAddress, Transaction from ..eip6110_deposits.helpers import DepositContract, DepositRequest, DepositTransaction from ..eip6110_deposits.spec import Spec as Spec_EIP6110 @@ -238,6 +240,7 @@ def test_valid_deposit_withdrawal_consolidation_request_from_same_tx( blockchain_test: BlockchainTestFiller, pre: Alloc, requests: List[DepositRequest | WithdrawalRequest | ConsolidationRequest], + fork: Fork, ): """ Test making a deposit to the beacon chain deposit contract and a withdrawal in the same tx. @@ -306,59 +309,161 @@ def test_valid_deposit_withdrawal_consolidation_request_from_same_tx( 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()) - ] + requests_hash=Requests( + *[ + request.with_source_address(contract_address) + for request in sorted(requests, key=lambda r: r.type) + ], + max_request_type=fork.max_request_type(block_number=1, timestamp=1), + ) ), ) ], ) +invalid_requests_block_combinations = [ + pytest.param( + [], + [], # Even with no requests, the requests hash is not sha256(b""), + # but sha256(sha256(b"\0") ++ sha256(b"\1") ++ sha256(b"\2") ++ ...) + BlockException.INVALID_REQUESTS, + id="no_requests_empty_list", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + ], + [ + single_deposit(0), + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_incomplete_requests_list", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + ], + [], + BlockException.INVALID_REQUESTS, + id="single_deposit_empty_requests_list", + ), + # Incorrect order tests + pytest.param( + [ + single_deposit_from_eoa(0), + ], + [ + b"", + single_deposit(0), + b"", + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_incorrect_order_1", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + ], + [ + b"", + b"", + single_deposit(0), + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_incorrect_order_2", + ), + pytest.param( + [ + single_withdrawal_from_eoa(0), + ], + [ + single_withdrawal(0).with_source_address(TestAddress), + b"", + b"", + ], + BlockException.INVALID_REQUESTS, + id="single_withdrawal_incorrect_order_1", + ), + pytest.param( + [ + single_withdrawal_from_eoa(0), + ], + [ + b"", + b"", + single_withdrawal(0).with_source_address(TestAddress), + ], + BlockException.INVALID_REQUESTS, + id="single_withdrawal_incorrect_order_2", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + ], + [ + single_consolidation(0).with_source_address(TestAddress), + b"", + b"", + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_incorrect_order_1", + ), + pytest.param( + [ + single_consolidation_from_eoa(0), + ], + [ + b"", + single_consolidation(0).with_source_address(TestAddress), + b"", + ], + BlockException.INVALID_REQUESTS, + id="single_consolidation_incorrect_order_2", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + single_withdrawal_from_eoa(0), + ], + [ + single_deposit(0), + single_withdrawal(0).with_source_address(TestAddress), + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_single_withdrawal_incomplete_requests_list", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + single_withdrawal_from_eoa(0), + ], + [ + single_deposit(0), + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_single_withdrawal_incomplete_requests_list_2", + ), + pytest.param( + [ + single_deposit_from_eoa(0), + single_withdrawal_from_eoa(0), + single_consolidation_from_eoa(0), + ], + [ + single_deposit(0), + single_withdrawal(0).with_source_address(TestAddress), + ], + BlockException.INVALID_REQUESTS, + id="single_deposit_single_withdrawal_single_consolidation_incomplete_requests_list", + ), +] + + @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", - ), - ], + invalid_requests_block_combinations, + indirect=["block_body_override_requests"], ) def test_invalid_deposit_withdrawal_consolidation_requests( blockchain_test: BlockchainTestFiller, @@ -366,7 +471,12 @@ def test_invalid_deposit_withdrawal_consolidation_requests( blocks: List[Block], ): """ - Negative testing for deposits and withdrawals in the same block. + Negative testing for all request types in the same block. + + In these tests, the requests hash in the header reflects what's received in the parameters + portion of the `engine_newPayloadVX` call, so the block hash calculation might pass if + a client copies the info received verbatim, but block validation must fail after + the block is executed (via RLP or Engine API). """ blockchain_test( genesis_environment=Environment(), @@ -374,3 +484,34 @@ def test_invalid_deposit_withdrawal_consolidation_requests( post={}, blocks=blocks, ) + + +@pytest.mark.parametrize( + "requests,block_body_override_requests,exception", + invalid_requests_block_combinations, + indirect=["block_body_override_requests"], +) +@pytest.mark.parametrize("correct_requests_hash_in_header", [True]) +def test_invalid_deposit_withdrawal_consolidation_requests_engine( + blockchain_test_engine: BlockchainTestEngineFiller, + pre: Alloc, + blocks: List[Block], +): + """ + Negative testing for all request types in the same block with incorrect parameters + in the Engine API new payload parameters, but with the correct requests hash in the header + so the block hash is correct. + + In these tests, the requests hash in the header reflects what's actually in the executed block, + so the block might execute properly if the client ignores the requests in the new payload + parameters. + + Also these tests would not fail if the block is imported via RLP (syncing from a peer), + so we only generate the BlockchainTestEngine for them. + """ + blockchain_test_engine( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) diff --git a/tests/prague/eip7685_general_purpose_el_requests/test_request_types.py b/tests/prague/eip7685_general_purpose_el_requests/test_request_types.py new file mode 100644 index 0000000000..e8d0de1a7e --- /dev/null +++ b/tests/prague/eip7685_general_purpose_el_requests/test_request_types.py @@ -0,0 +1,114 @@ +""" +Test the request types that can be included in a block by the given fork. +""" +from typing import List + +import pytest + +from ethereum_test_exceptions import BlockException +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment + +from ..eip6110_deposits.helpers import DepositInteractionBase, DepositRequest, DepositTransaction +from ..eip7002_el_triggerable_withdrawals.helpers import ( + WithdrawalRequest, + WithdrawalRequestInteractionBase, + WithdrawalRequestTransaction, +) +from ..eip7251_consolidations.helpers import ( + ConsolidationRequest, + ConsolidationRequestInteractionBase, + ConsolidationRequestTransaction, +) +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.fixture +def block_body_extra_requests(fork: Fork, invalid_request_data: bytes) -> List[bytes]: + """List of requests that overwrite the requests in the header. None by default.""" + invalid_request_type = fork.max_request_type() + 1 + return [bytes([invalid_request_type]) + invalid_request_data] + + +@pytest.fixture +def requests( + fork: Fork, + include_valid_requests: bool, +) -> List[ + DepositInteractionBase | WithdrawalRequestInteractionBase | ConsolidationRequestInteractionBase +]: + """List of valid requests that are added along with the invalid request.""" + if not include_valid_requests: + return [] + if fork.max_request_type() == 2: + return [ + DepositTransaction( + requests=[ + DepositRequest( + pubkey=1, + withdrawal_credentials=2, + amount=1_000_000_000, + signature=3, + index=0, + ) + ] + ), + WithdrawalRequestTransaction( + requests=[ + WithdrawalRequest( + validator_pubkey=1, + amount=0, + fee=1, + ) + ] + ), + ConsolidationRequestTransaction( + requests=[ + ConsolidationRequest( + source_pubkey=2, + target_pubkey=5, + fee=1, + ) + ] + ), + ] + raise NotImplementedError(f"Unsupported fork: {fork}") + + +@pytest.mark.parametrize( + "include_valid_requests", + [False, True], +) +@pytest.mark.parametrize( + "invalid_request_data", + [ + pytest.param(b"", id="no_data"), + pytest.param(b"\0", id="single_byte"), + pytest.param(b"\0" * 32, id="32_bytes"), + ], +) +@pytest.mark.parametrize( + "exception", + [ + pytest.param(BlockException.INVALID_REQUESTS, id=""), + ], +) +def test_invalid_request_type( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + blocks: List[Block], +): + """ + Test sending a block with an invalid request type. + """ + blockchain_test( + genesis_environment=Environment(), + pre=pre, + post={}, + blocks=blocks, + ) diff --git a/tests/prague/eip7702_set_code_tx/spec.py b/tests/prague/eip7702_set_code_tx/spec.py index 88ce7cd603..af226bc207 100644 --- a/tests/prague/eip7702_set_code_tx/spec.py +++ b/tests/prague/eip7702_set_code_tx/spec.py @@ -16,7 +16,7 @@ class ReferenceSpec: version: str -ref_spec_7702 = ReferenceSpec("EIPS/eip-7702.md", "a6bf54ffc1506ed00f8234731684ccfe935ec9a3") +ref_spec_7702 = ReferenceSpec("EIPS/eip-7702.md", "4334df83395693dc3f629bb43c18320d9e22e8c9") @dataclass(frozen=True) @@ -28,9 +28,10 @@ class Spec: SET_CODE_TX_TYPE = 0x04 MAGIC = 0x05 - PER_AUTH_BASE_COST = 2_500 + PER_AUTH_BASE_COST = 12_500 PER_EMPTY_ACCOUNT_COST = 25_000 DELEGATION_DESIGNATION = bytes.fromhex("ef0100") + RESET_DELEGATION_ADDRESS = Address(0) @staticmethod def delegation_designation(address: Address) -> Bytes: diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index 052314731c..001ec6a745 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -10,6 +10,8 @@ import pytest +from ethereum_test_base_types import HexNumber +from ethereum_test_forks import Fork from ethereum_test_tools import ( AccessList, Account, @@ -30,6 +32,7 @@ from ethereum_test_tools import Macros as Om from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import ( + Requests, StateTestFiller, Storage, Transaction, @@ -213,42 +216,6 @@ def test_set_code_to_sstore( ) -def test_set_code_to_zero_address( - state_test: StateTestFiller, - pre: Alloc, -): - """ - Test setting the code to the zero address (0x0) in a set-code transaction. - """ - auth_signer = pre.fund_eoa(auth_account_start_balance) - - tx = Transaction( - gas_limit=500_000, - to=auth_signer, - authorization_list=[ - AuthorizationTuple( - address=Address(0), - nonce=0, - signer=auth_signer, - ), - ], - sender=pre.fund_eoa(), - ) - - state_test( - env=Environment(), - pre=pre, - tx=tx, - post={ - auth_signer: Account( - nonce=1, - code=Spec.delegation_designation(Address(0)), - storage={}, - ), - }, - ) - - def test_set_code_to_sstore_then_sload( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -2103,40 +2070,10 @@ def test_set_code_using_chain_specific_id( [ pytest.param(0, 1, 1, id="v=0,r=1,s=1"), pytest.param(1, 1, 1, id="v=1,r=1,s=1"), - pytest.param( - 2, 1, 1, id="v=2,r=1,s=1", marks=pytest.mark.xfail(reason="invalid signature") - ), - pytest.param( - 1, 0, 1, id="v=1,r=0,s=1", marks=pytest.mark.xfail(reason="invalid signature") - ), - pytest.param( - 1, 1, 0, id="v=1,r=1,s=0", marks=pytest.mark.xfail(reason="invalid signature") - ), - pytest.param( - 0, - SECP256K1N - 0, - 1, - id="v=0,r=SECP256K1N,s=1", - marks=pytest.mark.xfail(reason="invalid signature"), - ), - pytest.param( - 0, - SECP256K1N - 1, - 1, - id="v=0,r=SECP256K1N-1,s=1", - marks=pytest.mark.xfail(reason="invalid signature"), - ), pytest.param(0, SECP256K1N - 2, 1, id="v=0,r=SECP256K1N-2,s=1"), pytest.param(1, SECP256K1N - 2, 1, id="v=1,r=SECP256K1N-2,s=1"), pytest.param(0, 1, SECP256K1N_OVER_2, id="v=0,r=1,s=SECP256K1N_OVER_2"), pytest.param(1, 1, SECP256K1N_OVER_2, id="v=1,r=1,s=SECP256K1N_OVER_2"), - pytest.param( - 0, - 1, - SECP256K1N_OVER_2 + 1, - id="v=0,r=1,s=SECP256K1N_OVER_2+1", - marks=pytest.mark.xfail(reason="invalid signature"), - ), ], ) def test_set_code_using_valid_synthetic_signatures( @@ -2193,25 +2130,10 @@ def test_set_code_using_valid_synthetic_signatures( @pytest.mark.parametrize( "v,r,s", [ - pytest.param(2, 1, 1, id="v_2,r_1,s_1"), - pytest.param( - 0, - 1, - SECP256K1N_OVER_2 + 1, - id="v_0,r_1,s_SECP256K1N_OVER_2+1", - ), - pytest.param( - 2**256 - 1, - 1, - 1, - id="v_2**256-1,r_1,s_1", - ), - pytest.param( - 0, - 1, - 2**256 - 1, - id="v_0,r_1,s_2**256-1", - ), + pytest.param(2**8, 1, 1, id="v=2**8"), + pytest.param(1, 2**256, 1, id="r=2**256"), + pytest.param(1, 1, 2**256, id="s=2**256"), + pytest.param(2**8, 2**256, 2**256, id="v=2**8,r=s=2**256"), ], ) def test_invalid_tx_invalid_auth_signature( @@ -2265,41 +2187,34 @@ def test_invalid_tx_invalid_auth_signature( @pytest.mark.parametrize( "v,r,s", [ - pytest.param(1, 0, 1, id="v_1,r_0,s_1"), - pytest.param(1, 1, 0, id="v_1,r_1,s_0"), - pytest.param( - 0, - SECP256K1N, - 1, - id="v_0,r_SECP256K1N,s_1", - ), - pytest.param( - 0, - SECP256K1N - 1, - 1, - id="v_0,r_SECP256K1N-1,s_1", - ), - pytest.param( - 0, - 1, - SECP256K1N_OVER_2, - id="v_0,r_1,s_SECP256K1N_OVER_2", - ), - pytest.param( - 0, - 1, - SECP256K1N_OVER_2 - 1, - id="v_0,r_1,s_SECP256K1N_OVER_2_minus_one", - ), - pytest.param( - 1, - 2**256 - 1, - 1, - id="v_1,r_2**256-1,s_1", - ), + # V + pytest.param(2, 1, 1, id="v=2"), + pytest.param(27, 1, 1, id="v=27"), # Type-0 transaction valid value + pytest.param(28, 1, 1, id="v=28"), # Type-0 transaction valid value + pytest.param(35, 1, 1, id="v=35"), # Type-0 replay-protected transaction valid value + pytest.param(36, 1, 1, id="v=36"), # Type-0 replay-protected transaction valid value + pytest.param(2**8 - 1, 1, 1, id="v=2**8-1"), + # R + pytest.param(1, 0, 1, id="r=0"), + pytest.param(0, SECP256K1N - 1, 1, id="r=SECP256K1N-1"), + pytest.param(0, SECP256K1N, 1, id="r=SECP256K1N"), + pytest.param(0, SECP256K1N + 1, 1, id="r=SECP256K1N+1"), + pytest.param(1, 2**256 - 1, 1, id="r=2**256-1"), + # S + pytest.param(1, 1, 0, id="s=0"), + pytest.param(0, 1, SECP256K1N_OVER_2 - 1, id="s=SECP256K1N_OVER_2-1"), + pytest.param(0, 1, SECP256K1N_OVER_2, id="s=SECP256K1N_OVER_2"), + pytest.param(0, 1, SECP256K1N_OVER_2 + 1, id="s=SECP256K1N_OVER_2+1"), + pytest.param(0, 1, SECP256K1N - 1, id="s=SECP256K1N-1"), + pytest.param(0, 1, SECP256K1N, id="s=SECP256K1N"), + pytest.param(0, 1, SECP256K1N + 1, id="s=SECP256K1N+1"), + pytest.param(0, 1, 2**256 - 1, id="s=2**256-1"), + # All Values + pytest.param(0, 0, 0, id="v=r=s=0"), + pytest.param(2**8 - 1, 2**256 - 1, 2**256 - 1, id="v=2**8-1,r=s=2**256-1"), ], ) -def test_set_code_using_invalid_signatures( +def test_valid_tx_invalid_auth_signature( state_test: StateTestFiller, pre: Alloc, v: int, @@ -2344,6 +2259,278 @@ def test_set_code_using_invalid_signatures( ) +def test_signature_s_out_of_range( + state_test: StateTestFiller, + pre: Alloc, +): + """ + Test sending a transaction with an authorization tuple where the signature s value is out of + range by modifying its value to be `SECP256K1N - S` and flipping the v value. + """ + auth_signer = pre.fund_eoa(0) + + set_code = Op.STOP + set_code_to_address = pre.deploy_contract(set_code) + + authorization_tuple = AuthorizationTuple( + address=set_code_to_address, + nonce=0, + chain_id=1, + signer=auth_signer, + ) + + authorization_tuple.s = HexNumber(SECP256K1N - authorization_tuple.s) + authorization_tuple.v = HexNumber(1 - authorization_tuple.v) + + assert authorization_tuple.s > SECP256K1N_OVER_2 + + success_slot = 1 + entry_code = Op.SSTORE(success_slot, 1) + Op.STOP + entry_address = pre.deploy_contract(entry_code) + + tx = Transaction( + gas_limit=100_000, + to=entry_address, + value=0, + authorization_list=[authorization_tuple], + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account.NONEXISTENT, + entry_address: Account( + storage={success_slot: 1}, + ), + }, + ) + + +@pytest.mark.parametrize( + "chain_id,transaction_exception", + [ + pytest.param( + 2**64, TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, id="chain_id=2**64" + ), + pytest.param(2**64 - 1, None, id="chain_id=2**64-1"), + ], +) +def test_tx_validity_chain_id( + state_test: StateTestFiller, + pre: Alloc, + chain_id: int, + transaction_exception: TransactionException | None, +): + """ + Test sending a transaction where the chain id field of an authorization overflows the + maximum value, or almost overflows the maximum value. + """ + auth_signer = pre.fund_eoa(auth_account_start_balance) + + success_slot = 1 + return_slot = 2 + + set_code = Op.RETURN(0, 1) + set_code_to_address = pre.deploy_contract(set_code) + + authorization = AuthorizationTuple( + address=set_code_to_address, + nonce=0, + chain_id=chain_id, + signer=auth_signer, + ) + + entry_code = ( + Op.SSTORE(success_slot, 1) + + Op.CALL(address=auth_signer) + + Op.SSTORE(return_slot, Op.RETURNDATASIZE) + ) + entry_address = pre.deploy_contract(entry_code) + + tx = Transaction( + gas_limit=100_000, + to=entry_address, + value=0, + authorization_list=[authorization], + error=transaction_exception, + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account.NONEXISTENT, + entry_address: Account( + storage={ + success_slot: 1 if transaction_exception is None else 0, + return_slot: 0, + }, + ), + }, + ) + + +@pytest.mark.parametrize( + "nonce,transaction_exception", + [ + pytest.param( + 2**64, TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, id="nonce=2**64" + ), + pytest.param( + 2**64 - 1, + None, + id="nonce=2**64-1", + marks=pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")), + ), + pytest.param( + 2**64 - 2, + None, + id="nonce=2**64-2", + marks=pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")), + ), + ], +) +def test_tx_validity_nonce( + state_test: StateTestFiller, + pre: Alloc, + nonce: int, + transaction_exception: TransactionException | None, +): + """ + Test sending a transaction where the nonce field of an authorization overflows the maximum + value, or almost overflows the maximum value. + + Also test calling the account of the authorization signer in order to verify that the account + is not warm. + """ + auth_signer = pre.fund_eoa( + auth_account_start_balance, nonce=nonce if nonce < 2**64 else None + ) + + success_slot = 1 + return_slot = 2 + + valid_authorization = nonce < 2**64 - 1 + set_code = Op.RETURN(0, 1) + set_code_to_address = pre.deploy_contract(set_code) + + authorization = AuthorizationTuple( + address=set_code_to_address, + nonce=nonce, + signer=auth_signer, + ) + + entry_code = ( + Op.SSTORE(success_slot, 1) + + Op.CALL(address=auth_signer) + + Op.SSTORE(return_slot, Op.RETURNDATASIZE) + ) + entry_address = pre.deploy_contract(entry_code) + + tx = Transaction( + gas_limit=100_000, + to=entry_address, + value=0, + authorization_list=[authorization], + error=transaction_exception, + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account( + nonce=(nonce + 1) if (nonce < (2**64 - 1)) else nonce, + code=Spec.delegation_designation(set_code_to_address) + if valid_authorization + else b"", + ) + if nonce < 2**64 + else Account.NONEXISTENT, + entry_address: Account( + storage={ + success_slot: 1 if transaction_exception is None else 0, + return_slot: 1 if valid_authorization else 0, + }, + ), + }, + ) + + +@pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")) +def test_nonce_overflow_after_first_authorization( + state_test: StateTestFiller, + pre: Alloc, +): + """ + Test sending a transaction with two authorization where the first one bumps the nonce + to 2**64-1 and the second would result in overflow. + """ + nonce = 2**64 - 2 + auth_signer = pre.fund_eoa(auth_account_start_balance, nonce=nonce) + + success_slot = 1 + return_slot = 2 + + set_code_1 = Op.RETURN(0, 1) + set_code_to_address_1 = pre.deploy_contract(set_code_1) + set_code_2 = Op.RETURN(0, 2) + set_code_to_address_2 = pre.deploy_contract(set_code_2) + + authorization_list = [ + AuthorizationTuple( + address=set_code_to_address_1, + nonce=nonce, + signer=auth_signer, + ), + AuthorizationTuple( + address=set_code_to_address_2, + nonce=nonce + 1, + signer=auth_signer, + ), + ] + + entry_code = ( + Op.SSTORE(success_slot, 1) + + Op.CALL(address=auth_signer) + + Op.SSTORE(return_slot, Op.RETURNDATASIZE) + ) + entry_address = pre.deploy_contract(entry_code) + + tx = Transaction( + gas_limit=200_000, + to=entry_address, + value=0, + authorization_list=authorization_list, + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account( + nonce=nonce + 1, + code=Spec.delegation_designation(set_code_to_address_1), + ), + entry_address: Account( + storage={ + success_slot: 1, + return_slot: 1, + }, + ), + }, + ) + + @pytest.mark.parametrize( "log_opcode", [ @@ -2480,6 +2667,7 @@ def deposit_contract_initial_storage() -> Storage: def test_set_code_to_system_contract( blockchain_test: BlockchainTestFiller, pre: Alloc, + fork: Fork, system_contract: int, call_opcode: Op, ): @@ -2526,7 +2714,7 @@ def test_set_code_to_system_contract( ) caller_payload = deposit_request.calldata call_value = deposit_request.value - case Address(0x00A3CA265EBCB825B45F985A16CEFB49958CE017): # EIP-7002 + case Address(0x09FC772D0857550724B07B850A4323F39112AAAA): # EIP-7002 # Fabricate a valid withdrawal request to the set-code account withdrawal_request = WithdrawalRequest( source_address=0x01, @@ -2536,7 +2724,7 @@ def test_set_code_to_system_contract( ) caller_payload = withdrawal_request.calldata call_value = withdrawal_request.value - case Address(0x00B42DBF2194E931E80326D950320F7D9DBEAC02): # EIP-7251 + case Address(0x01ABEA29659E5E97C95107F20BB753CD3E09BBBB): # EIP-7251 # Fabricate a valid consolidation request to the set-code account consolidation_request = ConsolidationRequest( source_address=0x01, @@ -2562,10 +2750,13 @@ def test_set_code_to_system_contract( + Op.STOP ) caller_code_address = pre.deploy_contract(caller_code) + sender = pre.fund_eoa() + if call_value > 0: + pre.fund_address(sender, call_value) txs = [ Transaction( - sender=pre.fund_eoa(), + sender=sender, gas_limit=500_000, to=caller_code_address, value=call_value, @@ -2585,7 +2776,9 @@ def test_set_code_to_system_contract( blocks=[ Block( txs=txs, - requests_root=[], # Verify nothing slipped into the requests trie + requests_hash=Requests( + max_request_type=fork.max_request_type(block_number=1) + ), # Verify nothing slipped into the requests trie ) ], post={ @@ -2601,7 +2794,12 @@ def test_set_code_to_system_contract( @pytest.mark.with_all_evm_code_types -@pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type != 4) +@pytest.mark.with_all_tx_types( + selector=lambda tx_type: tx_type != 4, + marks=lambda tx_type: pytest.mark.execute(pytest.mark.skip("incompatible tx")) + if tx_type in [0, 3] + else None, +) def test_eoa_tx_after_set_code( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -2813,3 +3011,179 @@ def test_contract_create( tx=tx, post={}, ) + + +@pytest.mark.parametrize( + "self_sponsored", + [ + pytest.param(False, id="not_self_sponsored"), + pytest.param(True, id="self_sponsored"), + ], +) +@pytest.mark.parametrize( + "pre_set_delegation_code", + [ + pytest.param(Op.RETURN(0, 1), id="delegated_account"), + pytest.param(None, id="undelegated_account"), + ], +) +def test_delegation_clearing( + state_test: StateTestFiller, + pre: Alloc, + pre_set_delegation_code: Bytecode | None, + self_sponsored: bool, +): + """ + Test clearing the delegation of an account under a variety of circumstances. + + Args: + pre_set_delegation_code: The code to set on the account before clearing delegation, or None + if the account should not have any code set. + self_sponsored: Whether the delegation clearing transaction is self-sponsored. + """ # noqa: D417 + pre_set_delegation_address: Address | None = None + if pre_set_delegation_code is not None: + pre_set_delegation_address = pre.deploy_contract(pre_set_delegation_code) + + if self_sponsored: + auth_signer = pre.fund_eoa(delegation=pre_set_delegation_address) + else: + auth_signer = pre.fund_eoa(0, delegation=pre_set_delegation_address) + + success_slot = 1 + return_slot = 2 + ext_code_size_slot = 3 + ext_code_hash_slot = 4 + ext_code_copy_slot = 5 + entry_code = ( + Op.SSTORE(success_slot, 1) + + Op.CALL(address=auth_signer) + + Op.SSTORE(return_slot, Op.RETURNDATASIZE) + + Op.SSTORE(ext_code_size_slot, Op.EXTCODESIZE(address=auth_signer)) + + Op.SSTORE(ext_code_hash_slot, Op.EXTCODEHASH(address=auth_signer)) + + Op.EXTCODECOPY(address=auth_signer, size=32) + + Op.SSTORE(ext_code_copy_slot, Op.MLOAD(0)) + + Op.STOP + ) + entry_address = pre.deploy_contract(entry_code) + + authorization = AuthorizationTuple( + address=Spec.RESET_DELEGATION_ADDRESS, # Reset + nonce=auth_signer.nonce + (1 if self_sponsored else 0), + signer=auth_signer, + ) + + tx = Transaction( + gas_limit=200_000, + to=entry_address, + value=0, + authorization_list=[authorization], + sender=pre.fund_eoa() if not self_sponsored else auth_signer, + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account( + nonce=auth_signer.nonce + 1, + code=b"", + storage={}, + ), + entry_address: Account( + storage={ + success_slot: 1, + return_slot: 0, + ext_code_size_slot: 0, + ext_code_hash_slot: Bytes().keccak256(), + ext_code_copy_slot: 0, + }, + ), + }, + ) + + +@pytest.mark.parametrize( + "entry_code", + [ + pytest.param(Om.OOG + Op.STOP, id="out_of_gas"), + pytest.param(Op.INVALID, id="invalid_opcode"), + pytest.param(Op.REVERT(0, 0), id="revert"), + ], +) +def test_delegation_clearing_failing_tx( + state_test: StateTestFiller, + pre: Alloc, + entry_code: Bytecode, +): + """ + Test clearing the delegation of an account in a transaction that fails, OOGs or reverts. + """ # noqa: D417 + pre_set_delegation_code = Op.RETURN(0, 1) + pre_set_delegation_address = pre.deploy_contract(pre_set_delegation_code) + + auth_signer = pre.fund_eoa(0, delegation=pre_set_delegation_address) + + entry_address = pre.deploy_contract(entry_code) + + authorization = AuthorizationTuple( + address=Spec.RESET_DELEGATION_ADDRESS, # Reset + nonce=auth_signer.nonce, + signer=auth_signer, + ) + + tx = Transaction( + gas_limit=100_000, + to=entry_address, + value=0, + authorization_list=[authorization], + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + auth_signer: Account( + nonce=auth_signer.nonce + 1, + code=b"", + storage={}, + ), + }, + ) + + +def test_deploying_delegation_designation_contract( + state_test: StateTestFiller, + pre: Alloc, +): + """ + Test attempting to deploy a contract that has the same format as a delegation designation. + """ + sender = pre.fund_eoa() + + set_to_code = Op.RETURN(0, 1) + set_to_address = pre.deploy_contract(set_to_code) + + initcode = Initcode(deploy_code=Spec.delegation_designation(set_to_address)) + + tx = Transaction( + sender=sender, + to=None, + gas_limit=100_000, + data=initcode, + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + sender: Account( + nonce=1, + ), + tx.created_contract: Account.NONEXISTENT, + }, + ) diff --git a/whitelist.txt b/whitelist.txt index 1514b13382..7eaa4fa2f2 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -450,6 +450,7 @@ v0 v1 v2 v3 +v4 validator validators venv From d20b3c48e1562527a03192180f2fb724791347cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 28 Oct 2024 19:35:12 +0100 Subject: [PATCH 4/4] new(tests): EOF - EIP-4750: add fibonacci and factorial tests for CALLF (#915) Add tests implementing fibonacci sequence and factorial using recursive CALLF instructions. They were originally contributed to ethereum/tests by @hugo-dc in https://github.com/ethereum/tests/commit/89c2147b9b7495421eecb4443012da0fc7d5a3a4. Tests were additionally parametrized. --- .../eip4750_functions/test_callf_execution.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/osaka/eip7692_eof_v1/eip4750_functions/test_callf_execution.py b/tests/osaka/eip7692_eof_v1/eip4750_functions/test_callf_execution.py index 62e8c3caeb..f49859b063 100644 --- a/tests/osaka/eip7692_eof_v1/eip4750_functions/test_callf_execution.py +++ b/tests/osaka/eip7692_eof_v1/eip4750_functions/test_callf_execution.py @@ -1,8 +1,11 @@ """ EOF CALLF execution tests """ +import math + import pytest +from ethereum_test_base_types import Hash from ethereum_test_specs import StateTestFiller from ethereum_test_tools import Account, EOFStateTestFiller from ethereum_test_tools import Opcodes as Op @@ -22,6 +25,92 @@ pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) +@pytest.mark.parametrize( + "n,result", + ((0, 1), (1, 1), (5, 120), (57, math.factorial(57)), (58, math.factorial(58) % 2**256)), +) +def test_callf_factorial(eof_state_test: EOFStateTestFiller, n, result): + """Test factorial implementation with recursive CALLF instructions""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + Op.CALLDATALOAD(0) + Op.SSTORE(0, Op.CALLF[1]) + Op.STOP, + max_stack_height=2, + ), + Section.Code( + Op.PUSH1[1] + + Op.DUP2 + + Op.GT + + Op.RJUMPI[4] + + Op.POP + + Op.PUSH1[1] + + Op.RETF + + Op.PUSH1[1] + + Op.DUP2 + + Op.SUB + + Op.CALLF[1] + + Op.DUP2 + + Op.MUL + + Op.SWAP1 + + Op.POP + + Op.RETF, + code_inputs=1, + code_outputs=1, + max_stack_height=3, + ), + ] + ), + tx_data=Hash(n), + container_post=Account(storage={0: result}), + ) + + +@pytest.mark.parametrize( + "n,result", + ((0, 1), (1, 1), (13, 233), (27, 196418)), +) +def test_callf_fibonacci(eof_state_test: EOFStateTestFiller, n, result): + """Test fibonacci sequence implementation with recursive CALLF instructions""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + Op.CALLDATALOAD(0) + Op.SSTORE(0, Op.CALLF[1]) + Op.STOP, + max_stack_height=2, + ), + Section.Code( + Op.PUSH1[2] + + Op.DUP2 + + Op.GT + + Op.RJUMPI[4] + + Op.POP + + Op.PUSH1[1] + + Op.RETF + + Op.PUSH1[2] + + Op.DUP2 + + Op.SUB + + Op.CALLF[1] + + Op.PUSH1[1] + + Op.DUP3 + + Op.SUB + + Op.CALLF[1] + + Op.ADD + + Op.SWAP1 + + Op.POP + + Op.RETF, + code_inputs=1, + code_outputs=1, + max_stack_height=4, + ), + ] + ), + tx_gas_limit=15_000_000, + tx_data=Hash(n), + container_post=Account(storage={0: result}), + ) + + @pytest.mark.parametrize( "container", (