diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 7454ee0f115..f2c69d5e7d8 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -208,10 +208,11 @@ def set_delegation(evm: Evm) -> None: account leaf does not yet exist. - ``GasCosts.ACCOUNT_WRITE`` (regular) when applying the authorization is the transaction's first write to the authority's - leaf. The sender's leaf was already written at inclusion (priced - into ``TX_BASE``), so a self-sponsored authority pays no - ``ACCOUNT_WRITE``, and repeated authorizations on one authority - pay it once. + leaf. Writes the transaction already prices elsewhere are + exempt: the sender's, covered by ``TX_BASE``, and, for a + value-bearing transaction, the recipient's, covered by + ``TX_VALUE_COST``. Repeated authorizations on one authority pay + it once. - ``StateGasCosts.AUTH_BASE`` (state) when a net-new delegation indicator is written: the authority held no delegation before the transaction, none was set for it earlier in the transaction, and @@ -232,9 +233,11 @@ def set_delegation(evm: Evm) -> None: """ message = evm.message tx_state = message.tx_env.state - # Accounts this transaction has already written: the sender's leaf - # was written at inclusion (nonce bump and fee deduction). The - # recipient is written when value is transferred. + # Accounts whose write the transaction has already priced: the + # sender's leaf was written at inclusion (nonce bump and fee + # deduction), and a value-bearing transaction prepays the + # recipient's balance write -- the transfer itself only happens at + # frame entry, after these charges. written_accounts: Set[Address] = {message.tx_env.origin} if evm.message.tx_env.value > U256(0): written_accounts.add(evm.message.current_target) diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index d24f930726c..c820a1b8748 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -260,6 +260,13 @@ def prepare_dispatch(evm: Evm) -> None: cold account access and pointing the frame at the delegated code. + The creation target is checked against the transaction pre-state: + ``process_create_message`` has already bumped the target's nonce + by the time this runs, so a live check would always see the + account. The recipient check is live, so an authority + materialized earlier in the transaction is not charged + ``NEW_ACCOUNT`` again. + This function must not mutate the transaction state. Every charge here pays for state that only materializes inside the dispatched frame and rolls back with it, so these charges stay refillable -- diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index 51c93b5ec08..aa176ea63f0 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -1373,6 +1373,9 @@ def test_create_tx_header_gas_used( floor = fork.transaction_data_floor_cost_calculator()( data=bytes(initcode), contract_creation=True ) + assert floor > regular_intrinsic, ( + "the floor must bind for this arm to pin floor-in-header" + ) expected_gas_used = max(regular_intrinsic, floor) else: # For a minimal CREATE tx deploying Op.STOP (1 byte), @@ -2144,12 +2147,15 @@ def test_create_account_charge_reduces_child_gas( @pytest.mark.parametrize( - "init_code", + ("init_code", "floor_binds"), [ pytest.param( - Op.REVERT(0, 10_000, new_memory_size=10_000), id="revert" + Op.REVERT(0, 10_000, new_memory_size=10_000), + False, + id="revert", ), - pytest.param(Op.INVALID, id="halt"), + pytest.param(Op.REVERT(0, 0), True, id="revert_floor_bound"), + pytest.param(Op.INVALID, None, id="halt"), ], ) @pytest.mark.valid_from("EIP8037") @@ -2158,6 +2164,7 @@ def test_failed_create_tx_refills_top_frame_new_account( pre: Alloc, fork: Fork, init_code: Bytecode, + floor_binds: bool | None, ) -> None: """ Verify the top-frame NEW_ACCOUNT of a creation tx is refilled when the @@ -2171,10 +2178,11 @@ def test_failed_create_tx_refills_top_frame_new_account( * REVERT preserves ``gas_left`` and ``refill_frame_state_gas`` returns the spilled ``NEW_ACCOUNT`` to it, so the state block nets to zero - and only the regular consumption counts as work. The tiny init code - leaves the decomposed calldata floor above that consumption, so the - amount billed (receipt) is pinned to the floor while the header - excludes the floor top-up. + and only the regular consumption counts as work. The calldata floor + tops up the billed amount and the block-level regular gas alike, so + receipt and header agree at the greater of consumption and floor: + the memory expansion keeps ``revert`` above the floor, while the + bare ``revert_floor_bound`` pins the floor in both. * HALT (INVALID) refills the spilled ``NEW_ACCOUNT`` to ``gas_left`` and then burns all of it, so the sender pays the full ``gas_limit``. """ @@ -2199,19 +2207,19 @@ def test_failed_create_tx_refills_top_frame_new_account( # Exceptional halt burns all gas_left (the refilled NEW_ACCOUNT # included). expected_gas_used = gas_limit - expected_header_gas = gas_limit else: # REVERT refills the spilled NEW_ACCOUNT, netting the state block - # to zero, so only the regular consumption counts as work. + # to zero, so only the regular consumption counts as work. The + # calldata floor binds the billed amount and the block-level + # regular gas alike, so receipt and header agree either way. regular_consumed = intrinsic_regular + init_code.regular_cost(fork) - # The tiny init code leaves the decomposed calldata floor above - # the regular gas consumed: the receipt bills at the floor, while - # the header's regular-gas accounting excludes the floor top-up. floor = fork.transaction_data_floor_cost_calculator()( data=bytes(init_code), contract_creation=True ) + assert (floor > regular_consumed) == floor_binds, ( + "init code lands on the wrong side of the floor" + ) expected_gas_used = max(regular_consumed, floor) - expected_header_gas = regular_consumed sender = pre.fund_eoa() created = compute_create_address(address=sender, nonce=0) @@ -2230,7 +2238,7 @@ def test_failed_create_tx_refills_top_frame_new_account( pre=pre, post={created: Account.NONEXISTENT}, tx=tx, - blockchain_test_header_verify=Header(gas_used=expected_header_gas), + blockchain_test_header_verify=Header(gas_used=expected_gas_used), )