Skip to content

Commit d9b46fa

Browse files
committed
Add test_check_order_triple
Co-Authored-By: Claude claude-opus-4-6
1 parent be14e49 commit d9b46fa

File tree

1 file changed

+176
-7
lines changed

1 file changed

+176
-7
lines changed

tests/monad_eight/staking_precompile/test_precompile_call.py

Lines changed: 176 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
REFERENCE_SPEC_GIT_PATH = ref_spec_staking.git_path
5151
REFERENCE_SPEC_VERSION = ref_spec_staking.version
5252

53+
GAS_STIPEND = 2300
54+
5355
slot_code_worked = 0x1
5456
value_code_worked = 0x1234
5557
slot_call_success = 0x2
@@ -238,6 +240,32 @@ def resolve_outcome(
238240
raise ValueError(f"Unknown scenario: {scenario}")
239241

240242

243+
def _stipend_neutralizes_low_gas(
244+
func: FunctionInfo,
245+
scenario_set: set[CallScenario],
246+
) -> bool:
247+
"""
248+
Check if the EVM value-transfer stipend overcomes how little
249+
gas is provided in LOW_GAS scenarios.
250+
"""
251+
if not (
252+
{CallScenario.LOW_GAS, CallScenario.NONZERO_VALUE} <= scenario_set
253+
):
254+
return False
255+
256+
# Given how LOW_GAS gas is calculated, unknown selector
257+
# scenarios can never have stipend compensate for gas
258+
# shortage.
259+
assert GAS_UNKNOWN_SELECTOR > GAS_STIPEND
260+
known_selector = scenario_set.isdisjoint(
261+
{
262+
CallScenario.TRUNCATED_SELECTOR,
263+
CallScenario.WRONG_SELECTOR,
264+
}
265+
)
266+
return known_selector and func.gas_cost <= GAS_STIPEND
267+
268+
241269
def resolve_outcome_pair(
242270
func: FunctionInfo,
243271
scenario1: CallScenario,
@@ -257,12 +285,10 @@ def resolve_outcome_pair(
257285
if scenario1.check_priority > scenario2.check_priority:
258286
scenario1, scenario2 = scenario2, scenario1
259287

260-
# EVM adds 2300 stipend for value>0; if the stipend covers the
261-
# function's gas cost, the call succeeds despite LOW_GAS.
262-
if (
263-
scenario1 == CallScenario.LOW_GAS
264-
and scenario2 == CallScenario.NONZERO_VALUE
265-
and func.gas_cost <= 2300
288+
if _stipend_neutralizes_low_gas(
289+
# True iff `scenario1 == LOW_GAS and scenario2 == NONZERO_VALUE`
290+
func,
291+
{scenario1, scenario2},
266292
):
267293
return resolve_outcome(func, scenario2)
268294

@@ -285,6 +311,28 @@ def resolve_outcome_pair(
285311
return resolve_outcome(func, scenario1)
286312

287313

314+
def resolve_outcome_triple(
315+
func: FunctionInfo,
316+
s1: CallScenario,
317+
s2: CallScenario,
318+
s3: CallScenario,
319+
) -> ExpectedOutcome:
320+
"""Resolve expected outcome for three combined scenarios."""
321+
scenarios = sorted(
322+
[_normalize(s, func) for s in (s1, s2, s3)],
323+
key=lambda s: s.check_priority,
324+
)
325+
if _stipend_neutralizes_low_gas(func, set(scenarios)):
326+
scenarios.remove(CallScenario.LOW_GAS)
327+
328+
if scenarios[0] == CallScenario.NONZERO_VALUE and func.is_payable:
329+
scenarios = scenarios[1:]
330+
331+
if len(scenarios) == 1:
332+
return resolve_outcome(func, scenarios[0])
333+
return resolve_outcome_pair(func, scenarios[0], scenarios[1])
334+
335+
288336
def scenario_call_code(
289337
*scenarios: CallScenario,
290338
func: FunctionInfo,
@@ -337,7 +385,12 @@ def scenario_call_code(
337385

338386
if gas is None:
339387
if CallScenario.LOW_GAS in scenario_set:
340-
gas = min(func.gas_cost, GAS_UNKNOWN_SELECTOR) - 1
388+
gas = max(
389+
0,
390+
# Subtracting also the stipend in case the
391+
# value sent would cause stipend to be added.
392+
min(func.gas_cost, GAS_UNKNOWN_SELECTOR) - GAS_STIPEND - 1,
393+
)
341394
else:
342395
gas = max(func.gas_cost, GAS_UNKNOWN_SELECTOR)
343396

@@ -976,6 +1029,122 @@ def test_check_order(
9761029
)
9771030

9781031

1032+
def _pairwise_compatible(
1033+
scenarios: tuple[CallScenario, ...],
1034+
) -> bool:
1035+
"""Check that no pair in the tuple is incompatible."""
1036+
return all(
1037+
frozenset({a, b}) not in _INCOMPATIBLE_SCENARIOS
1038+
for i, a in enumerate(scenarios)
1039+
for b in scenarios[i + 1 :]
1040+
)
1041+
1042+
1043+
_CHECK_ORDER_TRIPLES = [
1044+
pytest.param(
1045+
s1,
1046+
s2,
1047+
s3,
1048+
id=f"{s1.name.lower()}__{s2.name.lower()}__{s3.name.lower()}",
1049+
)
1050+
for s1 in CallScenario
1051+
for s2 in CallScenario
1052+
for s3 in CallScenario
1053+
if CallScenario.SUCCESS not in {s1, s2, s3}
1054+
and s1.check_priority < s2.check_priority < s3.check_priority
1055+
and _pairwise_compatible((s1, s2, s3))
1056+
]
1057+
1058+
1059+
@pytest.mark.parametrize(
1060+
"func",
1061+
[pytest.param(f, id=f.name) for f in REPRESENTATIVE_FUNCTIONS],
1062+
)
1063+
@pytest.mark.parametrize("scenario1,scenario2,scenario3", _CHECK_ORDER_TRIPLES)
1064+
def test_check_order_triple(
1065+
blockchain_test: BlockchainTestFiller,
1066+
pre: Alloc,
1067+
fork: Fork,
1068+
func: FunctionInfo,
1069+
scenario1: CallScenario,
1070+
scenario2: CallScenario,
1071+
scenario3: CallScenario,
1072+
) -> None:
1073+
"""
1074+
Test precompile check priority with three combined failures.
1075+
1076+
Each combination triggers exactly three failure causes. The test
1077+
derives the expected outcome from the priority interactions.
1078+
"""
1079+
normalized = {
1080+
_normalize(s, func) for s in (scenario1, scenario2, scenario3)
1081+
}
1082+
if not _pairwise_compatible(tuple(normalized)):
1083+
pytest.skip("normalized scenarios are incompatible")
1084+
1085+
mem_end = _calldata_mem_end(func.calldata_size)
1086+
ret_offset = max(mem_end, 96)
1087+
rdc_offset = ret_offset + 32
1088+
1089+
scenarios = {scenario1, scenario2, scenario3}
1090+
1091+
delegating_eoa: Address | None = None
1092+
authorization_list = None
1093+
if CallScenario.DELEGATE_TO_PRECOMPILE in scenarios:
1094+
delegating_eoa = pre.fund_eoa()
1095+
authorization_list = [
1096+
AuthorizationTuple(
1097+
address=STAKING_PRECOMPILE,
1098+
nonce=0,
1099+
signer=delegating_eoa,
1100+
)
1101+
]
1102+
1103+
contract = (
1104+
Op.SSTORE(
1105+
slot_call_success,
1106+
scenario_call_code(
1107+
scenario1,
1108+
scenario2,
1109+
scenario3,
1110+
func=func,
1111+
ret_offset=ret_offset,
1112+
ret_size=32,
1113+
delegating_eoa=delegating_eoa,
1114+
),
1115+
)
1116+
+ Op.SSTORE(slot_return_size, Op.RETURNDATASIZE)
1117+
+ Op.RETURNDATACOPY(rdc_offset, 0, Op.RETURNDATASIZE)
1118+
+ Op.SSTORE(slot_return_value, Op.MLOAD(rdc_offset))
1119+
+ Op.SSTORE(slot_code_worked, value_code_worked)
1120+
)
1121+
contract_address = pre.deploy_contract(contract, balance=1)
1122+
1123+
tx = Transaction(
1124+
gas_limit=generous_gas(fork),
1125+
to=contract_address,
1126+
sender=pre.fund_eoa(),
1127+
authorization_list=authorization_list,
1128+
)
1129+
1130+
outcome = resolve_outcome_triple(func, scenario1, scenario2, scenario3)
1131+
1132+
blockchain_test(
1133+
pre=pre,
1134+
post={
1135+
contract_address: Account(
1136+
storage={
1137+
slot_call_success: outcome.call_success,
1138+
slot_return_size: outcome.return_size,
1139+
slot_return_value: outcome.return_word,
1140+
slot_code_worked: value_code_worked,
1141+
}
1142+
),
1143+
},
1144+
blocks=[Block(txs=[tx])],
1145+
)
1146+
1147+
9791148
# --- Direct-transaction tests ---
9801149

9811150

0 commit comments

Comments
 (0)