Skip to content

Commit 3d50745

Browse files
committed
feat(spec-specs, tests): charge EIP-8037 account creation at access (#3116)
1 parent 695e5ef commit 3d50745

5 files changed

Lines changed: 166 additions & 69 deletions

File tree

src/ethereum/forks/amsterdam/fork.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,11 +1058,19 @@ def process_transaction(
10581058
effective_gas_fee = tx.gas * effective_gas_price
10591059

10601060
# Split execution gas into gas_left (capped by remaining regular gas
1061-
# budget) and state_gas_reservoir.
1061+
# budget) and state_gas_reservoir. The contract-creation component
1062+
# of the intrinsic state gas is seeded into the reservoir rather
1063+
# than pre-consumed; it funds the conditional charge at the
1064+
# deployment-address access.
10621065
execution_gas = tx.gas - intrinsic_gas
10631066
regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic.regular
10641067
gas = min(regular_gas_budget, execution_gas)
1065-
state_gas_reservoir = Uint(execution_gas - gas)
1068+
create_state_gas = (
1069+
Uint(StateGasCosts.NEW_ACCOUNT)
1070+
if isinstance(tx.to, Bytes0)
1071+
else Uint(0)
1072+
)
1073+
state_gas_reservoir = Uint(execution_gas - gas) + create_state_gas
10661074

10671075
increment_nonce(tx_state, sender)
10681076

@@ -1110,13 +1118,6 @@ def process_transaction(
11101118

11111119
tx_output = process_message_call(message)
11121120

1113-
if isinstance(tx.to, Bytes0) and (
1114-
tx_output.error is not None or tx_output.created_target_alive
1115-
):
1116-
new_account_refund = StateGasCosts.NEW_ACCOUNT
1117-
tx_output.state_gas_left += new_account_refund
1118-
tx_output.state_refund += new_account_refund
1119-
11201121
tx_gas_used_before_refund = (
11211122
tx.gas - tx_output.gas_left - tx_output.state_gas_left
11221123
)
@@ -1144,6 +1145,7 @@ def process_transaction(
11441145

11451146
tx_state_gas = (
11461147
int(tx_env.intrinsic_state_gas)
1148+
- int(create_state_gas)
11471149
+ tx_output.state_gas_used
11481150
- int(tx_output.state_refund)
11491151
)

src/ethereum/forks/amsterdam/vm/instructions/system.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,12 @@ def generic_create(
8484
if memory_size > U256(MAX_INIT_CODE_SIZE):
8585
raise OutOfGasError
8686

87-
# Charge state gas for account creation (pay-before-execute).
88-
# Refunded to the reservoir on any failure path below.
89-
charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT)
90-
9187
tx_state = evm.message.tx_env.state
9288

9389
call_data = memory_read_bytes(
9490
evm.memory, memory_start_position, memory_size
9591
)
9692

97-
create_message_gas = max_message_call_gas(Uint(evm.gas_left))
98-
evm.gas_left -= create_message_gas
99-
100-
# Move full reservoir to child (no 63/64 rule for state gas). Parent's
101-
# `state_gas_left` is zeroed and restored when the child returns.
102-
create_message_state_gas_reservoir = evm.state_gas_left
103-
evm.state_gas_left = Uint(0)
104-
10593
evm.return_data = b""
10694

10795
sender_address = evm.message.current_target
@@ -112,26 +100,36 @@ def generic_create(
112100
or sender.nonce == Uint(2**64 - 1)
113101
or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT
114102
):
115-
evm.gas_left += create_message_gas
116-
evm.state_gas_left += create_message_state_gas_reservoir
117-
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
118103
push(evm.stack, U256(0))
119104
return
120105

121106
evm.accessed_addresses.add(contract_address)
122107

108+
# The charge is decided by existence alone, independently of the
109+
# collision outcome.
110+
new_account_charged = not is_account_alive(tx_state, contract_address)
111+
if new_account_charged:
112+
charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT)
113+
114+
create_message_gas = max_message_call_gas(Uint(evm.gas_left))
115+
evm.gas_left -= create_message_gas
116+
123117
if not account_deployable(tx_state, contract_address):
124-
increment_nonce(tx_state, evm.message.current_target)
118+
increment_nonce(tx_state, sender_address)
125119
evm.regular_gas_used += create_message_gas
126-
evm.state_gas_left += create_message_state_gas_reservoir
127-
# Address collision — no account created, refund state gas.
128-
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
120+
# A storage-only collision target is non-existent: charged
121+
# above, refilled here.
122+
if new_account_charged:
123+
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
129124
push(evm.stack, U256(0))
130125
return
131126

132-
target_alive = is_account_alive(tx_state, contract_address)
127+
# Move full reservoir to child (no 63/64 rule for state gas). Parent's
128+
# `state_gas_left` is zeroed and restored when the child returns.
129+
create_message_state_gas_reservoir = evm.state_gas_left
130+
evm.state_gas_left = Uint(0)
133131

134-
increment_nonce(tx_state, evm.message.current_target)
132+
increment_nonce(tx_state, sender_address)
135133

136134
child_message = Message(
137135
block_env=evm.message.block_env,
@@ -157,14 +155,12 @@ def generic_create(
157155

158156
if child_evm.error:
159157
incorporate_child_on_error(evm, child_evm)
160-
# No account created, refund parent's CREATE state gas.
161-
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
158+
if new_account_charged:
159+
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
162160
evm.return_data = child_evm.output
163161
push(evm.stack, U256(0))
164162
else:
165163
incorporate_child_on_success(evm, child_evm)
166-
if target_alive:
167-
credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
168164
evm.return_data = b""
169165
push(evm.stack, U256.from_be_bytes(child_evm.message.current_target))
170166

src/ethereum/forks/amsterdam/vm/interpreter.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ class MessageCallOutput:
9797
authorities that already existed in state. Subtracted from
9898
`tx_state_gas` in block accounting so `block.gas_used`
9999
matches the receipt `cumulative_gas_used`.
100-
10. `created_target_alive`: Whether a top-level creation
101-
transaction targeted an already-existent account.
102100
"""
103101

104102
gas_left: Uint
@@ -111,7 +109,6 @@ class MessageCallOutput:
111109
regular_gas_used: Uint
112110
state_gas_used: int
113111
state_refund: Uint
114-
created_target_alive: bool
115112

116113

117114
def process_message_call(message: Message) -> MessageCallOutput:
@@ -133,12 +130,23 @@ def process_message_call(message: Message) -> MessageCallOutput:
133130
tx_state = message.tx_env.state
134131
refund_counter = U256(0)
135132
state_refund = Uint(0)
136-
target_alive = False
133+
new_account_charged = False
137134
if message.target == Bytes0(b""):
135+
# The charge is decided by existence alone, independently of
136+
# the collision outcome. The reservoir seeding guarantees the
137+
# charge is always covered.
138+
new_account_charged = not is_account_alive(
139+
tx_state, message.current_target
140+
)
141+
if new_account_charged:
142+
message.state_gas_reservoir -= Uint(StateGasCosts.NEW_ACCOUNT)
138143
if account_deployable(tx_state, message.current_target):
139-
target_alive = is_account_alive(tx_state, message.current_target)
140144
evm = process_create_message(message)
141145
else:
146+
if new_account_charged:
147+
# A storage-only collision target is non-existent:
148+
# charged above, refilled here.
149+
message.state_gas_reservoir += Uint(StateGasCosts.NEW_ACCOUNT)
142150
return MessageCallOutput(
143151
gas_left=Uint(0),
144152
refund_counter=U256(0),
@@ -150,7 +158,6 @@ def process_message_call(message: Message) -> MessageCallOutput:
150158
regular_gas_used=message.gas,
151159
state_gas_used=0,
152160
state_refund=Uint(0),
153-
created_target_alive=False,
154161
)
155162
else:
156163
if message.tx_env.authorizations != ():
@@ -182,6 +189,15 @@ def process_message_call(message: Message) -> MessageCallOutput:
182189
)
183190
evm_trace(evm, tx_end)
184191

192+
state_gas_used = frame_state_gas_used(evm)
193+
if new_account_charged:
194+
if evm.error:
195+
# Account creation rolled back: refill the charge taken at
196+
# the deployment-address access.
197+
evm.state_gas_left += Uint(StateGasCosts.NEW_ACCOUNT)
198+
else:
199+
state_gas_used += int(StateGasCosts.NEW_ACCOUNT)
200+
185201
return MessageCallOutput(
186202
gas_left=evm.gas_left,
187203
refund_counter=refund_counter,
@@ -191,9 +207,8 @@ def process_message_call(message: Message) -> MessageCallOutput:
191207
return_data=evm.output,
192208
state_gas_left=evm.state_gas_left,
193209
regular_gas_used=evm.regular_gas_used,
194-
state_gas_used=frame_state_gas_used(evm),
210+
state_gas_used=state_gas_used,
195211
state_refund=state_refund,
196-
created_target_alive=target_alive,
197212
)
198213

199214

tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3298,8 +3298,10 @@ def test_bal_create_and_oog(
32983298
CREATE/CREATE2 OOG boundary test at three gas levels.
32993299
33003300
OOG_BEFORE_TARGET_ACCESS and OOG_AFTER_TARGET_ACCESS differ by
3301-
exactly 1 gas, proving the static cost boundary: below it the
3302-
created address is NOT in BAL, at it the address IS in BAL.
3301+
exactly 1 gas, proving the pre-access cost boundary: below it the
3302+
created address is NOT in BAL, at it the address IS in BAL. Under
3303+
EIP-8037 charge-at-access this boundary excludes NEW_ACCOUNT, which
3304+
is charged at the access rather than before it.
33033305
"""
33043306
alice = pre.fund_eoa()
33053307

@@ -3344,14 +3346,19 @@ def test_bal_create_and_oog(
33443346
create_static_cost = factory_mstore.gas_cost(
33453347
fork
33463348
) + factory_create.gas_cost(fork)
3349+
# Under EIP-8037 charge-at-access the NEW_ACCOUNT state gas is charged
3350+
# at the destination access, not before it, so the pre-access cost that
3351+
# gates whether the address is read excludes it. `gas_cost` folds
3352+
# NEW_ACCOUNT into the create opcode total, so strip it back out.
3353+
pre_access_cost = create_static_cost - fork.gas_costs().NEW_ACCOUNT
33473354

33483355
if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS:
3349-
# 1 gas short of CREATE static cost — no state access
3350-
gas_limit = intrinsic_cost + create_static_cost - 1
3356+
# 1 gas short of the pre-access cost — no state access
3357+
gas_limit = intrinsic_cost + pre_access_cost - 1
33513358
elif oog_boundary == OutOfGasBoundary.OOG_AFTER_TARGET_ACCESS:
3352-
# Exactly the CREATE static cost — address accessed, child
3353-
# frame gets 0 gas, CREATE fails, sink forces OOG after access
3354-
gas_limit = intrinsic_cost + create_static_cost
3359+
# Exactly the pre-access cost — address accessed, then the
3360+
# NEW_ACCOUNT charge OOGs after access
3361+
gas_limit = intrinsic_cost + pre_access_cost
33553362
else:
33563363
# Full success: static cost + child frame (63/64 rule) +
33573364
# SSTORE + gas sink.

0 commit comments

Comments
 (0)