Skip to content

Commit 23602a0

Browse files
committed
fix(tests): resolve open spec questions Q1/Q2, update failing tests
Q1 (value transfer): callee pays. Updated test to use alive target (amount=1) and forward all gas (Op.GAS). Q2 (code deposit): all-or-nothing. Updated docstrings. Replaced test_nested_create_code_deposit_cannot_borrow_parent_gas with test_create_reverts_when_diff_exceeds_reservoir. Q3 (reservoir growth): resolved by saturating subtraction in fork.py. 2 tests still have TODOs for gas_limit tuning in the new model.
1 parent 984e12d commit 23602a0

2 files changed

Lines changed: 67 additions & 94 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_call.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -433,43 +433,39 @@ def test_call_value_transfer_new_account(
433433
state_test(env=env, pre=pre, post=post, tx=tx)
434434

435435

436-
# TODO(diff-at-return): With diff-at-return, value transfer to empty
437-
# account (EIP-161) creates state in the CHILD frame. The child needs
438-
# enough gas to cover GAS_NEW_ACCOUNT at return. Currently the child
439-
# only gets 100k gas which is less than GAS_NEW_ACCOUNT (131k).
440-
# Spec question: should value transfer create state in the caller's
441-
# diff or the callee's diff?
442436
@pytest.mark.valid_from("EIP8037")
443437
def test_call_value_transfer_existing_account_no_state_gas(
444438
state_test: StateTestFiller,
445439
pre: Alloc,
446440
fork: Fork,
447441
) -> None:
448442
"""
449-
Test CALL with value to existing account charges no state gas.
443+
Test CALL with value to existing alive account charges no state gas.
450444
451445
A CALL that transfers value to an already-alive account does not
452-
create new state, so no state gas is charged.
446+
create new state, so no state gas is charged at the callee's
447+
call return.
453448
"""
454449
gas_limit_cap = fork.transaction_gas_limit_cap()
455450
assert gas_limit_cap is not None
456-
# Existing target account
457-
target = pre.fund_eoa(amount=0)
451+
sstore_state_gas = fork.sstore_state_gas()
452+
# Target must be alive (non-empty) so no GAS_NEW_ACCOUNT is charged
453+
target = pre.fund_eoa(amount=1)
458454

459455
parent_storage = Storage()
460456
parent = pre.deploy_contract(
461457
code=(
462458
Op.SSTORE(
463459
parent_storage.store_next(1),
464-
Op.CALL(gas=100_000, address=target, value=1),
460+
Op.CALL(gas=Op.GAS, address=target, value=1),
465461
)
466462
),
467463
balance=1,
468464
)
469465

470466
tx = Transaction(
471467
to=parent,
472-
gas_limit=gas_limit_cap,
468+
gas_limit=gas_limit_cap + sstore_state_gas,
473469
sender=pre.fund_eoa(),
474470
)
475471

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py

Lines changed: 59 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -409,104 +409,92 @@ def test_create_tx_intrinsic_gas_boundary(
409409
state_test(pre=pre, post={}, tx=tx)
410410

411411

412-
# TODO(diff-at-return): Code deposit is now part of the state diff
413-
# at CREATE's call return, not a separate charge. This test's
414-
# concept of "code deposit OOG" changes — the CREATE call either
415-
# has enough reservoir for the full diff (account + code) or it
416-
# reverts entirely. Need to rethink this test.
417412
@pytest.mark.valid_from("EIP8037")
418-
def test_code_deposit_oog_preserves_parent_reservoir(
413+
def test_caller_reservoir_preserved_after_callee_diff_reverts(
419414
state_test: StateTestFiller,
420415
pre: Alloc,
421416
fork: Fork,
422417
) -> None:
423418
"""
424-
Test parent reservoir preserved after child code deposit OOG.
425-
426-
A caller contract invokes the factory via CALL with limited gas.
427-
The child CREATE returns enough bytes that code deposit state gas
428-
exceeds the child frame's available gas (reservoir spillover plus
429-
the limited gas_left). The factory's SSTORE after the failed
430-
CREATE proves the reservoir was not inflated by a spill-then-halt
431-
refund.
419+
Test caller reservoir preserved when callee reverts due to
420+
state diff exceeding its budget.
421+
422+
The caller CALLs a factory with limited gas. The factory does
423+
CREATE deploying large code. At the factory's call return, the
424+
state diff (account + code) exceeds the factory's budget, so the
425+
factory reverts. The caller's reservoir is preserved and can be
426+
used for a subsequent SSTORE.
432427
"""
433428
gas_limit_cap = fork.transaction_gas_limit_cap()
434429
assert gas_limit_cap is not None
435-
gas_costs = fork.gas_costs()
436-
new_account_state_gas = gas_costs.GAS_NEW_ACCOUNT
437430
sstore_state_gas = fork.sstore_state_gas()
438431

439-
# Small deploy size; code deposit state gas will exceed the
440-
# limited gas available in the CREATE child frame.
441432
deploy_size = 4096
442433
init_code = Op.RETURN(0, deploy_size)
443434

444-
# Limited regular gas forwarded to the factory. After CREATE
445-
# takes 63/64, the factory retains ~15 K for its SSTOREs.
446-
child_gas = 1_000_000
447-
448-
factory_storage = Storage()
449435
factory = pre.deploy_contract(
450436
code=(
451437
Op.MSTORE(0, Op.PUSH32(bytes(init_code)))
452-
+ Op.SSTORE(
453-
factory_storage.store_next(0, "create_fails"),
438+
+ Op.POP(
454439
Op.CREATE(
455440
value=0,
456441
offset=32 - len(init_code),
457442
size=len(init_code),
458443
),
459444
)
460-
# Reservoir must be fully preserved after failed CREATE;
461-
# parent can still perform its own SSTORE.
462-
+ Op.SSTORE(
463-
factory_storage.store_next(1, "parent_sstore"),
464-
1,
465-
)
466445
),
467446
)
468447

469-
# Caller invokes factory with limited gas via CALL.
448+
# Caller: CALL factory with limited gas (factory will revert
449+
# because its diff is too expensive). Then SSTORE to prove
450+
# the caller's reservoir is preserved.
451+
caller_storage = Storage()
470452
caller = pre.deploy_contract(
471-
code=Op.CALL(gas=child_gas, address=factory),
453+
code=(
454+
Op.POP(Op.CALL(gas=1_000_000, address=factory))
455+
+ Op.SSTORE(caller_storage.store_next(1, "sstore_ok"), 1)
456+
),
472457
)
473458

474-
# Reservoir = new-account state gas + one SSTORE's state gas.
475-
# Code deposit draws from the reservoir first then spills into
476-
# gas_left, which the limited CALL gas cannot cover.
477459
tx = Transaction(
478460
to=caller,
479-
gas_limit=(gas_limit_cap + new_account_state_gas + sstore_state_gas),
461+
gas_limit=gas_limit_cap + sstore_state_gas,
480462
sender=pre.fund_eoa(),
481463
)
482464

483-
post = {factory: Account(storage=factory_storage)}
465+
post = {caller: Account(storage=caller_storage)}
484466
state_test(pre=pre, post=post, tx=tx)
485467

486468

487-
# TODO(diff-at-return): Code deposit is now part of the state diff.
488-
# The concept of "borrowing parent gas for code deposit" no longer
489-
# applies — the CREATE call's diff includes code bytes and either
490-
# the reservoir covers it or the call reverts.
491469
@pytest.mark.valid_from("EIP8037")
492-
def test_nested_create_code_deposit_cannot_borrow_parent_gas(
470+
def test_parent_reverts_when_create_diff_exceeds_budget(
493471
state_test: StateTestFiller,
494472
pre: Alloc,
495473
fork: Fork,
496474
) -> None:
497475
"""
498-
Test nested CREATE code deposit does not borrow parent gas.
476+
Test parent call reverts when CREATE's state diff exceeds budget.
499477
500-
Provide just enough gas for CREATE to start (new account state
501-
gas + regular gas) but not enough for the child frame to cover
502-
code deposit after init code runs. The CREATE increments the
503-
factory nonce but code deposit fails, so no contract is deployed.
478+
With diff-at-return, account creation and code deployment from a
479+
CREATE are part of the parent's state diff (they happen outside
480+
the CREATE child's process_message). The parent pays at its own
481+
call return. If the parent's total budget (reservoir + gas_left)
482+
cannot cover the diff, the parent's call reverts.
483+
484+
Uses a large deploy size so the code deposit state gas exceeds
485+
the available budget (reservoir + gas_left spillover).
504486
"""
505-
init_code = Op.RETURN(0, 1)
506-
gas_costs = fork.gas_costs()
507-
new_acct_state = gas_costs.GAS_NEW_ACCOUNT
508-
code_deposit_state = fork.code_deposit_state_gas(code_size=1)
487+
gas_limit_cap = fork.transaction_gas_limit_cap()
488+
assert gas_limit_cap is not None
509489

490+
# 15000 bytes of code: state cost = (112 + 15000) * cpsb ≈ 17.7M
491+
# TX_MAX_GAS_LIMIT = 16.7M, so total budget can't cover it
492+
# when reservoir = 0 (gas_limit at cap).
493+
deploy_size = 15000
494+
init_code = Op.RETURN(0, deploy_size)
495+
496+
# Wrapper calls factory. Factory does CREATE. If the wrapper's
497+
# diff at return exceeds its budget, the wrapper reverts.
510498
factory = pre.deploy_contract(
511499
code=(
512500
Op.MSTORE(0, Op.PUSH32(bytes(init_code)))
@@ -519,42 +507,31 @@ def test_nested_create_code_deposit_cannot_borrow_parent_gas(
519507
)
520508
),
521509
)
522-
created = compute_create_address(address=factory, nonce=1)
523-
524-
# Gas consumed before the child CREATE frame receives gas:
525-
# Intrinsic + factory code (PUSH32+PUSH1+MSTORE+mem +
526-
# 3xPUSH1) + CREATE regular (+ init_code_cost) + new account
527-
# state gas (spilled from gas_left, no reservoir).
528-
init_code_word_cost = gas_costs.GAS_CODE_INIT_PER_WORD * (
529-
(len(init_code) + 31) // 32
530-
)
531-
pre_child_gas = (
532-
gas_costs.GAS_TX_BASE
533-
+ 7 * gas_costs.GAS_VERY_LOW
534-
+ gas_costs.GAS_MEMORY
535-
+ (gas_costs.GAS_CREATE - new_acct_state)
536-
+ init_code_word_cost
537-
+ new_acct_state
538-
)
539510

540-
# Init code cost: PUSH1 + PUSH1 + RETURN(+mem expansion)
541-
init_cost = 2 * gas_costs.GAS_VERY_LOW + gas_costs.GAS_MEMORY
542-
# Target child gas: enough for init, not enough for code deposit
543-
target_child = (init_cost + code_deposit_state) // 2
544-
# Invert EIP-150 63/64ths rule: ceil(target_child * 64 / 63)
545-
factory_remaining = (target_child * 64 + 62) // 63
546-
gas_limit = pre_child_gas + factory_remaining
511+
storage = Storage()
512+
wrapper = pre.deploy_contract(
513+
code=(
514+
# CALL returns 1 on success, 0 on failure.
515+
# Store the result to verify the factory reverted.
516+
Op.SSTORE(
517+
storage.store_next(0, "call_failed"),
518+
Op.CALL(gas=Op.GAS, address=factory),
519+
)
520+
),
521+
)
547522

523+
# gas_limit at cap = reservoir 0. Total budget = gas_left only.
524+
# State diff cost (17.7M) > TX_MAX_GAS_LIMIT (16.7M).
548525
tx = Transaction(
549-
to=factory,
550-
gas_limit=gas_limit,
526+
to=wrapper,
527+
gas_limit=gas_limit_cap,
551528
sender=pre.fund_eoa(),
552529
)
553530

554-
post = {
555-
factory: Account(nonce=2),
556-
created: Account.NONEXISTENT,
557-
}
531+
# Wrapper's CALL to factory should fail (diff too expensive).
532+
# The SSTORE stores 0 (CALL returned 0) not 1.
533+
# But wrapper itself continues — only the inner CALL reverted.
534+
post = {wrapper: Account(storage=storage)}
558535
state_test(pre=pre, post=post, tx=tx)
559536

560537

0 commit comments

Comments
 (0)