713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908 | @pytest.mark.parametrize(
("with_reservoir", "failure_op"),
[
pytest.param(True, Op.REVERT(0, 0), id="with_reservoir-revert"),
pytest.param(True, Op.INVALID, id="with_reservoir-halt"),
pytest.param(False, Op.REVERT(0, 0), id="no_reservoir-revert"),
pytest.param(False, Op.INVALID, id="no_reservoir-halt"),
],
)
@pytest.mark.valid_from("EIP8037")
def test_parent_state_gas_after_child_failure(
state_test: StateTestFiller,
pre: Alloc,
fork: Fork,
with_reservoir: bool,
failure_op: Bytecode,
) -> None:
"""
Test parent state-gas pools after CREATE child failure.
A factory runs CREATE whose initcode does an SSTORE, then either
REVERTs or hits INVALID. The factory's own SSTORE after the failed
CREATE checks the parent's reservoir and gas_left are correct.
Under EIP-8037 state-gas refunds are LIFO. Gas spilled from
gas_left refunds to gas_left, only the reservoir-funded portion
returns to the reservoir.
Four scenarios cover the gas-pool state space:
- `with_reservoir x revert`: child state gas refills LIFO. The
reservoir-funded portion returns to the parent reservoir, any
spill to the parent gas_left.
- `with_reservoir x halt`: halt refills the child frame LIFO then
burns its gas_left. Only the child's start reservoir survives.
- `no_reservoir x revert`: child state gas spilled wholly from
gas_left, so the LIFO refill returns it there. No phantom
reservoir forms.
- `no_reservoir x halt`: no phantom reservoir forms. The spilled
child state gas is burned with the child gas_left and the
factory's post-CREATE SSTORE spills from gas_left.
"""
gas_limit_cap = fork.transaction_gas_limit_cap()
assert gas_limit_cap is not None
gas_costs = fork.gas_costs()
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork)
new_account_state_gas = gas_costs.NEW_ACCOUNT
initcode = Op.SSTORE(0, 1, original_value=0, new_value=1) + failure_op
factory_storage = Storage()
factory_code = (
Op.MSTORE(0, Op.PUSH32(bytes(initcode)))
+ Op.SSTORE(
factory_storage.store_next(0, "create_fails"),
Op.CREATE(
value=0,
offset=32 - len(initcode),
size=len(initcode),
),
original_value=0,
new_value=0,
)
+ Op.SSTORE(
factory_storage.store_next(1, "post_create"),
1,
original_value=0,
new_value=1,
)
)
factory = pre.deploy_contract(code=factory_code)
gas_limit = (
gas_limit_cap + new_account_state_gas + sstore_state_gas * 2
if with_reservoir
else 5_000_000
)
# `bytecode.gas_cost(fork)` accounts for opcode base costs and
# state-gas charges, but does NOT track memory-expansion or CREATE
# init-code word costs. Add those back to recover runtime regular
# gas consumption.
init_code_word_count = (len(initcode) + 31) // 32
init_code_word_cost = gas_costs.CODE_INIT_PER_WORD * init_code_word_count
mstore_memory_expansion = gas_costs.MEMORY_PER_WORD # 1 word
gas_cost_helper_extras = init_code_word_cost + mstore_memory_expansion
# Factory bytecode shape costs, derived from fork.gas_costs():
# pre-CREATE: PUSH32 + PUSH1 + MSTORE (with 1-word expansion)
# + 3 PUSHes for CREATE inputs
# post-CREATE: PUSH key + SSTORE (cold no-op: access cost only)
# + 2 PUSHes + SSTORE (cold zero-to-nonzero:
# access + write, the compound COLD_STORAGE_WRITE)
factory_pre_create_regular = (
gas_costs.VERY_LOW * 2
+ gas_costs.OPCODE_MSTORE_BASE
+ mstore_memory_expansion
+ gas_costs.VERY_LOW * 3
)
factory_post_create_regular = (
gas_costs.VERY_LOW
+ gas_costs.COLD_STORAGE_ACCESS
+ gas_costs.VERY_LOW * 2
+ gas_costs.COLD_STORAGE_WRITE
)
factory_regular = (
factory_code.gas_cost(fork)
- new_account_state_gas
- sstore_state_gas
+ gas_cost_helper_extras
)
initcode_regular_revert = initcode.gas_cost(fork) - sstore_state_gas
if failure_op == Op.INVALID:
# Simulate runtime gas for HALT under EIP-8037 LIFO refills:
# 1. Regular pool capped by transaction_gas_limit_cap. The
# remainder forms the state reservoir.
# 2. CREATE charges new_account state gas, reservoir first
# then spilled to gas_left and tracked.
# 3. 63/64 retention: parent keeps gas_left // 64. The
# reservoir is forwarded to the child frame.
# 4. Child initcode SSTORE charges sstore_state_gas, child
# reservoir first then spilled to child gas_left.
# 5. INVALID refills the child frame LIFO then burns its
# gas_left. Only the child's start reservoir survives.
# 6. CREATE failure refills new_account LIFO: the spill to
# parent gas_left, the rest to the parent reservoir.
# 7. Factory post-CREATE SSTORE charges sstore_state_gas,
# reservoir first then spilled to gas_left.
execution_gas = gas_limit - intrinsic_cost
regular_budget = gas_limit_cap - intrinsic_cost
sim_gas_left = min(regular_budget, execution_gas)
sim_state_gas_left = execution_gas - sim_gas_left
sim_gas_left -= factory_pre_create_regular
sim_gas_left -= gas_costs.OPCODE_CREATE_BASE + init_code_word_cost
# CREATE new_account state gas: reservoir first, spill tracked.
new_account_from_reservoir = min(
sim_state_gas_left, new_account_state_gas
)
new_account_spill = new_account_state_gas - new_account_from_reservoir
sim_state_gas_left -= new_account_from_reservoir
sim_gas_left -= new_account_spill
# 63/64 retention: parent keeps gas_left // 64. The reservoir
# is forwarded to the child frame and survives on halt.
child_reservoir = sim_state_gas_left
sim_gas_left = sim_gas_left // 64
# INVALID burns child gas_left, including any spilled SSTORE
# state gas. Only the forwarded reservoir survives.
sim_state_gas_left = child_reservoir
# CREATE failure refills new_account LIFO: spilled portion to
# gas_left, reservoir-funded portion to the reservoir.
sim_gas_left += new_account_spill
sim_state_gas_left += new_account_from_reservoir
sim_gas_left -= factory_post_create_regular
# Factory post-CREATE SSTORE: reservoir first, spill otherwise.
if sim_state_gas_left >= sstore_state_gas:
sim_state_gas_left -= sstore_state_gas
else:
sim_gas_left -= sstore_state_gas - sim_state_gas_left
sim_state_gas_left = 0
expected_cumulative = gas_limit - sim_gas_left - sim_state_gas_left
else:
# REVERT preserves gas_left and refunds the child frame's
# state gas (initcode SSTORE + new account). Only the
# factory's own post-CREATE SSTORE consumes net state gas.
expected_cumulative = (
intrinsic_cost
+ factory_regular
+ initcode_regular_revert
+ sstore_state_gas
)
tx = Transaction(
to=factory,
gas_limit=gas_limit,
sender=pre.fund_eoa(),
expected_receipt=TransactionReceipt(
cumulative_gas_used=expected_cumulative,
),
)
state_test(
pre=pre,
post={factory: Account(storage=factory_storage)},
tx=tx,
)
|