Skip to content

Commit c00d017

Browse files
kclowesspencer-tb
authored andcommitted
feat(tests): top-level failure state gas refund
1 parent 7a0358f commit c00d017

1 file changed

Lines changed: 316 additions & 0 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Storage,
2929
Transaction,
3030
TransactionException,
31+
TransactionReceipt,
3132
)
3233
from execution_testing.checklists import EIPChecklist
3334

@@ -415,3 +416,318 @@ def test_create_tx_reservoir(
415416
)
416417

417418
state_test(env=env, pre=pre, post={}, tx=tx)
419+
420+
421+
@pytest.mark.parametrize(
422+
"failure_mode",
423+
[
424+
pytest.param("revert", id="revert"),
425+
pytest.param("halt", id="halt"),
426+
pytest.param("oog", id="oog"),
427+
],
428+
)
429+
@pytest.mark.valid_from("EIP8037")
430+
def test_top_level_failure_refunds_execution_state_gas(
431+
state_test: StateTestFiller,
432+
pre: Alloc,
433+
fork: Fork,
434+
failure_mode: str,
435+
) -> None:
436+
"""
437+
Verify top level tx failure returns execution state gas to the
438+
reservoir across revert, exceptional halt, and out of gas paths.
439+
440+
On top level failure no state was created, so execution state gas
441+
is credited back to the reservoir and `state_gas_used` is zeroed.
442+
The billing formula `tx.gas - gas_left - state_gas_left` sees a
443+
restored reservoir and refunds the sender. Without the refund the
444+
receipt would bill the consumed state gas despite the failure.
445+
"""
446+
gas_limit_cap = fork.transaction_gas_limit_cap()
447+
assert gas_limit_cap is not None
448+
sstore_state_gas = fork.sstore_state_gas()
449+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
450+
451+
if failure_mode == "revert":
452+
code = Op.SSTORE(0, 1) + Op.REVERT(0, 0)
453+
elif failure_mode == "halt":
454+
code = Op.SSTORE(0, 1) + Op.INVALID
455+
else:
456+
# OOG: perform the SSTORE then spin with JUMPDEST loop until
457+
# gas runs out.
458+
code = Op.SSTORE(0, 1) + Op.JUMPDEST + Op.JUMP(0x5)
459+
contract = pre.deploy_contract(code=code)
460+
461+
tx_gas = gas_limit_cap + sstore_state_gas
462+
463+
if failure_mode == "revert":
464+
# REVERT preserves unused gas_left.
465+
expected_cumulative = (
466+
intrinsic_cost + code.gas_cost(fork) - sstore_state_gas
467+
)
468+
else:
469+
# Exceptional halt and out of gas zero gas_left.
470+
expected_cumulative = tx_gas - sstore_state_gas
471+
472+
tx = Transaction(
473+
to=contract,
474+
gas_limit=tx_gas,
475+
sender=pre.fund_eoa(),
476+
expected_receipt=TransactionReceipt(
477+
cumulative_gas_used=expected_cumulative,
478+
),
479+
)
480+
481+
state_test(pre=pre, post={contract: Account(storage={})}, tx=tx)
482+
483+
484+
@pytest.mark.parametrize(
485+
"failure_mode",
486+
[
487+
pytest.param("revert", id="revert"),
488+
pytest.param("halt", id="halt"),
489+
pytest.param("oog", id="oog"),
490+
],
491+
)
492+
@pytest.mark.valid_from("EIP8037")
493+
def test_top_level_failure_zeros_block_state_gas(
494+
blockchain_test: BlockchainTestFiller,
495+
pre: Alloc,
496+
fork: Fork,
497+
failure_mode: str,
498+
) -> None:
499+
"""
500+
Verify the block header reflects zero execution state gas after a
501+
top level failure.
502+
503+
With `state_gas_used` zeroed on failure, `block_state_gas_used`
504+
excludes any state gas consumed during the failed transaction and
505+
the block header `gas_used` falls back to the regular gas
506+
component alone.
507+
"""
508+
gas_limit_cap = fork.transaction_gas_limit_cap()
509+
assert gas_limit_cap is not None
510+
sstore_state_gas = fork.sstore_state_gas()
511+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
512+
513+
if failure_mode == "revert":
514+
code = Op.SSTORE(0, 1) + Op.REVERT(0, 0)
515+
elif failure_mode == "halt":
516+
code = Op.SSTORE(0, 1) + Op.INVALID
517+
else:
518+
code = Op.SSTORE(0, 1) + Op.JUMPDEST + Op.JUMP(0x5)
519+
contract = pre.deploy_contract(code=code)
520+
521+
tx_gas = gas_limit_cap + sstore_state_gas
522+
tx = Transaction(
523+
to=contract,
524+
gas_limit=tx_gas,
525+
sender=pre.fund_eoa(),
526+
)
527+
528+
if failure_mode == "revert":
529+
expected_block_regular = (
530+
intrinsic_cost + code.gas_cost(fork) - sstore_state_gas
531+
)
532+
else:
533+
# Exceptional halt and out of gas zero gas_left.
534+
expected_block_regular = tx_gas - sstore_state_gas
535+
536+
blockchain_test(
537+
pre=pre,
538+
blocks=[
539+
Block(
540+
txs=[tx],
541+
header_verify=Header(gas_used=expected_block_regular),
542+
),
543+
],
544+
post={contract: Account(storage={})},
545+
)
546+
547+
548+
@pytest.mark.valid_from("EIP8037")
549+
def test_creation_tx_failure_preserves_intrinsic_state_gas(
550+
blockchain_test: BlockchainTestFiller,
551+
pre: Alloc,
552+
fork: Fork,
553+
) -> None:
554+
"""
555+
Verify a failed creation tx still counts intrinsic state gas.
556+
557+
A creation tx (to=None) whose initcode halts carries intrinsic
558+
state gas for the new account. The top level failure refund
559+
zeroes execution state gas but preserves intrinsic state gas, so
560+
the block header reflects the intrinsic state gas rather than
561+
zero.
562+
"""
563+
gas_limit_cap = fork.transaction_gas_limit_cap()
564+
assert gas_limit_cap is not None
565+
566+
create_intrinsic_state = fork.transaction_intrinsic_state_gas(
567+
contract_creation=True,
568+
)
569+
sstore_state_gas = fork.sstore_state_gas()
570+
tx_gas = gas_limit_cap + create_intrinsic_state + sstore_state_gas
571+
572+
tx = Transaction(
573+
to=None,
574+
data=Op.SSTORE(0, 1) + Op.INVALID,
575+
gas_limit=tx_gas,
576+
sender=pre.fund_eoa(),
577+
)
578+
579+
block_regular = tx_gas - create_intrinsic_state - sstore_state_gas
580+
expected_gas_used = max(block_regular, create_intrinsic_state)
581+
582+
blockchain_test(
583+
pre=pre,
584+
blocks=[
585+
Block(
586+
txs=[tx],
587+
header_verify=Header(gas_used=expected_gas_used),
588+
),
589+
],
590+
post={},
591+
)
592+
593+
594+
@pytest.mark.valid_from("EIP8037")
595+
def test_subcall_failure_does_not_zero_top_level_state_gas(
596+
blockchain_test: BlockchainTestFiller,
597+
pre: Alloc,
598+
fork: Fork,
599+
) -> None:
600+
"""
601+
Verify a subcall failure does not zero the top level execution
602+
state gas.
603+
604+
The top level tx succeeds end to end even though a subcall
605+
reverts, so the top level failure refund does not apply. The
606+
parent's own SSTORE contributes state gas that appears in
607+
`block_state_gas_used`.
608+
"""
609+
gas_limit_cap = fork.transaction_gas_limit_cap()
610+
assert gas_limit_cap is not None
611+
sstore_state_gas = fork.sstore_state_gas()
612+
613+
child = pre.deploy_contract(code=Op.REVERT(0, 0))
614+
parent_storage = Storage()
615+
parent = pre.deploy_contract(
616+
code=(
617+
Op.POP(Op.CALL(gas=Op.GAS, address=child))
618+
+ Op.SSTORE(parent_storage.store_next(1, "parent_sstore"), 1)
619+
),
620+
)
621+
622+
tx = Transaction(
623+
to=parent,
624+
gas_limit=gas_limit_cap + sstore_state_gas,
625+
sender=pre.fund_eoa(),
626+
)
627+
628+
# Parent's SSTORE state gas dominates tx_regular and surfaces in
629+
# the block header, proving the top level refund is scoped to
630+
# top level failures and not child reverts.
631+
blockchain_test(
632+
pre=pre,
633+
blocks=[
634+
Block(
635+
txs=[tx],
636+
header_verify=Header(gas_used=sstore_state_gas),
637+
),
638+
],
639+
post={parent: Account(storage=parent_storage)},
640+
)
641+
642+
643+
@pytest.mark.valid_from("EIP8037")
644+
def test_top_level_failure_refunds_spilled_state_gas(
645+
state_test: StateTestFiller,
646+
pre: Alloc,
647+
fork: Fork,
648+
) -> None:
649+
"""
650+
Verify the top level failure refund covers state gas that
651+
spilled from the reservoir into gas_left.
652+
653+
When the reservoir is smaller than the state gas charge, the
654+
overflow spills and is drawn from gas_left. On top level failure
655+
the full consumed state gas (reservoir portion plus spilled
656+
portion) is credited back to the reservoir so the sender is not
657+
billed for any of it.
658+
"""
659+
gas_limit_cap = fork.transaction_gas_limit_cap()
660+
assert gas_limit_cap is not None
661+
sstore_state_gas = fork.sstore_state_gas()
662+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
663+
664+
code = Op.SSTORE(0, 1) + Op.REVERT(0, 0)
665+
contract = pre.deploy_contract(code=code)
666+
667+
# Reservoir sized to cover only half the SSTORE state gas; the
668+
# other half must spill into gas_left.
669+
tx_gas = gas_limit_cap + sstore_state_gas // 2
670+
expected_cumulative = (
671+
intrinsic_cost + code.gas_cost(fork) - sstore_state_gas
672+
)
673+
674+
tx = Transaction(
675+
to=contract,
676+
gas_limit=tx_gas,
677+
sender=pre.fund_eoa(),
678+
expected_receipt=TransactionReceipt(
679+
cumulative_gas_used=expected_cumulative,
680+
),
681+
)
682+
683+
state_test(pre=pre, post={contract: Account(storage={})}, tx=tx)
684+
685+
686+
@pytest.mark.valid_from("EIP8037")
687+
def test_top_level_failure_refunds_state_gas_propagated_from_child(
688+
state_test: StateTestFiller,
689+
pre: Alloc,
690+
fork: Fork,
691+
) -> None:
692+
"""
693+
Verify the top level failure refund catches state gas propagated
694+
from a successful subcall.
695+
696+
The parent calls a child that runs SSTORE and returns. The
697+
child's state gas usage is folded into the parent frame via the
698+
success path. When the parent then reverts at the top level, the
699+
full propagated state gas must be refunded so the sender fee
700+
excludes it.
701+
"""
702+
gas_limit_cap = fork.transaction_gas_limit_cap()
703+
assert gas_limit_cap is not None
704+
sstore_state_gas = fork.sstore_state_gas()
705+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
706+
707+
child_code = Op.SSTORE(0, 1)
708+
child = pre.deploy_contract(code=child_code)
709+
parent_code = Op.POP(Op.CALL(gas=Op.GAS, address=child)) + Op.REVERT(0, 0)
710+
parent = pre.deploy_contract(code=parent_code)
711+
712+
# Reservoir sized for the child's SSTORE. After the propagated
713+
# state gas is refunded, the sender is billed only the regular
714+
# gas: parent + CALL dispatch + child regular (SSTORE minus its
715+
# state component).
716+
tx_gas = gas_limit_cap + sstore_state_gas
717+
expected_cumulative = (
718+
intrinsic_cost
719+
+ parent_code.gas_cost(fork)
720+
+ child_code.gas_cost(fork)
721+
- sstore_state_gas
722+
)
723+
724+
tx = Transaction(
725+
to=parent,
726+
gas_limit=tx_gas,
727+
sender=pre.fund_eoa(),
728+
expected_receipt=TransactionReceipt(
729+
cumulative_gas_used=expected_cumulative,
730+
),
731+
)
732+
733+
state_test(pre=pre, post={child: Account(storage={})}, tx=tx)

0 commit comments

Comments
 (0)