Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,11 @@ def transaction_top_frame_state_gas(
"""
Return the state gas charged at the top-level transaction
frame. Charges ``NEW_ACCOUNT`` when value is transferred to an
empty recipient; zero otherwise. Precompile recipients are
exempt: they are protocol-inherent rather than created by a
value transfer, so they never trigger ``NEW_ACCOUNT`` even
when state-empty.
empty recipient; zero otherwise.
"""
gas_costs = cls.gas_costs()
if contract_creation:
return 0
if recipient_type == RecipientType.PRECOMPILE:
return 0
if sends_value and recipient_type == RecipientType.EMPTY_ACCOUNT:
return gas_costs.NEW_ACCOUNT
return 0
11 changes: 3 additions & 8 deletions src/ethereum/forks/amsterdam/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,16 +312,11 @@ def process_message(message: Message) -> Evm:
# opcode runs. Reads pre-value-transfer state so the
# EIP-161-empty check sees the recipient as it was at the start
# of the frame. Gated to non-create top-level frames; creates
# pay the equivalent NEW_ACCOUNT state gas. Precompile
# recipients are exempt from the NEW_ACCOUNT charge: they are
# protocol-inherent rather than created by a value transfer.
# pay the equivalent NEW_ACCOUNT state gas.
if message.depth == Uint(0) and message.target != Bytes0(b""):
recipient = message.current_target
recipient_is_precompile = recipient in PRE_COMPILED_CONTRACTS
if (
message.value > U256(0)
and not recipient_is_precompile
and not is_account_alive(tx_state, recipient)
if message.value > U256(0) and not is_account_alive(
tx_state, recipient
):
charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT)
recipient_code = get_code(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pytest
from execution_testing import (
Account,
Address,
Alloc,
Fork,
Op,
Expand Down Expand Up @@ -104,6 +105,60 @@ def test_top_frame_state_charge(
state_test(pre=pre, tx=tx, post=post)


def test_top_frame_state_charge_empty_precompile(
fork: Fork,
pre: Alloc,
state_test: StateTestFiller,
) -> None:
"""
An empty precompile recipient is still empty per EIP-161, so a
value-moving transaction to it must pay the top-frame
``NEW_ACCOUNT`` state-gas charge.

The gas limit is one short of covering that state charge. Without
the charge, the transaction would reach the identity precompile and
transfer value, which makes this a direct regression test for a
precompile carve-out.
"""
sender_initial_balance = 10**18
sender = pre.fund_eoa(sender_initial_balance)
identity_precompile = Address(0x04)

value = 1
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(
sends_value=True,
recipient_type=RecipientType.PRECOMPILE,
return_cost_deducted_prior_execution=True,
)
top_frame_state_gas = fork.transaction_top_frame_state_gas(
sends_value=True,
recipient_type=RecipientType.EMPTY_ACCOUNT,
)
assert top_frame_state_gas > 0, (
"top-frame state gas must be non-zero for empty recipients"
)

gas_price = 1_000_000_000
gas_limit = intrinsic_gas + top_frame_state_gas - 1
tx = Transaction(
sender=sender,
to=identity_precompile,
value=value,
gas_limit=gas_limit,
gas_price=gas_price,
)

post = {
sender: Account(
nonce=1,
balance=sender_initial_balance - gas_limit * gas_price,
),
identity_precompile: None,
}

state_test(pre=pre, tx=tx, post=post)


@pytest.mark.parametrize("outcome", ["oog", "success", "evm_reverts"])
@pytest.mark.parametrize(
"value",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,18 +300,17 @@ def test_value_move_to_precompiles(
intrinsic time as any other non-self target -- access lists do
not warm transaction-level accounts. A value transfer to a
precompile additionally pays the transfer-log and value-transfer
charges. The top-frame ``NEW_ACCOUNT`` state charge is suppressed
for precompile recipients: they are protocol-inherent rather than
created by a value transfer.

The ``pre_funded`` parameter exercises both pre-tx states of the
precompile address: the ``empty`` variant proves the carve-out is
explicit -- without it, a value transfer to an empty precompile
would trip the ``NEW_ACCOUNT`` charge that ``is_account_alive``
would otherwise demand. The gas limit is pinned to
``intrinsic_gas`` plus a buffer that covers the precompile
execution body, so a miscalculated intrinsic or a missing
carve-out would trip the gas budget rather than pass silently.
charges.

The top-frame ``NEW_ACCOUNT`` state charge keys solely on EIP-161
emptiness; a precompile address is not special-cased. The
``pre_funded`` parameter exercises both pre-tx states:

- ``not_funded``: the precompile address is empty per EIP-161, so a
value transfer creates it and pays ``NEW_ACCOUNT`` -- exactly
like any other empty recipient.
- ``pre_funded``: the precompile already holds a balance and is
therefore alive, so no ``NEW_ACCOUNT`` charge applies.
"""
sender_initial_balance = 10**18
sender = pre.fund_eoa(sender_initial_balance)
Expand All @@ -329,34 +328,57 @@ def test_value_move_to_precompiles(
recipient_type=RecipientType.PRECOMPILE,
return_cost_deducted_prior_execution=True,
)
# A value transfer to an empty (not pre-funded) precompile fires the
# top-frame ``NEW_ACCOUNT`` state charge, modelled via
# ``EMPTY_ACCOUNT``; a pre-funded precompile is alive and exempt.
state_recipient_type = (
RecipientType.PRECOMPILE if pre_funded else RecipientType.EMPTY_ACCOUNT
)
top_frame_state_gas = fork.transaction_top_frame_state_gas(
sends_value=bool(value),
recipient_type=state_recipient_type,
)

# Precompile execution gas varies per precompile and input; pick
# a buffer large enough to cover the most expensive precompile in
# the matrix but still well under the ``NEW_ACCOUNT`` state
# charge, so a missing carve-out OOGs the ``empty`` variants.
precompile_execution_budget = 100_000
tx_gas_limit = intrinsic_gas + precompile_execution_budget
gas_price = 1_000_000_000

tx = Transaction(
sender=sender,
to=precompile,
value=value,
data=tx_data,
gas_limit=tx_gas_limit,
gas_price=gas_price,
)

# Exact sender balance is not checked because precompile execution
# gas varies; verify value receipt and sender nonce instead.
# Exact sender balance is generally not checked because precompile
# execution gas varies across the matrix. For identity with empty
# calldata, the execution gas is deterministic, so pin the exact
# balance to make the empty-precompile ``NEW_ACCOUNT`` charge a
# source-level assertion.
final_precompile_balance = pre_funded_amount + value
expected_precompile: Account | None
if final_precompile_balance > 0:
expected_precompile = Account(balance=final_precompile_balance)
else:
expected_precompile = None
expected_sender = Account(nonce=1)
if precompile == Address(0x04):
gas_costs = fork.gas_costs()
precompile_execution_gas = (
gas_costs.PRECOMPILE_IDENTITY_BASE
+ gas_costs.PRECOMPILE_IDENTITY_PER_WORD
* ((len(tx_data) + 31) // 32)
)
total_gas_cost = (
intrinsic_gas + top_frame_state_gas + precompile_execution_gas
)
expected_sender = Account(
nonce=1,
balance=(
sender_initial_balance - value - total_gas_cost * gas_price
),
)
post = {
sender: Account(nonce=1),
sender: expected_sender,
precompile: expected_precompile,
}

Expand Down
Loading