Skip to content

Commit

Permalink
json schema verification: verify block headers in generated blockchai…
Browse files Browse the repository at this point in the history
…n tests
  • Loading branch information
winsvega committed Nov 27, 2024
1 parent 73f8cd5 commit 31273b7
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/ethereum_test_fixtures/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .blockchain import Fixture as BlockchainFixture
from .eof import Fixture as EOFFixture
from .state import Fixture as StateFixture
from .verify_format import VerifyFixtureJson

FixtureModel = BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture

Expand Down Expand Up @@ -64,7 +65,9 @@ def collect_into_file(self, file_path: Path):
"""
json_fixtures: Dict[str, Dict[str, Any]] = {}
for name, fixture in self.items():
json_fixtures[name] = fixture.json_dict_with_info()
fixture_json = fixture.json_dict_with_info()
VerifyFixtureJson(name, fixture_json)
json_fixtures[name] = fixture_json
with open(file_path, "w") as f:
json.dump(json_fixtures, f, indent=4)

Expand Down
102 changes: 102 additions & 0 deletions src/ethereum_test_fixtures/schemas/blockchain/genesis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
Define genesisHeader schema for filled .json tests
"""

from pydantic import BaseModel, model_validator

from ..common.types import (
DataBytes,
FixedHash8,
FixedHash20,
FixedHash32,
FixedHash256,
PrefixedEvenHex,
)


class BlockRecord(BaseModel):
"""Block record in blockchain tests"""

blockHeader: dict # noqa: N815
rlp: str
transactions: list # noqa: N815
uncleHeaders: list # noqa: N815


class FrontierHeader(BaseModel):
"""Frontier block header in test json"""

bloom: FixedHash256
coinbase: FixedHash20
difficulty: PrefixedEvenHex
extraData: DataBytes # noqa: N815"
gasLimit: PrefixedEvenHex # noqa: N815"
gasUsed: PrefixedEvenHex # noqa: N815"
hash: FixedHash32
mixHash: FixedHash32 # noqa: N815"
nonce: FixedHash8
number: PrefixedEvenHex
parentHash: FixedHash32 # noqa: N815"
receiptTrie: FixedHash32 # noqa: N815"
stateRoot: FixedHash32 # noqa: N815"
timestamp: PrefixedEvenHex
transactionsTrie: FixedHash32 # noqa: N815"
uncleHash: FixedHash32 # noqa: N815"

class Config:
"""Forbids any extra fields that are not declared in the model"""

extra = "forbid"


class HomesteadHeader(FrontierHeader):
"""Homestead block header in test json"""


class ByzantiumHeader(HomesteadHeader):
"""Byzantium block header in test json"""


class ConstantinopleHeader(ByzantiumHeader):
"""Constantinople block header in test json"""


class IstanbulHeader(ConstantinopleHeader):
"""Istanbul block header in test json"""


class BerlinHeader(IstanbulHeader):
"""Berlin block header in test json"""


class LondonHeader(BerlinHeader):
"""London block header in test json"""

baseFeePerGas: PrefixedEvenHex # noqa: N815


class ParisHeader(LondonHeader):
"""Paris block header in test json"""

@model_validator(mode="after")
def check_block_header(self):
"""
Validate Paris block header rules
"""

if self.difficulty != "0x00":
raise ValueError("Starting from Paris, block difficulty must be 0x00")


class ShanghaiHeader(ParisHeader):
"""Shanghai block header in test json"""

withdrawalsRoot: FixedHash32 # noqa: N815


class CancunHeader(ShanghaiHeader):
"""Cancun block header in test json"""

blobGasUsed: PrefixedEvenHex # noqa: N815
excessBlobGas: PrefixedEvenHex # noqa: N815
parentBeaconBlockRoot: FixedHash32 # noqa: N815
69 changes: 69 additions & 0 deletions src/ethereum_test_fixtures/schemas/blockchain/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Schema for filled Blockchain Test
"""

from pydantic import BaseModel, Field, model_validator

from .genesis import (
BerlinHeader,
BlockRecord,
ByzantiumHeader,
CancunHeader,
ConstantinopleHeader,
FrontierHeader,
HomesteadHeader,
IstanbulHeader,
LondonHeader,
ParisHeader,
ShanghaiHeader,
)


class BlockchainTestFixtureModel(BaseModel):
"""
Blockchain test file
"""

info: dict = Field(alias="_info")
network: str
genesisBlockHeader: dict # noqa: N815
pre: dict
postState: dict # noqa: N815
lastblockhash: str
genesisRLP: str # noqa: N815
blocks: list[BlockRecord]
sealEngine: str # noqa: N815

class Config:
"""Forbids any extra fields that are not declared in the model"""

extra = "forbid"

@model_validator(mode="after")
def check_block_headers(self):
"""
Validate genesis header fields based by fork
"""
# TODO str to Fork class comparison
allowed_networks = {
"Frontier": FrontierHeader,
"Homestead": HomesteadHeader,
"EIP150": HomesteadHeader,
"EIP158": HomesteadHeader,
"Byzantium": ByzantiumHeader,
"Constantinople": ConstantinopleHeader,
"ConstantinopleFix": ConstantinopleHeader,
"Istanbul": IstanbulHeader,
"Berlin": BerlinHeader,
"London": LondonHeader,
"Paris": ParisHeader,
"Shanghai": ShanghaiHeader,
"Cancun": CancunHeader,
}

header_class = allowed_networks.get(self.network)
if not header_class:
raise ValueError("Incorrect value in network field: " + self.network)
header_class(**self.genesisBlockHeader)
for block in self.blocks:
header_class(**block.blockHeader)
139 changes: 139 additions & 0 deletions src/ethereum_test_fixtures/schemas/common/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
Base types for Pydantic json test fixtures
"""

