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 c1a13931d5..fd750ca4e0 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 @@ -7,6 +7,7 @@ Alloc, Bytes, Fork, + Op, RecipientType, StateTestFiller, Transaction, @@ -18,7 +19,11 @@ from ...prague.eip7623_increase_calldata_cost.helpers import ( find_floor_cost_threshold, ) -from .helpers import EOA_INITIAL_BALANCE +from .helpers import ( + EOA_INITIAL_BALANCE, + AuthorizationAction, + build_authorization, +) from .spec import ref_spec_2780 REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path @@ -323,3 +328,102 @@ def test_calldata_floor_contract_creation( } state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "outcome", + [ + pytest.param("floor_binds", id="floor_binds"), + pytest.param( + "below_floor", + id="below_floor_rejected", + marks=pytest.mark.exception_test, + ), + ], +) +def test_calldata_floor_with_authorizations( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + outcome: str, +) -> None: + """ + A data-heavy type-4 transaction whose calldata floor exceeds the + full authorization total: the intrinsic plus the authorization's + top-frame execution and state charges. + """ + sender = pre.fund_eoa() + recipient = pre.deploy_contract(code=Op.STOP) + scenario = build_authorization( + pre, AuthorizationAction.SETS_NEW_DELEGATION + ) + authorization_list = [scenario.authorization] + + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + floor_calc = fork.transaction_data_floor_cost_calculator() + top_frame_gas = fork.transaction_top_frame_gas_calculator()( + recipient_type=RecipientType.CONTRACT, + authorizations=authorization_list, + ) + top_frame_state_gas = fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.CONTRACT, + authorizations=authorization_list, + ) + + def total(byte_count: int) -> int: + return ( + intrinsic_calc( + calldata=b"\x00" * byte_count, + recipient_type=RecipientType.CONTRACT, + authorization_list_or_count=authorization_list, + return_cost_deducted_prior_execution=True, + ) + + top_frame_gas + + top_frame_state_gas + ) + + def floor(byte_count: int) -> int: + return floor_calc( + data=b"\x00" * byte_count, + recipient_type=RecipientType.CONTRACT, + ) + + threshold = find_floor_cost_threshold( + floor_data_gas_cost_calculator=floor, + intrinsic_gas_cost_calculator=total, + ) + byte_count = threshold + 1 + calldata = Bytes(b"\x00" * byte_count) + calldata_floor = floor(byte_count) + assert calldata_floor > total(byte_count), ( + "the calldata floor must dominate the full authorization total" + ) + + post: dict[Address, Account | None] + if outcome == "below_floor": + tx = Transaction( + sender=sender, + to=recipient, + data=calldata, + authorization_list=authorization_list, + gas_limit=calldata_floor - 1, + error=TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST, + ) + post = {scenario.authority: scenario.original_account} + else: + tx = Transaction( + sender=sender, + to=recipient, + data=calldata, + authorization_list=authorization_list, + gas_limit=calldata_floor, + expected_receipt=TransactionReceipt( + cumulative_gas_used=calldata_floor, + ), + ) + post = { + sender: Account(nonce=1), + scenario.authority: scenario.applied_account, + } + + state_test(pre=pre, tx=tx, post=post) 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 index 1888536681..8c8a2b221a 100644 --- a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_fork_transition.py @@ -27,15 +27,18 @@ Account, Address, Alloc, + AuthorizationTuple, Block, BlockchainTestFiller, Op, RecipientType, Transaction, + TransactionReceipt, TransitionFork, compute_create_address, ) +from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .helpers import EOA_INITIAL_BALANCE from .spec import ref_spec_2780 @@ -269,3 +272,99 @@ def test_creation_tx_intrinsic_across_amsterdam_transition( post[created] = Account(nonce=1, balance=value, code=b"") blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_setcode_tx_across_amsterdam_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """ + Pin the EIP-2780 authorization repricing across the Amsterdam + boundary. + """ + gas_price = 1_000_000_000 + + pre_costs = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP).gas_costs() + post_costs = fork.fork_at(timestamp=POST_FORK_TIMESTAMP).gas_costs() + + # Pre-fork: EIP-7702 charges the full per-authorization cost in the + # intrinsic; an empty authority earns no existing-authority refund. + expected_pre = pre_costs.TX_BASE + pre_costs.AUTH_PER_EMPTY_ACCOUNT + # Post-fork: EIP-2780 decomposition across the three charge layers. + expected_post = ( + post_costs.TX_BASE + + post_costs.COLD_ACCOUNT_ACCESS + + post_costs.EXECUTION_PER_AUTH_BASE_COST + + post_costs.ACCOUNT_WRITE + + post_costs.NEW_ACCOUNT + + post_costs.AUTH_BASE + ) + + timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] + expected_totals = [expected_pre, expected_post] + blocks = [] + post: dict[Address, Account] = {} + + for timestamp, expected_total in zip( + timestamps, expected_totals, strict=True + ): + sub_fork = fork.fork_at(timestamp=timestamp) + recipient = pre.deploy_contract(code=Op.STOP) + delegate_to = pre.deploy_contract(code=Op.STOP) + authority = pre.fund_eoa(amount=0) + authorization = AuthorizationTuple( + address=delegate_to, + nonce=0, + signer=authority, + creates_account=True, + ) + + intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()( + recipient_type=RecipientType.CONTRACT, + authorization_list_or_count=[authorization], + return_cost_deducted_prior_execution=True, + ) + top_frame_gas = sub_fork.transaction_top_frame_gas_calculator()( + recipient_type=RecipientType.CONTRACT, + authorizations=[authorization], + ) + top_frame_state_gas = sub_fork.transaction_top_frame_state_gas( + recipient_type=RecipientType.CONTRACT, + authorizations=[authorization], + ) + total_gas = intrinsic_gas + top_frame_gas + top_frame_state_gas + assert total_gas == expected_total, ( + f"set-code total at timestamp {timestamp} ({sub_fork}) is " + f"{total_gas}, expected {expected_total}" + ) + + sender_initial_balance = 10**18 + sender = pre.fund_eoa(sender_initial_balance) + + # Both recipient and delegate run ``STOP`` (no execution gas), + # so the receipt pins the intrinsic and top-frame layers alone. + tx = Transaction( + sender=sender, + to=recipient, + authorization_list=[authorization], + gas_limit=total_gas, + max_fee_per_gas=gas_price, + max_priority_fee_per_gas=gas_price, + expected_receipt=TransactionReceipt( + cumulative_gas_used=total_gas, + ), + ) + blocks.append(Block(timestamp=timestamp, txs=[tx])) + + post[sender] = Account( + nonce=1, + balance=sender_initial_balance - total_gas * gas_price, + ) + post[authority] = Account( + nonce=1, + balance=0, + code=Spec7702.delegation_designation(delegate_to), + ) + + blockchain_test(pre=pre, blocks=blocks, 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 d8bad2b8ea..e2c6fbc2be 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 @@ -12,16 +12,21 @@ Alloc, AuthorizationTuple, Fork, + Hash, Op, RecipientType, StateTestFiller, Transaction, TransactionException, + add_kzg_version, ) +from ...cancun.eip4844_blobs.spec import Spec as EIP4844_Spec from .helpers import ( EOA_INITIAL_BALANCE, RECIPIENT_TYPES_NON_CREATE, + AuthorizationAction, + build_authorization, setup_target, ) from .spec import ref_spec_2780 @@ -177,3 +182,58 @@ def test_intrinsic_gas_floor_boundary_with_authorizations( ) state_test(pre=pre, tx=tx, post=pre) + + +@pytest.mark.exception_test +@pytest.mark.with_all_tx_types +def test_intrinsic_gas_floor_boundary_all_tx_types( + fork: Fork, + pre: Alloc, + state_test: StateTestFiller, + tx_type: int, +) -> None: + """ + Reject every transaction type when ``gas_limit = intrinsic_gas - 1``. + + Each type layers its own intrinsic components on the shared + value-transfer shape -- ``EXECUTION_PER_AUTH_BASE_COST`` for type 4 -- + and the pre-execution check must reject one gas below the per-type + total. Blob gas is priced in its own dimension, so a type-3 + transaction's execution-gas boundary is identical to type 2. + """ + value = 1 + sender = pre.fund_eoa() + target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE) + + scenario = ( + build_authorization(pre, AuthorizationAction.SETS_NEW_DELEGATION) + if tx_type == 4 + else None + ) + authorizations = [scenario.authorization] if scenario else [] + + blob_versioned_hashes = ( + add_kzg_version([Hash(1)], EIP4844_Spec.BLOB_COMMITMENT_VERSION_KZG) + if tx_type == 3 + else None + ) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + sends_value=True, + recipient_type=RecipientType.EOA, + authorization_list_or_count=authorizations, + return_cost_deducted_prior_execution=True, + ) + + tx = Transaction( + ty=tx_type, + sender=sender, + to=target, + value=value, + authorization_list=authorizations or None, + blob_versioned_hashes=blob_versioned_hashes, + gas_limit=intrinsic_gas - 1, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + ) + + state_test(pre=pre, tx=tx, post=pre)