|
27 | 27 | Storage, |
28 | 28 | Transaction, |
29 | 29 | TransactionException, |
| 30 | + TransactionReceipt, |
30 | 31 | compute_create2_address, |
31 | 32 | compute_create_address, |
32 | 33 | ) |
@@ -596,6 +597,196 @@ def test_code_deposit_oog_preserves_parent_reservoir( |
596 | 597 | state_test(pre=pre, post=post, tx=tx) |
597 | 598 |
|
598 | 599 |
|
| 600 | +@pytest.mark.parametrize( |
| 601 | + ("with_reservoir", "failure_op"), |
| 602 | + [ |
| 603 | + pytest.param(True, Op.REVERT(0, 0), id="with_reservoir-revert"), |
| 604 | + pytest.param(True, Op.INVALID, id="with_reservoir-halt"), |
| 605 | + pytest.param(False, Op.REVERT(0, 0), id="no_reservoir-revert"), |
| 606 | + pytest.param(False, Op.INVALID, id="no_reservoir-halt"), |
| 607 | + ], |
| 608 | +) |
| 609 | +@pytest.mark.valid_from("EIP8037") |
| 610 | +def test_parent_state_gas_after_child_failure( |
| 611 | + state_test: StateTestFiller, |
| 612 | + pre: Alloc, |
| 613 | + fork: Fork, |
| 614 | + with_reservoir: bool, |
| 615 | + failure_op: Bytecode, |
| 616 | +) -> None: |
| 617 | + """ |
| 618 | + Test parent state-gas pools after CREATE child failure. |
| 619 | +
|
| 620 | + A factory invokes CREATE whose initcode performs an SSTORE |
| 621 | + (charging state gas) then either REVERTs or hits INVALID. The |
| 622 | + factory's own SSTORE after the failed CREATE acts as the |
| 623 | + discriminator that the parent's state-gas accounting (reservoir |
| 624 | + and gas_left) is in the expected state. |
| 625 | +
|
| 626 | + Four scenarios cover the gas-pool state space: |
| 627 | +
|
| 628 | + - `with_reservoir x revert`: child state gas (new account + |
| 629 | + initcode SSTORE) is fully refunded to the parent reservoir on |
| 630 | + REVERT. |
| 631 | + - `with_reservoir x halt`: HALT resets the child frame to |
| 632 | + `(0, R0_child)`; only the reservoir-portion entering the |
| 633 | + initcode is returned, any spilled gas stays burned. |
| 634 | + - `no_reservoir x revert`: child state gas refunded forms a |
| 635 | + fresh reservoir even though `R0_parent` started at 0. |
| 636 | + - `no_reservoir x halt`: no phantom reservoir may form; the |
| 637 | + factory's post-CREATE SSTORE must spill from gas_left. |
| 638 | + """ |
| 639 | + gas_limit_cap = fork.transaction_gas_limit_cap() |
| 640 | + assert gas_limit_cap is not None |
| 641 | + gas_costs = fork.gas_costs() |
| 642 | + intrinsic_cost = fork.transaction_intrinsic_cost_calculator()() |
| 643 | + sstore_state_gas = fork.sstore_state_gas() |
| 644 | + new_account_state_gas = gas_costs.NEW_ACCOUNT |
| 645 | + |
| 646 | + initcode = Op.SSTORE(0, 1, original_value=0, new_value=1) + failure_op |
| 647 | + |
| 648 | + factory_storage = Storage() |
| 649 | + factory_code = ( |
| 650 | + Op.MSTORE(0, Op.PUSH32(bytes(initcode))) |
| 651 | + + Op.SSTORE( |
| 652 | + factory_storage.store_next(0, "create_fails"), |
| 653 | + Op.CREATE( |
| 654 | + value=0, |
| 655 | + offset=32 - len(initcode), |
| 656 | + size=len(initcode), |
| 657 | + ), |
| 658 | + original_value=0, |
| 659 | + new_value=0, |
| 660 | + ) |
| 661 | + + Op.SSTORE( |
| 662 | + factory_storage.store_next(1, "post_create"), |
| 663 | + 1, |
| 664 | + original_value=0, |
| 665 | + new_value=1, |
| 666 | + ) |
| 667 | + ) |
| 668 | + factory = pre.deploy_contract(code=factory_code) |
| 669 | + |
| 670 | + gas_limit = ( |
| 671 | + gas_limit_cap + new_account_state_gas + sstore_state_gas * 2 |
| 672 | + if with_reservoir |
| 673 | + else 5_000_000 |
| 674 | + ) |
| 675 | + |
| 676 | + # `bytecode.gas_cost(fork)` accounts for opcode base costs and |
| 677 | + # state-gas charges, but does NOT track memory-expansion or CREATE |
| 678 | + # init-code word costs. Add those back to recover runtime regular |
| 679 | + # gas consumption. |
| 680 | + init_code_word_count = (len(initcode) + 31) // 32 |
| 681 | + init_code_word_cost = gas_costs.CODE_INIT_PER_WORD * init_code_word_count |
| 682 | + mstore_memory_expansion = gas_costs.MEMORY_PER_WORD # 1 word |
| 683 | + gas_cost_helper_extras = init_code_word_cost + mstore_memory_expansion |
| 684 | + |
| 685 | + # Factory bytecode shape costs, derived from fork.gas_costs(): |
| 686 | + # pre-CREATE: PUSH32 + PUSH1 + MSTORE (with 1-word expansion) |
| 687 | + # + 3 PUSHes for CREATE inputs |
| 688 | + # post-CREATE: PUSH key + SSTORE (no-op) + 2 PUSHes + SSTORE |
| 689 | + # (zero-to-nonzero regular) |
| 690 | + factory_pre_create_regular = ( |
| 691 | + gas_costs.VERY_LOW * 2 |
| 692 | + + gas_costs.OPCODE_MSTORE_BASE |
| 693 | + + mstore_memory_expansion |
| 694 | + + gas_costs.VERY_LOW * 3 |
| 695 | + ) |
| 696 | + factory_post_create_regular = ( |
| 697 | + gas_costs.VERY_LOW |
| 698 | + + gas_costs.COLD_STORAGE_ACCESS |
| 699 | + + gas_costs.WARM_ACCESS |
| 700 | + + gas_costs.VERY_LOW * 2 |
| 701 | + + gas_costs.COLD_STORAGE_WRITE |
| 702 | + ) |
| 703 | + |
| 704 | + factory_regular = ( |
| 705 | + factory_code.gas_cost(fork) |
| 706 | + - new_account_state_gas |
| 707 | + - sstore_state_gas |
| 708 | + + gas_cost_helper_extras |
| 709 | + ) |
| 710 | + initcode_regular_revert = initcode.gas_cost(fork) - sstore_state_gas |
| 711 | + |
| 712 | + if failure_op == Op.INVALID: |
| 713 | + # Simulate runtime gas accounting for HALT using fork helpers: |
| 714 | + # 1. Initial regular pool capped by transaction_gas_limit_cap; |
| 715 | + # remainder forms the state reservoir. |
| 716 | + # 2. CREATE op charges new_account state gas (from reservoir |
| 717 | + # first, spilled to gas_left otherwise). |
| 718 | + # 3. 63/64 retention rule: parent retains gas_left // 64. |
| 719 | + # 4. INVALID burns all forwarded regular gas in the child. |
| 720 | + # Per the updated EIP, child halt preserves its state-gas |
| 721 | + # counters and `incorporate_child_on_error` refunds the |
| 722 | + # full child charge — including any spilled portion — to |
| 723 | + # the parent's reservoir. |
| 724 | + # 5. CREATE failure refunds new_account state gas to the |
| 725 | + # parent's state pool (account creation rolled back). |
| 726 | + # 6. Factory's post-CREATE SSTORE charges sstore_state_gas |
| 727 | + # (state pool first, spilled to gas_left otherwise). |
| 728 | + execution_gas = gas_limit - intrinsic_cost |
| 729 | + regular_budget = gas_limit_cap - intrinsic_cost |
| 730 | + sim_gas_left = min(regular_budget, execution_gas) |
| 731 | + sim_state_gas_left = execution_gas - sim_gas_left |
| 732 | + |
| 733 | + sim_gas_left -= factory_pre_create_regular |
| 734 | + sim_gas_left -= gas_costs.OPCODE_CREATE_BASE + init_code_word_cost |
| 735 | + |
| 736 | + if sim_state_gas_left >= new_account_state_gas: |
| 737 | + sim_state_gas_left -= new_account_state_gas |
| 738 | + else: |
| 739 | + sim_gas_left -= new_account_state_gas - sim_state_gas_left |
| 740 | + sim_state_gas_left = 0 |
| 741 | + |
| 742 | + # `child_reservoir` is what the parent forwards to the child. |
| 743 | + # Under Policy A halt, incorporate refunds child.state_gas_used |
| 744 | + # + child.state_gas_left = max(sstore, child_reservoir) back to |
| 745 | + # the parent. The simulator already implicitly retains |
| 746 | + # `child_reservoir` in `sim_state_gas_left`, so the additional |
| 747 | + # Policy A refund versus the Policy B "burn the spill" rule is |
| 748 | + # `max(0, sstore_state_gas - child_reservoir)`. |
| 749 | + child_reservoir = sim_state_gas_left |
| 750 | + sim_gas_left = sim_gas_left // 64 |
| 751 | + sim_state_gas_left += max(0, sstore_state_gas - child_reservoir) |
| 752 | + sim_state_gas_left += new_account_state_gas |
| 753 | + |
| 754 | + sim_gas_left -= factory_post_create_regular |
| 755 | + |
| 756 | + if sim_state_gas_left >= sstore_state_gas: |
| 757 | + sim_state_gas_left -= sstore_state_gas |
| 758 | + else: |
| 759 | + sim_gas_left -= sstore_state_gas - sim_state_gas_left |
| 760 | + sim_state_gas_left = 0 |
| 761 | + |
| 762 | + expected_cumulative = gas_limit - sim_gas_left - sim_state_gas_left |
| 763 | + else: |
| 764 | + # REVERT preserves gas_left and refunds the child frame's |
| 765 | + # state gas (initcode SSTORE + new account). Only the |
| 766 | + # factory's own post-CREATE SSTORE consumes net state gas. |
| 767 | + expected_cumulative = ( |
| 768 | + intrinsic_cost |
| 769 | + + factory_regular |
| 770 | + + initcode_regular_revert |
| 771 | + + sstore_state_gas |
| 772 | + ) |
| 773 | + |
| 774 | + tx = Transaction( |
| 775 | + to=factory, |
| 776 | + gas_limit=gas_limit, |
| 777 | + sender=pre.fund_eoa(), |
| 778 | + expected_receipt=TransactionReceipt( |
| 779 | + cumulative_gas_used=expected_cumulative, |
| 780 | + ), |
| 781 | + ) |
| 782 | + |
| 783 | + state_test( |
| 784 | + pre=pre, |
| 785 | + post={factory: Account(storage=factory_storage)}, |
| 786 | + tx=tx, |
| 787 | + ) |
| 788 | + |
| 789 | + |
599 | 790 | @pytest.mark.valid_from("EIP8037") |
600 | 791 | def test_nested_create_code_deposit_cannot_borrow_parent_gas( |
601 | 792 | state_test: StateTestFiller, |
|
0 commit comments