|
| 1 | +"""Conftest for EIP-7823 tests.""" |
| 2 | + |
| 3 | +from typing import Dict |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from ethereum_test_forks import Fork, Osaka |
| 8 | +from ethereum_test_tools import Account, Address, Alloc, Bytes, Storage, Transaction, keccak256 |
| 9 | +from ethereum_test_tools.vm.opcode import Opcodes as Op |
| 10 | +from ethereum_test_types import Environment |
| 11 | + |
| 12 | +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput |
| 13 | +from ..eip7883_modexp_gas_increase.spec import Spec as Spec7883 |
| 14 | +from .spec import Spec |
| 15 | + |
| 16 | + |
| 17 | +@pytest.fixture |
| 18 | +def call_contract_post_storage() -> Storage: |
| 19 | + """ |
| 20 | + Storage of the test contract after the transaction is executed. |
| 21 | + Note: Fixture `call_contract_code` fills the actual expected storage values. |
| 22 | + """ |
| 23 | + return Storage() |
| 24 | + |
| 25 | + |
| 26 | +@pytest.fixture |
| 27 | +def call_succeeds( |
| 28 | + total_gas_used: int, fork: Fork, env: Environment, modexp_input: ModExpInput |
| 29 | +) -> bool: |
| 30 | + """ |
| 31 | + By default, depending on the expected output, we can deduce if the call is expected to succeed |
| 32 | + or fail. |
| 33 | + """ |
| 34 | + # Transaction gas limit exceeded |
| 35 | + tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit |
| 36 | + if total_gas_used > tx_gas_limit_cap: |
| 37 | + return False |
| 38 | + |
| 39 | + # Input length exceeded |
| 40 | + base_length, exp_length, mod_length = modexp_input.get_declared_lengths() |
| 41 | + if ( |
| 42 | + base_length > Spec.MAX_LENGTH_BYTES |
| 43 | + or exp_length > Spec.MAX_LENGTH_BYTES |
| 44 | + or mod_length > Spec.MAX_LENGTH_BYTES |
| 45 | + ) and fork >= Osaka: |
| 46 | + return False |
| 47 | + |
| 48 | + return True |
| 49 | + |
| 50 | + |
| 51 | +@pytest.fixture |
| 52 | +def gas_measure_contract( |
| 53 | + pre: Alloc, |
| 54 | + fork: Fork, |
| 55 | + modexp_expected: bytes, |
| 56 | + precompile_gas: int, |
| 57 | + precompile_gas_modifier: int, |
| 58 | + call_contract_post_storage: Storage, |
| 59 | + call_succeeds: bool, |
| 60 | +) -> Address: |
| 61 | + """ |
| 62 | + Deploys a contract that measures ModExp gas consumption and execution result. |
| 63 | +
|
| 64 | + Always stored: |
| 65 | + storage[0]: precompile call success |
| 66 | + storage[1]: return data length from precompile |
| 67 | + Only if the precompile call succeeds: |
| 68 | + storage[2]: gas consumed by precompile |
| 69 | + storage[3]: hash of return data from precompile |
| 70 | + """ |
| 71 | + call_code = Op.CALL( |
| 72 | + precompile_gas + precompile_gas_modifier, |
| 73 | + Spec.MODEXP_ADDRESS, |
| 74 | + 0, |
| 75 | + 0, |
| 76 | + Op.CALLDATASIZE(), |
| 77 | + 0, |
| 78 | + 0, |
| 79 | + ) |
| 80 | + |
| 81 | + gas_costs = fork.gas_costs() |
| 82 | + extra_gas = ( |
| 83 | + gas_costs.G_WARM_ACCOUNT_ACCESS |
| 84 | + + (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 2)) # type: ignore |
| 85 | + + gas_costs.G_BASE # CALLDATASIZE |
| 86 | + + gas_costs.G_BASE # GAS |
| 87 | + ) |
| 88 | + |
| 89 | + # Build the gas measurement contract code |
| 90 | + # Stack operations: |
| 91 | + # [gas_start] |
| 92 | + # [gas_start, call_result] |
| 93 | + # [gas_start, call_result, gas_end] |
| 94 | + # [gas_start, gas_end, call_result] |
| 95 | + call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1 |
| 96 | + |
| 97 | + # Calculate gas consumed: gas_start - (gas_end + extra_gas) |
| 98 | + # Stack Operation: |
| 99 | + # [gas_start, gas_end] |
| 100 | + # [gas_start, gas_end, extra_gas] |
| 101 | + # [gas_start, gas_end + extra_gas] |
| 102 | + # [gas_end + extra_gas, gas_start] |
| 103 | + # [gas_consumed] |
| 104 | + gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB |
| 105 | + |
| 106 | + code = ( |
| 107 | + Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE) |
| 108 | + + Op.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement) |
| 109 | + + Op.SSTORE( |
| 110 | + call_contract_post_storage.store_next(len(modexp_expected) if call_succeeds else 0), |
| 111 | + Op.RETURNDATASIZE(), |
| 112 | + ) |
| 113 | + ) |
| 114 | + |
| 115 | + if call_succeeds: |
| 116 | + code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) |
| 117 | + code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) |
| 118 | + code += Op.SSTORE( |
| 119 | + call_contract_post_storage.store_next(keccak256(Bytes(modexp_expected))), |
| 120 | + Op.SHA3(0, Op.RETURNDATASIZE()), |
| 121 | + ) |
| 122 | + return pre.deploy_contract(code) |
| 123 | + |
| 124 | + |
| 125 | +@pytest.fixture |
| 126 | +def precompile_gas(fork: Fork, modexp_input: ModExpInput) -> int: |
| 127 | + """Calculate gas cost for the ModExp precompile and verify it matches expected gas.""" |
| 128 | + spec = Spec if fork < Osaka else Spec7883 |
| 129 | + try: |
| 130 | + calculated_gas = spec.calculate_gas_cost(modexp_input) |
| 131 | + return calculated_gas |
| 132 | + except Exception: |
| 133 | + # Used for `test_modexp_invalid_inputs` we expect the call to not succeed. |
| 134 | + # Return is for completeness. |
| 135 | + return 500 if fork >= Osaka else 200 |
| 136 | + |
| 137 | + |
| 138 | +@pytest.fixture |
| 139 | +def precompile_gas_modifier() -> int: |
| 140 | + """Return the gas modifier for the ModExp precompile.""" |
| 141 | + return 0 |
| 142 | + |
| 143 | + |
| 144 | +@pytest.fixture |
| 145 | +def tx( |
| 146 | + pre: Alloc, |
| 147 | + gas_measure_contract: Address, |
| 148 | + modexp_input: ModExpInput, |
| 149 | + tx_gas_limit: int, |
| 150 | +) -> Transaction: |
| 151 | + """Transaction to measure gas consumption of the ModExp precompile.""" |
| 152 | + return Transaction( |
| 153 | + sender=pre.fund_eoa(), |
| 154 | + to=gas_measure_contract, |
| 155 | + data=bytes(modexp_input), |
| 156 | + gas_limit=tx_gas_limit, |
| 157 | + ) |
| 158 | + |
| 159 | + |
| 160 | +@pytest.fixture |
| 161 | +def total_gas_used( |
| 162 | + fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int |
| 163 | +) -> int: |
| 164 | + """Transaction gas limit used for the test (Can be overridden in the test).""" |
| 165 | + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() |
| 166 | + memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() |
| 167 | + extra_gas = 500_000 |
| 168 | + |
| 169 | + total_gas = ( |
| 170 | + extra_gas |
| 171 | + + intrinsic_gas_cost_calculator(calldata=bytes(modexp_input)) |
| 172 | + + memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input))) |
| 173 | + + precompile_gas |
| 174 | + ) |
| 175 | + |
| 176 | + return total_gas |
| 177 | + |
| 178 | + |
| 179 | +@pytest.fixture |
| 180 | +def tx_gas_limit(total_gas_used: int, fork: Fork, env: Environment) -> int: |
| 181 | + """Transaction gas limit used for the test (Can be overridden in the test).""" |
| 182 | + tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit |
| 183 | + return min(tx_gas_limit_cap, total_gas_used) |
| 184 | + |
| 185 | + |
| 186 | +@pytest.fixture |
| 187 | +def post( |
| 188 | + gas_measure_contract: Address, |
| 189 | + call_contract_post_storage: Storage, |
| 190 | +) -> Dict[Address, Account]: |
| 191 | + """Return expected post state with gas consumption check.""" |
| 192 | + return { |
| 193 | + gas_measure_contract: Account(storage=call_contract_post_storage), |
| 194 | + } |
0 commit comments