-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
json schema verification: verify block headers in generated blockchai…
…n tests
- Loading branch information
Showing
6 changed files
with
349 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
src/ethereum_test_fixtures/schemas/blockchain/genesis.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters