The SSTORE first-change cost is repriced across the Amsterdam
boundary, and EIP-8038 changes the model, not a single number.
Before the fork (parent schedule) a zero-to-nonzero SSTORE is a
flat execution charge (COLD_STORAGE_ACCESS + STORAGE_SET) with no
state-gas dimension. After the fork the charge splits: the execution
portion drops to COLD_STORAGE_ACCESS + STORAGE_WRITE while the
bulk moves into the new state-gas dimension, and the clear refund
rises. Every magnitude is derived from the two schedules; nothing is
hardcoded.
The transition is asserted at the derived-constant level (the
runtime opcode cost cannot isolate the execution portion without the
state-gas confounder) and a zero-to-nonzero SSTORE is exercised
in both blocks to prove it still sets the slot in each regime.
Source code in tests/amsterdam/eip8038_state_access_gas_cost_increase/test_fork_transition.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412 | @EIPChecklist.GasCostChanges.Test.ForkTransition.Before()
@EIPChecklist.GasCostChanges.Test.ForkTransition.After()
def test_sstore_write_cost_at_transition(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
) -> None:
"""
The ``SSTORE`` first-change cost is repriced across the Amsterdam
boundary, and EIP-8038 changes the *model*, not a single number.
Before the fork (parent schedule) a zero-to-nonzero ``SSTORE`` is a
flat execution charge (``COLD_STORAGE_ACCESS + STORAGE_SET``) with no
state-gas dimension. After the fork the charge splits: the execution
portion drops to ``COLD_STORAGE_ACCESS + STORAGE_WRITE`` while the
bulk moves into the new state-gas dimension, and the clear refund
rises. Every magnitude is derived from the two schedules; nothing is
hardcoded.
The transition is asserted at the derived-constant level (the
runtime opcode cost cannot isolate the execution portion without the
state-gas confounder) and a zero-to-nonzero ``SSTORE`` is exercised
in both blocks to prove it still sets the slot in each regime.
"""
before = fork.fork_at(timestamp=BEFORE_TS)
after = fork.fork_at(timestamp=AFTER_TS)
# First-change (zero -> nonzero, cold) SSTORE in each regime.
sstore = Op.SSTORE(new_value=1)
execution_before = sstore.execution_cost(before)
execution_after = sstore.execution_cost(after)
state_before = sstore.state_cost(before)
state_after = sstore.state_cost(after)
total_before = sstore.gas_cost(before)
total_after = sstore.gas_cost(after)
# The repricing changes the execution charge, introduces the state
# dimension, and therefore moves the total.
assert execution_after != execution_before
assert state_before == 0
assert state_after > 0
assert total_after != total_before
# The storage-clear refund also rises across the boundary.
clear_sstore = Op.SSTORE.with_metadata(
original_value=1, current_value=1, new_value=0
)
refund_before = clear_sstore.refund(before)
refund_after = clear_sstore.refund(after)
assert refund_after > refund_before
# Exercise the zero-to-nonzero SSTORE in both regimes; the slot ends
# set in each block.
storage_before = Storage()
contract_before = pre.deploy_contract(
code=Op.SSTORE(storage_before.store_next(1), 1),
)
storage_after = Storage()
contract_after = pre.deploy_contract(
code=Op.SSTORE(storage_after.store_next(1), 1),
)
blocks = transition_blocks(contract_before, contract_after, pre)
post = {
contract_before: Account(storage=storage_before),
contract_after: Account(storage=storage_after),
}
blockchain_test(pre=pre, blocks=blocks, post=post)
|