import re
from typing import Generic, TypeVar

from pydantic import RootModel, model_validator

T = TypeVar("T", bound="FixedHash")


class FixedHash(RootModel[str], Generic[T]):
"""Base class for fixed-length hashes."""

_length_in_bytes: int

@model_validator(mode="after")
def validate_hex_hash(self):
"""
Validate that the field is a 0x-prefixed hash of specified byte length.
"""
expected_length = 2 + 2 * self._length_in_bytes # 2 for '0x' + 2 hex chars per byte
if not self.root.startswith("0x"):
raise ValueError("The hash must start with '0x'.")
if len(self.root) != expected_length:
raise ValueError(
f"The hash must be {expected_length} characters long "
f"(2 for '0x' and {2 * self._length_in_bytes} hex characters)."
)
if not re.fullmatch(rf"0x[a-fA-F0-9]{{{2 * self._length_in_bytes}}}", self.root):
raise ValueError(
f"The hash must be a valid hexadecimal string of "
f"{2 * self._length_in_bytes} characters after '0x'."
)


class FixedHash32(FixedHash):
"""FixedHash32 type (32 bytes)"""

_length_in_bytes = 32


class FixedHash20(FixedHash):
"""FixedHash20 type (20 bytes)"""

_length_in_bytes = 20


class FixedHash8(FixedHash):
"""FixedHash8 type (8 bytes)"""

_length_in_bytes = 8


class FixedHash256(FixedHash):
"""FixedHash256 type (256 bytes)"""

_length_in_bytes = 256


class PrefixedEvenHex(RootModel[str]):
"""Class to validate a hexadecimal integer encoding in test files."""

def __eq__(self, other):
"""
For python str comparison
"""
if isinstance(other, str):
return self.root == other
return NotImplemented

@model_validator(mode="after")
def validate_hex_integer(self):
"""
Validate that the field is a hexadecimal integer with specific rules:
- Must start with '0x'.
- Must be even in length after '0x'.
- Cannot be '0x0', '0x0000', etc. (minimum is '0x00').
"""
# Ensure it starts with '0x'
if not self.root.startswith("0x"):
raise ValueError("The value must start with '0x'.")

# Extract the hex portion (after '0x')
hex_part = self.root[2:]

# Ensure the length of the hex part is even
if len(hex_part) % 2 != 0:
raise ValueError(
"The hexadecimal value must have an even number of characters after '0x'."
)

# Special rule: Only '0x00' is allowed; disallow '0x0000', '0x0001', etc.
if hex_part.startswith("00") and hex_part != "00":
raise ValueError("Leading zeros are not allowed except for '0x00'.")

# Ensure it's a valid hexadecimal string
if not re.fullmatch(r"[a-fA-F0-9]+", hex_part):
raise ValueError("The value must be a valid hexadecimal string.")

return self


class DataBytes(RootModel[str]):
"""Class to validate DataBytes."""

@model_validator(mode="after")
def validate_data_bytes(self):
"""
Validate that the field follows the rules for DataBytes:
- Must start with '0x'.
- Can be empty (just '0x').
- Must be even in length after '0x'.
- Allows prefixed '00' values (e.g., '0x00000001').
- Must be a valid hexadecimal string.
"""
# Ensure it starts with '0x'
if not self.root.startswith("0x"):
raise ValueError("The value must start with '0x'.")

# Extract the hex portion (after '0x')
hex_part = self.root[2:]

# Allow empty '0x'
if len(hex_part) == 0:
return self

# Ensure the length of the hex part is even
if len(hex_part) % 2 != 0:
raise ValueError(
"The hexadecimal value must have an even number of characters after '0x'."
)

# Ensure it's a valid hexadecimal string
if not re.fullmatch(r"[a-fA-F0-9]*", hex_part): # `*` allows empty hex_part for '0x'
raise ValueError("The value must be a valid hexadecimal string.")

return self
30 changes: 30 additions & 0 deletions src/ethereum_test_fixtures/verify_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Verify the sanity of fixture .json format
"""

from pydantic import ValidationError

from .schemas.blockchain.test import BlockchainTestFixtureModel


class VerifyFixtureJson:
"""
Class to verify the correctness of a fixture JSON.
"""

def __init__(self, name: str, fixture_json: dict):
self.fixture_json = fixture_json
self.fixture_name = name
if self.fixture_json.get("network") and not self.fixture_json.get("engineNewPayloads"):
self.verify_blockchain_fixture_json()

def verify_blockchain_fixture_json(self):
"""
Function to verify blockchain json fixture
"""
try:
BlockchainTestFixtureModel(**self.fixture_json)
except ValidationError as e:
raise Exception(
f"Error in generated blockchain test json ({self.fixture_name})" + e.json()
)
6 changes: 5 additions & 1 deletion whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ fp2
fromhex
frozenbidict
func
fullmatch
g1
g1add
g1msm
Expand Down Expand Up @@ -207,7 +208,10 @@ Golang
gwei
hacky
hardfork
hash8
hash32
hash20
hash256
Hashable
hasher
HeaderNonce
Expand Down Expand Up @@ -810,4 +814,4 @@ E9
EB
EF
F6
FC
FC

0 comments on commit 31273b7

Please sign in to comment.