diff --git a/prototypes/glamsterdam-write-prepayment/README.md b/prototypes/glamsterdam-write-prepayment/README.md new file mode 100644 index 00000000000..5915c6a9fad --- /dev/null +++ b/prototypes/glamsterdam-write-prepayment/README.md @@ -0,0 +1,137 @@ +# Glamsterdam Write Prepayment — Phase 1 + +Base: `ethereum/execution-specs@20f7f6271a720091e5fea0a82e7bc802866ae36a` (`forks/amsterdam`, 2026-08-26). + +## Decision gate + +Do not change consensus semantics and do not scan mainnet until the current +execution-spec constants prove both invariants: + +1. `Osaka PASS -> Amsterdam FAIL -> Amsterdam + prepayment PASS` under the + same immutable child-call gas budget. +2. Moving the repricing delta outside the child preserves the full Amsterdam + execution-resource charge. + +The executable proof is: + +`tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py` + +This phase is deliberately a branch-pinned accounting/liveness proof. It does +**not** yet define a new transaction encoding or modify EVM consensus behavior. +That implementation is the next gate only if this proof survives CI. + +## Minimal reproduction + +Use an existing non-zero storage slot and a child that executes: + +```text +PUSH value +PUSH key +SSTORE +``` + +Two `PUSH` instructions cost 6 execution gas. Give the child an immutable +5,006-gas budget. + +### Osaka + +For the first non-zero -> non-zero overwrite of a cold slot: + +```text +2,100 cold storage access ++ 2,900 write component ++ 6 stack setup += 5,006 +``` + +Result: `PASS`. + +### Amsterdam, unchanged + +Current `forks/amsterdam` constants: + +```text +2,100 COLD_STORAGE_ACCESS ++ 10,000 STORAGE_WRITE ++ 6 stack setup += 12,106 +``` + +The same immutable 5,006-gas child cannot execute it. + +Result: `FAIL`. + +### Amsterdam, write-prepaid + +Decompose the Amsterdam write price: + +```text +10,000 STORAGE_WRITE += 2,800 historical write component ++ 7,200 repricing delta +``` + +Prepay the 7,200 delta outside the child and prewarm the storage key. The +child sees: + +```text +100 warm access ++ 2,800 historical write component ++ 6 stack setup += 2,906 +``` + +Result: `PASS` under the same 5,006-gas child budget. + +## Conservation proof + +The mechanism is invalid if it makes Amsterdam work cheaper globally. + +Ordinary cold Amsterdam storage write: + +```text +2,100 cold access + 10,000 write = 12,100 +``` + +Prepaid split: + +```text +2,000 access-list storage-key prepayment ++ 100 warm access in the child ++ 7,200 write-repricing prepayment ++ 2,800 historical write component in the child += 12,100 +``` + +Account access conserves independently: + +```text +ordinary: 3,000 cold account access +prepaid: 2,900 access-list address prepayment + 100 warm runtime access + = 3,000 +``` + +Therefore the core state/call resource accounting is identical: + +```text +ordinary Amsterdam core charge = 3,000 + 12,100 = 15,100 +prepaid Amsterdam core charge = 3,000 + 12,100 = 15,100 +``` + +The proposal changes **where** the repricing delta is charged, not whether it +is charged. + +## Next gate + +Only after the phase-1 test passes: + +1. choose an explicit opt-in transaction representation for write vouchers; +2. implement voucher state and rollback semantics in EELS; +3. prove change -> restore -> change net-metering, nested-call revert/halt + behavior, duplicate voucher handling, and EIP-8037 state-gas interaction; +4. run the Amsterdam/Osaka test suites and independent client fixtures; +5. only then scan deployed mainnet bytecode for fixed-gas asset-release paths + and quantify value reachable only through failing paths. + +A mainnet value estimate before those gates would measure a hypothesis rather +than a demonstrated compatibility failure. 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 00000000000..75e2b5445c9 --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/test_write_prepayment_prototype.py @@ -0,0 +1,133 @@ +""" +Executable phase-1 proof for Glamsterdam storage-write prepayment. + +The proof reads the Osaka and Amsterdam gas constants directly from the +current execution-spec source tree. It must fail if those branch constants +move enough to invalidate either the liveness transition or conservation +claim. +""" + +OSAKA_GAS_PATH = "src/ethereum/forks/osaka/vm/gas.py" +AMSTERDAM_GAS_PATH = "src/ethereum/forks/amsterdam/vm/gas.py" + + +def _uint_constant(path: str, name: str) -> int: + """Read a direct ``Uint(...)`` gas constant from a fork gas module.""" + with open(path, encoding="utf-8") as source_file: + for line in source_file: + stripped = line.strip() + if stripped.startswith(f"{name}:") and "Uint(" in stripped: + raw_value = stripped.rsplit("Uint(", 1)[1].split(")", 1)[0] + return int(raw_value) + raise AssertionError(f"direct Uint constant not found: {path}:{name}") + + +def _amsterdam_access_list_formulas_survive() -> None: + """Pin the access-list prepayment formulas used by the proof.""" + with open(AMSTERDAM_GAS_PATH, encoding="utf-8") as source_file: + source = source_file.read() + + assert "TX_ACCESS_LIST_ADDRESS: Final[ExecutionGas] = (" in source + assert "COLD_ACCOUNT_ACCESS - WARM_ACCESS" in source + assert "TX_ACCESS_LIST_STORAGE_KEY: Final[ExecutionGas] = (" in source + assert "COLD_STORAGE_ACCESS - WARM_ACCESS" in source + + +def test_osaka_pass_amsterdam_fail_prepaid_pass() -> None: + """Prove one immutable child budget flips PASS -> FAIL -> PASS.""" + osaka_warm = _uint_constant(OSAKA_GAS_PATH, "WARM_ACCESS") + osaka_cold_storage = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_ACCESS") + osaka_cold_write = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_WRITE") + osaka_push = _uint_constant(OSAKA_GAS_PATH, "VERY_LOW") + + amsterdam_warm = _uint_constant(AMSTERDAM_GAS_PATH, "WARM_ACCESS") + amsterdam_cold_storage = _uint_constant( + AMSTERDAM_GAS_PATH, "COLD_STORAGE_ACCESS" + ) + amsterdam_write = _uint_constant(AMSTERDAM_GAS_PATH, "STORAGE_WRITE") + amsterdam_push = _uint_constant(AMSTERDAM_GAS_PATH, "VERY_LOW") + + legacy_write = osaka_cold_write - osaka_cold_storage - osaka_warm + fixed_child_gas = osaka_cold_write + 2 * osaka_push + + osaka_required = osaka_cold_write + 2 * osaka_push + amsterdam_required = ( + amsterdam_cold_storage + amsterdam_write + 2 * amsterdam_push + ) + prepaid_required = amsterdam_warm + legacy_write + 2 * amsterdam_push + + assert legacy_write == 2_800 + assert fixed_child_gas == 5_006 + assert osaka_required == 5_006 + assert amsterdam_required == 12_106 + assert prepaid_required == 2_906 + + assert osaka_required <= fixed_child_gas + assert amsterdam_required > fixed_child_gas + assert prepaid_required <= fixed_child_gas + + +def test_prepayment_preserves_glamsterdam_execution_resource_charge() -> None: + """Prove repricing relocation preserves the full Amsterdam core charge.""" + _amsterdam_access_list_formulas_survive() + + osaka_warm = _uint_constant(OSAKA_GAS_PATH, "WARM_ACCESS") + osaka_cold_storage = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_ACCESS") + osaka_cold_write = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_WRITE") + + amsterdam_warm = _uint_constant(AMSTERDAM_GAS_PATH, "WARM_ACCESS") + amsterdam_cold_account = _uint_constant( + AMSTERDAM_GAS_PATH, "COLD_ACCOUNT_ACCESS" + ) + amsterdam_cold_storage = _uint_constant( + AMSTERDAM_GAS_PATH, "COLD_STORAGE_ACCESS" + ) + amsterdam_write = _uint_constant(AMSTERDAM_GAS_PATH, "STORAGE_WRITE") + + legacy_write = osaka_cold_write - osaka_cold_storage - osaka_warm + write_repricing_delta = amsterdam_write - legacy_write + access_list_address = amsterdam_cold_account - amsterdam_warm + access_list_storage_key = amsterdam_cold_storage - amsterdam_warm + + baseline_call_access = amsterdam_cold_account + baseline_storage = amsterdam_cold_storage + amsterdam_write + baseline_core_charge = baseline_call_access + baseline_storage + + prepaid_call_access = access_list_address + amsterdam_warm + prepaid_storage = ( + access_list_storage_key + + amsterdam_warm + + write_repricing_delta + + legacy_write + ) + prepaid_core_charge = prepaid_call_access + prepaid_storage + + assert write_repricing_delta == 7_200 + assert baseline_call_access == prepaid_call_access == 3_000 + assert baseline_storage == prepaid_storage == 12_100 + assert baseline_core_charge == prepaid_core_charge == 15_100 + + +def test_prepayment_is_not_a_subsidy() -> None: + """Prove the rescued local frame does not make global work cheaper.""" + osaka_warm = _uint_constant(OSAKA_GAS_PATH, "WARM_ACCESS") + osaka_cold_storage = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_ACCESS") + osaka_cold_write = _uint_constant(OSAKA_GAS_PATH, "COLD_STORAGE_WRITE") + + amsterdam_warm = _uint_constant(AMSTERDAM_GAS_PATH, "WARM_ACCESS") + amsterdam_cold_storage = _uint_constant( + AMSTERDAM_GAS_PATH, "COLD_STORAGE_ACCESS" + ) + amsterdam_write = _uint_constant(AMSTERDAM_GAS_PATH, "STORAGE_WRITE") + + legacy_write = osaka_cold_write - osaka_cold_storage - osaka_warm + write_repricing_delta = amsterdam_write - legacy_write + access_list_storage_key = amsterdam_cold_storage - amsterdam_warm + + local_prepaid_sstore = amsterdam_warm + legacy_write + globally_paid_sstore = ( + access_list_storage_key + local_prepaid_sstore + write_repricing_delta + ) + + assert local_prepaid_sstore == 2_900 + assert globally_paid_sstore == amsterdam_cold_storage + amsterdam_write