Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 80a49e6

Browse files
committedDec 4, 2024·
verify blockHeader == encoded rlp in generated tests
1 parent 31273b7 commit 80a49e6

File tree

5 files changed

+234
-122
lines changed

5 files changed

+234
-122
lines changed
 

‎src/ethereum_test_fixtures/schemas/blockchain/genesis.py

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
Define genesisHeader schema for filled .json tests
3+
"""
4+
5+
from typing import Any, Optional
6+
7+
from pydantic import BaseModel, Field, model_validator
8+
from rlp import decode as rlp_decode
9+
10+
from ..common.types import (
11+
DataBytes,
12+
FixedHash8,
13+
FixedHash20,
14+
FixedHash32,
15+
FixedHash256,
16+
PrefixedEvenHex,
17+
)
18+
19+
20+
class InvalidBlockRecord(BaseModel):
21+
"""Invalid block rlp only provided"""
22+
23+
rlp: str
24+
expectException: str # noqa: N815
25+
rlp_decoded: Optional[dict] = None
26+
27+
class Config:
28+
"""Forbids any extra fields that are not declared in the model"""
29+
30+
extra = "forbid"
31+
32+
33+
class BlockRecord(BaseModel):
34+
"""Block record in blockchain tests"""
35+
36+
blockHeader: dict # noqa: N815
37+
rlp: str
38+
transactions: list # noqa: N815
39+
uncleHeaders: list # noqa: N815
40+
41+
blocknumber: Optional[str] = Field(
42+
None, description="Block number for the users to read in the tests"
43+
)
44+
45+
class Config:
46+
"""Forbids any extra fields that are not declared in the model"""
47+
48+
extra = "forbid"
49+
50+
51+
class BlockRecordShanghai(BlockRecord):
52+
"""Block record in blockchain tests"""
53+
54+
withdrawals: list
55+
56+
57+
class FrontierHeader(BaseModel):
58+
"""Frontier block header in test json"""
59+
60+
bloom: FixedHash256
61+
coinbase: FixedHash20
62+
difficulty: PrefixedEvenHex
63+
extraData: DataBytes # noqa: N815"
64+
gasLimit: PrefixedEvenHex # noqa: N815"
65+
gasUsed: PrefixedEvenHex # noqa: N815"
66+
hash: FixedHash32
67+
mixHash: FixedHash32 # noqa: N815"
68+
nonce: FixedHash8
69+
number: PrefixedEvenHex
70+
parentHash: FixedHash32 # noqa: N815"
71+
receiptTrie: FixedHash32 # noqa: N815"
72+
stateRoot: FixedHash32 # noqa: N815"
73+
timestamp: PrefixedEvenHex
74+
transactionsTrie: FixedHash32 # noqa: N815"
75+
uncleHash: FixedHash32 # noqa: N815"
76+
77+
class Config:
78+
"""Forbids any extra fields that are not declared in the model"""
79+
80+
extra = "forbid"
81+
82+
def get_field_rlp_order(self) -> dict[str, Any]:
83+
"""The order fields are encoded into rlp"""
84+
rlp_order: dict[str, Any] = {
85+
"parentHash": self.parentHash,
86+
"uncleHash": self.uncleHash,
87+
"coinbase": self.coinbase,
88+
"stateRoot": self.stateRoot,
89+
"transactionsTrie": self.transactionsTrie,
90+
"receiptTrie": self.receiptTrie,
91+
"bloom": self.bloom,
92+
"difficulty": self.difficulty,
93+
"number": self.number,
94+
"gasLimit": self.gasLimit,
95+
"gasUsed": self.gasUsed,
96+
"timestamp": self.timestamp,
97+
"extraData": self.extraData,
98+
"mixHash": self.mixHash,
99+
"nonce": self.nonce,
100+
}
101+
return rlp_order
102+
103+
104+
class HomesteadHeader(FrontierHeader):
105+
"""Homestead block header in test json"""
106+
107+
108+
class ByzantiumHeader(HomesteadHeader):
109+
"""Byzantium block header in test json"""
110+
111+
112+
class ConstantinopleHeader(ByzantiumHeader):
113+
"""Constantinople block header in test json"""
114+
115+
116+
class IstanbulHeader(ConstantinopleHeader):
117+
"""Istanbul block header in test json"""
118+
119+
120+
class BerlinHeader(IstanbulHeader):
121+
"""Berlin block header in test json"""
122+
123+
124+
class LondonHeader(BerlinHeader):
125+
"""London block header in test json"""
126+
127+
baseFeePerGas: PrefixedEvenHex # noqa: N815
128+
129+
def get_field_rlp_order(self) -> dict[str, Any]:
130+
"""The order fields are encoded into rlp"""
131+
rlp_order: dict[str, Any] = super().get_field_rlp_order()
132+
rlp_order["baseFeePerGas"] = self.baseFeePerGas
133+
return rlp_order
134+
135+
136+
class ParisHeader(LondonHeader):
137+
"""Paris block header in test json"""
138+
139+
@model_validator(mode="after")
140+
def check_block_header(self):
141+
"""
142+
Validate Paris block header rules
143+
"""
144+
if self.difficulty != "0x00":
145+
raise ValueError("Starting from Paris, block difficulty must be 0x00")
146+
147+
148+
class ShanghaiHeader(ParisHeader):
149+
"""Shanghai block header in test json"""
150+
151+
withdrawalsRoot: FixedHash32 # noqa: N815
152+
153+
def get_field_rlp_order(self) -> dict[str, Any]:
154+
"""The order fields are encoded into rlp"""
155+
rlp_order: dict[str, Any] = super().get_field_rlp_order()
156+
rlp_order["withdrawalsRoot"] = self.withdrawalsRoot
157+
return rlp_order
158+
159+
160+
class CancunHeader(ShanghaiHeader):
161+
"""Cancun block header in test json"""
162+
163+
blobGasUsed: PrefixedEvenHex # noqa: N815
164+
excessBlobGas: PrefixedEvenHex # noqa: N815
165+
parentBeaconBlockRoot: FixedHash32 # noqa: N815
166+
167+
def get_field_rlp_order(self) -> dict[str, Any]:
168+
"""The order fields are encoded into rlp"""
169+
rlp_order: dict[str, Any] = super().get_field_rlp_order()
170+
rlp_order["blobGasUsed"] = self.blobGasUsed
171+
rlp_order["excessBlobGas"] = self.excessBlobGas
172+
rlp_order["parentBeaconBlockRoot"] = self.parentBeaconBlockRoot
173+
return rlp_order
174+
175+
176+
def verify_block_header_vs_rlp_string(header: FrontierHeader, rlp_string: str):
177+
"""Check that rlp encoding of block header match header object"""
178+
rlp = rlp_decode(bytes.fromhex(rlp_string[2:]))[0]
179+
for rlp_index, (field_name, field) in enumerate(header.get_field_rlp_order().items()):
180+
rlp_hex = rlp[rlp_index].hex()
181+
"""special rlp rule"""
182+
if rlp_hex == "" and field_name not in ["data", "to", "extraData"]:
183+
rlp_hex = "00"
184+
""""""
185+
json_hex = field.root[2:]
186+
if rlp_hex != json_hex:
187+
raise ValueError(
188+
f"Field `{field_name}` in json not equal to it's rlp encoding:"
189+
f"\n {json_hex} != {rlp_hex}"
190+
)

‎src/ethereum_test_fixtures/schemas/blockchain/test.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@
22
Schema for filled Blockchain Test
33
"""
44

