From d6eed52434b55df69532735bf57cc2b6568d107b Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Wed, 24 Jun 2026 11:37:48 +0200 Subject: [PATCH 1/6] feat(amsterdam): add EIP-2780 fork-transition tests --- .../test_fork_transition.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py new file mode 100644 index 0000000000..b14dafe5e4 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py @@ -0,0 +1,153 @@ +""" +Fork-transition tests for EIP-2780. + +EIP-2780 reshapes the intrinsic transaction cost at the Amsterdam fork +boundary. These tests send identical transactions in a pre-fork block +and a post-fork block (straddling the transition timestamp) and assert +that the per-transaction gas paid changes by the EIP-2780 amount only +once the fork activates. + +For these shapes the post-fork intrinsic decomposes from the flat +pre-fork ``TX_BASE`` of 21_000 as follows: + +- A plain call to an existing account drops to ``TX_BASE`` (12_000) + plus the new ``COLD_ACCOUNT_ACCESS`` recipient charge; adding value + re-raises it to exactly 21_000 (the value-transfer cost is invariant + across the fork by design). +- A self-transfer is fully carved out post-fork: it pays only the + lowered ``TX_BASE`` with no recipient or value-transfer charge, + regardless of value, the largest reduction. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + RecipientType, + Transaction, + TransitionFork, +) + +from .helpers import EOA_INITIAL_BALANCE +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") + +# Transition forks switch at timestamp 15_000. +PRE_FORK_TIMESTAMP = 14_999 +POST_FORK_TIMESTAMP = 15_000 + + +@pytest.mark.parametrize( + "self_transfer", + [ + pytest.param(False, id="plain_call"), + pytest.param(True, id="self_transfer"), + ], +) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_reduction_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + self_transfer: bool, + value: int, +) -> None: + """ + Pin the EIP-2780 intrinsic change across the Amsterdam boundary. + + The same transaction shape is sent in a pre-fork block (Osaka + rules, flat 21_000 intrinsic) and a post-fork block (Amsterdam + rules, decomposed intrinsic). Each block uses a distinct sender so + its post-tx balance pins the fork-appropriate intrinsic; the + recipient is an existing EOA (or the sender itself for + ``self_transfer``), so neither block runs EVM bytecode and + ``gas_used`` equals the intrinsic exactly. + + The per-fork intrinsic returned by the calculator is also checked + against a hand-derived decomposition built from each fork's gas + constants, so a calculator regression fails here with a clear + message rather than only as a downstream balance mismatch. + """ + gas_price = 10**9 + recipient_type = RecipientType.SELF if self_transfer else RecipientType.EOA + + pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP) + post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP) + + # Pre-fork: flat ``TX_BASE`` regardless of recipient kind or value. + expected_pre = pre_fork.gas_costs().TX_BASE + # Post-fork: EIP-2780 decomposition. Self-transfers are fully + # carved out; other recipients pay the recipient access charge plus + # the value-transfer charges when value is moved. + post_gas_costs = post_fork.gas_costs() + expected_post = post_gas_costs.TX_BASE + if not self_transfer: + expected_post += post_gas_costs.COLD_ACCOUNT_ACCESS + if value: + expected_post += ( + post_gas_costs.TRANSFER_LOG_COST + post_gas_costs.TX_VALUE_COST + ) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_intrinsics = [expected_pre, expected_post] + blocks = [] + post: dict = {} + + for timestamp, expected_intrinsic in zip( + timestamps, expected_intrinsics, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()( + sends_value=bool(value), + recipient_type=recipient_type, + return_cost_deducted_prior_execution=True, + ) + assert intrinsic_gas == expected_intrinsic, ( + f"intrinsic at timestamp {timestamp} ({sub_fork}) is " + f"{intrinsic_gas}, expected {expected_intrinsic}" + ) + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + if self_transfer: + target = sender + else: + target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + + # No EVM bytecode runs (recipient is an EOA or the sender), so + # gas_used == intrinsic_gas; the gas limit is pinned to exactly + # the intrinsic, leaving no buffer. + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=intrinsic_gas, + gas_price=gas_price, + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + # A self-transfer returns the value to the sender (net zero); + # a plain call moves ``value`` to the distinct recipient. + sender_value_delta = 0 if self_transfer else value + sender_final_balance = ( + sender_initial_balance + - sender_value_delta + - intrinsic_gas * gas_price + ) + post[sender] = Account(nonce=1, balance=sender_final_balance) + if not self_transfer: + post[target] = Account(balance=EOA_INITIAL_BALANCE + value) + + blockchain_test(pre=pre, blocks=blocks, post=post) From e85a8e5a94213a69db86223dd89dd8bdc8bd5e55 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Wed, 24 Jun 2026 12:10:48 +0200 Subject: [PATCH 2/6] feat(amsterdam): pin EIP-2780 top-frame NEW_ACCOUNT state-gas dimension --- .../test_top_frame_charges.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index c56c06439b..737ff209a8 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -21,7 +21,10 @@ Account, Address, Alloc, + Block, + BlockchainTestFiller, Fork, + Header, Op, RecipientType, StateTestFiller, @@ -159,6 +162,78 @@ def test_top_frame_state_charge_empty_precompile( state_test(pre=pre, tx=tx, post=post) +def test_top_frame_new_account_charged_as_state_gas( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + The top-frame ``NEW_ACCOUNT`` charge for a value transfer to an + empty recipient is *state* gas, not regular gas. This pins the + dimension via the block header ``gas_used``, which the spec + computes as ``max(block_regular_gas, block_state_gas)``. + + Correctly attributed, the ``NEW_ACCOUNT`` state gas dominates the + small regular intrinsic, so ``gas_used == NEW_ACCOUNT``. A + regression mis-classifying the charge as regular gas would instead + yield ``intrinsic_regular + NEW_ACCOUNT``. + + ``state_test``-based balance assertions (e.g. + ``test_top_frame_state_charge``) only observe the *sum* of the two + dimensions, so they cannot distinguish this; a block-level + ``gas_used`` assertion is required. + """ + sender = pre.fund_eoa(10**18) + target = pre.fund_eoa(amount=0) + value = 1 + + intrinsic_regular = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + return_cost_deducted_prior_execution=True, + ) + new_account_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EMPTY_ACCOUNT, + ) + # The state charge must dominate the regular intrinsic for the + # header ``gas_used`` to distinguish a state vs regular + # mis-classification. + assert new_account_state_gas > intrinsic_regular, ( + "test only distinguishes the dimension when NEW_ACCOUNT " + f"({new_account_state_gas}) dominates the regular intrinsic " + f"({intrinsic_regular})" + ) + + # No EVM bytecode runs (empty recipient), so the only regular gas + # is the intrinsic and the only state gas is the top-frame + # ``NEW_ACCOUNT`` charge. + expected_gas_used = max(intrinsic_regular, new_account_state_gas) + + gas_price = 10**9 + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=intrinsic_regular + new_account_state_gas + 1000, + gas_price=gas_price, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + header_verify=Header(gas_used=expected_gas_used), + ), + ], + post={ + sender: Account(nonce=1), + target: Account(balance=value), + }, + ) + + @pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"]) @pytest.mark.parametrize( "value", From 027be0ff0f69338a1b4508b68dbf6a3c40c2311b Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Wed, 24 Jun 2026 12:27:45 +0200 Subject: [PATCH 3/6] feat(amsterdam): assert EIP-7708 transfer log tracks EIP-2780 TRANSFER_LOG_COST --- .../test_value_moving_transactions.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py index ace321454a..5ce7601ac3 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_value_moving_transactions.py @@ -22,9 +22,11 @@ RecipientType, StateTestFiller, Transaction, + TransactionReceipt, compute_create_address, ) +from ..eip7708_eth_transfer_logs.spec import transfer_log from .helpers import ( EOA_INITIAL_BALANCE, RECIPIENT_TYPES_NON_CREATE, @@ -63,6 +65,10 @@ def test_value_moving_transactions( delegations on the recipient surface as an extra top-frame ``COLD_ACCOUNT_ACCESS``; empty recipients trigger the top-frame ``NEW_ACCOUNT`` state charge when value is transferred. + + The EIP-7708 transfer log is asserted to fire exactly when + ``TRANSFER_LOG_COST`` is charged: for a non-self value transfer, + and never for a self-transfer (carve-out) or a zero-value tx. """ sender_initial_balance = 10**18 sender = pre.fund_eoa(sender_initial_balance) @@ -92,15 +98,25 @@ def test_value_moving_transactions( tx_gas_limit = total_gas_cost + 1000 # add a small buffer gas_price = 1_000_000_000 + is_self_transfer = recipient_type == RecipientType.SELF + + # A transfer log is emitted iff value moves to a distinct account, + # which is exactly when the intrinsic includes ``TRANSFER_LOG_COST``. + # ``logs=[]`` asserts no log fires for the carved-out cases. + if value > 0 and not is_self_transfer: + expected_logs = [transfer_log(sender, target, value)] + else: + expected_logs = [] + tx = Transaction( sender=sender, to=target, value=value, gas_limit=tx_gas_limit, gas_price=gas_price, + expected_receipt=TransactionReceipt(logs=expected_logs), ) - is_self_transfer = recipient_type == RecipientType.SELF sender_value_delta = 0 if is_self_transfer else value sender_final_balance = ( sender_initial_balance From 1e7a3f6bc6ba90179a24e7571d8c30f2ed9221fd Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 25 Jun 2026 14:06:23 +0200 Subject: [PATCH 4/6] feat(amsterdam): add EIP-2780 intrinsic/floor boundary tests - test_calldata_floor.py: a data-heavy value transfer whose EIP-7623/7976 calldata floor (built on the lowered TX_BASE) dominates the decomposed intrinsic, masking the recipient/value charges so the gas paid is identical at value 0 and 1; plus a one-below-floor rejection (INTRINSIC_GAS_BELOW_FLOOR_GAS_COST). The floor-dominating calldata size is derived from the shared EIP-7623 find_floor_cost_threshold search rather than hardcoded. - test_intrinsic_gas_boundary.py: contract-creation intrinsic boundary (gas_limit = intrinsic - 1 -> INTRINSIC_GAS_TOO_LOW), pinning the combined regular + NEW_ACCOUNT state intrinsic for creates. - test_top_frame_charges.py: a nonce-only-alive recipient (nonce=1, zero balance, no code) is not empty per EIP-161, so a value transfer does not incur the top-frame NEW_ACCOUNT charge; the gate keys on is_account_alive, not balance == 0. --- .../test_calldata_floor.py | 173 ++++++++++++++++++ .../test_intrinsic_gas_boundary.py | 46 +++++ .../test_top_frame_charges.py | 62 +++++++ 3 files changed, 281 insertions(+) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py new file mode 100644 index 0000000000..dd711b457b --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py @@ -0,0 +1,173 @@ +""" +EIP-2780 interaction with the EIP-7623/7976 calldata floor. + +A transaction's gas accounting uses ``max(intrinsic, calldata_floor)``. +EIP-2780 decomposes the intrinsic (``TX_BASE`` + recipient access + +value-transfer charges) and lowers ``TX_BASE`` to 12_000; that lowered +base also feeds the calldata floor. These tests pin the data-heavy +regime where the floor dominates: + +- The floor binds, so ``gas_used`` equals the floor and the + recipient/value charges folded into the intrinsic are masked: the + gas paid is identical for a zero-value and a value-bearing + transaction of the same calldata size. +- One gas below the floor, the transaction is rejected with + ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST`` even though it covers the + (smaller) decomposed intrinsic. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Bytes, + Fork, + RecipientType, + StateTestFiller, + Transaction, + TransactionException, +) + +from ...prague.eip7623_increase_calldata_cost.helpers import ( + find_floor_cost_threshold, +) +from .helpers import EOA_INITIAL_BALANCE +from .spec import ref_spec_2780 + +REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path +REFERENCE_SPEC_VERSION = ref_spec_2780.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +def _floor_dominating_calldata(fork: Fork) -> Bytes: + """ + Return zero-byte calldata sized so its calldata floor strictly + exceeds the decomposed value-transfer intrinsic for a non-create + call to an existing EOA. + + Reuses the shared EIP-7623 ``find_floor_cost_threshold`` binary + search against this transaction shape, then steps one byte past the + threshold (the last size where the floor does not yet dominate) so + the floor strictly binds. + """ + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + floor_calc = fork.transaction_data_floor_cost_calculator() + + def intrinsic(byte_count: int) -> int: + return intrinsic_calc( + calldata=b"\x00" * byte_count, + sends_value=True, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + + def floor(byte_count: int) -> int: + return floor_calc(data=b"\x00" * byte_count) + + threshold = find_floor_cost_threshold( + floor_data_gas_cost_calculator=floor, + intrinsic_gas_cost_calculator=intrinsic, + ) + byte_count = threshold + 1 + + assert floor(byte_count) > intrinsic(byte_count) + return Bytes(b"\x00" * byte_count) + + +@pytest.mark.parametrize( + "outcome", + [ + pytest.param("floor_binds", id="floor_binds"), + pytest.param( + "below_floor", + id="below_floor_rejected", + marks=pytest.mark.exception_test, + ), + ], +) +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_calldata_floor( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + outcome: str, + value: int, +) -> None: + """ + A data-heavy transaction to an existing EOA whose calldata floor + exceeds the decomposed value-transfer intrinsic. + + - ``floor_binds``: with a gas limit above the floor, ``gas_used`` + pins to the floor, so the value-transfer charges + (``TRANSFER_LOG_COST + TX_VALUE_COST``) folded into the intrinsic + are masked -- the gas paid is identical at ``value == 0`` and + ``value == 1`` and only the moved wei differs. + - ``below_floor``: a gas limit one short of the floor still covers + the (smaller) decomposed intrinsic, so the floor -- built on the + EIP-2780-lowered ``TX_BASE`` -- is the only thing that can reject + it, with ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST``. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + + calldata = _floor_dominating_calldata(fork) + calldata_floor = fork.transaction_data_floor_cost_calculator()( + data=calldata, + ) + gas_price = 1_000_000_000 + + post: dict[Address, Account] = {} + if outcome == "below_floor": + # ``gas_limit`` one short of the floor still covers the + # decomposed intrinsic, so the floor is the only thing that can + # reject it; the post state is empty (transaction rejected). + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=calldata, + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + gas_limit = calldata_floor - 1 + assert intrinsic_gas <= gas_limit, ( + "gas_limit must still cover the decomposed intrinsic so the " + "rejection is pinned to the calldata floor" + ) + tx = Transaction( + sender=sender, + to=target, + value=value, + data=calldata, + gas_limit=gas_limit, + gas_price=gas_price, + error=TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST, + ) + else: + # ``floor_binds``: no explicit gas limit (auto-fills above the + # floor). The gas component is the floor regardless of value + # (charges masked); only the transferred wei changes the + # balance. + tx = Transaction( + sender=sender, + to=target, + value=value, + data=calldata, + gas_price=gas_price, + ) + sender_final_balance = ( + sender_initial_balance - value - calldata_floor * gas_price + ) + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(balance=EOA_INITIAL_BALANCE + value), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py index d07370531f..e62a93c899 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_intrinsic_gas_boundary.py @@ -11,6 +11,7 @@ from execution_testing import ( Alloc, Fork, + Op, RecipientType, StateTestFiller, Transaction, @@ -67,3 +68,48 @@ def test_intrinsic_gas_floor_boundary( ) state_test(pre=pre, tx=tx, post={}) + + +@pytest.mark.exception_test +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(1, id="non-zero_value"), + ], +) +def test_intrinsic_gas_floor_boundary_contract_creation( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + value: int, +) -> None: + """ + Reject a contract-creation transaction when + ``gas_limit = intrinsic_gas - 1``. + + A creation tx's intrinsic includes the ``NEW_ACCOUNT`` state gas, so + the pre-execution check rejects against the combined + ``regular + state`` intrinsic. The init code never runs. + """ + sender = pre.fund_eoa(10**18) + init_code = Op.STOP + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=init_code, + contract_creation=True, + sends_value=bool(value), + return_cost_deducted_prior_execution=True, + ) + + tx = Transaction( + sender=sender, + to=None, + value=value, + data=init_code, + gas_limit=intrinsic_gas - 1, + gas_price=1_000_000_000, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, tx=tx, post={}) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py index 737ff209a8..3ec22a582c 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py @@ -234,6 +234,68 @@ def test_top_frame_new_account_charged_as_state_gas( ) +@pytest.mark.pre_alloc_mutable +def test_top_frame_new_account_skipped_for_nonce_only_recipient( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, +) -> None: + """ + A recipient that is alive only by its nonce (``nonce=1``, zero + balance, no code) is not empty per EIP-161, so a value transfer to + it does *not* incur the top-frame ``NEW_ACCOUNT`` charge. This pins + that the gate keys on ``is_account_alive``, not ``balance == 0``. + + Such an account is reachable on-chain: any EOA that has sent a + transaction (nonce bumped) and been fully drained sits at + ``nonce>0, balance=0, no code``. + + The gas limit is pinned to exactly the intrinsic, leaving no room + for any extra charge: an implementation that wrongly charged + ``NEW_ACCOUNT`` (keying on the zero balance) would out-of-gas + rather than succeed. The recipient has no code, so no EVM runs and + the intrinsic is fully consumed with nothing to refund. + """ + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + # Alive via nonce only: not empty per EIP-161 because nonce != 0. + target = pre.fund_eoa(amount=0, nonce=1) + value = 1 + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + sends_value=True, + recipient_type=RecipientType.EOA, + ) + assert top_frame_state_gas == 0, ( + "a nonce-only-alive recipient must not incur the NEW_ACCOUNT charge" + ) + + gas_price = 1_000_000_000 + gas_limit = intrinsic_gas + tx = Transaction( + sender=sender, + to=target, + value=value, + gas_limit=gas_limit, + gas_price=gas_price, + ) + + sender_final_balance = ( + sender_initial_balance - value - intrinsic_gas * gas_price + ) + post = { + sender: Account(nonce=1, balance=sender_final_balance), + target: Account(nonce=1, balance=value), + } + + state_test(pre=pre, tx=tx, post=post) + + @pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"]) @pytest.mark.parametrize( "value", From 94f55dbbff88be169f7dcb87ecc879f18a5641a1 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Fri, 26 Jun 2026 13:08:41 +0200 Subject: [PATCH 5/6] chore(amsterdam): bump EIP-2780 reference spec to latest --- tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py index 869b3d72bf..e6fcb6bb52 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py @@ -13,5 +13,5 @@ class ReferenceSpec: ref_spec_2780 = ReferenceSpec( git_path="EIPS/eip-2780.md", - version="4b612eec2ef70611bba3e0819d137dcfb9b6cd81", + version="992074053f12f24fed9e6d6bf6099d3a44707dca", ) From f7c6e948685401930839e62daa9a42fdb2fc51d3 Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Wed, 1 Jul 2026 12:26:12 +0200 Subject: [PATCH 6/6] chore(amsterdam): post review fixes --- .../test_calldata_floor.py | 88 +++++++------------ 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py index dd711b457b..3060c5d177 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py @@ -1,19 +1,6 @@ """ EIP-2780 interaction with the EIP-7623/7976 calldata floor. -A transaction's gas accounting uses ``max(intrinsic, calldata_floor)``. -EIP-2780 decomposes the intrinsic (``TX_BASE`` + recipient access + -value-transfer charges) and lowers ``TX_BASE`` to 12_000; that lowered -base also feeds the calldata floor. These tests pin the data-heavy -regime where the floor dominates: - -- The floor binds, so ``gas_used`` equals the floor and the - recipient/value charges folded into the intrinsic are masked: the - gas paid is identical for a zero-value and a value-bearing - transaction of the same calldata size. -- One gas below the floor, the transaction is rejected with - ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST`` even though it covers the - (smaller) decomposed intrinsic. """ import pytest @@ -77,12 +64,12 @@ def floor(byte_count: int) -> int: @pytest.mark.parametrize( - "outcome", + "gas_modifier", [ - pytest.param("floor_binds", id="floor_binds"), + pytest.param(0, id="at_floor"), pytest.param( - "below_floor", - id="below_floor_rejected", + -1, + id="below_floor", marks=pytest.mark.exception_test, ), ], @@ -98,7 +85,7 @@ def test_calldata_floor( fork: Fork, pre: Alloc, state_test: StateTestFiller, - outcome: str, + gas_modifier: int, value: int, ) -> None: """ @@ -126,42 +113,35 @@ def test_calldata_floor( gas_price = 1_000_000_000 post: dict[Address, Account] = {} - if outcome == "below_floor": - # ``gas_limit`` one short of the floor still covers the - # decomposed intrinsic, so the floor is the only thing that can - # reject it; the post state is empty (transaction rejected). - intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( - calldata=calldata, - sends_value=bool(value), - recipient_type=RecipientType.EOA, - return_cost_deducted_prior_execution=True, - ) - gas_limit = calldata_floor - 1 - assert intrinsic_gas <= gas_limit, ( - "gas_limit must still cover the decomposed intrinsic so the " - "rejection is pinned to the calldata floor" - ) - tx = Transaction( - sender=sender, - to=target, - value=value, - data=calldata, - gas_limit=gas_limit, - gas_price=gas_price, - error=TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST, - ) - else: - # ``floor_binds``: no explicit gas limit (auto-fills above the - # floor). The gas component is the floor regardless of value - # (charges masked); only the transferred wei changes the - # balance. - tx = Transaction( - sender=sender, - to=target, - value=value, - data=calldata, - gas_price=gas_price, - ) + gas_limit = calldata_floor + gas_modifier + # Even at the reduced limit the decomposed intrinsic is still + # covered, so the calldata floor is the sole gate on the + # transaction. + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=calldata, + sends_value=bool(value), + recipient_type=RecipientType.EOA, + return_cost_deducted_prior_execution=True, + ) + assert intrinsic_gas <= gas_limit, ( + "gas_limit must still cover the decomposed intrinsic so the " + "outcome is pinned to the calldata floor" + ) + + tx = Transaction( + sender=sender, + to=target, + value=value, + data=calldata, + gas_limit=gas_limit, + gas_price=gas_price, + error=( + TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST + if gas_modifier < 0 + else None + ), + ) + if gas_modifier == 0: sender_final_balance = ( sender_initial_balance - value - calldata_floor * gas_price )