Skip to content

Commit 1cd18b8

Browse files
authored
feat(test): add intermediate revert sstore test for 8037 (#7)
1 parent 7897291 commit 1cd18b8

2 files changed

Lines changed: 149 additions & 18 deletions

File tree

src/ethereum/forks/amsterdam/fork.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ def process_transaction(
10761076
if tx_output.error is not None:
10771077
tx_output.state_gas_left += tx_output.state_gas_used
10781078
tx_output.state_gas_used = Uint(0)
1079-
if tx.to == Bytes0(b""):
1079+
if isinstance(tx.to, Bytes0):
10801080
new_account_refund = (
10811081
STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE
10821082
)

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py

Lines changed: 148 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -872,11 +872,15 @@ def test_sstore_restoration_sub_frame_revert(
872872
state_test(pre=pre, tx=tx, post=post)
873873

874874

875+
@pytest.mark.with_all_call_opcodes(
876+
selector=lambda call_opcode: call_opcode != Op.STATICCALL
877+
)
875878
@pytest.mark.valid_from("EIP8037")
876879
def test_sstore_restoration_ancestor_revert(
877880
state_test: StateTestFiller,
878881
pre: Alloc,
879882
fork: Fork,
883+
call_opcode: Op,
880884
) -> None:
881885
"""
882886
Verify the SSTORE 0 to x to 0 refund returns to the caller when an
@@ -890,6 +894,7 @@ def test_sstore_restoration_ancestor_revert(
890894
replenished by `sstore_state_gas`.
891895
"""
892896
gas_costs = fork.gas_costs()
897+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
893898
# Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
894899
# so it OOGs by 1 when the reservoir is 0 and succeeds otherwise.
895900
probe_gas = (
@@ -899,24 +904,46 @@ def test_sstore_restoration_ancestor_revert(
899904
- 1
900905
)
901906

902-
inner = pre.deploy_contract(
903-
code=Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.STOP,
904-
)
905-
middle = pre.deploy_contract(
906-
code=Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.REVERT(0, 0),
907-
)
908-
probe = pre.deploy_contract(code=Op.SSTORE(0, 1))
907+
set_op = Op.SSTORE.with_metadata(
908+
key_warm=False,
909+
original_value=0,
910+
current_value=0,
911+
new_value=1,
912+
)(0, 1)
913+
clear_op = Op.SSTORE.with_metadata(
914+
key_warm=True,
915+
original_value=0,
916+
current_value=1,
917+
new_value=0,
918+
)(0, 0)
919+
inner_code = set_op + clear_op + Op.STOP
920+
inner = pre.deploy_contract(code=inner_code)
921+
922+
middle_code = Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.REVERT(0, 0)
923+
middle = pre.deploy_contract(code=middle_code)
924+
925+
probe_code = Op.SSTORE(0, 1)
926+
probe = pre.deploy_contract(code=probe_code)
909927

910928
caller_storage = Storage()
911-
caller = pre.deploy_contract(
912-
code=(
913-
Op.POP(Op.CALL(gas=Op.GAS, address=middle))
914-
+ Op.SSTORE(
915-
caller_storage.store_next(1, "probe_must_succeed"),
916-
Op.CALL(gas=probe_gas, address=probe),
917-
)
918-
),
929+
caller_code = Op.POP(call_opcode(gas=Op.GAS, address=middle)) + Op.SSTORE(
930+
caller_storage.store_next(1, "probe_must_succeed"),
931+
Op.CALL(gas=probe_gas, address=probe),
919932
)
933+
caller = pre.deploy_contract(code=caller_code)
934+
935+
# Block state gas commits: probe's SSTORE-set and caller's outer
936+
# SSTORE-set; inner's set+clear cancel before middle reverts and
937+
# don't propagate. Header gas_used is max(regular, state).
938+
expected_regular = (
939+
intrinsic_cost
940+
+ caller_code.regular_cost(fork)
941+
+ middle_code.regular_cost(fork)
942+
+ inner_code.regular_cost(fork)
943+
+ probe_code.regular_cost(fork)
944+
)
945+
expected_state = 2 * fork.sstore_state_gas()
946+
expected_gas_used = max(expected_regular, expected_state)
920947

921948
# gas_limit at the cap means the caller's reservoir starts at 0.
922949
tx = Transaction(
@@ -925,8 +952,112 @@ def test_sstore_restoration_ancestor_revert(
925952
gas_limit=fork.transaction_gas_limit_cap(),
926953
)
927954

928-
post = {caller: Account(storage=caller_storage)}
929-
state_test(pre=pre, tx=tx, post=post)
955+
state_test(
956+
pre=pre,
957+
tx=tx,
958+
post={caller: Account(storage=caller_storage)},
959+
blockchain_test_header_verify=Header(gas_used=expected_gas_used),
960+
)
961+
962+
963+
@pytest.mark.with_all_call_opcodes(
964+
selector=lambda call_opcode: call_opcode in (Op.DELEGATECALL, Op.CALLCODE)
965+
)
966+
@pytest.mark.valid_from("EIP8037")
967+
def test_sstore_restoration_charge_in_ancestor_intermediate_revert(
968+
state_test: StateTestFiller,
969+
pre: Alloc,
970+
fork: Fork,
971+
call_opcode: Op,
972+
) -> None:
973+
"""
974+
Verify a deferred refund applied in an intermediate frame still
975+
flows back to the caller when that frame REVERTs.
976+
977+
Caller's SSTORE charges; the matching clear in inner is deferred
978+
through the chain and lands on middle's own SSTORE-set during
979+
`incorporate_child_on_success`. Middle REVERTs; the applied
980+
amount must reach the caller via `incorporate_child_on_error`.
981+
A probe SSTORE sized to OOG by 1 detects loss.
982+
"""
983+
gas_costs = fork.gas_costs()
984+
sstore_state_gas = fork.sstore_state_gas()
985+
gas_limit_cap = fork.transaction_gas_limit_cap()
986+
assert gas_limit_cap is not None
987+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
988+
# Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
989+
# so it OOGs by 1 when the reservoir is 0 and succeeds otherwise.
990+
probe_gas = (
991+
2 * gas_costs.VERY_LOW
992+
+ gas_costs.COLD_STORAGE_WRITE
993+
+ sstore_state_gas
994+
- 1
995+
)
996+
997+
inner_code = (
998+
Op.SSTORE.with_metadata(
999+
key_warm=True,
1000+
original_value=0,
1001+
current_value=1,
1002+
new_value=0,
1003+
)(0, 0)
1004+
+ Op.STOP
1005+
)
1006+
inner = pre.deploy_contract(code=inner_code)
1007+
1008+
# Middle's own SSTORE on slot 1 supplies the `state_gas_used`
1009+
# that inner's deferred credit lands on, then middle REVERTs.
1010+
middle_code = (
1011+
Op.SSTORE(1, 1)
1012+
+ Op.POP(call_opcode(gas=Op.GAS, address=inner))
1013+
+ Op.REVERT(0, 0)
1014+
)
1015+
middle = pre.deploy_contract(code=middle_code)
1016+
1017+
probe_code = Op.SSTORE(0, 1)
1018+
probe = pre.deploy_contract(code=probe_code)
1019+
1020+
caller_storage = Storage()
1021+
caller_code = (
1022+
Op.SSTORE(caller_storage.store_next(1, "caller_set_persists"), 1)
1023+
+ Op.POP(call_opcode(gas=Op.GAS, address=middle))
1024+
+ Op.SSTORE(
1025+
caller_storage.store_next(1, "probe_must_succeed"),
1026+
Op.CALL(gas=probe_gas, address=probe),
1027+
)
1028+
)
1029+
caller = pre.deploy_contract(code=caller_code)
1030+
1031+
# Block state gas commits: caller's slot-0 set + probe's
1032+
# SSTORE-set + caller's outer SSTORE-set on slot 1. Middle's
1033+
# own slot-1 set is washed by inner's deferred credit before
1034+
# middle reverts, so it does not propagate. Header gas_used
1035+
# is max(regular, state).
1036+
expected_regular = (
1037+
intrinsic_cost
1038+
+ caller_code.regular_cost(fork)
1039+
+ middle_code.regular_cost(fork)
1040+
+ inner_code.regular_cost(fork)
1041+
+ probe_code.regular_cost(fork)
1042+
)
1043+
expected_state = 3 * sstore_state_gas
1044+
expected_gas_used = max(expected_regular, expected_state)
1045+
1046+
# Reservoir = 2 * sstore_state_gas covers caller's and middle's
1047+
# sets; the deferred credit refills middle by sstore_state_gas,
1048+
# which flows to the caller on revert.
1049+
tx = Transaction(
1050+
sender=pre.fund_eoa(),
1051+
to=caller,
1052+
gas_limit=gas_limit_cap + 2 * sstore_state_gas,
1053+
)
1054+
1055+
state_test(
1056+
pre=pre,
1057+
tx=tx,
1058+
post={caller: Account(storage=caller_storage)},
1059+
blockchain_test_header_verify=Header(gas_used=expected_gas_used),
1060+
)
9301061

9311062

9321063
@pytest.mark.with_all_create_opcodes

0 commit comments

Comments
 (0)