|
| 1 | +""" |
| 2 | +Fixtures for the EIP-7623 tests. |
| 3 | +""" |
| 4 | + |
| 5 | +from typing import Callable, List, Sequence |
| 6 | + |
| 7 | +import pytest |
| 8 | + |
| 9 | +from ethereum_test_forks import Fork |
| 10 | +from ethereum_test_tools import ( |
| 11 | + EOA, |
| 12 | + AccessList, |
| 13 | + Address, |
| 14 | + Alloc, |
| 15 | + AuthorizationTuple, |
| 16 | + Bytecode, |
| 17 | + Bytes, |
| 18 | + Hash, |
| 19 | +) |
| 20 | +from ethereum_test_tools import Opcodes as Op |
| 21 | +from ethereum_test_tools import Transaction, TransactionException |
| 22 | + |
| 23 | +from .helpers import DataTestType |
| 24 | + |
| 25 | + |
| 26 | +@pytest.fixture |
| 27 | +def sender(pre: Alloc) -> EOA: |
| 28 | + """ |
| 29 | + Create the sender account. |
| 30 | + """ |
| 31 | + return pre.fund_eoa() |
| 32 | + |
| 33 | + |
| 34 | +@pytest.fixture |
| 35 | +def to( |
| 36 | + request: pytest.FixtureRequest, |
| 37 | + pre: Alloc, |
| 38 | +) -> Address | None: |
| 39 | + """ |
| 40 | + Create the sender account. |
| 41 | + """ |
| 42 | + if hasattr(request, "param"): |
| 43 | + param = request.param |
| 44 | + else: |
| 45 | + param = Op.STOP |
| 46 | + |
| 47 | + if param is None: |
| 48 | + return None |
| 49 | + if isinstance(param, Address): |
| 50 | + return param |
| 51 | + if isinstance(param, Bytecode): |
| 52 | + return pre.deploy_contract(param) |
| 53 | + |
| 54 | + raise ValueError(f"Invalid value for `to` fixture: {param}") |
| 55 | + |
| 56 | + |
| 57 | +@pytest.fixture |
| 58 | +def protected() -> bool: |
| 59 | + """ |
| 60 | + Whether the transaction is protected or not. Only valid for type-0 transactions. |
| 61 | + """ |
| 62 | + return True |
| 63 | + |
| 64 | + |
| 65 | +@pytest.fixture |
| 66 | +def access_list() -> List[AccessList] | None: |
| 67 | + """ |
| 68 | + Access list for the transaction. |
| 69 | + """ |
| 70 | + return None |
| 71 | + |
| 72 | + |
| 73 | +@pytest.fixture |
| 74 | +def authorization_existing_authority() -> bool: |
| 75 | + """ |
| 76 | + Whether the transaction has an existing authority in the authorization list. |
| 77 | + """ |
| 78 | + return False |
| 79 | + |
| 80 | + |
| 81 | +@pytest.fixture |
| 82 | +def authorization_list( |
| 83 | + request: pytest.FixtureRequest, |
| 84 | + pre: Alloc, |
| 85 | + authorization_existing_authority: bool, |
| 86 | +) -> List[AuthorizationTuple] | None: |
| 87 | + """ |
| 88 | + Authorization list for the transaction. |
| 89 | +
|
| 90 | + This fixture needs to be parametrized indirectly in order to generate the authorizations with |
| 91 | + valid signers using `pre` in this function, and the parametrized value should be a list of |
| 92 | + addresses. |
| 93 | + """ |
| 94 | + if not hasattr(request, "param"): |
| 95 | + return None |
| 96 | + if request.param is None: |
| 97 | + return None |
| 98 | + return [ |
| 99 | + AuthorizationTuple( |
| 100 | + signer=pre.fund_eoa(1 if authorization_existing_authority else 0), address=address |
| 101 | + ) |
| 102 | + for address in request.param |
| 103 | + ] |
| 104 | + |
| 105 | + |
| 106 | +@pytest.fixture |
| 107 | +def blob_versioned_hashes() -> Sequence[Hash] | None: |
| 108 | + """ |
| 109 | + Versioned hashes for the transaction. |
| 110 | + """ |
| 111 | + return None |
| 112 | + |
| 113 | + |
| 114 | +@pytest.fixture |
| 115 | +def contract_creating_tx(to: Address | None) -> bool: |
| 116 | + """ |
| 117 | + Whether the transaction creates a contract or not. |
| 118 | + """ |
| 119 | + return to is None |
| 120 | + |
| 121 | + |
| 122 | +def floor_cost_find( |
| 123 | + floor_data_gas_cost_calculator: Callable[[int], int], |
| 124 | + intrinsic_gas_cost_calculator: Callable[[int], int], |
| 125 | +) -> int: |
| 126 | + """ |
| 127 | + Find the minimum amount of tokens that will trigger the floor gas cost, by using a binary |
| 128 | + search and the intrinsic gas cost and floor data calculators. |
| 129 | + """ |
| 130 | + # Start with 1000 tokens and if the intrinsic gas cost is greater than the floor gas cost, |
| 131 | + # multiply the number of tokens by 2 until it's not. |
| 132 | + tokens = 1000 |
| 133 | + while floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens): |
| 134 | + tokens *= 2 |
| 135 | + |
| 136 | + # Binary search to find the minimum number of tokens that will trigger the floor gas cost. |
| 137 | + left = 0 |
| 138 | + right = tokens |
| 139 | + while left < right: |
| 140 | + tokens = (left + right) // 2 |
| 141 | + if floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens): |
| 142 | + left = tokens + 1 |
| 143 | + else: |
| 144 | + right = tokens |
| 145 | + tokens = left |
| 146 | + |
| 147 | + if floor_data_gas_cost_calculator(tokens) > intrinsic_gas_cost_calculator(tokens): |
| 148 | + tokens -= 1 |
| 149 | + |
| 150 | + # Verify that increasing the tokens by one would always trigger the floor gas cost. |
| 151 | + assert ( |
| 152 | + floor_data_gas_cost_calculator(tokens) <= intrinsic_gas_cost_calculator(tokens) |
| 153 | + ) and floor_data_gas_cost_calculator(tokens + 1) > intrinsic_gas_cost_calculator( |
| 154 | + tokens + 1 |
| 155 | + ), "invalid case" |
| 156 | + |
| 157 | + return tokens |
| 158 | + |
| 159 | + |
| 160 | +@pytest.fixture |
| 161 | +def intrinsic_gas_data_floor_minimum_delta() -> int: |
| 162 | + """ |
| 163 | + Induce a minimum delta between the transaction intrinsic gas cost and the |
| 164 | + floor data gas cost. |
| 165 | + """ |
| 166 | + return 0 |
| 167 | + |
| 168 | + |
| 169 | +@pytest.fixture |
| 170 | +def tx_data( |
| 171 | + fork: Fork, |
| 172 | + data_test_type: DataTestType, |
| 173 | + access_list: List[AccessList] | None, |
| 174 | + authorization_list: List[AuthorizationTuple] | None, |
| 175 | + contract_creating_tx: bool, |
| 176 | + intrinsic_gas_data_floor_minimum_delta: int, |
| 177 | +) -> Bytes: |
| 178 | + """ |
| 179 | + All tests in this file use data that is generated dynamically depending on the case and the |
| 180 | + attributes of the transaction in order to reach the edge cases where the floor gas cost is |
| 181 | + equal or barely greater than the intrinsic gas cost. |
| 182 | +
|
| 183 | + We have two different types of tests: |
| 184 | + - FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: The floor gas cost is less than or equal |
| 185 | + to the intrinsic gas cost, which means that the size of the tokens in the data are not |
| 186 | + enough to trigger the floor gas cost. |
| 187 | + - FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: The floor gas cost is greater than the intrinsic |
| 188 | + gas cost, which means that the size of the tokens in the data are enough to trigger the |
| 189 | + floor gas cost. |
| 190 | +
|
| 191 | + E.g. Given a transaction with a single access list and a single storage key, its intrinsic gas |
| 192 | + cost (as of Prague fork) can be calculated as: |
| 193 | + - 21,000 gas for the transaction |
| 194 | + - 2,400 gas for the access list |
| 195 | + - 1,900 gas for the storage key |
| 196 | + - 16 gas for each non-zero byte in the data |
| 197 | + - 4 gas for each zero byte in the data |
| 198 | +
|
| 199 | + Its floor data gas cost can be calculated as: |
| 200 | + - 21,000 gas for the transaction |
| 201 | + - 40 gas for each non-zero byte in the data |
| 202 | + - 10 gas for each zero byte in the data |
| 203 | +
|
| 204 | + Notice that the data included in the transaction affects both the intrinsic gas cost and the |
| 205 | + floor data cost, but at different rates. |
| 206 | +
|
| 207 | + The purpose of this function is to find the exact amount of data where the floor data gas |
| 208 | + cost starts exceeding the intrinsic gas cost. |
| 209 | +
|
| 210 | + After a binary search we find that adding 717 tokens of data (179 non-zero bytes + |
| 211 | + 1 zero byte) triggers the floor gas cost. |
| 212 | +
|
| 213 | + Therefore, this function will return a Bytes object with 179 non-zero bytes and 1 zero byte |
| 214 | + for `FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS` and a Bytes object with 179 non-zero bytes |
| 215 | + and no zero bytes for `FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS` |
| 216 | + """ |
| 217 | + |
| 218 | + def tokens_to_data(tokens: int) -> Bytes: |
| 219 | + return Bytes(b"\x01" * (tokens // 4) + b"\x00" * (tokens % 4)) |
| 220 | + |
| 221 | + fork_intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator() |
| 222 | + |
| 223 | + def transaction_intrinsic_cost_calculator(tokens: int) -> int: |
| 224 | + return ( |
| 225 | + fork_intrinsic_cost_calculator( |
| 226 | + calldata=tokens_to_data(tokens), |
| 227 | + contract_creation=contract_creating_tx, |
| 228 | + access_list=access_list, |
| 229 | + authorization_list_or_count=authorization_list, |
| 230 | + return_cost_deducted_prior_execution=True, |
| 231 | + ) |
| 232 | + + intrinsic_gas_data_floor_minimum_delta |
| 233 | + ) |
| 234 | + |
| 235 | + fork_data_floor_cost_calculator = fork.transaction_data_floor_cost_calculator() |
| 236 | + |
| 237 | + def transaction_data_floor_cost_calculator(tokens: int) -> int: |
| 238 | + return fork_data_floor_cost_calculator(data=tokens_to_data(tokens)) |
| 239 | + |
| 240 | + # Start with zero data and check the difference in the gas calculator between the |
| 241 | + # intrinsic gas cost and the floor gas cost. |
| 242 | + if transaction_data_floor_cost_calculator(0) >= transaction_intrinsic_cost_calculator(0): |
| 243 | + # Special case which is a transaction with no extra intrinsic gas costs other than the |
| 244 | + # data cost, any data will trigger the floor gas cost. |
| 245 | + if data_test_type == DataTestType.FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: |
| 246 | + return Bytes(b"") |
| 247 | + else: |
| 248 | + return Bytes(b"\0") |
| 249 | + |
| 250 | + tokens = floor_cost_find( |
| 251 | + floor_data_gas_cost_calculator=transaction_data_floor_cost_calculator, |
| 252 | + intrinsic_gas_cost_calculator=transaction_intrinsic_cost_calculator, |
| 253 | + ) |
| 254 | + |
| 255 | + if data_test_type == DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: |
| 256 | + return tokens_to_data(tokens + 1) |
| 257 | + return tokens_to_data(tokens) |
| 258 | + |
| 259 | + |
| 260 | +@pytest.fixture |
| 261 | +def tx_gas_delta() -> int: |
| 262 | + """ |
| 263 | + Gas delta to modify the gas amount included with the transaction. |
| 264 | +
|
| 265 | + If negative, the transaction will be invalid because the intrinsic gas cost is greater than the |
| 266 | + gas limit. |
| 267 | +
|
| 268 | + This value operates regardless of whether the floor data gas cost is reached or not. |
| 269 | +
|
| 270 | + If the value is greater than zero, the transaction will also be valid and the test will check |
| 271 | + that transaction processing does not consume more gas than it should. |
| 272 | + """ |
| 273 | + return 0 |
| 274 | + |
| 275 | + |
| 276 | +@pytest.fixture |
| 277 | +def tx_gas( |
| 278 | + fork: Fork, |
| 279 | + tx_data: Bytes, |
| 280 | + access_list: List[AccessList] | None, |
| 281 | + authorization_list: List[AuthorizationTuple] | None, |
| 282 | + contract_creating_tx: bool, |
| 283 | + tx_gas_delta: int, |
| 284 | +) -> int: |
| 285 | + """ |
| 286 | + Gas limit for the transaction. |
| 287 | +
|
| 288 | + The calculated value takes into account the normal intrinsic gas cost and the floor data gas |
| 289 | + cost. |
| 290 | +
|
| 291 | + The gas delta is added to the intrinsic gas cost to generate different test scenarios. |
| 292 | + """ |
| 293 | + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() |
| 294 | + return ( |
| 295 | + intrinsic_gas_cost_calculator( |
| 296 | + calldata=tx_data, |
| 297 | + contract_creation=contract_creating_tx, |
| 298 | + access_list=access_list, |
| 299 | + authorization_list_or_count=authorization_list, |
| 300 | + ) |
| 301 | + + tx_gas_delta |
| 302 | + ) |
| 303 | + |
| 304 | + |
| 305 | +@pytest.fixture |
| 306 | +def tx_error(tx_gas_delta: int) -> TransactionException | None: |
| 307 | + """ |
| 308 | + Transaction error, only expected if the gas delta is negative. |
| 309 | + """ |
| 310 | + return TransactionException.INTRINSIC_GAS_TOO_LOW if tx_gas_delta < 0 else None |
| 311 | + |
| 312 | + |
| 313 | +@pytest.fixture |
| 314 | +def tx( |
| 315 | + sender: EOA, |
| 316 | + ty: int, |
| 317 | + tx_data: Bytes, |
| 318 | + to: Address | None, |
| 319 | + protected: bool, |
| 320 | + access_list: List[AccessList] | None, |
| 321 | + authorization_list: List[AuthorizationTuple] | None, |
| 322 | + blob_versioned_hashes: Sequence[Hash] | None, |
| 323 | + tx_gas: int, |
| 324 | + tx_error: TransactionException | None, |
| 325 | +) -> Transaction: |
| 326 | + """ |
| 327 | + Create the transaction used in each test. |
| 328 | + """ |
| 329 | + return Transaction( |
| 330 | + ty=ty, |
| 331 | + sender=sender, |
| 332 | + data=tx_data, |
| 333 | + to=to, |
| 334 | + protected=protected, |
| 335 | + access_list=access_list, |
| 336 | + authorization_list=authorization_list, |
| 337 | + gas_limit=tx_gas, |
| 338 | + blob_versioned_hashes=blob_versioned_hashes, |
| 339 | + error=tx_error, |
| 340 | + ) |
0 commit comments