From 375551170d13df815cf378810cbc808304586396 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 14:10:33 +0200 Subject: [PATCH 1/6] fix(test-execute): Raise when implicit tx gas limits have no remaining gas `calculate_max_transaction_gas_limit()` returned 0 both when no transaction required an implicit gas limit (benign) and when explicit transaction gas limits already consumed the full environment gas limit (a test correctness error). The execute formats (`TransactionPost`, `BlobTransaction`) call it unconditionally from `prepare_transactions()` and never inspect the return value, so in the over-subscribed case the ambiguous 0 flowed into `set_gas_limit(max_gas_limit=0)`: transactions were signed with `gas_limit=0` and sent to the client, failing late with a confusing client-side rejection instead of a clear test definition error. Split the sentinel into its two meanings: return 0 only when there is nothing to do, and raise a "test correctness" error when transactions with implicit gas limits are left without remaining gas. This puts the check inside the calculation itself so every present and future caller gets it, rather than relying on a call-site convention. --- .../testing/src/execution_testing/execution/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/execution/base.py b/packages/testing/src/execution_testing/execution/base.py index 4ba11b06824..6c4cfa44084 100644 --- a/packages/testing/src/execution_testing/execution/base.py +++ b/packages/testing/src/execution_testing/execution/base.py @@ -60,9 +60,17 @@ def calculate_max_transaction_gas_limit( else: available_gas -= int(tx.gas_limit) - if unset_gas_limit_tx_count == 0 or available_gas <= 0: + if unset_gas_limit_tx_count == 0: return 0 + if available_gas <= 0: + raise Exception( + "test correctness: unable to automatically calculate gas " + "limit for transactions (no remaining gas: explicit " + "transaction gas limits already consume the full " + f"environment gas limit of {int(env.gas_limit)})." + ) + max_gas_limit = available_gas // unset_gas_limit_tx_count tx_gas_limit_cap = fork.transaction_gas_limit_cap() if fork.state_gas_reservoir_enabled(): From 7b864ab56f7d31b2503c30fe41c4772b9a2199e8 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 14:10:44 +0200 Subject: [PATCH 2/6] refactor(test-specs): Move no-remaining-gas check into gas calculation Mirror the equivalent change made to `BaseExecute` in `execution/base.py`: `BlockchainTest.calculate_max_transaction_gas_limit()` now raises the "test correctness" error directly when transactions with implicit gas limits are left without remaining gas, instead of returning an ambiguous 0 that callers must remember to check. The caller-side check in `generate_block_data()` is now dead code and is removed: the function can no longer return 0 there, since it is only invoked when at least one transaction has an unset gas limit. No behavior change on the fill path: the same condition raises the same kind of error, one frame earlier. --- .../src/execution_testing/specs/blockchain.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 7a1d6e9df7d..5ea19d10057 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -820,9 +820,17 @@ def calculate_max_transaction_gas_limit( else: available_gas -= int(tx.gas_limit) - if unset_gas_limit_tx_count == 0 or available_gas <= 0: + if unset_gas_limit_tx_count == 0: return 0 + if available_gas <= 0: + raise Exception( + "test correctness: unable to automatically calculate gas " + "limit for transactions (no remaining gas: explicit " + "transaction gas limits already consume the full " + f"environment gas limit of {int(env.gas_limit)})." + ) + max_tx_gas_limit = available_gas // unset_gas_limit_tx_count tx_gas_limit_cap = fork.transaction_gas_limit_cap() if fork.state_gas_reservoir_enabled(): @@ -856,11 +864,6 @@ def generate_block_data( max_tx_gas_limit = self.calculate_max_transaction_gas_limit( txs, env, fork ) - if max_tx_gas_limit == 0: - raise Exception( - "test correctness: unable to automatically calculate gas " - "limit for transactions (No remaining gas)." - ) for tx in txs: tx.set_gas_limit( max_gas_limit=max_tx_gas_limit, From fc016b98e80cd0c690337c66f943adbc179d9e53 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 14:27:03 +0200 Subject: [PATCH 3/6] refactor(test-types,test-specs,test-execute): Deduplicate implicit gas-limit calculation `calculate_max_transaction_gas_limit()` existed as two verbatim copies on either side of the spec/execute split: one on `BlockchainTest` for the fill path and one on `BaseExecute` for the execute formats. The function encodes the policy for sizing implicit transaction gas limits (even split of remaining environment gas, capping, state gas reservoir handling), and both copies must agree for filled and executed tests to behave the same: the recently added no-remaining-gas check already had to be applied twice. Hoist it to a single module-level function in `test_types/transaction_types.py`, next to `Transaction.set_gas_limit()`, so that all implicit gas-limit policy lives in one place. Neither class used `self` or `cls` (both copies were static), so a free function is the natural shape. The parameter is narrowed from `env: Environment` to `env_gas_limit: int`: the function only ever read `env.gas_limit`, and taking the full `Environment` would have required `transaction_types` to depend on `block_types` for a single field. --- .../src/execution_testing/execution/base.py | 40 +---------------- .../execution/blob_transaction.py | 5 ++- .../execution/transaction_post.py | 5 ++- .../src/execution_testing/specs/blockchain.py | 41 ++---------------- .../execution_testing/test_types/__init__.py | 2 + .../test_types/transaction_types.py | 43 +++++++++++++++++++ 6 files changed, 56 insertions(+), 80 deletions(-) diff --git a/packages/testing/src/execution_testing/execution/base.py b/packages/testing/src/execution_testing/execution/base.py index 6c4cfa44084..159d66e9fe1 100644 --- a/packages/testing/src/execution_testing/execution/base.py +++ b/packages/testing/src/execution_testing/execution/base.py @@ -1,7 +1,7 @@ """Ethereum test execution base types.""" from abc import abstractmethod -from typing import Annotated, Any, ClassVar, Dict, List, Type +from typing import Annotated, Any, ClassVar, Dict, Type from pydantic import PlainSerializer, PlainValidator from pytest import FixtureRequest @@ -9,7 +9,7 @@ from execution_testing.base_types import Address, CamelModel from execution_testing.forks import Fork from execution_testing.rpc import EngineRPC, EthRPC -from execution_testing.test_types import Environment, Transaction +from execution_testing.test_types import Environment class ExecuteResult(CamelModel): @@ -43,42 +43,6 @@ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: # Register the new execute format BaseExecute.formats[cls.format_name] = cls - @staticmethod - def calculate_max_transaction_gas_limit( - txs: List[Transaction], env: Environment, fork: Fork - ) -> int: - """ - Calculate the maximum gas limit that can be set in a transaction - given a list of transactions with and without gas-limits set - and a maximum available environment gas. - """ - available_gas = int(env.gas_limit) - unset_gas_limit_tx_count = 0 - for tx in txs: - if tx.gas_limit is None: - unset_gas_limit_tx_count += 1 - else: - available_gas -= int(tx.gas_limit) - - if unset_gas_limit_tx_count == 0: - return 0 - - if available_gas <= 0: - raise Exception( - "test correctness: unable to automatically calculate gas " - "limit for transactions (no remaining gas: explicit " - "transaction gas limits already consume the full " - f"environment gas limit of {int(env.gas_limit)})." - ) - - max_gas_limit = available_gas // unset_gas_limit_tx_count - tx_gas_limit_cap = fork.transaction_gas_limit_cap() - if fork.state_gas_reservoir_enabled(): - tx_gas_limit_cap = None - if tx_gas_limit_cap: - max_gas_limit = min(max_gas_limit, tx_gas_limit_cap) - return max_gas_limit - def prepare_transactions( self, *, diff --git a/packages/testing/src/execution_testing/execution/blob_transaction.py b/packages/testing/src/execution_testing/execution/blob_transaction.py index 4ce76b995bf..fb7daa47375 100644 --- a/packages/testing/src/execution_testing/execution/blob_transaction.py +++ b/packages/testing/src/execution_testing/execution/blob_transaction.py @@ -22,6 +22,7 @@ Environment, NetworkWrappedTransaction, Transaction, + calculate_max_transaction_gas_limit, ) from execution_testing.test_types.transaction_types import ( TransactionTestMetadata, @@ -167,8 +168,8 @@ def prepare_transactions( txs.append(tx.tx) else: txs.append(tx) - max_tx_gas_limit = self.calculate_max_transaction_gas_limit( - txs, env, fork + max_tx_gas_limit = calculate_max_transaction_gas_limit( + txs, env_gas_limit=int(env.gas_limit), fork=fork ) for tx in txs: tx.set_gas_limit( diff --git a/packages/testing/src/execution_testing/execution/transaction_post.py b/packages/testing/src/execution_testing/execution/transaction_post.py index a391227916a..d92f33c76da 100644 --- a/packages/testing/src/execution_testing/execution/transaction_post.py +++ b/packages/testing/src/execution_testing/execution/transaction_post.py @@ -19,6 +19,7 @@ TestPhase, Transaction, TransactionTestMetadata, + calculate_max_transaction_gas_limit, ) from .base import BaseExecute, ExecuteResult @@ -52,8 +53,8 @@ def prepare_transactions( ) -> None: """Prepare transactions by setting their final gas properties.""" for block in self.blocks: - max_tx_gas_limit = self.calculate_max_transaction_gas_limit( - block, env, fork + max_tx_gas_limit = calculate_max_transaction_gas_limit( + block, env_gas_limit=int(env.gas_limit), fork=fork ) for tx in block: tx.set_gas_limit( diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 5ea19d10057..6235996cce4 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -92,6 +92,7 @@ TestPhase, Transaction, Withdrawal, + calculate_max_transaction_gas_limit, ) from execution_testing.test_types.block_access_list import ( BlockAccessList, @@ -803,42 +804,6 @@ def make_genesis( ).with_rlp(txs=[]), ) - @staticmethod - def calculate_max_transaction_gas_limit( - txs: List[Transaction], env: Environment, fork: Fork - ) -> int: - """ - Calculate the maximum gas limit that can be set in a transaction - given a list of transactions with and without gas-limits set - and a maximum available environment gas. - """ - available_gas = int(env.gas_limit) - unset_gas_limit_tx_count = 0 - for tx in txs: - if tx.gas_limit is None: - unset_gas_limit_tx_count += 1 - else: - available_gas -= int(tx.gas_limit) - - if unset_gas_limit_tx_count == 0: - return 0 - - if available_gas <= 0: - raise Exception( - "test correctness: unable to automatically calculate gas " - "limit for transactions (no remaining gas: explicit " - "transaction gas limits already consume the full " - f"environment gas limit of {int(env.gas_limit)})." - ) - - max_tx_gas_limit = available_gas // unset_gas_limit_tx_count - tx_gas_limit_cap = fork.transaction_gas_limit_cap() - if fork.state_gas_reservoir_enabled(): - tx_gas_limit_cap = None - if tx_gas_limit_cap: - max_tx_gas_limit = min(max_tx_gas_limit, tx_gas_limit_cap) - return max_tx_gas_limit - def generate_block_data( self, t8n: FillerBackend, @@ -861,8 +826,8 @@ def generate_block_data( env = env.set_fork_requirements(fork) txs = block.txs[:] if any(tx.gas_limit is None for tx in block.txs): - max_tx_gas_limit = self.calculate_max_transaction_gas_limit( - txs, env, fork + max_tx_gas_limit = calculate_max_transaction_gas_limit( + txs, env_gas_limit=int(env.gas_limit), fork=fork ) for tx in txs: tx.set_gas_limit( diff --git a/packages/testing/src/execution_testing/test_types/__init__.py b/packages/testing/src/execution_testing/test_types/__init__.py index b9029ceb3ad..0c6af2be80a 100644 --- a/packages/testing/src/execution_testing/test_types/__init__.py +++ b/packages/testing/src/execution_testing/test_types/__init__.py @@ -47,6 +47,7 @@ TransactionDefaults, TransactionTestMetadata, TransactionType, + calculate_max_transaction_gas_limit, ) from .utils import Removable, keccak256 @@ -88,6 +89,7 @@ "Withdrawal", "WithdrawalRequest", "add_kzg_version", + "calculate_max_transaction_gas_limit", "ceiling_division", "compute_create_address", "compute_create2_address", diff --git a/packages/testing/src/execution_testing/test_types/transaction_types.py b/packages/testing/src/execution_testing/test_types/transaction_types.py index eb381d03eb0..59c10870ba3 100644 --- a/packages/testing/src/execution_testing/test_types/transaction_types.py +++ b/packages/testing/src/execution_testing/test_types/transaction_types.py @@ -943,6 +943,49 @@ def __str__(self) -> str: return self.__repr__() +def calculate_max_transaction_gas_limit( + txs: Sequence[Transaction], + *, + env_gas_limit: int, + fork: Fork, +) -> int: + """ + Calculate the maximum gas limit that can be set in a transaction + given a list of transactions with and without gas limits set and + the maximum available environment gas. + + Return 0 if no transaction requires an implicit gas limit. Raise a + test correctness error if transactions with implicit gas limits + are left without remaining gas. + """ + available_gas = env_gas_limit + unset_gas_limit_tx_count = 0 + for tx in txs: + if tx.gas_limit is None: + unset_gas_limit_tx_count += 1 + else: + available_gas -= int(tx.gas_limit) + + if unset_gas_limit_tx_count == 0: + return 0 + + if available_gas <= 0: + raise Exception( + "test correctness: unable to automatically calculate gas " + "limit for transactions (no remaining gas: explicit " + "transaction gas limits already consume the full " + f"environment gas limit of {env_gas_limit})." + ) + + max_gas_limit = available_gas // unset_gas_limit_tx_count + tx_gas_limit_cap = fork.transaction_gas_limit_cap() + if fork.state_gas_reservoir_enabled(): + tx_gas_limit_cap = None + if tx_gas_limit_cap: + max_gas_limit = min(max_gas_limit, tx_gas_limit_cap) + return max_gas_limit + + class NetworkWrappedTransaction(CamelModel, RLPSerializable): """ Network wrapped transaction as defined in From 366732508d8a41f9a3c19d691d63e0a70ae965f0 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 14:34:28 +0200 Subject: [PATCH 4/6] fix(test-types): Raise when state gas reservoir is requested on unsupported fork `Transaction.state_gas_reservoir` is only meaningful on forks where `fork.state_gas_reservoir_enabled()` is true (currently Amsterdam via EIP-8037). Previously, a positive reservoir on any other fork was silently dropped by `set_gas_limit()`: clamped to the gas limit cap on Osaka, ignored entirely pre-Osaka. A test parametrized across a fork range could therefore appear to cover reservoir behavior while actually filling plain capped transactions, with no signal to the author. Raise a "test correctness" error when a transaction requests a positive reservoir on a fork that cannot honor it. The guard runs before the unset-gas-limit check so that transactions with explicit gas limits are also caught, and it deliberately uses `state_gas_reservoir > 0` rather than `model_fields_set` so the explicit `state_gas_reservoir=0` idiom (pin the gas limit to exactly the cap, a no-op before EIP-8037) keeps working across fork ranges. No current test trips the guard: all positive usages are in `tests/amsterdam/eip8037_*` behind `valid_at("EIP8037")` markers, and the fork transition test only attaches reservoir transactions to post-transition blocks. --- .../src/execution_testing/test_types/transaction_types.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/testing/src/execution_testing/test_types/transaction_types.py b/packages/testing/src/execution_testing/test_types/transaction_types.py index 59c10870ba3..cfeb6547cf5 100644 --- a/packages/testing/src/execution_testing/test_types/transaction_types.py +++ b/packages/testing/src/execution_testing/test_types/transaction_types.py @@ -850,6 +850,13 @@ def set_gas_limit( state_gas_reservoir_enabled: bool = False, ) -> None: """Set the transaction gas limit if unset.""" + if self.state_gas_reservoir > 0 and not state_gas_reservoir_enabled: + raise Exception( + "test correctness: transaction requests a state gas " + f"reservoir of {self.state_gas_reservoir} but the fork " + "does not enable the state gas reservoir; the request " + "would be silently ignored." + ) if "gas_limit" not in self.model_fields_set or self.gas_limit is None: tx_gas_limit = max_gas_limit if state_gas_reservoir_enabled: From cbcb47482dc1b5f2fe63664ef2a3f09bb553782d Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 14:50:23 +0200 Subject: [PATCH 5/6] fix(test-types): Clarify `set_gas_limit` errors and document reservoir semantics The check that a state gas reservoir request fits within the available gas was a bare `assert` with no message. The condition is reachable through normal test authoring: on the blockchain fill path, `max_gas_limit` is the per-transaction even split of the block's remaining gas, so a block crowded with enough implicit-gas-limit transactions pushes the share below `transaction_gas_limit_cap + state_gas_reservoir` and the fill died with an opaque `AssertionError` pointing into framework internals. Raise a "test correctness" error instead, reporting the requested reservoir, the required gas limit, and the gas actually available. The adjacent assert that a reservoir-enabled fork carries a gas limit cap kept as an `assert` (it guards an internal invariant that test authors cannot violate through the `Transaction` API), but its message had a typo ("set calculate") and presumed the source of the inconsistency. State the violated precondition neutrally (both argument names and values) followed by the protocol rationale: the reservoir is defined as gas above the cap, as EIP-8037 builds on EIP-7825. Also document the three-state semantics of `state_gas_reservoir`, which previously lived only in the implementation: unset keeps the full implicit gas limit, an explicit 0 pins the gas limit to exactly the cap, and a positive value pins it to the cap plus the requested reservoir. The field `description` and the expanded `set_gas_limit` docstring now both spell this out. --- .../test_types/transaction_types.py | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/testing/src/execution_testing/test_types/transaction_types.py b/packages/testing/src/execution_testing/test_types/transaction_types.py index cfeb6547cf5..d9637a640f6 100644 --- a/packages/testing/src/execution_testing/test_types/transaction_types.py +++ b/packages/testing/src/execution_testing/test_types/transaction_types.py @@ -331,7 +331,20 @@ def strip_hash_from_t8n_output(cls, data: Any) -> Any: expected_receipt: TransactionReceipt | None = Field(None, exclude=True) - state_gas_reservoir: int = Field(0, exclude=True) + state_gas_reservoir: int = Field( + 0, + exclude=True, + description=( + "Extra gas on top of the transaction gas limit cap, reserved " + "for state gas (EIP-8037). Only takes effect when `gas_limit` " + "is unset and the fork enables the state gas reservoir: " + "leaving it unset keeps the full implicit gas limit, an " + "explicit 0 pins the gas limit to exactly the cap (no " + "reservoir), and a positive value pins it to the cap plus the " + "requested reservoir. Requesting a positive reservoir on a " + "fork without the state gas reservoir raises an error." + ), + ) zero: ClassVar[Literal[0]] = 0 @@ -849,7 +862,18 @@ def set_gas_limit( transaction_gas_limit_cap: int | None, state_gas_reservoir_enabled: bool = False, ) -> None: - """Set the transaction gas limit if unset.""" + """ + Set the transaction gas limit if unset. + + The implicit gas limit defaults to `max_gas_limit`, clamped to + the fork's transaction gas limit cap if there is one. On forks + with the state gas reservoir enabled (EIP-8037), + `state_gas_reservoir` refines this: unset keeps the full + `max_gas_limit` (any excess above the cap acts as an implicit + reservoir), an explicit 0 pins the gas limit to exactly the + cap, and a positive value pins it to the cap plus the requested + reservoir. + """ if self.state_gas_reservoir > 0 and not state_gas_reservoir_enabled: raise Exception( "test correctness: transaction requests a state gas " @@ -862,15 +886,29 @@ def set_gas_limit( if state_gas_reservoir_enabled: if "state_gas_reservoir" in self.model_fields_set: assert transaction_gas_limit_cap is not None, ( - "Impossible to set calculate the tx gas limit for the " - "required state gas reservoir without a gas limit cap" + "state_gas_reservoir_enabled is True but " + "transaction_gas_limit_cap is None; the state " + "gas reservoir is defined as gas above the cap " + "(EIP-8037 builds on EIP-7825), so a fork that " + "enables it must also define a cap" ) if self.state_gas_reservoir > 0: minimum_gas_with_reservoir = ( transaction_gas_limit_cap + self.state_gas_reservoir ) - assert tx_gas_limit >= minimum_gas_with_reservoir + if tx_gas_limit < minimum_gas_with_reservoir: + raise Exception( + "test correctness: the requested state " + "gas reservoir of " + f"{self.state_gas_reservoir} requires a " + f"gas limit of {minimum_gas_with_reservoir} " + "(transaction gas limit cap of " + f"{transaction_gas_limit_cap} plus " + "reservoir), but only " + f"{tx_gas_limit} gas is available for " + "this transaction." + ) tx_gas_limit = minimum_gas_with_reservoir else: if tx_gas_limit > transaction_gas_limit_cap: From ebbe563a9be150622f3a44535fec5a815000b681 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 11 Jun 2026 15:05:23 +0200 Subject: [PATCH 6/6] feat(test-tests): Add unit tests for implicit gas-limit resolution `Transaction.set_gas_limit()` and `calculate_max_transaction_gas_limit()` size every transaction in the suite that omits an explicit gas limit, but had no direct unit coverage: existing framework tests were only updated to pass explicit limits. Lock in the established behavior: - Base resolution: unset limits resolve to the maximum, clamped to the fork's transaction gas limit cap; explicit limits (including the explicit `gas_limit=None` unset idiom) are never modified; the resolution is sticky across repeated calls; signing without a gas limit raises. - State gas reservoir (EIP-8037) three-state semantics: unset keeps the full uncapped maximum, an explicit 0 pins the gas limit to exactly the cap (also valid on forks without a reservoir), and a positive value pins it to the cap plus the requested reservoir. - Test correctness errors: a reservoir exceeding the available gas, a positive reservoir on a fork without the state gas reservoir (for both implicit and explicit gas limits), and explicit limits leaving implicit transactions no remaining environment gas. - Even split: remaining environment gas is divided across implicit transactions after deducting explicit limits, clamped to the cap on Osaka and uncapped on Amsterdam where the reservoir removes it; all benign cases (no implicit transactions, empty list) return 0 without raising. --- .../tests/test_implicit_gas_limit.py | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 packages/testing/src/execution_testing/test_types/tests/test_implicit_gas_limit.py diff --git a/packages/testing/src/execution_testing/test_types/tests/test_implicit_gas_limit.py b/packages/testing/src/execution_testing/test_types/tests/test_implicit_gas_limit.py new file mode 100644 index 00000000000..071d07439f5 --- /dev/null +++ b/packages/testing/src/execution_testing/test_types/tests/test_implicit_gas_limit.py @@ -0,0 +1,277 @@ +""" +Test suite for implicit transaction gas-limit resolution. + +Covers `Transaction.set_gas_limit` and +`calculate_max_transaction_gas_limit`: the even split of remaining +environment gas, gas limit cap clamping, the state gas reservoir +(EIP-8037) semantics, and the test correctness errors raised on +contradictory test definitions. +""" + +import pytest + +from execution_testing.forks import Amsterdam, Osaka, Prague + +from ..transaction_types import ( + Transaction, + calculate_max_transaction_gas_limit, +) + +_osaka_cap = Osaka.transaction_gas_limit_cap() +assert _osaka_cap is not None +OSAKA_CAP: int = _osaka_cap +_amsterdam_cap = Amsterdam.transaction_gas_limit_cap() +assert _amsterdam_cap is not None +AMSTERDAM_CAP: int = _amsterdam_cap + +assert Prague.transaction_gas_limit_cap() is None +assert not Prague.state_gas_reservoir_enabled() +assert not Osaka.state_gas_reservoir_enabled() +assert Amsterdam.state_gas_reservoir_enabled() + + +class TestSetGasLimit: + """Test `Transaction.set_gas_limit` resolution of unset limits.""" + + def test_unset_no_cap(self) -> None: + """An unset gas limit resolves to the maximum, uncapped.""" + tx = Transaction() + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=None) + assert tx.gas_limit == 100 + + def test_unset_clamped_to_cap(self) -> None: + """An unset gas limit is clamped to the gas limit cap.""" + tx = Transaction() + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=60) + assert tx.gas_limit == 60 + + def test_unset_cap_above_max(self) -> None: + """A cap above the maximum does not raise the gas limit.""" + tx = Transaction() + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=200) + assert tx.gas_limit == 100 + + def test_explicit_gas_limit_untouched(self) -> None: + """An explicit gas limit is never modified.""" + tx = Transaction(gas_limit=21_000) + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=60) + assert tx.gas_limit == 21_000 + + def test_explicit_none_treated_as_unset(self) -> None: + """An explicit `gas_limit=None` is treated as unset.""" + tx = Transaction(gas_limit=None) + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=None) + assert tx.gas_limit == 100 + + def test_resolution_is_sticky(self) -> None: + """A second call does not overwrite the resolved gas limit.""" + tx = Transaction() + tx.set_gas_limit(max_gas_limit=100, transaction_gas_limit_cap=None) + tx.set_gas_limit(max_gas_limit=50, transaction_gas_limit_cap=None) + assert tx.gas_limit == 100 + + def test_signing_requires_gas_limit(self) -> None: + """Signing a transaction with an unset gas limit raises.""" + with pytest.raises(ValueError, match="gas_limit must be set"): + Transaction().with_signature_and_sender() + + +class TestSetGasLimitStateGasReservoir: + """Test the state gas reservoir (EIP-8037) gas-limit semantics.""" + + def test_reservoir_unset_keeps_full_maximum(self) -> None: + """With the reservoir unset, the cap does not clamp the limit.""" + tx = Transaction() + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=True, + ) + assert tx.gas_limit == 100 + + def test_reservoir_zero_pins_to_cap(self) -> None: + """An explicit zero reservoir pins the limit to exactly the cap.""" + tx = Transaction(state_gas_reservoir=0) + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=True, + ) + assert tx.gas_limit == 60 + + def test_reservoir_pins_to_cap_plus_reservoir(self) -> None: + """A positive reservoir pins the limit to cap plus reservoir.""" + tx = Transaction(state_gas_reservoir=40) + tx.set_gas_limit( + max_gas_limit=200, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=True, + ) + assert tx.gas_limit == 100 + + def test_reservoir_ignored_with_explicit_gas_limit(self) -> None: + """A reservoir is ignored when the gas limit is explicit.""" + tx = Transaction(gas_limit=21_000, state_gas_reservoir=40) + tx.set_gas_limit( + max_gas_limit=200, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=True, + ) + assert tx.gas_limit == 21_000 + + def test_reservoir_exceeding_available_gas_raises(self) -> None: + """A reservoir that does not fit the available gas raises.""" + tx = Transaction(state_gas_reservoir=50) + with pytest.raises( + Exception, match="test correctness: the requested state" + ): + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=True, + ) + + @pytest.mark.parametrize( + "gas_limit", + [ + pytest.param(None, id="implicit_gas_limit"), + pytest.param(21_000, id="explicit_gas_limit"), + ], + ) + def test_reservoir_on_unsupported_fork_raises( + self, gas_limit: int | None + ) -> None: + """A positive reservoir raises if the fork has no reservoir.""" + tx = Transaction(gas_limit=gas_limit, state_gas_reservoir=1) + with pytest.raises( + Exception, match="test correctness: transaction requests" + ): + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=False, + ) + + def test_reservoir_zero_on_unsupported_fork_clamps_to_cap(self) -> None: + """An explicit zero reservoir is valid on forks without one.""" + tx = Transaction(state_gas_reservoir=0) + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=60, + state_gas_reservoir_enabled=False, + ) + assert tx.gas_limit == 60 + + def test_reservoir_without_cap_is_internal_invariant(self) -> None: + """A reservoir request without a cap violates an invariant.""" + tx = Transaction(state_gas_reservoir=1) + with pytest.raises(AssertionError, match="must also define a cap"): + tx.set_gas_limit( + max_gas_limit=100, + transaction_gas_limit_cap=None, + state_gas_reservoir_enabled=True, + ) + + +class TestCalculateMaxTransactionGasLimit: + """Test the even split of environment gas across transactions.""" + + def test_no_implicit_transactions(self) -> None: + """Return 0 when all transactions have explicit gas limits.""" + txs = [Transaction(gas_limit=200_000)] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + == 0 + ) + + def test_empty_transaction_list(self) -> None: + """Return 0 for an empty transaction list.""" + assert ( + calculate_max_transaction_gas_limit( + [], env_gas_limit=100_000, fork=Prague + ) + == 0 + ) + + def test_single_implicit_transaction(self) -> None: + """A single implicit transaction gets the full environment gas.""" + txs = [Transaction()] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + == 100_000 + ) + + def test_explicit_limits_reduce_available_gas(self) -> None: + """Explicit gas limits are deducted from the environment gas.""" + txs = [Transaction(gas_limit=40_000), Transaction()] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + == 60_000 + ) + + def test_even_split_across_implicit_transactions(self) -> None: + """Remaining gas is split evenly across implicit transactions.""" + txs = [Transaction(gas_limit=10_000), Transaction(), Transaction()] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + == 45_000 + ) + + def test_split_clamped_to_cap(self) -> None: + """The per-transaction share is clamped to the fork's cap.""" + env_gas_limit = 100_000_000 + assert env_gas_limit > OSAKA_CAP + txs = [Transaction()] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=env_gas_limit, fork=Osaka + ) + == OSAKA_CAP + ) + + def test_state_gas_reservoir_fork_removes_cap(self) -> None: + """A fork with the state gas reservoir does not clamp the share.""" + env_gas_limit = 100_000_000 + assert env_gas_limit > AMSTERDAM_CAP + txs = [Transaction()] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=env_gas_limit, fork=Amsterdam + ) + == env_gas_limit + ) + + @pytest.mark.parametrize( + "explicit_gas_limit", + [ + pytest.param(100_000, id="exactly_consumed"), + pytest.param(150_000, id="over_consumed"), + ], + ) + def test_no_remaining_gas_raises(self, explicit_gas_limit: int) -> None: + """Raise when explicit limits leave implicit transactions no gas.""" + txs = [Transaction(gas_limit=explicit_gas_limit), Transaction()] + with pytest.raises( + Exception, match="test correctness: unable to automatically" + ): + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + + def test_no_remaining_gas_all_explicit_does_not_raise(self) -> None: + """Over-consumption without implicit transactions returns 0.""" + txs = [Transaction(gas_limit=150_000)] + assert ( + calculate_max_transaction_gas_limit( + txs, env_gas_limit=100_000, fork=Prague + ) + == 0 + )