From f2553786b4ed7e83eac396969986f38c157c0a6a Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:16:58 -0500 Subject: [PATCH 1/8] test(eip8198): add schedule-free fork-local competitor --- .../fork_local_competitor.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py diff --git a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py new file mode 100644 index 00000000000..f8faa486e51 --- /dev/null +++ b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py @@ -0,0 +1,112 @@ +"""Schedule-free EIP-8198 execution-layer competitor. + +Each synthetic fork carries only the execution constants needed by that fork. +There is no runtime slot-duration schedule, epoch lookup, or slot lookup here. +""" + +from dataclasses import dataclass +from typing import Final, final + +from ethereum_types.numeric import U64, Uint + +from ethereum.utils.numeric import taylor_exponential + +ELASTICITY_MULTIPLIER: Final[Uint] = Uint(2) +BLOB_MIN_GASPRICE: Final[Uint] = Uint(1) + + +@final +@dataclass(frozen=True) +class ForkLocalELParameters: + """Execution constants compiled into one fork.""" + + gas_limit_target: Uint + base_fee_numerator: Uint + base_fee_denominator: Uint + blob_maximum: U64 + blob_target: U64 + blob_update_fraction: Uint + + +BASELINE_12S: Final[ForkLocalELParameters] = ForkLocalELParameters( + gas_limit_target=Uint(72_000_000), + base_fee_numerator=Uint(1), + base_fee_denominator=Uint(8), + blob_maximum=U64(21), + blob_target=U64(14), + blob_update_fraction=Uint(11_684_671), +) + +FORK_10S: Final[ForkLocalELParameters] = ForkLocalELParameters( + gas_limit_target=Uint(60_000_000), + base_fee_numerator=Uint(5), + base_fee_denominator=Uint(48), + blob_maximum=U64(17), + blob_target=U64(12), + blob_update_fraction=Uint(10_015_432), +) + +FORK_8S: Final[ForkLocalELParameters] = ForkLocalELParameters( + gas_limit_target=Uint(48_000_000), + base_fee_numerator=Uint(1), + base_fee_denominator=Uint(12), + blob_maximum=U64(13), + blob_target=U64(10), + blob_update_fraction=Uint(7_511_574), +) + +FORK_6S: Final[ForkLocalELParameters] = ForkLocalELParameters( + gas_limit_target=Uint(36_000_000), + base_fee_numerator=Uint(1), + base_fee_denominator=Uint(16), + blob_maximum=U64(9), + blob_target=U64(8), + blob_update_fraction=Uint(3_338_477), +) + + +def calculate_fork_local_base_fee( + parent_gas_limit: Uint, + parent_gas_used: Uint, + parent_base_fee_per_gas: Uint, + parameters: ForkLocalELParameters, +) -> Uint: + """Calculate base fee using only constants compiled into the active fork.""" + parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER + + if parent_gas_used == parent_gas_target: + return parent_base_fee_per_gas + + if parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + base_fee_delta = max( + target_fee_gas_delta + * parameters.base_fee_numerator + // parameters.base_fee_denominator, + Uint(1), + ) + return Uint(parent_base_fee_per_gas + base_fee_delta) + + gas_used_delta = parent_gas_target - parent_gas_used + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + base_fee_delta = ( + target_fee_gas_delta + * parameters.base_fee_numerator + // parameters.base_fee_denominator + ) + return Uint(parent_base_fee_per_gas - base_fee_delta) + + +def calculate_fork_local_blob_price( + excess_blob_gas: U64, + parameters: ForkLocalELParameters, +) -> Uint: + """Calculate blob price using only constants compiled into the active fork.""" + return taylor_exponential( + BLOB_MIN_GASPRICE, + Uint(excess_blob_gas), + parameters.blob_update_fraction, + ) From 11d5313fd2ae3b5d07f04d8a0b154704ee76e07b Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:17:01 -0500 Subject: [PATCH 2/8] test(eip8198): compare fork-local constants with schedule oracle --- .../test_fork_local_reparameterization.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py diff --git a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py new file mode 100644 index 00000000000..a4b82d006c0 --- /dev/null +++ b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py @@ -0,0 +1,228 @@ +"""Try to falsify fork-local EIP-8198 re-parameterisation. + +The schedule-driven implementation is the behavioral oracle. The competitor +contains no runtime duration schedule: each synthetic fork has only fixed EL +constants. If fixed constants cannot reproduce an invariant, this suite should +expose the first counterexample. +""" + +from fractions import Fraction +import inspect + +import pytest +from ethereum_types.numeric import U64, Uint + +from ethereum.forks.amsterdam.fork import calculate_base_fee_per_gas +from ethereum.forks.amsterdam.slot_timing import ( + BASE_BLOB_SCHEDULE, + BASE_SLOT_DURATION_MS, + BlobScheduleParameters, + SlotDurationEntry, + calculate_blob_gas_price_for_slot, + get_blob_schedule, + scale_blob_schedule, + scale_transition_limit, +) + +from . import fork_local_competitor as local + +SLOTS_PER_EPOCH = U64(32) +EPOCH_10S = U64(10) +EPOCH_8S = U64(15) +EPOCH_6S = U64(20) + +ORACLE_SCHEDULE = ( + SlotDurationEntry(EPOCH_10S, Uint(10_000)), + SlotDurationEntry(EPOCH_8S, Uint(8_000)), + SlotDurationEntry(EPOCH_6S, Uint(6_000)), +) + +ERA_CASES = ( + (12_000, U64(0), local.BASELINE_12S), + (10_000, U64(EPOCH_10S * SLOTS_PER_EPOCH), local.FORK_10S), + (8_000, U64(EPOCH_8S * SLOTS_PER_EPOCH), local.FORK_8S), + (6_000, U64(EPOCH_6S * SLOTS_PER_EPOCH), local.FORK_6S), +) + + +def _blob_parameters( + parameters: local.ForkLocalELParameters, +) -> BlobScheduleParameters: + return BlobScheduleParameters( + maximum=parameters.blob_maximum, + target=parameters.blob_target, + update_fraction=parameters.blob_update_fraction, + ) + + +def _rate_error(value: int, period_ms: int, baseline: Fraction) -> Fraction: + return abs(Fraction(value, period_ms) - baseline) + + +def test_competitor_has_no_runtime_schedule_dependency() -> None: + """The competing runtime model contains no schedule/slot/epoch machinery.""" + fields = tuple(local.ForkLocalELParameters.__dataclass_fields__) + assert fields == ( + "gas_limit_target", + "base_fee_numerator", + "base_fee_denominator", + "blob_maximum", + "blob_target", + "blob_update_fraction", + ) + + source = inspect.getsource(local) + for forbidden in ( + "SLOT_DURATION_SCHEDULE", + "SlotDurationEntry", + "get_slot_duration_ms", + "slot_number", + "current_epoch", + ): + assert forbidden not in source + + +@pytest.mark.parametrize("duration_ms,_slot,parameters", ERA_CASES) +@pytest.mark.parametrize("parent_base_fee", [1, 7, 960, 1_000_000_000]) +@pytest.mark.parametrize("gas_fraction", [0, 1, 2, 3, 4]) +def test_fork_local_base_fee_matches_schedule_oracle( + duration_ms: int, + _slot: U64, + parameters: local.ForkLocalELParameters, + parent_base_fee: int, + gas_fraction: int, +) -> None: + """Fixed fork constants reproduce the oracle base-fee response exactly.""" + gas_limit = parameters.gas_limit_target + parent_gas_target = gas_limit // Uint(2) + parent_gas_used = Uint(parent_gas_target * Uint(gas_fraction) // Uint(2)) + + oracle = calculate_base_fee_per_gas( + block_gas_limit=gas_limit, + parent_gas_limit=gas_limit, + parent_gas_used=parent_gas_used, + parent_base_fee_per_gas=Uint(parent_base_fee), + gas_limit_reference=gas_limit, + slot_duration_ms=Uint(duration_ms), + ) + competitor = local.calculate_fork_local_base_fee( + parent_gas_limit=gas_limit, + parent_gas_used=parent_gas_used, + parent_base_fee_per_gas=Uint(parent_base_fee), + parameters=parameters, + ) + assert competitor == oracle + + +def test_fork_local_gas_targets_match_transition_oracle() -> None: + """Pre-coordinated fork targets compose without runtime duration state.""" + gas_12 = local.BASELINE_12S.gas_limit_target + gas_10 = scale_transition_limit(gas_12, Uint(12_000), Uint(10_000)) + gas_8 = scale_transition_limit(gas_10, Uint(10_000), Uint(8_000)) + gas_6 = scale_transition_limit(gas_8, Uint(8_000), Uint(6_000)) + + assert gas_10 == local.FORK_10S.gas_limit_target + assert gas_8 == local.FORK_8S.gas_limit_target + assert gas_6 == local.FORK_6S.gas_limit_target + + +@pytest.mark.parametrize("_duration_ms,slot,parameters", ERA_CASES) +def test_fork_local_blob_constants_can_match_schedule_oracle( + _duration_ms: int, + slot: U64, + parameters: local.ForkLocalELParameters, +) -> None: + """A fork can carry the same blob constants without runtime schedule lookup.""" + oracle = get_blob_schedule(slot, ORACLE_SCHEDULE) + assert _blob_parameters(parameters) == oracle + + +@pytest.mark.parametrize("_duration_ms,slot,parameters", ERA_CASES) +@pytest.mark.parametrize( + "excess_blob_gas", + [0, 131_072, 1_000_000, 20_000_000, 50_000_000], +) +def test_fork_local_blob_price_matches_schedule_oracle( + _duration_ms: int, + slot: U64, + parameters: local.ForkLocalELParameters, + excess_blob_gas: int, +) -> None: + """Fixed fork-local blob pricing reproduces the schedule oracle exactly.""" + oracle = calculate_blob_gas_price_for_slot( + U64(excess_blob_gas), + slot, + ORACLE_SCHEDULE, + ) + competitor = local.calculate_fork_local_blob_price( + U64(excess_blob_gas), + parameters, + ) + assert competitor == oracle + + +@pytest.mark.parametrize( + "duration_ms,sequential", + [ + (8_000, local.FORK_8S), + (6_000, local.FORK_6S), + ], +) +def test_direct_baseline_blob_constants_are_not_blocked_by_rounding( + duration_ms: int, + sequential: local.ForkLocalELParameters, +) -> None: + """Discrete blob rounding does not force the EL to own duration history. + + A fork may choose explicit constants against the original wall-clock + objective rather than mechanically carrying forward sequential rounding. + For the synthetic 8s and 6s eras, direct-baseline constants are at least as + close to the original target/max blob throughput as the sequential oracle. + """ + direct = scale_blob_schedule( + BASE_BLOB_SCHEDULE, + BASE_SLOT_DURATION_MS, + Uint(duration_ms), + ) + sequential_blob = _blob_parameters(sequential) + + assert direct != sequential_blob + + baseline_target_rate = Fraction( + int(BASE_BLOB_SCHEDULE.target), + int(BASE_SLOT_DURATION_MS), + ) + baseline_max_rate = Fraction( + int(BASE_BLOB_SCHEDULE.maximum), + int(BASE_SLOT_DURATION_MS), + ) + + assert _rate_error( + int(direct.target), + duration_ms, + baseline_target_rate, + ) <= _rate_error( + int(sequential_blob.target), + duration_ms, + baseline_target_rate, + ) + assert _rate_error( + int(direct.maximum), + duration_ms, + baseline_max_rate, + ) <= _rate_error( + int(sequential_blob.maximum), + duration_ms, + baseline_max_rate, + ) + + +def test_schedule_free_competitor_covers_every_oracle_era() -> None: + """The experiment has a fixed EL snapshot for every oracle duration era.""" + assert tuple(case[0] for case in ERA_CASES) == (12_000, 10_000, 8_000, 6_000) + assert tuple(case[2].gas_limit_target for case in ERA_CASES) == ( + Uint(72_000_000), + Uint(60_000_000), + Uint(48_000_000), + Uint(36_000_000), + ) From bfdea5b54fa9d74741079cc35f305cf9aa950c04 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:18:39 -0500 Subject: [PATCH 3/8] ci(eip8198): run fork-local falsifier on hosted runner --- .../eip8198-fork-local-falsifier.yaml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/eip8198-fork-local-falsifier.yaml diff --git a/.github/workflows/eip8198-fork-local-falsifier.yaml b/.github/workflows/eip8198-fork-local-falsifier.yaml new file mode 100644 index 00000000000..de323a4ff77 --- /dev/null +++ b/.github/workflows/eip8198-fork-local-falsifier.yaml @@ -0,0 +1,29 @@ +name: EIP-8198 Fork-Local Falsifier + +on: + pull_request: + paths: + - "packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/**" + - ".github/workflows/eip8198-fork-local-falsifier.yaml" + workflow_dispatch: + +jobs: + focused: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/setup-uv + - name: Ruff check + run: > + uv run ruff check + packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py + packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py + - name: Ruff format check + run: > + uv run ruff format --check + packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py + packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py + - name: Focused falsification suite + run: > + uv run pytest -q + packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py From 962436227de0106be6c8377f891931f9bd72bec2 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:20:32 -0500 Subject: [PATCH 4/8] style(eip8198): conform fork-local competitor to repo lint --- .../eip8198_quick_slots/fork_local_competitor.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py index f8faa486e51..731eaf3c218 100644 --- a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py +++ b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/fork_local_competitor.py @@ -1,4 +1,5 @@ -"""Schedule-free EIP-8198 execution-layer competitor. +""" +Schedule-free EIP-8198 execution-layer competitor. Each synthetic fork carries only the execution constants needed by that fork. There is no runtime slot-duration schedule, epoch lookup, or slot lookup here. @@ -7,9 +8,8 @@ from dataclasses import dataclass from typing import Final, final -from ethereum_types.numeric import U64, Uint - from ethereum.utils.numeric import taylor_exponential +from ethereum_types.numeric import U64, Uint ELASTICITY_MULTIPLIER: Final[Uint] = Uint(2) BLOB_MIN_GASPRICE: Final[Uint] = Uint(1) @@ -71,7 +71,9 @@ def calculate_fork_local_base_fee( parent_base_fee_per_gas: Uint, parameters: ForkLocalELParameters, ) -> Uint: - """Calculate base fee using only constants compiled into the active fork.""" + """ + Calculate base fee using only constants compiled into the active fork. + """ parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER if parent_gas_used == parent_gas_target: @@ -104,7 +106,9 @@ def calculate_fork_local_blob_price( excess_blob_gas: U64, parameters: ForkLocalELParameters, ) -> Uint: - """Calculate blob price using only constants compiled into the active fork.""" + """ + Calculate blob price using only constants compiled into the active fork. + """ return taylor_exponential( BLOB_MIN_GASPRICE, Uint(excess_blob_gas), From ab9a285e8186b678b0b4e80f8023a1dd11694a89 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:20:35 -0500 Subject: [PATCH 5/8] style(eip8198): conform falsifier suite to repo lint --- .../test_fork_local_reparameterization.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py index a4b82d006c0..04bae21e1b4 100644 --- a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py +++ b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py @@ -1,4 +1,5 @@ -"""Try to falsify fork-local EIP-8198 re-parameterisation. +""" +Try to falsify fork-local EIP-8198 re-parameterisation. The schedule-driven implementation is the behavioral oracle. The competitor contains no runtime duration schedule: each synthetic fork has only fixed EL @@ -6,11 +7,10 @@ expose the first counterexample. """ -from fractions import Fraction import inspect +from fractions import Fraction import pytest -from ethereum_types.numeric import U64, Uint from ethereum.forks.amsterdam.fork import calculate_base_fee_per_gas from ethereum.forks.amsterdam.slot_timing import ( @@ -23,6 +23,7 @@ scale_blob_schedule, scale_transition_limit, ) +from ethereum_types.numeric import U64, Uint from . import fork_local_competitor as local @@ -60,7 +61,9 @@ def _rate_error(value: int, period_ms: int, baseline: Fraction) -> Fraction: def test_competitor_has_no_runtime_schedule_dependency() -> None: - """The competing runtime model contains no schedule/slot/epoch machinery.""" + """ + Confirm that the competing runtime model has no schedule machinery. + """ fields = tuple(local.ForkLocalELParameters.__dataclass_fields__) assert fields == ( "gas_limit_target", @@ -132,7 +135,9 @@ def test_fork_local_blob_constants_can_match_schedule_oracle( slot: U64, parameters: local.ForkLocalELParameters, ) -> None: - """A fork can carry the same blob constants without runtime schedule lookup.""" + """ + Confirm a fork can carry matching blob constants without runtime lookup. + """ oracle = get_blob_schedule(slot, ORACLE_SCHEDULE) assert _blob_parameters(parameters) == oracle @@ -172,7 +177,8 @@ def test_direct_baseline_blob_constants_are_not_blocked_by_rounding( duration_ms: int, sequential: local.ForkLocalELParameters, ) -> None: - """Discrete blob rounding does not force the EL to own duration history. + """ + Show that discrete blob rounding does not require duration history. A fork may choose explicit constants against the original wall-clock objective rather than mechanically carrying forward sequential rounding. @@ -218,8 +224,15 @@ def test_direct_baseline_blob_constants_are_not_blocked_by_rounding( def test_schedule_free_competitor_covers_every_oracle_era() -> None: - """The experiment has a fixed EL snapshot for every oracle duration era.""" - assert tuple(case[0] for case in ERA_CASES) == (12_000, 10_000, 8_000, 6_000) + """ + Confirm a fixed EL snapshot exists for every oracle duration era. + """ + assert tuple(case[0] for case in ERA_CASES) == ( + 12_000, + 10_000, + 8_000, + 6_000, + ) assert tuple(case[2].gas_limit_target for case in ERA_CASES) == ( Uint(72_000_000), Uint(60_000_000), From 4000c1a2b8c150638700a560365df590e2a2b88b Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:22:03 -0500 Subject: [PATCH 6/8] style(eip8198): match package import grouping From dbe7cd89bc5dafd99d0f0a0471c3169e2264c067 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:22:51 -0500 Subject: [PATCH 7/8] style(eip8198): apply ruff import ordering From 4cecc230422ad01815c0535d20d877dca4525d34 Mon Sep 17 00:00:00 2001 From: chugarchugarr Date: Wed, 23 Sep 2026 10:24:59 -0500 Subject: [PATCH 8/8] style(eip8198): align third-party import block --- .../eip8198_quick_slots/test_fork_local_reparameterization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py index 04bae21e1b4..8e6a71af702 100644 --- a/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py +++ b/packages/testing/src/execution_testing/evm_tools/tests/eip8198_quick_slots/test_fork_local_reparameterization.py @@ -11,7 +11,6 @@ from fractions import Fraction import pytest - from ethereum.forks.amsterdam.fork import calculate_base_fee_per_gas from ethereum.forks.amsterdam.slot_timing import ( BASE_BLOB_SCHEDULE,