Skip to content
6 changes: 6 additions & 0 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,12 @@ def cost_per_state_byte(cls) -> int:
pass

# Fee helpers
@classmethod
@abstractmethod
def base_fee_max_change_numerator(cls) -> int:
"""Return the base fee max change numerator at a given fork."""
pass

@classmethod
@abstractmethod
def base_fee_max_change_denominator(cls) -> int:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
EIP-8198: Quick Slots.

Reduce the consensus-layer slot duration. The execution layer lowers the
maximum per-block base fee change and updates the blob schedule so that fee
dynamics and blob throughput per unit of wall-clock time stay close to their
previous values.

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

from ....base_fork import BaseFork


class EIP8198(
BaseFork,
# Provisional values, pending a joint decision with the consensus layer.
update_blob_constants={
"TARGET_BLOBS_PER_BLOCK": 12,
"MAX_BLOBS_PER_BLOCK": 18,
"BLOB_BASE_FEE_UPDATE_FRACTION": 12018519,
},
):
"""EIP-8198 class."""

@classmethod
def base_fee_max_change_numerator(cls) -> int:
"""The maximum base fee change per block is lowered to 5/48."""
return 5

@classmethod
def base_fee_max_change_denominator(cls) -> int:
"""The maximum base fee change per block is lowered to 5/48."""
return 48
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def contract_creating_tx_types(cls) -> List[int]:
"""Dynamic fee transactions can create contracts."""
return [2] + super(EIP1559, cls).contract_creating_tx_types()

@classmethod
def base_fee_max_change_numerator(cls) -> int:
"""Return the base fee max change numerator."""
return 1

@classmethod
def base_fee_max_change_denominator(cls) -> int:
"""Return the base fee max change denominator."""
Expand Down Expand Up @@ -73,6 +78,7 @@ def base_fee_per_gas_calculator(cls) -> BaseFeePerGasCalculator:
expected_base_fee_per_gas = parent_base_fee_per_gas -
base_fee_per_gas_delta
"""
base_fee_max_change_numerator = cls.base_fee_max_change_numerator()
base_fee_max_change_denominator = cls.base_fee_max_change_denominator()
elasticity_multiplier = cls.base_fee_elasticity_multiplier()

Expand All @@ -91,6 +97,7 @@ def fn(
parent_base_fee_per_gas
* gas_used_delta
// parent_gas_target
* base_fee_max_change_numerator
// base_fee_max_change_denominator,
1,
)
Expand All @@ -101,6 +108,7 @@ def fn(
parent_base_fee_per_gas
* gas_used_delta
// parent_gas_target
* base_fee_max_change_numerator
// base_fee_max_change_denominator
)
return parent_base_fee_per_gas - base_fee_per_gas_delta
Expand All @@ -113,6 +121,7 @@ def base_fee_change_calculator(cls) -> BaseFeeChangeCalculator:
Return a callable that calculates the gas that needs to be used
to change the base fee.
"""
base_fee_max_change_numerator = cls.base_fee_max_change_numerator()
base_fee_max_change_denominator = cls.base_fee_max_change_denominator()
elasticity_multiplier = cls.base_fee_elasticity_multiplier()
base_fee_per_gas_calculator = cls.base_fee_per_gas_calculator()
Expand All @@ -137,25 +146,52 @@ def fn(
* base_fee_max_change_denominator
* parent_gas_target
)
// parent_base_fee_per_gas
// (
parent_base_fee_per_gas * base_fee_max_change_numerator
)
) + parent_gas_target
# Flooring twice in the forward calculation can leave the
# estimate a little short when the numerator is not one.
while (
base_fee_per_gas_calculator(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_used=parent_gas_used,
parent_gas_limit=parent_gas_limit,
)
< required_base_fee_per_gas
):
parent_gas_used += 1
elif required_base_fee_per_gas < parent_base_fee_per_gas:
base_fee_per_gas_delta = (
parent_base_fee_per_gas - required_base_fee_per_gas
)

