diff --git a/tests/byzantium/eip196_ec_add_mul/conftest.py b/tests/byzantium/eip196_ec_add_mul/conftest.py new file mode 100644 index 00000000000..2c4a1ede538 --- /dev/null +++ b/tests/byzantium/eip196_ec_add_mul/conftest.py @@ -0,0 +1,36 @@ +"""Shared pytest definitions local to EIP-196 tests.""" + +import pytest +from execution_testing import ( + Address, + Fork, +) + +from ...common.precompile_fixtures import ( + call_contract_address, # noqa: F401 + call_contract_code, # noqa: F401 + call_contract_post_storage, # noqa: F401 + call_opcode, # noqa: F401 + call_succeeds, # noqa: F401 + post, # noqa: F401 + precompile_gas_modifier, # noqa: F401 + sender, # noqa: F401 + tx, # noqa: F401 + tx_gas_limit, # noqa: F401 +) +from .spec import Spec + + +@pytest.fixture +def precompile_gas(precompile_address: Address, fork: Fork) -> int: + """Gas cost for the precompile.""" + gas_costs = fork.gas_costs() + match precompile_address: + case Spec.ECADD: + return gas_costs.G_PRECOMPILE_ECADD + case Spec.ECMUL: + return gas_costs.G_PRECOMPILE_ECMUL + case _: + raise ValueError( + f"Unexpected precompile address: {precompile_address}" + ) diff --git a/tests/byzantium/eip196_ec_add_mul/spec.py b/tests/byzantium/eip196_ec_add_mul/spec.py new file mode 100644 index 00000000000..dec6f159152 --- /dev/null +++ b/tests/byzantium/eip196_ec_add_mul/spec.py @@ -0,0 +1,52 @@ +"""Defines EIP-196 specification constants and functions.""" + +from dataclasses import dataclass + +from execution_testing import Address, BytesConcatenation + +from ...constantinople.eip145_bitwise_shift.spec import ReferenceSpec + +ref_spec_196 = ReferenceSpec( + "EIPS/eip-196.md", "6538d198b1db10784ddccd6931888d7ae718de75" +) + + +@dataclass(frozen=True) +class FP(BytesConcatenation): + """Dataclass that defines an element of the BN254 Prime Field (Fp).""" + + x: int = 0 + + def __bytes__(self) -> bytes: + """Convert field element to bytes.""" + return self.x.to_bytes(32, byteorder="big") + + +@dataclass(frozen=True) +class PointG1(BytesConcatenation): + """Dataclass that defines an affine point in the BN254 E(Fp) group (G1).""" + + x: int = 0 + y: int = 0 + + def __bytes__(self) -> bytes: + """Convert point to bytes.""" + return FP(self.x) + FP(self.y) + + +@dataclass(frozen=True) +class Spec: + """ + Parameters from the EIP-196 specification (https://eips.ethereum.org/EIPS/eip-196) + with some modifications for readability. + """ + + # Addresses + ECADD = Address(0x06) + ECMUL = Address(0x07) + + # G1 generator point + G1 = PointG1(1, 2) + + # The point at infinity in G1 + INF_G1 = PointG1() diff --git a/tests/byzantium/eip196_ec_add_mul/test_ecadd.py b/tests/byzantium/eip196_ec_add_mul/test_ecadd.py new file mode 100644 index 00000000000..e79f0a1870c --- /dev/null +++ b/tests/byzantium/eip196_ec_add_mul/test_ecadd.py @@ -0,0 +1,69 @@ +"""Tests the ecadd precompiled contract.""" + +import pytest +from execution_testing import ( + Alloc, + Environment, + StateTestFiller, + Transaction, +) + +from .spec import PointG1, Spec, ref_spec_196 + +REFERENCE_SPEC_GIT_PATH = ref_spec_196.git_path +REFERENCE_SPEC_VERSION = ref_spec_196.version + +pytestmark = [ + pytest.mark.valid_from("Byzantium"), + pytest.mark.parametrize("precompile_address", [Spec.ECADD], ids=["ecadd"]), +] + + +@pytest.mark.parametrize( + "input_data, expected_output", + [ + pytest.param( + Spec.G1 + Spec.INF_G1, + Spec.G1, + id="generator_plus_inf", + ), + ], +) +def test_valid( + state_test: StateTestFiller, + pre: Alloc, + post: dict, + tx: Transaction, +) -> None: + """Test the valid inputs to the ECADD precompile.""" + state_test( + env=Environment(), + pre=pre, + tx=tx, + post=post, + ) + + +@pytest.mark.parametrize( + "input_data, expected_output", + [ + pytest.param( + PointG1(1, 1) + Spec.INF_G1, + b"", + id="pt_1_1_plus_inf", + ), + ], +) +def test_invalid( + state_test: StateTestFiller, + pre: Alloc, + post: dict, + tx: Transaction, +) -> None: + """Test the invalid inputs to the ECADD precompile.""" + state_test( + env=Environment(), + pre=pre, + tx=tx, + post=post, + ) diff --git a/tests/byzantium/eip196_ec_add_mul/test_gas.py b/tests/byzantium/eip196_ec_add_mul/test_gas.py index 66f44218ea7..1bce8fc06dc 100644 --- a/tests/byzantium/eip196_ec_add_mul/test_gas.py +++ b/tests/byzantium/eip196_ec_add_mul/test_gas.py @@ -3,28 +3,27 @@ import pytest from execution_testing import ( Account, + Address, Alloc, StateTestFiller, Transaction, ) -from execution_testing.base_types.base_types import Address from execution_testing.forks import Byzantium from execution_testing.forks.helpers import Fork from execution_testing.vm import Opcodes as Op -REFERENCE_SPEC_GIT_PATH = "EIPS/eip-196.md" -REFERENCE_SPEC_VERSION = "6538d198b1db10784ddccd6931888d7ae718de75" +from .spec import Spec, ref_spec_196 -EC_ADD_ADDRESS = Address(0x06) -EC_MUL_ADDRESS = Address(0x07) +REFERENCE_SPEC_GIT_PATH = ref_spec_196.git_path +REFERENCE_SPEC_VERSION = ref_spec_196.version @pytest.mark.valid_from("Byzantium") @pytest.mark.parametrize( "address", [ - pytest.param(EC_ADD_ADDRESS, id="ecadd"), - pytest.param(EC_MUL_ADDRESS, id="ecmul"), + pytest.param(Spec.ECADD, id="ecadd"), + pytest.param(Spec.ECMUL, id="ecmul"), ], ) @pytest.mark.parametrize("enough_gas", [True, False]) @@ -42,7 +41,7 @@ def test_gas_costs( gas_costs = fork.gas_costs() gas = ( gas_costs.G_PRECOMPILE_ECADD - if address == EC_ADD_ADDRESS + if address == Spec.ECADD else gas_costs.G_PRECOMPILE_ECMUL ) if not enough_gas: diff --git a/tests/common/precompile_fixtures.py b/tests/common/precompile_fixtures.py new file mode 100644 index 00000000000..fec15b87b69 --- /dev/null +++ b/tests/common/precompile_fixtures.py @@ -0,0 +1,208 @@ +""" +A set of common pytest fixtures for tests executing precompiled contracts. + +These can help creating a wrapping contract that calls the precompile and +records the results in storage, so that tests can verify the expected behavior. +Also, the gas calculation and transaction setup is handled here. +""" + +from typing import SupportsBytes + +import pytest +from execution_testing import ( + EOA, + Address, + Alloc, + Bytecode, + Fork, + Op, + Storage, + Transaction, + keccak256, +) + + +@pytest.fixture +def precompile_gas_modifier() -> int: + """ + Modify the gas passed to the precompile, for testing purposes. + + By default the call is made with the exact gas amount required for the + given opcode, but when this fixture is overridden, the gas amount can be + modified to, e.g., test a lower amount and test if the precompile call + fails. + """ + return 0 + + +@pytest.fixture +def call_opcode() -> Op: + """ + Type of call used to call the precompile. + + By default it is Op.CALL, but it can be overridden in the test. + """ + return Op.CALL + + +@pytest.fixture +def call_contract_post_storage() -> Storage: + """ + Storage of the test contract after the transaction is executed. + + Note: + Fixture `call_contract_code` fills the actual expected storage values. + + """ + return Storage() + + +@pytest.fixture +def call_succeeds( + expected_output: bytes | SupportsBytes, +) -> bool: + """ + By default, depending on the expected output, we can deduce if the call is + expected to succeed or fail. + """ + return len(bytes(expected_output)) > 0 + + +@pytest.fixture +def call_contract_code( + precompile_address: int, + precompile_gas: int | None, + precompile_gas_modifier: int, + expected_output: bytes | SupportsBytes, + call_succeeds: bool, + call_opcode: Op, + call_contract_post_storage: Storage, +) -> Bytecode: + """ + Code of the precompile wrapping test contract. + The code calls the precompile and stores the call return code, output size, + and output hash in storage. The information about the call output is + collected with the RETURNDATASIZE and RETURNDATACOPY opcodes. + Therefore, the test contract doesn't work correctly in forks pre Byzantium. + + Args: + precompile_address: Address of the precompile to call. + precompile_gas: Gas cost for the precompile, which is automatically + calculated by the `precompile_gas` fixture, but can + be overridden in the test. + precompile_gas_modifier: Gas cost modifier for the precompile, which + is automatically set to zero by the + `precompile_gas_modifier` fixture, but + can be overridden in the test. + expected_output: Expected output of the precompile call. + This value is used to determine if the call is + expected to succeed or fail. + call_succeeds: Boolean that indicates if the call is expected to + succeed or fail. + call_opcode: Type of call used to call the precompile (Op.CALL, + Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL). + call_contract_post_storage: Storage of the test contract after the + transaction is executed. + + """ + expected_output = bytes(expected_output) + + assert call_opcode in [ + Op.CALL, + Op.CALLCODE, + Op.DELEGATECALL, + Op.STATICCALL, + ] + value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else [] + + precompile_gas_value_opcode: int | Op + if precompile_gas is None: + precompile_gas_value_opcode = Op.GAS + else: + precompile_gas_value_opcode = precompile_gas + precompile_gas_modifier + + code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) + + Op.SSTORE( + call_contract_post_storage.store_next(call_succeeds), + call_opcode( + precompile_gas_value_opcode, + precompile_address, + *value, # Optional, only used for CALL and CALLCODE. + 0, + Op.CALLDATASIZE(), + 0, + 0, + ), + ) + + Op.SSTORE( + call_contract_post_storage.store_next(len(expected_output)), + Op.RETURNDATASIZE(), + ) + ) + if call_succeeds: + # Add integrity check only if the call is expected to succeed. + code += Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE()) + Op.SSTORE( + call_contract_post_storage.store_next(keccak256(expected_output)), + Op.SHA3(0, Op.RETURNDATASIZE()), + ) + + return code + + +@pytest.fixture +def call_contract_address(pre: Alloc, call_contract_code: Bytecode) -> Address: + """Address where the test contract will be deployed.""" + return pre.deploy_contract(call_contract_code) + + +@pytest.fixture +def sender(pre: Alloc) -> EOA: + """Sender of the transaction.""" + return pre.fund_eoa() + + +@pytest.fixture +def post( + call_contract_address: Address, call_contract_post_storage: Storage +) -> dict: + """Test expected post outcome.""" + return { + call_contract_address: { + "storage": call_contract_post_storage, + }, + } + + +@pytest.fixture +def tx_gas_limit(fork: Fork, input_data: bytes, precompile_gas: int) -> int: + """ + Transaction gas limit used for the test (Can be overridden in the test). + """ + intrinsic_gas_cost_calculator = ( + fork.transaction_intrinsic_cost_calculator() + ) + memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() + extra_gas = 100_000 + return ( + extra_gas + + intrinsic_gas_cost_calculator(calldata=input_data) + + memory_expansion_gas_calculator(new_bytes=len(input_data)) + + precompile_gas + ) + + +@pytest.fixture +def tx( + input_data: bytes, + tx_gas_limit: int, + call_contract_address: Address, + sender: EOA, +) -> Transaction: + """Transaction for the test.""" + return Transaction( + gas_limit=tx_gas_limit, + data=input_data, + to=call_contract_address, + sender=sender, + ) diff --git a/tests/prague/eip2537_bls_12_381_precompiles/conftest.py b/tests/prague/eip2537_bls_12_381_precompiles/conftest.py index 6f279bafc87..a339b5b50b4 100644 --- a/tests/prague/eip2537_bls_12_381_precompiles/conftest.py +++ b/tests/prague/eip2537_bls_12_381_precompiles/conftest.py @@ -1,20 +1,19 @@ """Shared pytest definitions local to EIP-2537 tests.""" -from typing import SupportsBytes - import pytest -from execution_testing import ( - EOA, - Address, - Alloc, - Bytecode, - Fork, - Op, - Storage, - Transaction, - keccak256, -) +from ...common.precompile_fixtures import ( + call_contract_address, # noqa: F401 + call_contract_code, # noqa: F401 + call_contract_post_storage, # noqa: F401 + call_opcode, # noqa: F401 + call_succeeds, # noqa: F401 + post, # noqa: F401 + precompile_gas_modifier, # noqa: F401 + sender, # noqa: F401 + tx, # noqa: F401 + tx_gas_limit, # noqa: F401 +) from .helpers import BLSPointGenerator from .spec import GAS_CALCULATION_FUNCTION_MAP @@ -48,188 +47,6 @@ def precompile_gas( return calculated_gas -@pytest.fixture -def precompile_gas_modifier() -> int: - """ - Modify the gas passed to the precompile, for testing purposes. - - By default the call is made with the exact gas amount required for the - given opcode, but when this fixture is overridden, the gas amount can be - modified to, e.g., test a lower amount and test if the precompile call - fails. - """ - return 0 - - -@pytest.fixture -def call_opcode() -> Op: - """ - Type of call used to call the precompile. - - By default it is Op.CALL, but it can be overridden in the test. - """ - return Op.CALL - - -@pytest.fixture -def call_contract_post_storage() -> Storage: - """ - Storage of the test contract after the transaction is executed. - - Note: - Fixture `call_contract_code` fills the actual expected storage values. - - """ - return Storage() - - -@pytest.fixture -def call_succeeds( - expected_output: bytes | SupportsBytes, -) -> bool: - """ - By default, depending on the expected output, we can deduce if the call is - expected to succeed or fail. - """ - return len(bytes(expected_output)) > 0 - - -@pytest.fixture -def call_contract_code( - precompile_address: int, - precompile_gas: int | None, - precompile_gas_modifier: int, - expected_output: bytes | SupportsBytes, - call_succeeds: bool, - call_opcode: Op, - call_contract_post_storage: Storage, -) -> Bytecode: - """ - Code of the test contract. - - Args: - precompile_address: Address of the precompile to call. - precompile_gas: Gas cost for the precompile, which is automatically - calculated by the `precompile_gas` fixture, but can - be overridden in the test. - precompile_gas_modifier: Gas cost modifier for the precompile, which - is automatically set to zero by the - `precompile_gas_modifier` fixture, but - can be overridden in the test. - expected_output: Expected output of the precompile call. - This value is used to determine if the call is - expected to succeed or fail. - call_succeeds: Boolean that indicates if the call is expected to - succeed or fail. - call_opcode: Type of call used to call the precompile (Op.CALL, - Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL). - call_contract_post_storage: Storage of the test contract after the - transaction is executed. - - """ - expected_output = bytes(expected_output) - - assert call_opcode in [ - Op.CALL, - Op.CALLCODE, - Op.DELEGATECALL, - Op.STATICCALL, - ] - value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else [] - - precompile_gas_value_opcode: int | Op - if precompile_gas is None: - precompile_gas_value_opcode = Op.GAS - else: - precompile_gas_value_opcode = precompile_gas + precompile_gas_modifier - - code = ( - Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) - + Op.SSTORE( - call_contract_post_storage.store_next(call_succeeds), - call_opcode( - precompile_gas_value_opcode, - precompile_address, - *value, # Optional, only used for CALL and CALLCODE. - 0, - Op.CALLDATASIZE(), - 0, - 0, - ), - ) - + Op.SSTORE( - call_contract_post_storage.store_next(len(expected_output)), - Op.RETURNDATASIZE(), - ) - ) - if call_succeeds: - # Add integrity check only if the call is expected to succeed. - code += Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE()) + Op.SSTORE( - call_contract_post_storage.store_next(keccak256(expected_output)), - Op.SHA3(0, Op.RETURNDATASIZE()), - ) - - return code - - -@pytest.fixture -def call_contract_address(pre: Alloc, call_contract_code: Bytecode) -> Address: - """Address where the test contract will be deployed.""" - return pre.deploy_contract(call_contract_code) - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Sender of the transaction.""" - return pre.fund_eoa() - - -@pytest.fixture -def post( - call_contract_address: Address, call_contract_post_storage: Storage -) -> dict: - """Test expected post outcome.""" - return { - call_contract_address: { - "storage": call_contract_post_storage, - }, - } - - -@pytest.fixture -def tx_gas_limit(fork: Fork, input_data: bytes, precompile_gas: int) -> int: - """ - Transaction gas limit used for the test (Can be overridden in the test). - """ - intrinsic_gas_cost_calculator = ( - fork.transaction_intrinsic_cost_calculator() - ) - memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() - extra_gas = 100_000 - return ( - extra_gas - + intrinsic_gas_cost_calculator(calldata=input_data) - + memory_expansion_gas_calculator(new_bytes=len(input_data)) - + precompile_gas - ) - - -@pytest.fixture -def tx( - input_data: bytes, - tx_gas_limit: int, - call_contract_address: Address, - sender: EOA, -) -> Transaction: - """Transaction for the test.""" - return Transaction( - gas_limit=tx_gas_limit, - data=input_data, - to=call_contract_address, - sender=sender, - ) - - NUM_TEST_POINTS = 5 # Random points not in the subgroup (fast to generate)