From 73ddd7ef5c0b880cdef93f81f6b293485f6c5d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Wed, 1 Jul 2026 01:31:20 +0200 Subject: [PATCH 1/2] feat(tests): EIP-8037 reject tx exceeding remaining block state-gas budget Two txs each fit the block alone, but tx1's SSTORE-sets consume the block's state budget down to a small remainder. tx2's gas_limit exceeds that remainder while the regular budget still has room, so it is rejected on the state dimension and the block is invalid. The regular-dimension cumulative gate is already covered by test_multi_transaction_gas_accounting (EIP-7778); this pins the independent state-dimension gate (check_transaction, fork.py:573). --- .../test_block_2d_gas_accounting.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py index 78f793db4c9..5a7400e22da 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py @@ -884,4 +884,58 @@ def test_base_fee_per_gas_follows_dominant_dimension( ), ], post=post, +@pytest.mark.exception_test +@pytest.mark.valid_from("EIP8037") +def test_cumulative_block_state_gas_rejection( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Reject a tx whose gas exceeds the remaining block state-gas budget. + + Both txs fit the block alone. tx1's SSTORE-sets consume the block's + state budget down to a small remainder; tx2's gas_limit exceeds that + remainder while the regular budget still has ample room, so tx2 is + rejected on the state dimension and the block is invalid. The + regular-dimension cumulative gate is covered by + test_multi_transaction_gas_accounting (EIP-7778). + """ + storage_set = Op.SSTORE(new_value=1).state_cost(fork) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + n = 6 + + set_op = Op.SSTORE.with_metadata( + key_warm=False, original_value=0, current_value=0, new_value=1 + ) + sstore_code = sum((set_op(i, 1) for i in range(n)), Bytecode()) + Op.STOP + tx1_regular = intrinsic + sstore_code.regular_cost(fork) + # tx1 exactly fills the block; the leftover state budget is tx1_regular. + block_gas_limit = tx1_regular + n * storage_set + # tx2 stays within the remaining regular budget, so only the state + # dimension can reject it. + assert tx1_regular + 1 <= block_gas_limit - tx1_regular + + sstore_contract = pre.deploy_contract(code=sstore_code) + + tx1 = Transaction( + to=sstore_contract, gas_limit=block_gas_limit, sender=pre.fund_eoa() + ) + tx2 = Transaction( + to=sstore_contract, + gas_limit=tx1_regular + 1, + sender=pre.fund_eoa(), + error=TransactionException.GAS_ALLOWANCE_EXCEEDED, + ) + + blockchain_test( + genesis_environment=Environment(gas_limit=block_gas_limit), + pre=pre, + blocks=[ + Block( + txs=[tx1, tx2], + exception=TransactionException.GAS_ALLOWANCE_EXCEEDED, + ) + ], + post={}, ) From f6b0314c2c1670e7af6b430184930587d6090bb1 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 2 Jul 2026 13:10:37 +0100 Subject: [PATCH 2/2] chore(tests): parametrize block state-gas boundary and simplify costs --- .../test_block_2d_gas_accounting.py | 65 ++++++++++++------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py index 5a7400e22da..962e26fbdec 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_block_2d_gas_accounting.py @@ -884,58 +884,79 @@ def test_base_fee_per_gas_follows_dominant_dimension( ), ], post=post, -@pytest.mark.exception_test + ) + + +@pytest.mark.parametrize( + "delta", + [ + pytest.param(0, id="exact_fit"), + pytest.param(1, id="exceeded", marks=pytest.mark.exception_test), + ], +) @pytest.mark.valid_from("EIP8037") -def test_cumulative_block_state_gas_rejection( +def test_cumulative_block_state_gas_boundary( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, + delta: int, ) -> None: """ - Reject a tx whose gas exceeds the remaining block state-gas budget. - - Both txs fit the block alone. tx1's SSTORE-sets consume the block's - state budget down to a small remainder; tx2's gas_limit exceeds that - remainder while the regular budget still has ample room, so tx2 is - rejected on the state dimension and the block is invalid. The - regular-dimension cumulative gate is covered by - test_multi_transaction_gas_accounting (EIP-7778). + Probe the block state-gas inclusion gate with spill-funded usage. + + tx1 gets no state-gas reservoir (small gas_limit), so its SSTORE-set + state gas reaches block_state_gas_used only via spillover, and its + gas_limit exactly fills the block. tx2's gas_limit is the remaining + state budget plus delta, below both the per-tx cap and the remaining + regular budget, so only the state gate can reject it: delta=0 must + be accepted (strict >) and delta=1 rejected. + test_block_state_gas_limit_boundary covers this gate with a + reservoir-funded tx1 and an above-cap tx2. """ - storage_set = Op.SSTORE(new_value=1).state_cost(fork) - intrinsic = fork.transaction_intrinsic_cost_calculator()() n = 6 - - set_op = Op.SSTORE.with_metadata( - key_warm=False, original_value=0, current_value=0, new_value=1 + intrinsic = fork.transaction_intrinsic_cost_calculator()() + sstore_code = ( + sum((Op.SSTORE(i, 1) for i in range(n)), Bytecode()) + Op.STOP ) - sstore_code = sum((set_op(i, 1) for i in range(n)), Bytecode()) + Op.STOP tx1_regular = intrinsic + sstore_code.regular_cost(fork) + tx1_state = sstore_code.state_cost(fork) # tx1 exactly fills the block; the leftover state budget is tx1_regular. - block_gas_limit = tx1_regular + n * storage_set + block_gas_limit = tx1_regular + tx1_state # tx2 stays within the remaining regular budget, so only the state # dimension can reject it. assert tx1_regular + 1 <= block_gas_limit - tx1_regular sstore_contract = pre.deploy_contract(code=sstore_code) + stop_contract = pre.deploy_contract(code=Op.STOP) + error = TransactionException.GAS_ALLOWANCE_EXCEEDED if delta else None tx1 = Transaction( to=sstore_contract, gas_limit=block_gas_limit, sender=pre.fund_eoa() ) tx2 = Transaction( - to=sstore_contract, - gas_limit=tx1_regular + 1, + to=stop_contract, + gas_limit=tx1_regular + delta, sender=pre.fund_eoa(), - error=TransactionException.GAS_ALLOWANCE_EXCEEDED, + error=error, ) + post: dict = {} + header_verify: Header | None = None + if not delta: + post = {sstore_contract: Account(storage=dict.fromkeys(range(n), 1))} + header_verify = Header( + gas_used=max(tx1_regular + intrinsic, tx1_state) + ) + blockchain_test( genesis_environment=Environment(gas_limit=block_gas_limit), pre=pre, blocks=[ Block( txs=[tx1, tx2], - exception=TransactionException.GAS_ALLOWANCE_EXCEEDED, + exception=error, + header_verify=header_verify, ) ], - post={}, + post=post, )