5+
from typing import Tuple
6+
57
from pydantic import BaseModel, Field, model_validator
68

7-
from .genesis import (
9+
from .headers import (
810
BerlinHeader,
911
BlockRecord,
12+
BlockRecordShanghai,
1013
ByzantiumHeader,
1114
CancunHeader,
1215
ConstantinopleHeader,
1316
FrontierHeader,
1417
HomesteadHeader,
18+
InvalidBlockRecord,
1519
IstanbulHeader,
1620
LondonHeader,
1721
ParisHeader,
1822
ShanghaiHeader,
23+
verify_block_header_vs_rlp_string,
1924
)
2025

2126

@@ -31,7 +36,7 @@ class BlockchainTestFixtureModel(BaseModel):
3136
postState: dict # noqa: N815
3237
lastblockhash: str
3338
genesisRLP: str # noqa: N815
34-
blocks: list[BlockRecord]
39+
blocks: list
3540
sealEngine: str # noqa: N815
3641

3742
class Config:
@@ -45,25 +50,40 @@ def check_block_headers(self):
4550
Validate genesis header fields based by fork
4651
"""
4752
# TODO str to Fork class comparison
48-
allowed_networks = {
49-
"Frontier": FrontierHeader,
50-
"Homestead": HomesteadHeader,
51-
"EIP150": HomesteadHeader,
52-
"EIP158": HomesteadHeader,
53-
"Byzantium": ByzantiumHeader,
54-
"Constantinople": ConstantinopleHeader,
55-
"ConstantinopleFix": ConstantinopleHeader,
56-
"Istanbul": IstanbulHeader,
57-
"Berlin": BerlinHeader,
58-
"London": LondonHeader,
59-
"Paris": ParisHeader,
60-
"Shanghai": ShanghaiHeader,
61-
"Cancun": CancunHeader,
53+
allowed_networks: dict[str, Tuple[FrontierHeader, BlockRecord]] = {
54+
"Frontier": (FrontierHeader, BlockRecord),
55+
"Homestead": (HomesteadHeader, BlockRecord),
56+
"EIP150": (HomesteadHeader, BlockRecord),
57+
"EIP158": (HomesteadHeader, BlockRecord),
58+
"Byzantium": (ByzantiumHeader, BlockRecord),
59+
"Constantinople": (ConstantinopleHeader, BlockRecord),
60+
"ConstantinopleFix": (ConstantinopleHeader, BlockRecord),
61+
"Istanbul": (IstanbulHeader, BlockRecord),
62+
"Berlin": (BerlinHeader, BlockRecord),
63+
"London": (LondonHeader, BlockRecord),
64+
"Paris": (ParisHeader, BlockRecord),
65+
"Shanghai": (ShanghaiHeader, BlockRecordShanghai),
66+
"Cancun": (CancunHeader, BlockRecordShanghai),
67+
"ShanghaiToCancunAtTime15k": (ShanghaiHeader, BlockRecordShanghai),
6268
}
6369

64-
header_class = allowed_networks.get(self.network)
65-
if not header_class:
70+
# Check that each block in test is of format of the test declared fork
71+
header_class, record_type = allowed_networks.get(self.network, (None, None))
72+
if header_class is None:
6673
raise ValueError("Incorrect value in network field: " + self.network)
67-
header_class(**self.genesisBlockHeader)
74+
header = header_class(**self.genesisBlockHeader)
75+
verify_block_header_vs_rlp_string(header, self.genesisRLP)
6876
for block in self.blocks:
69-
header_class(**block.blockHeader)
77+
if "expectException" in block:
78+
record: InvalidBlockRecord = InvalidBlockRecord(**block)
79+
# Do not verify rlp_decoded with invalid block rlp
80+
else:
81+
if (
82+
self.network == "ShanghaiToCancunAtTime15k"
83+
and int(block["blockHeader"]["timestamp"], 16) >= 15000
84+
):
85+
header_class = CancunHeader
86+
87+
record: BlockRecord = record_type(**block)
88+
header = header_class(**record.blockHeader)
89+
verify_block_header_vs_rlp_string(header, record.rlp)

‎src/ethereum_test_fixtures/schemas/common/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def validate_hex_hash(self):
3333
f"The hash must be a valid hexadecimal string of "
3434
f"{2 * self._length_in_bytes} characters after '0x'."
3535
)
36+
return self
3637

3738

3839
class FixedHash32(FixedHash):
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.