Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forks,specs,fixtures): Add Blob Schedule #1040

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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
22 changes: 13 additions & 9 deletions src/ethereum_test_fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ClassVar,
List,
Literal,
Mapping,
Tuple,
Union,
cast,
Expand Down Expand Up @@ -126,9 +127,6 @@ class FixtureHeader(CamelModel):
None
)
requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None)
target_blobs_per_block: (
Annotated[ZeroPaddedHexNumber, HeaderForkRequirement("target_blobs_per_block")] | None
) = Field(None)

fork: Fork | None = Field(None, exclude=True)

Expand Down Expand Up @@ -329,12 +327,6 @@ def from_fixture_header(
raise ValueError(f"Requests are required for ${fork}.")
params.append(requests)

if fork.engine_new_payload_target_blobs_per_block(header.number, header.timestamp):
target_blobs_per_block = header.target_blobs_per_block
if target_blobs_per_block is None:
raise ValueError(f"Target blobs per block is required for ${fork}.")
params.append(target_blobs_per_block)

payload_params: EngineNewPayloadParameters = cast(
EngineNewPayloadParameters,
tuple(params),
Expand Down Expand Up @@ -454,6 +446,16 @@ class InvalidFixtureBlock(CamelModel):
rlp_decoded: FixtureBlockBase | None = Field(None, alias="rlp_decoded")


class FixtureForkBlobSchedule(CamelModel):
"""
Representation of the blob schedule within a test Fixture, which is valid throughout all the
blocks in the fixture.
"""

target_blobs_per_block: HexNumber = Field(..., alias="target")
max_blobs_per_block: HexNumber = Field(..., alias="max")


class FixtureCommon(BaseFixture):
"""
Base blockchain test fixture model.
Expand All @@ -465,6 +467,8 @@ class FixtureCommon(BaseFixture):
post_state: Alloc | None = Field(None)
last_block_hash: Hash = Field(..., alias="lastblockhash") # FIXME: lastBlockHash

blob_schedule: Mapping[str, FixtureForkBlobSchedule] | None = None

def get_fork(self) -> str | None:
"""
Returns the fork of the fixture as a string.
Expand Down
4 changes: 4 additions & 0 deletions src/ethereum_test_forks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from .forks.transition import (
BerlinToLondonAt5,
CancunToPragueAtTime15k,
ParisToShanghaiAtTime15k,
ShanghaiToCancunAtTime15k,
)
Expand All @@ -44,10 +45,12 @@
transition_fork_from_to,
transition_fork_to,
)
from .transition_base_fork import TransitionFork

__all__ = [
"Fork",
"ForkAttribute",
"TransitionFork",
"ArrowGlacier",
"Berlin",
"BerlinToLondonAt5",
Expand All @@ -66,6 +69,7 @@
"Shanghai",
"ShanghaiToCancunAtTime15k",
"Cancun",
"CancunToPragueAtTime15k",
"Prague",
"Osaka",
"get_transition_forks",
Expand Down
40 changes: 30 additions & 10 deletions src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ def name(cls) -> str:
"""
return ""

def parent(cls) -> Type["BaseFork"] | None:
"""
Returns the parent of the fork.
"""
base_class = cls.__bases__[0]
assert issubclass(base_class, BaseFork)
if base_class == BaseFork:
return None
return base_class

def parents(cls) -> List[Type["BaseFork"]]:
"""
To be implemented by the fork base class.
"""
parents: List[Type["BaseFork"]] = []
parent = cls.parent()
while parent is not None:
parents.insert(0, parent)
parent = parent.parent()
return parents

def __repr__(cls) -> str:
"""
Used to properly print the name of the fork, instead of the class.
Expand Down Expand Up @@ -283,14 +304,6 @@ def transaction_intrinsic_cost_calculator(
"""
pass

@classmethod
@abstractmethod
def header_target_blobs_per_block_required(cls, block_number: int, timestamp: int) -> bool:
"""
Returns true if the header must contain target blobs per block.
"""
pass

@classmethod
@abstractmethod
def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:
Expand All @@ -301,15 +314,15 @@ def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:

@classmethod
@abstractmethod
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Returns the target blobs per block for a given fork.
"""
pass

@classmethod
@abstractmethod
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Returns the max blobs per block for a given fork.
"""
Expand Down Expand Up @@ -532,6 +545,13 @@ def fork_at(cls, block_number: int = 0, timestamp: int = 0) -> Type["BaseFork"]:
"""
return cls

@classmethod
def is_transition_fork(cls) -> bool:
"""
Returns whether the fork is a transition fork or not.
"""
return False

@classmethod
@abstractmethod
def transition_tool_name(cls, block_number: int = 0, timestamp: int = 0) -> str:
Expand Down
35 changes: 6 additions & 29 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,18 +252,18 @@ def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:
return 0

@classmethod
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Returns the target number of blobs per block for a given fork.
"""
return 0
return None

@classmethod
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Returns the max number of blobs per block for a given fork.
"""
return 0
return None

@classmethod
def header_requests_required(cls, block_number: int, timestamp: int) -> bool:
Expand All @@ -288,17 +288,6 @@ def header_beacon_root_required(cls, block_number: int = 0, timestamp: int = 0)
"""
return False

@classmethod
def header_target_blobs_per_block_required(
cls,
block_number: int = 0,
timestamp: int = 0,
) -> bool:
"""
At genesis, header must not contain target blobs per block.
"""
return False

@classmethod
def engine_new_payload_blob_hashes(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""
Expand Down Expand Up @@ -1018,14 +1007,14 @@ def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:
return 2**17

@classmethod
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Blobs are enabled starting from Cancun, with a static target of 3 blobs.
"""
return 3

@classmethod
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int:
def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int | None:
"""
Blobs are enabled starting from Cancun, with a static max of 6 blobs.
"""
Expand Down Expand Up @@ -1324,18 +1313,6 @@ def header_requests_required(cls, block_number: int, timestamp: int) -> bool:
"""
return True

@classmethod
def header_target_blobs_per_block_required(
cls,
block_number: int = 0,
timestamp: int = 0,
) -> bool:
"""
Prague requires that the execution layer header contains the beacon
chain target blobs per block.
"""
return True

@classmethod
def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""
Expand Down
11 changes: 11 additions & 0 deletions src/ethereum_test_forks/tests/test_forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def test_forks():
assert cast(Fork, ParisToShanghaiAtTime15k).header_withdrawals_required(0, 15_000) is True
assert cast(Fork, ParisToShanghaiAtTime15k).header_withdrawals_required() is True


def test_fork_comparison(): # noqa: D103
# Test fork comparison
assert Paris > Berlin
assert not Berlin > Paris
Expand Down Expand Up @@ -159,6 +161,15 @@ def test_forks():
assert fork == Berlin


def test_fork_parents(): # noqa: D103
assert London.parent() == Berlin
assert Paris.parent() == London

assert London.parents()[-1] == Berlin
assert London.parents()[0] == Frontier
assert len(London.parents()) == 7


def test_get_forks(): # noqa: D103
all_forks = get_forks()
assert all_forks[0] == FIRST_DEPLOYED
Expand Down
19 changes: 19 additions & 0 deletions src/ethereum_test_forks/transition_base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ def transitions_from(cls) -> Fork:
"""
raise Exception("Not implemented")

@classmethod
def is_transition_fork(cls) -> bool:
"""
Returns whether the fork is a transition fork or not.
"""
return True


def base_fork_abstract_methods() -> List[str]:
"""
Expand Down Expand Up @@ -109,3 +116,15 @@ def transition_method(
return NewTransitionClass

return decorator


class TransitionForkCombinedBase(TransitionBaseClass, BaseFork):
"""
Combined base class
"""

pass


# Transition Fork Type
TransitionFork = Type[TransitionForkCombinedBase]
40 changes: 35 additions & 5 deletions src/ethereum_test_specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from pprint import pprint
from typing import Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type
from typing import Any, Callable, ClassVar, Dict, Generator, List, Mapping, Optional, Tuple, Type

import pytest
from pydantic import ConfigDict, Field, field_validator
Expand Down Expand Up @@ -35,12 +35,13 @@
FixtureBlock,
FixtureBlockBase,
FixtureEngineNewPayload,
FixtureForkBlobSchedule,
FixtureHeader,
FixtureTransaction,
FixtureWithdrawal,
InvalidFixtureBlock,
)
from ethereum_test_forks import Fork
from ethereum_test_forks import Fork, TransitionFork
from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal

from .base import BaseTest, verify_result
Expand Down Expand Up @@ -119,7 +120,6 @@ class Header(CamelModel):
excess_blob_gas: Removable | HexNumber | None = None
parent_beacon_block_root: Removable | Hash | None = None
requests_hash: Removable | Hash | None = None
target_blobs_per_block: Removable | HexNumber | None = None

REMOVE_FIELD: ClassVar[Removable] = Removable()
"""
Expand Down Expand Up @@ -270,8 +270,6 @@ def set_environment(self, env: Environment) -> Environment:
new_env_values["blob_gas_used"] = self.blob_gas_used
if not isinstance(self.parent_beacon_block_root, Removable):
new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root
if not isinstance(self.target_blobs_per_block, Removable):
new_env_values["target_blobs_per_block"] = self.target_blobs_per_block
"""
These values are required, but they depend on the previous environment,
so they can be calculated here.
Expand Down Expand Up @@ -515,6 +513,36 @@ def network_info(self, fork: Fork, eips: Optional[List[int]] = None):
else fork.blockchain_test_network_name()
)

def blob_schedule(
self, fork: Fork | TransitionFork, eips: Optional[List[int]] = None
) -> Mapping[str, FixtureForkBlobSchedule] | None:
"""
Returns the blob schedule for the given fork and EIPs.
"""
if fork.is_transition_fork():
fork: Fork = fork.transitions_to() # type: ignore
blob_schedule: Dict[str, FixtureForkBlobSchedule] = {}
last_fork_blob_schedule: FixtureForkBlobSchedule | None = None
for fork in fork.parents() + [fork]:
if fork.target_blobs_per_block(0, 0) is None or fork.max_blobs_per_block(0, 0) is None:
continue
current_fork_blob_schedule = FixtureForkBlobSchedule(
target_blobs_per_block=fork.target_blobs_per_block(0, 0),
max_blobs_per_block=fork.max_blobs_per_block(0, 0),
)
if (
last_fork_blob_schedule is None
or current_fork_blob_schedule != last_fork_blob_schedule
):
blob_schedule[fork.name()] = current_fork_blob_schedule

last_fork_blob_schedule = current_fork_blob_schedule

if not blob_schedule:
return None

return blob_schedule

def verify_post_state(self, t8n, alloc: Alloc):
"""
Verifies the post alloc after all block/s or payload/s are generated.
Expand Down Expand Up @@ -607,6 +635,7 @@ def make_fixture(
last_block_hash=head,
pre=pre,
post_state=alloc,
blob_schedule=self.blob_schedule(fork, eips),
)

def make_hive_fixture(
Expand Down Expand Up @@ -697,6 +726,7 @@ def make_hive_fixture(
post_state=alloc,
sync_payload=sync_payload,
last_block_hash=head_hash,
blob_schedule=self.blob_schedule(fork, eips),
)

def generate(
Expand Down
Loading
Loading