Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/testing/src/execution_testing/forks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Amsterdam,
ArrowGlacier,
Berlin,
Bogota,
Byzantium,
Cancun,
Constantinople,
Expand Down Expand Up @@ -93,6 +94,7 @@
"ArrowGlacier",
"Berlin",
"BerlinToLondonAt5",
"Bogota",
"Byzantium",
"Constantinople",
"ConstantinopleFix",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
EIP-3298: Removal of refunds.

https://eips.ethereum.org/EIPS/eip-3298
"""

from dataclasses import replace

from execution_testing.vm import OpcodeBase

from ....base_fork import BaseFork
from ....gas_costs import GasCosts


class EIP3298(BaseFork):
"""EIP-3298 class."""

@classmethod
def gas_costs(cls) -> GasCosts:
"""The storage-clearing refund leaves the gas schedule."""
return replace(
super(EIP3298, cls).gas_costs(),
REFUND_STORAGE_CLEAR=0,
)

@classmethod
def max_refund_quotient(cls) -> int:
"""
EIP-3298 removes the EIP-3529 refund cap.

A quotient of one leaves ``min(refund, gas_used // quotient)``
equal to the full refund, since a refund never exceeds the gas
charged in the same transaction.
"""
return 1

@classmethod
def _calculate_sstore_refund(
cls, opcode: OpcodeBase, gas_costs: GasCosts
) -> int:
"""
Calculate the execution SSTORE gas refund.

Only the net-metered STORAGE_WRITE reversal survives EIP-3298:
the storage-clearing refund and its restoration reversal are
removed with the constant.
"""
metadata = opcode.metadata

original_value = metadata["original_value"]
current_value = metadata["current_value"]
if current_value is None:
current_value = original_value
new_value = metadata["new_value"]

refund = 0
if current_value != new_value and original_value == new_value:
# Refund the STORAGE_WRITE charged on the first-time
# change earlier in the transaction.
refund += (
gas_costs.COLD_STORAGE_WRITE - gas_costs.COLD_STORAGE_ACCESS
)

return refund
14 changes: 14 additions & 0 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1642,3 +1642,17 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool:
limit.
"""
return True


class Bogota(
eips.EIP3298,
Amsterdam,
deployed=False,
):
"""
Pseudo Bogota fork.

For testing purposes only. Labels fixtures for features slated for the
fork after Amsterdam while the specs repository has no dedicated Bogota
fork module yet; execution uses the Amsterdam spec module.
"""
13 changes: 5 additions & 8 deletions src/ethereum/forks/amsterdam/vm/gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,6 @@ class GasCosts:
MEMORY_PER_WORD: Final[ExecutionGas] = ExecutionGas(Uint(3))
FAST_STEP: Final[ExecutionGas] = ExecutionGas(Uint(5))

# Refunds
REFUND_STORAGE_CLEAR: Final[int] = int(
(STORAGE_WRITE + COLD_STORAGE_ACCESS) * Uint(4800) // Uint(5000)
)

# Precompiles
PRECOMPILE_ECRECOVER: Final[ExecutionGas] = ExecutionGas(Uint(3000))
PRECOMPILE_P256VERIFY: Final[ExecutionGas] = ExecutionGas(Uint(6900))
Expand Down Expand Up @@ -1152,7 +1147,8 @@ def settle_transaction_gas(

- the gas used before refunds, from the gas limit less the
execution gas and reservoir the top frame returned;
- the refund, capped at one fifth of that pre-refund usage;
- the refund, applied in full ([EIP-3298] removes the [EIP-3529]
cap);
- the gas used, taken as the larger of the post-refund usage and the
calldata floor, so a transaction never pays below the floor; and
- the per-dimension block amounts: the state gas used (clamped to
Expand Down Expand Up @@ -1183,11 +1179,12 @@ def settle_transaction_gas(
The settled gas amounts.

[EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778
[EIP-3298]: https://eips.ethereum.org/EIPS/eip-3298
[EIP-3529]: https://eips.ethereum.org/EIPS/eip-3529

"""
gas_used_before_refund = tx_gas - gas_left - state_gas_left
gas_refund = min(gas_used_before_refund // Uint(5), Uint(refund_counter))
gas_used_after_refund = gas_used_before_refund - gas_refund
gas_used_after_refund = gas_used_before_refund - Uint(refund_counter)
gas_used = max(gas_used_after_refund, calldata_floor)

settled_state_gas_used = StateGas(Uint(max(0, state_gas_used)))
Expand Down
17 changes: 4 additions & 13 deletions src/ethereum/forks/amsterdam/vm/instructions/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,10 @@ def sstore(evm: Evm) -> None:
gas_cost += GasCosts.STORAGE_WRITE

# Refund Counter Calculation
if current_value != new_value:
if original_value != 0 and current_value != 0 and new_value == 0:
# Storage is cleared for the first time in the transaction
evm.gas_meter.refund_counter += GasCosts.REFUND_STORAGE_CLEAR

if original_value != 0 and current_value == 0:
# Gas refund issued earlier to be reversed
evm.gas_meter.refund_counter -= GasCosts.REFUND_STORAGE_CLEAR

if original_value == new_value:
# Slot restored to its original value: refund the STORAGE_WRITE
# charged on the first-time change earlier this transaction.
evm.gas_meter.refund_counter += int(GasCosts.STORAGE_WRITE)
if current_value != new_value and original_value == new_value:
# Slot restored to its original value: refund the STORAGE_WRITE
# charged on the first-time change earlier this transaction.
evm.gas_meter.refund_counter += int(GasCosts.STORAGE_WRITE)

# STATE GAS
# A first-time set of a zero slot pays for the state it creates; a
Expand Down
9 changes: 9 additions & 0 deletions src/ethereum_spec_tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"BPO4": {
"fork_blocks": [("osaka", 0)],
},
"Bogota": {
"fork_blocks": [("amsterdam", 0)],
},
"FrontierToHomesteadAt5": {
"fork_blocks": [("frontier", 0), ("homestead", 5)],
},
Expand Down Expand Up @@ -136,10 +139,16 @@ def find_fork(
# * ``DAOFork`` would snake-case to ``d_a_o_fork``.
# * ``ConstantinopleFix`` is a testing-side distinction that the spec
# folds into the ``constantinople`` module.
# * ``Bogota`` is a testing-side pseudo-fork that executes with the
# ``amsterdam`` spec module until the spec repository grows a
# dedicated Bogota fork module.
# TODO: Remove the ``Bogota`` alias (and its ``EXCEPTION_MAPS`` entry)
# once a dedicated ``bogota`` fork module exists in the spec.
_SPEC_SHORT_NAME_OVERRIDES: Dict[str, str] = {
"Merge": "paris",
"DAOFork": "dao_fork",
"ConstantinopleFix": "constantinople",
"Bogota": "amsterdam",
}


Expand Down
9 changes: 9 additions & 0 deletions tests/amsterdam/eip3298_removal_of_refunds/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Tests for [EIP-3298: Removal of refunds](https://eips.ethereum.org/EIPS/eip-3298).
"""

# TODO: Re-derive the tests parked with `valid_before("EIP3298")` once
# the EIP settles: the quotient-cap and clear-refund scenarios have no
# post-3298 life, but their gas-boundary shapes (cap saturation,
# floor-vs-refund windows) should be re-expressed against the
# surviving write reversal. The marker itself enumerates the worklist.
16 changes: 16 additions & 0 deletions tests/amsterdam/eip3298_removal_of_refunds/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Defines EIP-3298 specification constants and types."""

from dataclasses import dataclass


@dataclass(frozen=True)
class ReferenceSpec:
"""Defines the reference spec version and git path."""

git_path: str
version: str


ref_spec_3298 = ReferenceSpec(
"EIPS/eip-3298.md", "548ebfb1e987ddb8dc260324e02ee9ec643525a1"
)
103 changes: 103 additions & 0 deletions tests/amsterdam/eip3298_removal_of_refunds/test_fork_transition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Tests for EIP-3298 fork transition behavior.

Before the fork, clearing a non-zero-original slot grants the
storage-clearing refund (capped by the EIP-3529 quotient); from the fork
on, the same transaction pays gross gas in full.
"""

import pytest
from execution_testing import (
Account,
Alloc,
Block,
BlockchainTestFiller,
Fork,
Op,
Transaction,
TransactionReceipt,
)

from .spec import ref_spec_3298

REFERENCE_SPEC_GIT_PATH = ref_spec_3298.git_path
REFERENCE_SPEC_VERSION = ref_spec_3298.version


# TODO: Re-enable when a real post-Amsterdam fork module and transition
# fork exist. The pseudo-fork shares the Amsterdam spec module, so it
# cannot execute both sides of this behavior-changing boundary.
@pytest.mark.skip(reason="requires a real post-Amsterdam fork module")
@pytest.mark.valid_at_transition_to("EIP3298")
def test_clear_refund_across_transition(
blockchain_test: BlockchainTestFiller, fork: Fork, pre: Alloc
) -> None:
"""
Clear one slot on each side of the fork boundary.

The pre-fork receipt reflects the capped clearing refund; the
post-fork receipt reports gross gas with no refund.
"""
code = Op.SSTORE.with_metadata(
key_warm=False,
original_value=1,
current_value=1,
new_value=0,
)(0, 0)
pre_fork_contract = pre.deploy_contract(code=code, storage={0: 1})
post_fork_contract = pre.deploy_contract(code=code, storage={0: 1})
sender = pre.fund_eoa()

blocks = []
expected_gas_used = []
for timestamp, contract in (
(14_999, pre_fork_contract),
(15_000, post_fork_contract),
):
sub_fork = fork.fork_at(timestamp=timestamp)
intrinsic = sub_fork.transaction_intrinsic_cost_calculator()(
return_cost_deducted_prior_execution=True
)
gross = (
intrinsic
+ code.execution_cost(sub_fork)
+ code.state_cost(sub_fork)
)
accrued_refund = code.refund(sub_fork)
applied_refund = min(
accrued_refund,
gross // sub_fork.max_refund_quotient(),
)
floor = sub_fork.transaction_data_floor_cost_calculator()(data=b"")
cumulative_gas_used = max(gross - applied_refund, floor)
expected_gas_used.append(cumulative_gas_used)

blocks.append(
Block(
timestamp=timestamp,
txs=[
Transaction(
to=contract,
sender=sender,
expected_receipt=TransactionReceipt(
cumulative_gas_used=cumulative_gas_used
),
)
],
)
)

pre_fork = fork.fork_at(timestamp=14_999)
post_fork = fork.fork_at(timestamp=15_000)
assert code.refund(pre_fork) > 0
assert code.refund(post_fork) == 0
assert expected_gas_used[0] < expected_gas_used[1]

blockchain_test(
pre=pre,
blocks=blocks,
post={
pre_fork_contract: Account(storage={0: 0}),
post_fork_contract: Account(storage={0: 0}),
},
)
Loading
Loading