From e4a5f739b0b335e7138e3a1d13a9bd5f7e3315c2 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 12:52:44 -0500 Subject: [PATCH 01/11] test(amsterdam): prove fixed-gas storage repricing liveness gap --- .../test_write_prepayment_prototype.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py new file mode 100644 index 0000000000..03fdd342f2 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -0,0 +1,151 @@ +""" +Prototype tests for preserving fixed-gas call liveness across EIP-8038. + +This file deliberately separates the problem proof from the protocol mechanism. +The first test executes the same fixed-gas child write on both sides of the +Amsterdam transition and demonstrates the liveness regression. The second test +proves the conservation equation required by a write-prepayment remedy: + + legacy child-frame charge + prepaid repricing delta + == Amsterdam child-frame charge + +The prototype does not assign a final wire encoding to the prepayment. That is +kept out of the first proof so the liveness finding can survive or fail on its +own. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Alloc, + Block, + BlockchainTestFiller, + Fork, + Op, + Transaction, +) + +BEFORE_TS = 14_999 +AFTER_TS = 15_000 + +pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") + + +def _warm_existing_slot_write(fork: Fork): + """Return one warm nonzero-to-nonzero SSTORE with explicit metadata.""" + return Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=1, + new_value=2, + )(0, 2) + + +def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + A fixed child CALL budget can be live before Amsterdam and dead after it. + + Both children begin with slot 0 == 1 and execute identical bytecode that + changes the already-existing slot to 2. The access list pre-warms the slot, + removing cold-access and EIP-8037 state-creation confounders. The CALL gas + budget is chosen strictly between the pre- and post-fork execution costs. + + The first block therefore commits the write. The second child runs out of + gas and rolls back the write even though the outer transaction has ample + gas. Increasing only outer transaction gas cannot alter the immutable CALL + operand. + """ + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + + write = _warm_existing_slot_write(fork) + cost_before = write.execution_cost(before) + cost_after = write.execution_cost(after) + + assert cost_after > cost_before + fixed_child_gas = (cost_before + cost_after) // 2 + assert cost_before <= fixed_child_gas < cost_after + + child_before = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + child_after = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + + parent_before = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_before)) + ) + parent_after = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_after)) + ) + + blocks = [ + Block( + timestamp=BEFORE_TS, + txs=[ + Transaction( + to=parent_before, + sender=pre.fund_eoa(), + access_list=[ + AccessList(address=child_before, storage_keys=[0]) + ], + ) + ], + ), + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=parent_after, + sender=pre.fund_eoa(), + access_list=[ + AccessList(address=child_after, storage_keys=[0]) + ], + ) + ], + ), + ] + + post = { + child_before: Account(storage={0: 2}), + child_after: Account(storage={0: 1}), + } + blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_write_prepayment_conserves_amsterdam_execution_charge( + fork: Fork, +) -> None: + """ + Splitting only the repricing delta preserves the Amsterdam gas charge. + + A write voucher must not subsidize the operation. It moves exactly the + Amsterdam-minus-parent execution-gas delta to transaction scope and leaves + the child frame paying the parent-fork execution charge. The total remains + identical to the unmodified Amsterdam execution charge while restoring a + fixed budget that lies between the two costs. + """ + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + + write = _warm_existing_slot_write(fork) + legacy_frame_charge = write.execution_cost(before) + amsterdam_frame_charge = write.execution_cost(after) + prepaid_repricing_delta = amsterdam_frame_charge - legacy_frame_charge + + fixed_child_gas = (legacy_frame_charge + amsterdam_frame_charge) // 2 + + # Osaka/current-parent execution remains live under the fixed budget. + assert legacy_frame_charge <= fixed_child_gas + # Unmodified Amsterdam execution is no longer live. + assert amsterdam_frame_charge > fixed_child_gas + # The voucher restores the old frame-local requirement. + voucher_frame_charge = amsterdam_frame_charge - prepaid_repricing_delta + assert voucher_frame_charge == legacy_frame_charge + assert voucher_frame_charge <= fixed_child_gas + # No resource discount: transaction prepayment + frame charge is exactly + # the unmodified Amsterdam execution charge. + assert prepaid_repricing_delta > 0 + assert voucher_frame_charge + prepaid_repricing_delta == amsterdam_frame_charge From 3a763c579ce8663df358fe4c608d220b23a2b2fa Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 12:54:25 -0500 Subject: [PATCH 02/11] ci: run glamsterdam write-prepayment proof --- .../glamsterdam-write-prepayment.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/glamsterdam-write-prepayment.yaml diff --git a/.github/workflows/glamsterdam-write-prepayment.yaml b/.github/workflows/glamsterdam-write-prepayment.yaml new file mode 100644 index 0000000000..807cc92699 --- /dev/null +++ b/.github/workflows/glamsterdam-write-prepayment.yaml @@ -0,0 +1,20 @@ +name: Glamsterdam write-prepayment proof + +on: + push: + branches: + - "experiment/glamsterdam-write-prepayment" + workflow_dispatch: + +jobs: + proof: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/setup-uv + with: + python-version: "3.14" + - name: Fill targeted Osaka-to-Amsterdam proof + run: > + just fill --from Osaka --until Amsterdam + -k write_prepayment_prototype From 3bf87c588f09a2c5048636fee8bf5863c382a4af Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:01:03 -0500 Subject: [PATCH 03/11] test(amsterdam): keep conservation proof in blockchain fixture --- .../test_write_prepayment_prototype.py | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index 03fdd342f2..09d84fdf90 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -1,17 +1,15 @@ """ -Prototype tests for preserving fixed-gas call liveness across EIP-8038. +Prototype test for preserving fixed-gas call liveness across EIP-8038. -This file deliberately separates the problem proof from the protocol mechanism. -The first test executes the same fixed-gas child write on both sides of the -Amsterdam transition and demonstrates the liveness regression. The second test -proves the conservation equation required by a write-prepayment remedy: +The test first proves the liveness regression independently of any proposed +protocol change, then pins the conservation equation a write-prepayment remedy +must satisfy: - legacy child-frame charge + prepaid repricing delta + child-frame charge + prepaid repricing delta == Amsterdam child-frame charge -The prototype does not assign a final wire encoding to the prepayment. That is -kept out of the first proof so the liveness finding can survive or fail on its -own. +A later commit replaces the algebraic voucher leg with executable Amsterdam +semantics only after the regression itself survives the filler. """ import pytest @@ -59,6 +57,10 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( gas and rolls back the write even though the outer transaction has ample gas. Increasing only outer transaction gas cannot alter the immutable CALL operand. + + The same fixture also proves the accounting invariant required by a later + voucher implementation: moving exactly the repricing delta outside the + child leaves the total Amsterdam execution charge unchanged. """ before = fork.fork_at(timestamp=BEFORE_TS) after = fork.fork_at(timestamp=AFTER_TS) @@ -71,6 +73,14 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( fixed_child_gas = (cost_before + cost_after) // 2 assert cost_before <= fixed_child_gas < cost_after + # Conservation gate for the future voucher implementation. + prepaid_repricing_delta = cost_after - cost_before + voucher_frame_charge = cost_after - prepaid_repricing_delta + assert prepaid_repricing_delta > 0 + assert voucher_frame_charge == cost_before + assert voucher_frame_charge <= fixed_child_gas + assert voucher_frame_charge + prepaid_repricing_delta == cost_after + child_before = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) child_after = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) @@ -113,39 +123,3 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( child_after: Account(storage={0: 1}), } blockchain_test(pre=pre, blocks=blocks, post=post) - - -def test_write_prepayment_conserves_amsterdam_execution_charge( - fork: Fork, -) -> None: - """ - Splitting only the repricing delta preserves the Amsterdam gas charge. - - A write voucher must not subsidize the operation. It moves exactly the - Amsterdam-minus-parent execution-gas delta to transaction scope and leaves - the child frame paying the parent-fork execution charge. The total remains - identical to the unmodified Amsterdam execution charge while restoring a - fixed budget that lies between the two costs. - """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - write = _warm_existing_slot_write(fork) - legacy_frame_charge = write.execution_cost(before) - amsterdam_frame_charge = write.execution_cost(after) - prepaid_repricing_delta = amsterdam_frame_charge - legacy_frame_charge - - fixed_child_gas = (legacy_frame_charge + amsterdam_frame_charge) // 2 - - # Osaka/current-parent execution remains live under the fixed budget. - assert legacy_frame_charge <= fixed_child_gas - # Unmodified Amsterdam execution is no longer live. - assert amsterdam_frame_charge > fixed_child_gas - # The voucher restores the old frame-local requirement. - voucher_frame_charge = amsterdam_frame_charge - prepaid_repricing_delta - assert voucher_frame_charge == legacy_frame_charge - assert voucher_frame_charge <= fixed_child_gas - # No resource discount: transaction prepayment + frame charge is exactly - # the unmodified Amsterdam execution charge. - assert prepaid_repricing_delta > 0 - assert voucher_frame_charge + prepaid_repricing_delta == amsterdam_frame_charge From b01a05fd1d42a550940f76a3e4931da72090e7eb Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:04:57 -0500 Subject: [PATCH 04/11] test(amsterdam): bind write-prepayment proof to EIP-8038 spec --- .../test_write_prepayment_prototype.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index 09d84fdf90..aa9a9d160c 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -24,6 +24,11 @@ Transaction, ) +from .spec import ref_spec_8038 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path +REFERENCE_SPEC_VERSION = ref_spec_8038.version + BEFORE_TS = 14_999 AFTER_TS = 15_000 From a4a4626e7d5b74507af38d7d712e2aa68e3a1810 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:05:12 -0500 Subject: [PATCH 05/11] ci: target write-prepayment fixture directly --- .github/workflows/glamsterdam-write-prepayment.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/glamsterdam-write-prepayment.yaml b/.github/workflows/glamsterdam-write-prepayment.yaml index 807cc92699..d7344c8c60 100644 --- a/.github/workflows/glamsterdam-write-prepayment.yaml +++ b/.github/workflows/glamsterdam-write-prepayment.yaml @@ -16,5 +16,10 @@ jobs: python-version: "3.14" - name: Fill targeted Osaka-to-Amsterdam proof run: > - just fill --from Osaka --until Amsterdam - -k write_prepayment_prototype + uv run fill + --skip-index + --clean + --from Osaka + --until Amsterdam + --output=.just/write-prepayment/fixtures + tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py From b03e5aae1d8bdd8dd4959435b3814736acdffdbe Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:07:18 -0500 Subject: [PATCH 06/11] experiment(amsterdam): prepay bounded SSTORE write gas --- .../amsterdam/vm/instructions/storage.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 432a3500ba..87dc276bc6 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -11,6 +11,8 @@ Implementations of the EVM storage related instructions. """ +from ethereum.crypto.hash import keccak256 +from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import Uint from ...fork_types import ExecutionGas, StateGas @@ -34,6 +36,41 @@ from ..stack import pop, push +# Experimental, wire-compatible proof encoding only. Three domain-separated +# access-list storage keys prepay 3 * TX_ACCESS_LIST_STORAGE_KEY outside the +# child frame. The matching SSTORE pays the remainder of STORAGE_WRITE inside +# the frame. A final EIP should use an explicit transaction representation. +_WRITE_PREPAYMENT_DOMAIN = b"glamsterdam-write-prepayment-v1" +_WRITE_PREPAYMENT_MARKER_COUNT = 3 + + +def _write_prepayment_markers(evm: Evm, key: Bytes32) -> tuple[Bytes32, ...]: + """Return the access-list marker keys for one target storage slot.""" + prefix = _WRITE_PREPAYMENT_DOMAIN + bytes(evm.current_target) + bytes(key) + return tuple( + keccak256(prefix + bytes([index])) + for index in range(_WRITE_PREPAYMENT_MARKER_COUNT) + ) + + +def _consume_write_prepayment(evm: Evm, key: Bytes32) -> bool: + """Consume one experimental prepayment if all marker keys are present.""" + declared = evm.tx_env.access_list_storage_keys + if (evm.current_target, key) not in declared: + return False + + markers = _write_prepayment_markers(evm, key) + marker_entries = tuple((evm.current_target, marker) for marker in markers) + if not all(entry in declared for entry in marker_entries): + return False + + # The intrinsic access-list charges are paid once. Consume the markers + # transaction-wide so a reverted/failed child cannot reuse that prepayment. + for entry in marker_entries: + declared.remove(entry) + return True + + def sload(evm: Evm) -> None: """ Loads to the stack, the value corresponding to a certain key from the @@ -120,7 +157,13 @@ def sstore(evm: Evm) -> None: # Write cost: charged on the first change to the slot this transaction. if original_value == current_value and current_value != new_value: - gas_cost += GasCosts.STORAGE_WRITE + if _consume_write_prepayment(evm, key): + prepaid = GasCosts.TX_ACCESS_LIST_STORAGE_KEY * Uint( + _WRITE_PREPAYMENT_MARKER_COUNT + ) + gas_cost += GasCosts.STORAGE_WRITE - prepaid + else: + gas_cost += GasCosts.STORAGE_WRITE # Refund Counter Calculation if current_value != new_value: From 868db77e3bbf5d02003452b7e7e5dd977fb076d2 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:07:55 -0500 Subject: [PATCH 07/11] test(amsterdam): execute write-prepayment rescue leg --- .../test_write_prepayment_prototype.py | 101 ++++++++++++------ 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index aa9a9d160c..1dd7e6540b 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -1,21 +1,26 @@ """ -Prototype test for preserving fixed-gas call liveness across EIP-8038. +Executable proof for preserving fixed-gas call liveness across EIP-8038. -The test first proves the liveness regression independently of any proposed -protocol change, then pins the conservation equation a write-prepayment remedy -must satisfy: +The fixture proves three legs with identical child bytecode: - child-frame charge + prepaid repricing delta - == Amsterdam child-frame charge + parent schedule PASS -> Amsterdam FAIL -> Amsterdam + voucher PASS -A later commit replaces the algebraic voucher leg with executable Amsterdam -semantics only after the regression itself survives the filler. +The experimental voucher is encoded with three domain-separated EIP-2930 +storage-key markers. Their existing intrinsic charges move a bounded portion of +the Amsterdam STORAGE_WRITE price outside the child frame. The remaining +write charge stays inside the child. The core storage-access/write charge is +conserved exactly; marker bytes add ordinary transaction-data overhead on top. + +This marker encoding is a proof vehicle, not a proposed final wire format. """ import pytest +from ethereum.crypto.hash import keccak256 +from ethereum_types.bytes import Bytes32 from execution_testing import ( AccessList, Account, + Address, Alloc, Block, BlockchainTestFiller, @@ -31,6 +36,9 @@ BEFORE_TS = 14_999 AFTER_TS = 15_000 +VOUCHER_TS = 15_001 +WRITE_PREPAYMENT_DOMAIN = b"glamsterdam-write-prepayment-v1" +WRITE_PREPAYMENT_MARKER_COUNT = 3 pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") @@ -45,27 +53,36 @@ def _warm_existing_slot_write(fork: Fork): )(0, 2) -def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( +def _write_prepayment_markers(address: Address, key: int) -> list[Bytes32]: + """Mirror the experimental Amsterdam marker derivation.""" + key_bytes = Bytes32(key.to_bytes(32, "big")) + prefix = WRITE_PREPAYMENT_DOMAIN + bytes(address) + bytes(key_bytes) + return [ + keccak256(prefix + bytes([index])) + for index in range(WRITE_PREPAYMENT_MARKER_COUNT) + ] + + +def test_fixed_gas_sstore_liveness_and_write_prepayment( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, ) -> None: """ - A fixed child CALL budget can be live before Amsterdam and dead after it. - - Both children begin with slot 0 == 1 and execute identical bytecode that - changes the already-existing slot to 2. The access list pre-warms the slot, - removing cold-access and EIP-8037 state-creation confounders. The CALL gas - budget is chosen strictly between the pre- and post-fork execution costs. - - The first block therefore commits the write. The second child runs out of - gas and rolls back the write even though the outer transaction has ample - gas. Increasing only outer transaction gas cannot alter the immutable CALL - operand. - - The same fixture also proves the accounting invariant required by a later - voucher implementation: moving exactly the repricing delta outside the - child leaves the total Amsterdam execution charge unchanged. + Prove parent PASS, Amsterdam FAIL, and Amsterdam+prepayment PASS. + + All three children begin with slot 0 == 1 and execute identical bytecode + that changes the existing slot to 2. The target slot is access-listed in + every leg, removing cold-access and EIP-8037 state-creation confounders. + The CALL gas budget is strictly between the parent and Amsterdam execution + costs, so merely increasing outer transaction gas cannot fix the middle + leg. + + In the voucher leg, three additional access-list storage keys pay their + existing intrinsic storage-key charges. Amsterdam consumes that prepayment + once and reduces only the frame-local STORAGE_WRITE charge by the same + amount. Thus the child fits the immutable CALL budget without reducing the + total core storage-access/write price. """ before = fork.fork_at(timestamp=BEFORE_TS) after = fork.fork_at(timestamp=AFTER_TS) @@ -78,16 +95,22 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( fixed_child_gas = (cost_before + cost_after) // 2 assert cost_before <= fixed_child_gas < cost_after - # Conservation gate for the future voucher implementation. - prepaid_repricing_delta = cost_after - cost_before - voucher_frame_charge = cost_after - prepaid_repricing_delta - assert prepaid_repricing_delta > 0 - assert voucher_frame_charge == cost_before + # One EIP-2930 storage-key prepayment equals cold minus warm SLOAD. + storage_key_prepayment = Op.SLOAD(key_warm=False).gas_cost( + after + ) - Op.SLOAD(key_warm=True).gas_cost(after) + marker_prepayment = storage_key_prepayment * WRITE_PREPAYMENT_MARKER_COUNT + voucher_frame_charge = cost_after - marker_prepayment + + assert marker_prepayment > 0 assert voucher_frame_charge <= fixed_child_gas - assert voucher_frame_charge + prepaid_repricing_delta == cost_after + # Core conservation: the amount removed from the child is exactly the + # amount already paid by the three marker-key intrinsic charges. + assert voucher_frame_charge + marker_prepayment == cost_after child_before = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) child_after = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + child_voucher = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) parent_before = pre.deploy_contract( code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_before)) @@ -95,6 +118,11 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( parent_after = pre.deploy_contract( code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_after)) ) + parent_voucher = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_voucher)) + ) + + voucher_keys = [0, *_write_prepayment_markers(child_voucher, 0)] blocks = [ Block( @@ -121,10 +149,23 @@ def test_fixed_gas_sstore_liveness_regresses_at_amsterdam( ) ], ), + Block( + timestamp=VOUCHER_TS, + txs=[ + Transaction( + to=parent_voucher, + sender=pre.fund_eoa(), + access_list=[ + AccessList(address=child_voucher, storage_keys=voucher_keys) + ], + ) + ], + ), ] post = { child_before: Account(storage={0: 2}), child_after: Account(storage={0: 1}), + child_voucher: Account(storage={0: 2}), } blockchain_test(pre=pre, blocks=blocks, post=post) From 2e49f8c838368637198dbd34ef010b8b3f1faf18 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:11:06 -0500 Subject: [PATCH 08/11] test(amsterdam): contain write-prepayment replay and partial markers --- .../test_write_prepayment_prototype.py | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index 1dd7e6540b..99520a25b8 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -1,7 +1,7 @@ """ Executable proof for preserving fixed-gas call liveness across EIP-8038. -The fixture proves three legs with identical child bytecode: +The primary fixture proves three legs with identical child bytecode: parent schedule PASS -> Amsterdam FAIL -> Amsterdam + voucher PASS @@ -11,6 +11,10 @@ write charge stays inside the child. The core storage-access/write charge is conserved exactly; marker bytes add ordinary transaction-data overhead on top. +Additional fixtures contain the proof mechanism: partial marker sets do not +activate it, an OOG attempt consumes the prepaid credit instead of making it +replayable, and ordinary Amsterdam writes retain their normal semantics. + This marker encoding is a proof vehicle, not a proposed final wire format. """ @@ -63,6 +67,22 @@ def _write_prepayment_markers(address: Address, key: int) -> list[Bytes32]: ] +def _fixed_gas_window(fork: Fork) -> tuple[int, int, int, int]: + """Return parent cost, Amsterdam cost, voucher cost, and fixed budget.""" + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + write = _warm_existing_slot_write(fork) + cost_before = write.execution_cost(before) + cost_after = write.execution_cost(after) + storage_key_prepayment = Op.SLOAD(key_warm=False).gas_cost( + after + ) - Op.SLOAD(key_warm=True).gas_cost(after) + marker_prepayment = storage_key_prepayment * WRITE_PREPAYMENT_MARKER_COUNT + voucher_frame_charge = cost_after - marker_prepayment + fixed_child_gas = (cost_before + cost_after) // 2 + return cost_before, cost_after, voucher_frame_charge, fixed_child_gas + + def test_fixed_gas_sstore_liveness_and_write_prepayment( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -169,3 +189,117 @@ def test_fixed_gas_sstore_liveness_and_write_prepayment( child_voucher: Account(storage={0: 2}), } blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_partial_write_prepayment_does_not_discount( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Two of the three markers are insufficient to rescue the fixed call.""" + _, cost_after, _, fixed_child_gas = _fixed_gas_window(fork) + assert fixed_child_gas < cost_after + + child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + parent = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child)) + ) + markers = _write_prepayment_markers(child, 0) + + blocks = [ + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=parent, + sender=pre.fund_eoa(), + access_list=[ + AccessList(address=child, storage_keys=[0, *markers[:2]]) + ], + ) + ], + ) + ] + + post = {child: Account(storage={0: 1})} + blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_write_prepayment_is_not_replayable_after_oog( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + An OOG attempt burns its prepaid write credit transaction-wide. + + The first call has one gas less than the discounted frame requires. The + SSTORE recognizes and consumes the marker set, then fails its gas charge. + A second call in the same transaction has enough gas for the discounted + write but not the ordinary Amsterdam write. It must still fail; otherwise + one intrinsic prepayment could subsidize arbitrarily many retrying frames. + """ + _, cost_after, voucher_frame_charge, fixed_child_gas = _fixed_gas_window( + fork + ) + assert voucher_frame_charge > 2_301 + first_attempt_gas = voucher_frame_charge - 1 + assert first_attempt_gas < voucher_frame_charge <= fixed_child_gas + assert fixed_child_gas < cost_after + + child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + parent = pre.deploy_contract( + code=( + Op.POP(Op.CALL(gas=first_attempt_gas, address=child)) + + Op.POP(Op.CALL(gas=fixed_child_gas, address=child)) + ) + ) + voucher_keys = [0, *_write_prepayment_markers(child, 0)] + + blocks = [ + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=parent, + sender=pre.fund_eoa(), + access_list=[ + AccessList(address=child, storage_keys=voucher_keys) + ], + ) + ], + ) + ] + + post = {child: Account(storage={0: 1})} + blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_ordinary_amsterdam_sstore_remains_unchanged( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Without marker keys, an adequately funded Amsterdam write still works.""" + _, cost_after, _, _ = _fixed_gas_window(fork) + + child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + parent = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=cost_after + 10_000, address=child)) + ) + + blocks = [ + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=parent, + sender=pre.fund_eoa(), + access_list=[AccessList(address=child, storage_keys=[0])], + ) + ], + ) + ] + + post = {child: Account(storage={0: 2})} + blockchain_test(pre=pre, blocks=blocks, post=post) From e02da8989a2abbb99e19a2f301549820f7dfc298 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:13:42 -0500 Subject: [PATCH 09/11] style(amsterdam): type prepaid execution gas --- src/ethereum/forks/amsterdam/vm/instructions/storage.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 87dc276bc6..0f11b76b9d 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -11,10 +11,11 @@ Implementations of the EVM storage related instructions. """ -from ethereum.crypto.hash import keccak256 from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import Uint +from ethereum.crypto.hash import keccak256 + from ...fork_types import ExecutionGas, StateGas from ...state_tracker import ( get_storage, @@ -158,8 +159,9 @@ def sstore(evm: Evm) -> None: # Write cost: charged on the first change to the slot this transaction. if original_value == current_value and current_value != new_value: if _consume_write_prepayment(evm, key): - prepaid = GasCosts.TX_ACCESS_LIST_STORAGE_KEY * Uint( - _WRITE_PREPAYMENT_MARKER_COUNT + prepaid = ExecutionGas( + GasCosts.TX_ACCESS_LIST_STORAGE_KEY + * Uint(_WRITE_PREPAYMENT_MARKER_COUNT) ) gas_cost += GasCosts.STORAGE_WRITE - prepaid else: From 6fb60a75d06a043696d0443d85029bbd773ab105 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:15:20 -0500 Subject: [PATCH 10/11] style(amsterdam): fix write-prepayment proof checks --- .../test_write_prepayment_prototype.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index 99520a25b8..2ac58dc7db 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -19,8 +19,9 @@ """ import pytest -from ethereum.crypto.hash import keccak256 from ethereum_types.bytes import Bytes32 + +from ethereum.crypto.hash import keccak256 from execution_testing import ( AccessList, Account, @@ -28,6 +29,7 @@ Alloc, Block, BlockchainTestFiller, + Bytecode, Fork, Op, Transaction, @@ -47,7 +49,7 @@ pytestmark = pytest.mark.valid_at_transition_to("Amsterdam") -def _warm_existing_slot_write(fork: Fork): +def _warm_existing_slot_write(fork: Fork) -> Bytecode: """Return one warm nonzero-to-nonzero SSTORE with explicit metadata.""" return Op.SSTORE.with_metadata( key_warm=True, @@ -176,7 +178,10 @@ def test_fixed_gas_sstore_liveness_and_write_prepayment( to=parent_voucher, sender=pre.fund_eoa(), access_list=[ - AccessList(address=child_voucher, storage_keys=voucher_keys) + AccessList( + address=child_voucher, + storage_keys=voucher_keys, + ) ], ) ], @@ -214,7 +219,10 @@ def test_partial_write_prepayment_does_not_discount( to=parent, sender=pre.fund_eoa(), access_list=[ - AccessList(address=child, storage_keys=[0, *markers[:2]]) + AccessList( + address=child, + storage_keys=[0, *markers[:2]], + ) ], ) ], @@ -280,7 +288,9 @@ def test_ordinary_amsterdam_sstore_remains_unchanged( pre: Alloc, fork: Fork, ) -> None: - """Without marker keys, an adequately funded Amsterdam write still works.""" + """ + Verify an adequately funded Amsterdam write still works without markers. + """ _, cost_after, _, _ = _fixed_gas_window(fork) child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) From e16d4096a95ad1409fe4b26af0dbeb8de7dd1fe9 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 26 Aug 2026 13:42:16 -0500 Subject: [PATCH 11/11] experiment(amsterdam): prepay exact SSTORE repricing delta --- src/ethereum/forks/amsterdam/fork.py | 7 + src/ethereum/forks/amsterdam/transactions.py | 24 ++ src/ethereum/forks/amsterdam/vm/__init__.py | 1 + .../amsterdam/vm/instructions/storage.py | 44 +-- .../forks/amsterdam/write_prepayment.py | 42 +++ .../test_write_prepayment_prototype.py | 328 ++++++++++++------ 6 files changed, 303 insertions(+), 143 deletions(-) create mode 100644 src/ethereum/forks/amsterdam/write_prepayment.py diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 5c4a10809c..93825da950 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -103,6 +103,7 @@ settle_transaction_gas, ) from .vm.interpreter import TransactionOutput, process_top_level +from .write_prepayment import find_write_prepayments BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) ELASTICITY_MULTIPLIER = Uint(2) @@ -591,6 +592,10 @@ def check_transaction( for slot in access.slots: access_list_storage_keys.add((access.account, slot)) + write_prepaid_storage_keys = find_write_prepayments( + access_list_storage_keys + ) + authorizations: Tuple[Authorization, ...] = () if isinstance(tx, SetCodeTransaction): authorizations = tx.authorizations @@ -621,6 +626,7 @@ def check_transaction( calldata_floor=intrinsic.calldata_floor, access_list_addresses=access_list_addresses, access_list_storage_keys=access_list_storage_keys, + write_prepaid_storage_keys=write_prepaid_storage_keys, accounts_with_paid_writes=accounts_with_paid_writes, state=tx_state, blob_versioned_hashes=blob_versioned_hashes, @@ -767,6 +773,7 @@ def process_unchecked_system_transaction( calldata_floor=Uint(0), access_list_addresses=set(), access_list_storage_keys=set(), + write_prepaid_storage_keys=set(), # A system transaction charges no gas, so no write is paid for. accounts_with_paid_writes=set(), state=system_tx_state, diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index 693064688d..3dcc08a8ec 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -35,6 +35,11 @@ TransactionTypeError, ) from .fork_types import Authorization, ExecutionGas, VersionedHash +from .write_prepayment import ( + STORAGE_WRITE_REPRICING_DELTA, + WRITE_PREPAYMENT_MARKER_COUNT, + find_write_prepayments, +) @final @@ -728,6 +733,7 @@ def calculate_intrinsic_cost( recipient_execution_gas += GasCosts.TX_VALUE_COST access_list_cost = Uint(0) + access_list_entries: set[tuple[Address, Bytes32]] = set() tokens_in_access_list = Uint(0) if has_access_list(tx): for access in tx.access_list: @@ -739,6 +745,23 @@ def calculate_intrinsic_cost( tokens_in_access_list += ( ulen(access.slots) * ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS ) + for slot in access.slots: + access_list_entries.add((access.account, slot)) + + # Experimental encoding: three ordinary access-list marker keys carry + # 6,000 gas of the 7,200 write-repricing delta. Charge the exact 1,200 + # remainder here so a complete voucher has paid the full delta before + # any frame starts executing. + write_prepayments = find_write_prepayments(access_list_entries) + marker_prepayment = GasCosts.TX_ACCESS_LIST_STORAGE_KEY * Uint( + WRITE_PREPAYMENT_MARKER_COUNT + ) + write_prepayment_remainder = ( + STORAGE_WRITE_REPRICING_DELTA - marker_prepayment + ) + write_prepayment_cost = write_prepayment_remainder * ulen( + write_prepayments + ) # Data token floor cost for access list bytes. access_list_cost += tokens_in_access_list * GasCosts.TX_DATA_TOKEN_FLOOR @@ -770,6 +793,7 @@ def calculate_intrinsic_cost( + init_code_gas + data_cost + access_list_cost + + write_prepayment_cost + auth_cost ), calldata_floor=ExecutionGas(data_floor_gas_cost), diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 3125187719..263e40f19d 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -139,6 +139,7 @@ class TransactionEnvironment: calldata_floor: Uint access_list_addresses: Set[Address] access_list_storage_keys: Set[Tuple[Address, Bytes32]] + write_prepaid_storage_keys: Set[Tuple[Address, Bytes32]] accounts_with_paid_writes: Set[Address] state: TransactionState blob_versioned_hashes: Tuple[VersionedHash, ...] diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 0f11b76b9d..335735ffaf 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -14,8 +14,6 @@ from ethereum_types.bytes import Bytes32 from ethereum_types.numeric import Uint -from ethereum.crypto.hash import keccak256 - from ...fork_types import ExecutionGas, StateGas from ...state_tracker import ( get_storage, @@ -24,6 +22,7 @@ set_storage, set_transient_storage, ) +from ...write_prepayment import STORAGE_WRITE_REPRICING_DELTA from .. import Evm from ..exceptions import WriteInStaticContext from ..gas import ( @@ -37,38 +36,15 @@ from ..stack import pop, push -# Experimental, wire-compatible proof encoding only. Three domain-separated -# access-list storage keys prepay 3 * TX_ACCESS_LIST_STORAGE_KEY outside the -# child frame. The matching SSTORE pays the remainder of STORAGE_WRITE inside -# the frame. A final EIP should use an explicit transaction representation. -_WRITE_PREPAYMENT_DOMAIN = b"glamsterdam-write-prepayment-v1" -_WRITE_PREPAYMENT_MARKER_COUNT = 3 - - -def _write_prepayment_markers(evm: Evm, key: Bytes32) -> tuple[Bytes32, ...]: - """Return the access-list marker keys for one target storage slot.""" - prefix = _WRITE_PREPAYMENT_DOMAIN + bytes(evm.current_target) + bytes(key) - return tuple( - keccak256(prefix + bytes([index])) - for index in range(_WRITE_PREPAYMENT_MARKER_COUNT) - ) - - def _consume_write_prepayment(evm: Evm, key: Bytes32) -> bool: - """Consume one experimental prepayment if all marker keys are present.""" - declared = evm.tx_env.access_list_storage_keys - if (evm.current_target, key) not in declared: - return False - - markers = _write_prepayment_markers(evm, key) - marker_entries = tuple((evm.current_target, marker) for marker in markers) - if not all(entry in declared for entry in marker_entries): + """Consume one transaction-boundary write prepayment.""" + entry = (evm.current_target, key) + if entry not in evm.tx_env.write_prepaid_storage_keys: return False - # The intrinsic access-list charges are paid once. Consume the markers - # transaction-wide so a reverted/failed child cannot reuse that prepayment. - for entry in marker_entries: - declared.remove(entry) + # Consumption is transaction-wide, so a reverted or OOG child cannot + # reuse one intrinsic prepayment in another frame. + evm.tx_env.write_prepaid_storage_keys.remove(entry) return True @@ -159,11 +135,7 @@ def sstore(evm: Evm) -> None: # Write cost: charged on the first change to the slot this transaction. if original_value == current_value and current_value != new_value: if _consume_write_prepayment(evm, key): - prepaid = ExecutionGas( - GasCosts.TX_ACCESS_LIST_STORAGE_KEY - * Uint(_WRITE_PREPAYMENT_MARKER_COUNT) - ) - gas_cost += GasCosts.STORAGE_WRITE - prepaid + gas_cost += GasCosts.STORAGE_WRITE - STORAGE_WRITE_REPRICING_DELTA else: gas_cost += GasCosts.STORAGE_WRITE diff --git a/src/ethereum/forks/amsterdam/write_prepayment.py b/src/ethereum/forks/amsterdam/write_prepayment.py new file mode 100644 index 0000000000..a7120e27b3 --- /dev/null +++ b/src/ethereum/forks/amsterdam/write_prepayment.py @@ -0,0 +1,42 @@ +"""Experimental transaction-boundary storage-write prepayments.""" + +from typing import Iterable + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.state import Address + +from .fork_types import ExecutionGas + +WRITE_PREPAYMENT_DOMAIN = b"glamsterdam-write-prepayment-v1" +WRITE_PREPAYMENT_MARKER_COUNT = 3 + +# EIP-8038 raises the existing-slot write component from 2,800 to 10,000. +# A voucher moves only that 7,200 repricing delta to transaction scope. +STORAGE_WRITE_REPRICING_DELTA = ExecutionGas(Uint(7_200)) + + +def write_prepayment_markers( + address: Address, key: Bytes32 +) -> tuple[Bytes32, ...]: + """Return the provisional access-list markers for one storage write.""" + prefix = WRITE_PREPAYMENT_DOMAIN + bytes(address) + bytes(key) + return tuple( + keccak256(prefix + bytes([index])) + for index in range(WRITE_PREPAYMENT_MARKER_COUNT) + ) + + +def find_write_prepayments( + entries: Iterable[tuple[Address, Bytes32]], +) -> set[tuple[Address, Bytes32]]: + """Decode complete provisional vouchers from access-list entries.""" + declared = set(entries) + prepaid: set[tuple[Address, Bytes32]] = set() + for address, key in declared: + markers = write_prepayment_markers(address, key) + if all((address, marker) in declared for marker in markers): + prepaid.add((address, key)) + return prepaid diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py index 2ac58dc7db..724bbc4c8b 100644 --- a/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -1,15 +1,14 @@ """ -Executable proof for preserving fixed-gas call liveness across EIP-8038. +Executable four-assertion proof for fixed-gas call liveness across EIP-8038. -The primary fixture proves three legs with identical child bytecode: +Four independently named tests prove Osaka PASS, Amsterdam FAIL, Amsterdam +with a write prepayment PASS, and preservation of the 12,100 Amsterdam storage +resource charge. - parent schedule PASS -> Amsterdam FAIL -> Amsterdam + voucher PASS - -The experimental voucher is encoded with three domain-separated EIP-2930 -storage-key markers. Their existing intrinsic charges move a bounded portion of -the Amsterdam STORAGE_WRITE price outside the child frame. The remaining -write charge stays inside the child. The core storage-access/write charge is -conserved exactly; marker bytes add ordinary transaction-data overhead on top. +The experimental voucher is signaled by three domain-separated EIP-2930 +storage-key markers. Their 6,000 core intrinsic charge plus a 1,200 prototype +intrinsic surcharge pay the exact 7,200 repricing delta at the transaction +boundary. The child retains the historical 2,800 write component. Additional fixtures contain the proof mechanism: partial marker sets do not activate it, an OOG attempt consumes the prepaid credit instead of making it @@ -19,9 +18,8 @@ """ import pytest -from ethereum_types.bytes import Bytes32 - -from ethereum.crypto.hash import keccak256 +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint from execution_testing import ( AccessList, Account, @@ -35,6 +33,18 @@ Transaction, ) +from ethereum.crypto.hash import keccak256 +from ethereum.forks.amsterdam.transactions import ( + ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS, + AccessListTransaction, + calculate_intrinsic_cost, +) +from ethereum.forks.amsterdam.transactions import ( + Access as AmsterdamAccess, +) +from ethereum.forks.amsterdam.vm.gas import GasCosts +from ethereum.state import Address as StateAddress + from .spec import ref_spec_8038 REFERENCE_SPEC_GIT_PATH = ref_spec_8038.git_path @@ -59,7 +69,9 @@ def _warm_existing_slot_write(fork: Fork) -> Bytecode: )(0, 2) -def _write_prepayment_markers(address: Address, key: int) -> list[Bytes32]: +def _write_prepayment_markers( + address: Address | StateAddress, key: int +) -> list[Bytes32]: """Mirror the experimental Amsterdam marker derivation.""" key_bytes = Bytes32(key.to_bytes(32, "big")) prefix = WRITE_PREPAYMENT_DOMAIN + bytes(address) + bytes(key_bytes) @@ -76,126 +88,228 @@ def _fixed_gas_window(fork: Fork) -> tuple[int, int, int, int]: write = _warm_existing_slot_write(fork) cost_before = write.execution_cost(before) cost_after = write.execution_cost(after) - storage_key_prepayment = Op.SLOAD(key_warm=False).gas_cost( - after - ) - Op.SLOAD(key_warm=True).gas_cost(after) - marker_prepayment = storage_key_prepayment * WRITE_PREPAYMENT_MARKER_COUNT - voucher_frame_charge = cost_after - marker_prepayment + repricing_delta = cost_after - cost_before + voucher_frame_charge = cost_after - repricing_delta fixed_child_gas = (cost_before + cost_after) // 2 return cost_before, cost_after, voucher_frame_charge, fixed_child_gas -def test_fixed_gas_sstore_liveness_and_write_prepayment( +def _execute_fixed_gas_leg( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, + *, + timestamp: int, + voucher: bool, + expected_value: int, ) -> None: - """ - Prove parent PASS, Amsterdam FAIL, and Amsterdam+prepayment PASS. - - All three children begin with slot 0 == 1 and execute identical bytecode - that changes the existing slot to 2. The target slot is access-listed in - every leg, removing cold-access and EIP-8037 state-creation confounders. - The CALL gas budget is strictly between the parent and Amsterdam execution - costs, so merely increasing outer transaction gas cannot fix the middle - leg. - - In the voucher leg, three additional access-list storage keys pay their - existing intrinsic storage-key charges. Amsterdam consumes that prepayment - once and reduces only the frame-local STORAGE_WRITE charge by the same - amount. Thus the child fits the immutable CALL budget without reducing the - total core storage-access/write price. - """ - before = fork.fork_at(timestamp=BEFORE_TS) - after = fork.fork_at(timestamp=AFTER_TS) - - write = _warm_existing_slot_write(fork) - cost_before = write.execution_cost(before) - cost_after = write.execution_cost(after) - - assert cost_after > cost_before - fixed_child_gas = (cost_before + cost_after) // 2 + """Execute one parent/child pair under the shared immutable budget.""" + cost_before, cost_after, voucher_cost, fixed_child_gas = _fixed_gas_window( + fork + ) assert cost_before <= fixed_child_gas < cost_after + assert voucher_cost == cost_before - # One EIP-2930 storage-key prepayment equals cold minus warm SLOAD. - storage_key_prepayment = Op.SLOAD(key_warm=False).gas_cost( - after - ) - Op.SLOAD(key_warm=True).gas_cost(after) - marker_prepayment = storage_key_prepayment * WRITE_PREPAYMENT_MARKER_COUNT - voucher_frame_charge = cost_after - marker_prepayment - - assert marker_prepayment > 0 - assert voucher_frame_charge <= fixed_child_gas - # Core conservation: the amount removed from the child is exactly the - # amount already paid by the three marker-key intrinsic charges. - assert voucher_frame_charge + marker_prepayment == cost_after - - child_before = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) - child_after = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) - child_voucher = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) - - parent_before = pre.deploy_contract( - code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_before)) - ) - parent_after = pre.deploy_contract( - code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_after)) - ) - parent_voucher = pre.deploy_contract( - code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child_voucher)) + child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + parent = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=fixed_child_gas, address=child)) ) - voucher_keys = [0, *_write_prepayment_markers(child_voucher, 0)] + storage_keys: list[int | Bytes32] = [0] + if voucher: + storage_keys.extend(_write_prepayment_markers(child, 0)) blocks = [ Block( - timestamp=BEFORE_TS, - txs=[ - Transaction( - to=parent_before, - sender=pre.fund_eoa(), - access_list=[ - AccessList(address=child_before, storage_keys=[0]) - ], - ) - ], - ), - Block( - timestamp=AFTER_TS, - txs=[ - Transaction( - to=parent_after, - sender=pre.fund_eoa(), - access_list=[ - AccessList(address=child_after, storage_keys=[0]) - ], - ) - ], - ), - Block( - timestamp=VOUCHER_TS, + timestamp=timestamp, txs=[ Transaction( - to=parent_voucher, + to=parent, sender=pre.fund_eoa(), access_list=[ - AccessList( - address=child_voucher, - storage_keys=voucher_keys, - ) + AccessList(address=child, storage_keys=storage_keys) ], ) ], - ), + ) ] - - post = { - child_before: Account(storage={0: 2}), - child_after: Account(storage={0: 1}), - child_voucher: Account(storage={0: 2}), - } + post = {child: Account(storage={0: expected_value})} blockchain_test(pre=pre, blocks=blocks, post=post) +def test_osaka_pass( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """OSAKA_PASS: the fixed-gas child changes the existing nonzero slot.""" + _execute_fixed_gas_leg( + blockchain_test, + pre, + fork, + timestamp=BEFORE_TS, + voucher=False, + expected_value=2, + ) + + +def test_amsterdam_fail( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """AMSTERDAM_FAIL: identical child execution runs out of gas.""" + _execute_fixed_gas_leg( + blockchain_test, + pre, + fork, + timestamp=AFTER_TS, + voucher=False, + expected_value=1, + ) + + +def test_amsterdam_voucher_pass( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """AMSTERDAM_VOUCHER_PASS: prepaid delta restores child liveness.""" + _execute_fixed_gas_leg( + blockchain_test, + pre, + fork, + timestamp=VOUCHER_TS, + voucher=True, + expected_value=2, + ) + + +def test_amsterdam_total_charge_preserved( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """AMSTERDAM_TOTAL_CHARGE_PRESERVED: both paths charge 12,100.""" + before = fork.fork_at(timestamp=BEFORE_TS) + after = fork.fork_at(timestamp=AFTER_TS) + bare_write = Op.SSTORE.with_metadata( + key_warm=True, + original_value=1, + current_value=1, + new_value=2, + ) + + warm_access = Op.SLOAD(key_warm=True).gas_cost(after) + cold_access = Op.SLOAD(key_warm=False).gas_cost(after) + legacy_write = bare_write.execution_cost(before) - warm_access + amsterdam_write = bare_write.execution_cost(after) - warm_access + access_list_prepayment = cold_access - warm_access + repricing_delta = amsterdam_write - legacy_write + marker_prepayment = access_list_prepayment * WRITE_PREPAYMENT_MARKER_COUNT + prototype_remainder = repricing_delta - marker_prepayment + + ordinary_amsterdam = cold_access + amsterdam_write + prepaid_amsterdam = ( + access_list_prepayment + + marker_prepayment + + prototype_remainder + + warm_access + + legacy_write + ) + + assert access_list_prepayment == 2_000 + assert repricing_delta == 7_200 + assert marker_prepayment == 6_000 + assert prototype_remainder == 1_200 + assert warm_access == 100 + assert legacy_write == 2_800 + assert ordinary_amsterdam == prepaid_amsterdam == 12_100 + + # Bind the equation to the implemented transaction-boundary charge. The + # provisional marker bytes have ordinary EIP-7981 encoding overhead; after + # removing only that overhead, the actual intrinsic delta is exactly 7,200. + target = StateAddress(b"\x11" * 20) + sender = StateAddress(b"\x22" * 20) + key = Bytes32(b"\x00" * 32) + markers = tuple(_write_prepayment_markers(target, 0)) + + def intrinsic_for(slots: tuple[Bytes32, ...]) -> int: + tx = AccessListTransaction( + chain_id=U64(1), + nonce=U256(0), + gas_price=Uint(1), + gas=Uint(1_000_000), + to=target, + value=U256(0), + data=Bytes(b""), + access_list=(AmsterdamAccess(account=target, slots=slots),), + y_parity=U256(0), + r=U256(0), + s=U256(0), + ) + return int(calculate_intrinsic_cost(tx, sender).execution) + + ordinary_intrinsic = intrinsic_for((key,)) + voucher_intrinsic = intrinsic_for((key, *markers)) + marker_encoding_overhead = int( + Uint(WRITE_PREPAYMENT_MARKER_COUNT) + * ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS + * GasCosts.TX_DATA_TOKEN_FLOOR + ) + assert ( + voucher_intrinsic - ordinary_intrinsic - marker_encoding_overhead + == repricing_delta + == 7_200 + ) + + _, cost_after, voucher_cost, _ = _fixed_gas_window(fork) + ordinary_child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + voucher_child = pre.deploy_contract(code=Op.SSTORE(0, 2), storage={0: 1}) + ordinary_parent = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=cost_after, address=ordinary_child)) + ) + voucher_parent = pre.deploy_contract( + code=Op.POP(Op.CALL(gas=voucher_cost, address=voucher_child)) + ) + voucher_keys = [0, *_write_prepayment_markers(voucher_child, 0)] + + blockchain_test( + pre=pre, + blocks=[ + Block( + timestamp=AFTER_TS, + txs=[ + Transaction( + to=ordinary_parent, + sender=pre.fund_eoa(), + access_list=[ + AccessList( + address=ordinary_child, + storage_keys=[0], + ) + ], + ), + Transaction( + to=voucher_parent, + sender=pre.fund_eoa(), + access_list=[ + AccessList( + address=voucher_child, + storage_keys=voucher_keys, + ) + ], + ), + ], + ) + ], + post={ + ordinary_child: Account(storage={0: 2}), + voucher_child: Account(storage={0: 2}), + }, + ) + + def test_partial_write_prepayment_does_not_discount( blockchain_test: BlockchainTestFiller, pre: Alloc,