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
8 changes: 7 additions & 1 deletion src/ethereum/forks/amsterdam/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import List, Optional, Tuple

from ethereum_rlp import rlp
from ethereum_types.bytes import Bytes
from ethereum_types.bytes import Bytes, Bytes0
from ethereum_types.frozen import slotted_freezable
from ethereum_types.numeric import U64, U256, Uint, ulen

Expand Down Expand Up @@ -1076,6 +1076,12 @@ def process_transaction(
if tx_output.error is not None:
tx_output.state_gas_left += tx_output.state_gas_used
tx_output.state_gas_used = Uint(0)
if isinstance(tx.to, Bytes0):
new_account_refund = (
STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE
)
tx_output.state_gas_left += new_account_refund
tx_output.state_refund += new_account_refund
else:
# Refund state gas for accounts created and destroyed in the
# same tx (EIP-6780). Covers account, storage, and code.
Expand Down
32 changes: 11 additions & 21 deletions src/ethereum/forks/amsterdam/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ class Evm:
accessed_storage_keys: Set[Tuple[Address, Bytes32]]
regular_gas_used: Uint = Uint(0)
state_gas_used: Uint = Uint(0)
state_gas_refund: Uint = Uint(0)
state_gas_refund_pending: Uint = Uint(0)


Expand All @@ -191,10 +190,8 @@ def credit_state_gas_refund(evm: Evm, amount: Uint) -> None:

Clamp the applied portion to this frame's `state_gas_used` — the
matching charge may sit in an ancestor sharing storage via
CALLCODE/DELEGATECALL. Track it in `state_gas_refund` so
`incorporate_child_on_error` can undo the inflation, and defer the
unapplied remainder in `state_gas_refund_pending` for propagation
on success.
CALLCODE/DELEGATECALL. Defer the unapplied remainder in
`state_gas_refund_pending` for propagation on success.

Parameters
----------
Expand All @@ -207,18 +204,16 @@ def credit_state_gas_refund(evm: Evm, amount: Uint) -> None:
applied = min(amount, evm.state_gas_used)
evm.state_gas_left += applied
evm.state_gas_used -= applied
evm.state_gas_refund += applied
evm.state_gas_refund_pending += amount - applied


def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None:
"""
Incorporate the state of a successful `child_evm` into the parent `evm`.

Propagate `state_gas_refund` (inline credits the child applied) so
an ancestor revert can undo the inflation, and apply
`state_gas_refund_pending` (the unapplied remainder) to the parent
via `credit_state_gas_refund`; any leftover propagates further up.
Apply `state_gas_refund_pending` (the unapplied remainder of any
refund credited inside the child) to the parent via
`credit_state_gas_refund`; any leftover propagates further up.

Parameters
----------
Expand All @@ -237,7 +232,6 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None:
evm.accessed_storage_keys.update(child_evm.accessed_storage_keys)
evm.regular_gas_used += child_evm.regular_gas_used
evm.state_gas_used += child_evm.state_gas_used
evm.state_gas_refund += child_evm.state_gas_refund
credit_state_gas_refund(evm, child_evm.state_gas_refund_pending)


Expand All @@ -253,11 +247,11 @@ def incorporate_child_on_error(
that spilled into `gas_left`, is restored to the parent's reservoir and
the child's `state_gas_used` is not accumulated.

Inline state-gas refunds (SSTORE 0 to x to 0, CREATE silent failure)
credited by the child inflated its `state_gas_left`; subtract
`state_gas_refund` from the amount returned to the parent's
reservoir so the inflation does not leak across the error boundary.
`state_gas_refund_pending` is discarded with the child frame.
`state_gas_refund_pending` is discarded with the child frame: any
inline credits the child applied are keyed to charges (its own
SSTORE or CREATE pre-charge) that are themselves rolled back, so
the matching `state_gas_left + state_gas_used` sum already reflects
the correct amount to return to the parent.

Parameters
----------
Expand All @@ -268,11 +262,7 @@ def incorporate_child_on_error(

"""
evm.gas_left += child_evm.gas_left
evm.state_gas_left += (
child_evm.state_gas_used
+ child_evm.state_gas_left
- child_evm.state_gas_refund
)
evm.state_gas_left += child_evm.state_gas_used + child_evm.state_gas_left
evm.regular_gas_used += child_evm.regular_gas_used


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1844,15 +1844,16 @@ def test_create_code_deposit_oog_refunds_state_gas(
],
)
@pytest.mark.valid_from("EIP8037")
def test_failed_create_tx_state_gas_dominates(
def test_failed_create_tx_refunds_intrinsic_new_account(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
init_code: Bytecode,
) -> None:
"""
Verify the header gas is set by intrinsic state gas when a
creation tx fails with a tight regular budget.
Verify the NEW_ACCOUNT × CPSB portion of intrinsic_state_gas is
refunded on creation-tx revert/halt, so block state-gas excludes
it and header gas_used reflects only the regular component.
"""
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
create_state_gas = fork.create_state_gas(code_size=0)
Expand All @@ -1863,9 +1864,12 @@ def test_failed_create_tx_state_gas_dominates(
intrinsic_regular = intrinsic_total - create_state_gas
gas_limit = intrinsic_total + 1000

assert intrinsic_regular + 1000 < create_state_gas, (
"tight gas budget must keep block_regular below create_state_gas"
)
if init_code == Op.INVALID:
regular_consumed = gas_limit - intrinsic_total
else:
regular_consumed = init_code.regular_cost(fork)

expected_gas_used = intrinsic_regular + regular_consumed

tx = Transaction(
to=None,
Expand All @@ -1879,7 +1883,54 @@ def test_failed_create_tx_state_gas_dominates(
blocks=[
Block(
txs=[tx],
header_verify=Header(gas_used=create_state_gas),
header_verify=Header(gas_used=expected_gas_used),
),
],
post={},
)


@pytest.mark.pre_alloc_mutable()
@pytest.mark.valid_from("EIP8037")
def test_create_tx_collision_refunds_intrinsic_new_account(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
) -> None:
"""
Verify the NEW_ACCOUNT × CPSB portion of intrinsic_state_gas is
refunded on creation-tx address collision, so block state-gas
excludes it and header gas_used reflects only the regular
consumption (full forwarded gas, no initcode runs).
"""
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
create_state_gas = fork.create_state_gas(code_size=0)

init_code = Op.STOP
intrinsic_total = intrinsic_calc(
calldata=bytes(init_code), contract_creation=True
)
gas_limit = intrinsic_total + 1000

sender = pre.fund_eoa()
collision_target = compute_create_address(address=sender, nonce=0)
pre[collision_target] = Account(nonce=1)

expected_gas_used = gas_limit - create_state_gas

tx = Transaction(
to=None,
data=init_code,
gas_limit=gas_limit,
sender=sender,
)

blockchain_test(
pre=pre,
blocks=[
Block(
txs=[tx],
header_verify=Header(gas_used=expected_gas_used),
),
],
post={},
Expand Down Expand Up @@ -2131,11 +2182,6 @@ def test_inner_create_succeeds_code_deposit_state_gas(
initcode_gas = initcode.gas_cost(fork)
gas_limit = intrinsic_total + initcode_gas + inner_code_deposit + 1000

if outer_outcome == "succeeds":
expected_state = outer_state_gas + inner_state_gas
else:
expected_state = outer_state_gas

create_address = compute_create_address(address=sender, nonce=0)

tx = Transaction(
Expand All @@ -2147,19 +2193,15 @@ def test_inner_create_succeeds_code_deposit_state_gas(

if outer_outcome == "succeeds":
post: dict = {create_address: Account(code=b"")}
block = Block(
txs=[tx],
header_verify=Header(gas_used=outer_state_gas + inner_state_gas),
)
else:
post = {create_address: Account.NONEXISTENT}
block = Block(txs=[tx])

blockchain_test(
pre=pre,
blocks=[
Block(
txs=[tx],
header_verify=Header(gas_used=expected_state),
),
],
post=post,
)
blockchain_test(pre=pre, blocks=[block], post=post)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -2355,8 +2397,6 @@ def test_inner_create_fail_refunds_in_creation_tx(
+ num_inner_ops * (gas_costs.NEW_ACCOUNT + per_inner_slack)
)

expected_state = outer_state_gas

create_address = compute_create_address(address=sender, nonce=0)

tx = Transaction(
Expand All @@ -2368,19 +2408,15 @@ def test_inner_create_fail_refunds_in_creation_tx(

if outer_outcome == "succeeds":
post: dict = {create_address: Account(code=b"")}
block = Block(
txs=[tx],
header_verify=Header(gas_used=outer_state_gas),
)
else:
post = {create_address: Account.NONEXISTENT}
block = Block(txs=[tx])

blockchain_test(
pre=pre,
blocks=[
Block(
txs=[tx],
header_verify=Header(gas_used=expected_state),
),
],
post=post,
)
blockchain_test(pre=pre, blocks=[block], post=post)


@pytest.mark.pre_alloc_mutable
Expand Down
Loading
Loading