parent_gas_used = (
parent_gas_target
- (
(
# Round the gas below target up, so that an exact division
# does not overshoot the required decrease.
parent_gas_used = parent_gas_target - (
-(
-(
base_fee_per_gas_delta
* base_fee_max_change_denominator
* parent_gas_target
)
// parent_base_fee_per_gas
// (
parent_base_fee_per_gas
* base_fee_max_change_numerator
)
)
- 1
)
# Flooring twice in the forward calculation can leave the
# estimate a little short when the numerator is not one.
while (
base_fee_per_gas_calculator(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_used=parent_gas_used,
parent_gas_limit=parent_gas_limit,
)
> required_base_fee_per_gas
):
parent_gas_used -= 1

assert (
base_fee_per_gas_calculator(
Expand Down
7 changes: 7 additions & 0 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,13 @@ def cost_per_state_byte(cls) -> int:
"""
return 0

@classmethod
def base_fee_max_change_numerator(cls) -> int:
"""Return the base fee max change numerator at a given fork."""
raise NotImplementedError(
f"Base fee max change numerator is not supported in {cls.name()}"
)

@classmethod
def base_fee_max_change_denominator(cls) -> int:
"""Return the base fee max change denominator at a given fork."""
Expand Down
80 changes: 80 additions & 0 deletions packages/testing/src/execution_testing/forks/tests/test_forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,86 @@ def test_blob_schedules(
)


@pytest.mark.parametrize(
"fork,expected_max_change",
[
pytest.param(London, (1, 8), id="London"),
pytest.param(Amsterdam, (1, 8), id="Amsterdam"),
pytest.param(Bogota, (5, 48), id="Bogota"),
],
)
def test_base_fee_max_change(
fork: Fork, expected_max_change: Tuple[int, int]
) -> None:
"""Test the maximum base fee change per block of each fork."""
assert (
fork.base_fee_max_change_numerator(),
fork.base_fee_max_change_denominator(),
) == expected_max_change


@pytest.mark.parametrize(
"parent_base_fee_per_gas,parent_gas_used,expected_base_fee_per_gas",
[
pytest.param(10**9, 30_000_000, 1_104_166_666, id="full"),
pytest.param(10**9, 0, 895_833_334, id="empty"),
pytest.param(9, 30_000_000, 10, id="one_wei_increase_floor"),
pytest.param(9, 0, 9, id="decrease_truncated_to_zero"),
pytest.param(20, 29_900_000, 21, id="near_full"),
pytest.param(20, 100_000, 19, id="near_empty"),
],
)
def test_base_fee_per_gas_calculator_bogota(
parent_base_fee_per_gas: int,
parent_gas_used: int,
expected_base_fee_per_gas: int,
) -> None:
"""
Test the base fee update with a numerator other than one, which
multiplies before dividing.
"""
assert (
Bogota.base_fee_per_gas_calculator()(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_used=parent_gas_used,
parent_gas_limit=30_000_000,
)
== expected_base_fee_per_gas
)


@pytest.mark.parametrize("parent_base_fee_per_gas", [20, 10**9, 10**9 + 7])
def test_base_fee_change_calculator_round_trip_bogota(
parent_base_fee_per_gas: int,
) -> None:
"""
Test that the gas returned for a required base fee yields that base
fee, where flooring twice can leave the first estimate short.
"""
gas_limit = 30_000_000
base_fee_per_gas = Bogota.base_fee_per_gas_calculator()
gas_for_base_fee = Bogota.base_fee_change_calculator()
for parent_gas_used in range(0, gas_limit + 1, 1_000_000):
required = base_fee_per_gas(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_used=parent_gas_used,
parent_gas_limit=gas_limit,
)
gas_used = gas_for_base_fee(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_limit=gas_limit,
required_base_fee_per_gas=required,
)
assert (
base_fee_per_gas(
parent_base_fee_per_gas=parent_base_fee_per_gas,
parent_gas_used=gas_used,
parent_gas_limit=gas_limit,
)
== required
)


def test_bpo_fork() -> None: # noqa: D103
assert Osaka.bpo_fork() is False
assert BPO1.bpo_fork() is True
Expand Down
12 changes: 10 additions & 2 deletions packages/testing/src/execution_testing/specs/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,17 @@ def _generate_blockchain_genesis_environment(self) -> Environment:
kwargs["gas_limit"] = self.env.gas_limit

if self.env.base_fee_per_gas:
# Calculate genesis base fee per gas from state test's block#1 env
# Calculate genesis base fee per gas from state test's block#1
# env: the empty genesis block lowers the base fee by the fork's
# maximum change.
numerator, denominator = 1, 8
if genesis_fork.header_base_fee_required():
numerator = genesis_fork.base_fee_max_change_numerator()
denominator = genesis_fork.base_fee_max_change_denominator()
kwargs["base_fee_per_gas"] = HexNumber(
int(str(self.env.base_fee_per_gas), 0) * 8 // 7
int(str(self.env.base_fee_per_gas), 0)
* denominator
// (denominator - numerator)
)

if self.env.excess_blob_gas:
Expand Down
28 changes: 27 additions & 1 deletion packages/testing/src/execution_testing/specs/tests/test_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
LabeledFixtureFormat,
StateFixture,
)
from execution_testing.forks import Amsterdam, Istanbul
from execution_testing.forks import Amsterdam, Bogota, Fork, Istanbul
from execution_testing.test_types import Alloc, Environment, Transaction

from ..base import BaseTest
Expand Down Expand Up @@ -202,6 +202,32 @@ def test_state_test_conversion_checks_the_env_first() -> None:
state_test.generate_blockchain_test()


@pytest.mark.parametrize("fork", [Amsterdam, Bogota])
@pytest.mark.parametrize("base_fee_per_gas", [7, 10, 20, 10**9])
def test_state_test_conversion_genesis_base_fee(
fork: Fork, base_fee_per_gas: int
) -> None:
"""
Verify converting a state test derives a genesis base fee from which
the first block, on the fork's base fee rule, gets the state test's.
"""
state_test = StateTest(
env=Environment(base_fee_per_gas=base_fee_per_gas),
pre=Alloc(),
post=Alloc(),
tx=Transaction(),
fork=fork,
)
genesis = state_test.generate_blockchain_test().genesis_environment
assert genesis.base_fee_per_gas is not None
first_block_base_fee = fork.base_fee_per_gas_calculator()(
parent_base_fee_per_gas=int(genesis.base_fee_per_gas),
parent_gas_used=0,
parent_gas_limit=int(genesis.gas_limit),
)
assert first_block_base_fee == base_fee_per_gas


def test_blobs_test_rejects_empty_custody_columns_updates() -> None:
"""
Verify a blobs test accepts any non-empty sequence of `custodyColumns`
Expand Down
7 changes: 4 additions & 3 deletions src/ethereum/forks/bogota/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""
The Bogota fork ([EIP-8081]) is the development fork after Amsterdam. It
carries no protocol changes yet: EIPs targeting it are prototyped on their
own branches and land here once accepted.
lowers the maximum per-block base fee change and updates the blob schedule
to accompany a shorter consensus-layer slot duration.

### Changes

None yet.
- [EIP-8198: Quick Slots][EIP-8198]

### Releases

[EIP-8081]: https://eips.ethereum.org/EIPS/eip-8081
[EIP-8198]: https://eips.ethereum.org/EIPS/eip-8198
"""

from ethereum.fork_criteria import ForkCriteria, Unscheduled
Expand Down
28 changes: 25 additions & 3 deletions src/ethereum/forks/bogota/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,25 @@
)
from .vm.interpreter import TransactionOutput, process_top_level

BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8)
BASE_FEE_MAX_CHANGE_NUMERATOR = Uint(5)
"""
Numerator of the largest fraction by which the base fee can change from one
block to the next, used with [`BASE_FEE_MAX_CHANGE_DENOMINATOR`].

Blocks arrive more often from this fork onward, so the maximum change per
block is lowered to keep the base fee's reaction to congestion, per unit of
wall-clock time, close to what it was. The fraction is a hard-coded constant:
the execution layer does not consume the slot duration.

[`BASE_FEE_MAX_CHANGE_DENOMINATOR`]: ref:ethereum.forks.bogota.fork.BASE_FEE_MAX_CHANGE_DENOMINATOR
""" # noqa: E501
BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(48)
"""
Denominator of the largest fraction by which the base fee can change from one
block to the next. See [`BASE_FEE_MAX_CHANGE_NUMERATOR`].

[`BASE_FEE_MAX_CHANGE_NUMERATOR`]: ref:ethereum.forks.bogota.fork.BASE_FEE_MAX_CHANGE_NUMERATOR
""" # noqa: E501
ELASTICITY_MULTIPLIER = Uint(2)
EMPTY_OMMER_HASH = keccak256(rlp.encode([]))
SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe")
Expand Down Expand Up @@ -404,7 +422,9 @@ def calculate_base_fee_per_gas(
target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target

base_fee_per_gas_delta = max(
target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR,
target_fee_gas_delta
* BASE_FEE_MAX_CHANGE_NUMERATOR
// BASE_FEE_MAX_CHANGE_DENOMINATOR,
Uint(1),
)

Expand All @@ -418,7 +438,9 @@ def calculate_base_fee_per_gas(
target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target

base_fee_per_gas_delta = (
target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR
target_fee_gas_delta
* BASE_FEE_MAX_CHANGE_NUMERATOR
// BASE_FEE_MAX_CHANGE_DENOMINATOR
)

expected_base_fee_per_gas = (
Expand Down
Loading
Loading