-
Notifications
You must be signed in to change notification settings - Fork 176
Blocktest builder #2190
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
base: main
Are you sure you want to change the base?
Blocktest builder #2190
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @bshastry, really nice idea to "dynamically" generate blockchain tests from fuzzer output using EEST!
I have a few suggestions below, very happy to help you get this in a form that is more compatible with EEST's libraries and pydantic models. I'll message you on Signal to figure out how we'd like to proceed!
```json | ||
{ | ||
"version": "2.0", | ||
"fork": "Prague", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this is called network
in blockchain fixtures. I guess we can live with the inconsistency :)
src/cli/fuzzer_bridge/README.md
Outdated
|
||
## Production Test Suite | ||
|
||
See `production_test.py` for a complete example that: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
production_test.py
in missing from this PR.
- Remove incorrect venv activation instructions from CLAUDE.md - Replace ASCII art with Mermaid diagram in README - Add fuzzer_bridge as CLI entry point in pyproject.toml - Create comprehensive documentation in docs/writing_tests/ - Add Pydantic models for type-safe fuzzer output parsing - Add BlockchainTest.from_fuzzer() classmethod for better integration - Simplify BlocktestBuilder to use new architecture - Fix README inconsistencies (punctuation, references, sections) This refactoring aligns the fuzzer bridge with EEST code standards and makes it more maintainable and future-proof. π€ Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Add ruff noqa comment for mixedCase variables in Pydantic models - Fix line length issues in performance_utils.py - Replace bare except with specific Exception handling - Apply automated formatting from ruff π€ Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
862d70e
to
5472e9b
Compare
- Fix markdown linting errors in fuzzer_bridge.md documentation - Apply ruff formatting to blockchain.py - Fix mypy type errors in fuzzer bridge modules: - Make orjson import optional with proper error handling - Fix TransitionTool instantiation to use GethTransitionTool - Correct EOA usage and add missing class attributes - Fix json.dump parameter usage to avoid kwargs issues - Fix docstring line lengths to comply with 79-char limit - Fix line length issues in function signatures All linting (ruff) and type checking (mypy) now pass successfully. π€ Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
Add support for two critical Ethereum features in the fuzzer bridge: 1. EIP-7702 Authorization Lists (Prague+) - Add FuzzerAuthorization model for authorization tuples - Parse authorizationList from transactions - Translate to AuthorizationTuple in from_fuzzer() - Enables testing of SetCode transactions (tx type 0x04) 2. EIP-4788 Parent Beacon Block Root (Cancun+) - Add parentBeaconBlockRoot as top-level field in FuzzerOutput - Pass to Block during creation in from_fuzzer() - Must be 32-byte hash from consensus layer - Enables testing of beacon root contract (EIP-4788) These changes close significant coverage gaps in geth core validation: - tx_setcode.go: Authorization validation paths - block_validator.go: Beacon root handling - state_transition.go: Beacon root system contract calls The implementation follows existing patterns (similar to blob sidecar handling) and maintains backwards compatibility through optional fields. π€ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add support for splitting fuzzer transactions across multiple blocks to enable testing of state transitions and block boundaries. New CLI Options: - --num-blocks: Number of blocks to generate (default: 1) - --block-strategy: Transaction distribution strategy - "distribute": Sequential chunks preserving nonce order - "first-block": All txs in first block, rest empty - --block-time: Seconds between blocks (default: 12) Implementation: - Sequential distribution maintains nonce ordering per account - Timestamps increment by block_time for each block - Only first block receives parent_beacon_block_root - Fully backward compatible (single-block default) Example Usage: fuzzer_bridge --num-blocks 3 --block-strategy distribute \ --fork Osaka input.json output/ This enables testing multi-block scenarios like: - State evolution across blocks - Transaction dependencies spanning blocks - Block time-sensitive operations - Cross-block state transitions π€ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add --random-blocks flag to enable intelligent automatic selection of block counts for comprehensive testing coverage. New Feature: - --random-blocks: Randomly choose number of blocks (1 to min(num_txs, 10)) Implementation: - choose_random_num_blocks() helper with uniform distribution - Each file gets independent random selection in parallel mode - Edge case handling: empty blocks (0 txs β 1 block) - Cap at 10 blocks to prevent fixture bloat with large tx counts Algorithm Rationale: - Uniform distribution provides equal coverage for testing - Max of 10 blocks balances thoroughness with practicality - Independent per-file randomization maximizes corpus diversity Example Usage: # Random mode - each file gets random block count fuzzer_bridge --random-blocks --fork Osaka input/ output/ # Still works with fixed mode fuzzer_bridge --num-blocks 3 --fork Osaka input/ output/ Benefits: - Automated testing of various block configurations - Discovers edge cases in block boundary handling - Comprehensive multi-block scenario coverage - Zero configuration needed for random testing π€ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for implementing this.
Some comments primarily related to pydantic models in order to reuse as much code as possible from what we already had.
BlockchainTest instance ready for fixture generation | ||
""" | ||
from cli.fuzzer_bridge.models import FuzzerOutput |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to this import, I think this function (from_fuzzer
) does not belong here as a class method, I think it should be an external function defined in cli.fuzzer_bridge
.
@classmethod | ||
def from_fuzzer( | ||
cls, | ||
fuzzer_output: Dict[str, Any], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fuzzer_output: Dict[str, Any], | |
fuzzer_output: FuzzerOutput, |
And parse when calling.
class FuzzerEnvironment(BaseModel): | ||
"""Environment definition in fuzzer output.""" | ||
|
||
currentCoinbase: Address = Field(..., alias="currentCoinbase") | ||
currentDifficulty: HexNumber = Field(HexNumber(0), alias="currentDifficulty") | ||
currentGasLimit: HexNumber = Field(..., alias="currentGasLimit") | ||
currentNumber: HexNumber = Field(..., alias="currentNumber") | ||
currentTimestamp: HexNumber = Field(..., alias="currentTimestamp") | ||
currentBaseFee: Optional[HexNumber] = Field(None, alias="currentBaseFee") | ||
currentRandom: Optional[Hash] = Field(None, alias="currentRandom") | ||
currentExcessBlobGas: Optional[HexNumber] = Field(None, alias="currentExcessBlobGas") | ||
currentBlobGasUsed: Optional[HexNumber] = Field(None, alias="currentBlobGasUsed") | ||
parentBeaconBlockRoot: Optional[Hash] = Field(None, alias="parentBeaconBlockRoot") | ||
parentUncleHash: Optional[Hash] = Field(None, alias="parentUncleHash") | ||
parentDifficulty: Optional[HexNumber] = Field(None, alias="parentDifficulty") | ||
parentBaseFee: Optional[HexNumber] = Field(None, alias="parentBaseFee") | ||
parentGasUsed: Optional[HexNumber] = Field(None, alias="parentGasUsed") | ||
parentGasLimit: Optional[HexNumber] = Field(None, alias="parentGasLimit") | ||
|
||
class Config: | ||
"""Pydantic configuration.""" | ||
|
||
populate_by_name = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically from ethereum_test_types import Environment
, so we should just simply reuse that.
class FuzzerAccount(BaseModel): | ||
"""Account definition in fuzzer output.""" | ||
|
||
balance: HexNumber | ||
nonce: HexNumber = HexNumber(0) | ||
code: Bytes = Bytes(b"") | ||
storage: Dict[HexNumber, HexNumber] = Field(default_factory=dict) | ||
privateKey: Optional[Bytes] = Field(None, alias="privateKey") | ||
|
||
@field_validator("storage", mode="before") | ||
@classmethod | ||
def validate_storage(cls, v): | ||
"""Convert storage keys and values to HexNumber.""" | ||
if not v: | ||
return {} | ||
return {HexNumber(k): HexNumber(v_) for k, v_ in v.items()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically from ethereum_test_base_types import Account
so we can do:
class FuzzerAccount(BaseModel): | |
"""Account definition in fuzzer output.""" | |
balance: HexNumber | |
nonce: HexNumber = HexNumber(0) | |
code: Bytes = Bytes(b"") | |
storage: Dict[HexNumber, HexNumber] = Field(default_factory=dict) | |
privateKey: Optional[Bytes] = Field(None, alias="privateKey") | |
@field_validator("storage", mode="before") | |
@classmethod | |
def validate_storage(cls, v): | |
"""Convert storage keys and values to HexNumber.""" | |
if not v: | |
return {} | |
return {HexNumber(k): HexNumber(v_) for k, v_ in v.items()} | |
class FuzzerAccount(Account): | |
"""Account definition in fuzzer output.""" | |
private_key: Hash | None = None |
nonce: HexNumber = HexNumber(0) | ||
code: Bytes = Bytes(b"") | ||
storage: Dict[HexNumber, HexNumber] = Field(default_factory=dict) | ||
privateKey: Optional[Bytes] = Field(None, alias="privateKey") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use | None
instead of optional.
|
||
version: str = Field(..., pattern="^2\\.0$") | ||
fork: str | ||
chainId: int = Field(1, alias="chainId") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like some example use JSON ints and some others use hexadecimal strings, so we can do:
chainId: int = Field(1, alias="chainId") | |
chain_id: HexNumber = Field(HexNumber(1)) |
which will accept both.
class FuzzerTransaction(BaseModel): | ||
"""Transaction definition in fuzzer output.""" | ||
|
||
from_: Address = Field(..., alias="from") | ||
to: Optional[Address] = None | ||
value: HexNumber = HexNumber(0) | ||
gas: HexNumber | ||
gasPrice: Optional[HexNumber] = Field(None, alias="gasPrice") | ||
maxFeePerGas: Optional[HexNumber] = Field(None, alias="maxFeePerGas") | ||
maxPriorityFeePerGas: Optional[HexNumber] = Field(None, alias="maxPriorityFeePerGas") | ||
nonce: HexNumber | ||
data: Bytes = Bytes(b"") | ||
accessList: Optional[List[Dict[str, Union[str, List[str]]]]] = Field(None, alias="accessList") | ||
blobVersionedHashes: Optional[List[Hash]] = Field(None, alias="blobVersionedHashes") | ||
maxFeePerBlobGas: Optional[HexNumber] = Field(None, alias="maxFeePerBlobGas") | ||
authorizationList: Optional[List[FuzzerAuthorization]] = Field(None, alias="authorizationList") | ||
|
||
class Config: | ||
"""Pydantic configuration.""" | ||
|
||
populate_by_name = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically from ethereum_test_types import Transaction
, so we can reduce this model to:
class FuzzerTransaction(BaseModel): | |
"""Transaction definition in fuzzer output.""" | |
from_: Address = Field(..., alias="from") | |
to: Optional[Address] = None | |
value: HexNumber = HexNumber(0) | |
gas: HexNumber | |
gasPrice: Optional[HexNumber] = Field(None, alias="gasPrice") | |
maxFeePerGas: Optional[HexNumber] = Field(None, alias="maxFeePerGas") | |
maxPriorityFeePerGas: Optional[HexNumber] = Field(None, alias="maxPriorityFeePerGas") | |
nonce: HexNumber | |
data: Bytes = Bytes(b"") | |
accessList: Optional[List[Dict[str, Union[str, List[str]]]]] = Field(None, alias="accessList") | |
blobVersionedHashes: Optional[List[Hash]] = Field(None, alias="blobVersionedHashes") | |
maxFeePerBlobGas: Optional[HexNumber] = Field(None, alias="maxFeePerBlobGas") | |
authorizationList: Optional[List[FuzzerAuthorization]] = Field(None, alias="authorizationList") | |
class Config: | |
"""Pydantic configuration.""" | |
populate_by_name = True | |
class FuzzerTransaction(Transaction): | |
"""Transaction definition in fuzzer output.""" | |
from_: Address = Field(..., alias="from") |
@field_validator("accounts", mode="before") | ||
@classmethod | ||
def validate_accounts(cls, v): | ||
"""Convert account addresses to Address type.""" | ||
if not v: | ||
return {} | ||
return { | ||
Address(addr): (FuzzerAccount(**acc_data) if isinstance(acc_data, dict) else acc_data) | ||
for addr, acc_data in v.items() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this one is automatically done by the model so no need for this validator method.
@field_validator("accounts", mode="before") | |
@classmethod | |
def validate_accounts(cls, v): | |
"""Convert account addresses to Address type.""" | |
if not v: | |
return {} | |
return { | |
Address(addr): (FuzzerAccount(**acc_data) if isinstance(acc_data, dict) else acc_data) | |
for addr, acc_data in v.items() | |
} |
def get_sender_private_key(self, sender: Address) -> Bytes: | ||
"""Get the private key for a sender address.""" | ||
if sender not in self.accounts: | ||
raise ValueError(f"Sender {sender} not found in accounts") | ||
private_key = self.accounts[sender].privateKey | ||
if not private_key: | ||
raise ValueError(f"No private key for sender {sender}") | ||
return private_key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this method is not used elsewhere.
def get_sender_private_key(self, sender: Address) -> Bytes: | |
"""Get the private key for a sender address.""" | |
if sender not in self.accounts: | |
raise ValueError(f"Sender {sender} not found in accounts") | |
private_key = self.accounts[sender].privateKey | |
if not private_key: | |
raise ValueError(f"No private key for sender {sender}") | |
return private_key |
def validate_private_keys(self) -> None: | ||
"""Validate that all transaction senders have private keys.""" | ||
senders = {tx.from_ for tx in self.transactions} | ||
for sender in senders: | ||
if sender not in self.accounts: | ||
raise ValueError(f"Sender {sender} not found in accounts") | ||
if not self.accounts[sender].privateKey: | ||
raise ValueError(f"No private key for sender {sender}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be converted to a model_post_init
which pydantic can run automatically for verification:
def validate_private_keys(self) -> None: | |
"""Validate that all transaction senders have private keys.""" | |
senders = {tx.from_ for tx in self.transactions} | |
for sender in senders: | |
if sender not in self.accounts: | |
raise ValueError(f"Sender {sender} not found in accounts") | |
if not self.accounts[sender].privateKey: | |
raise ValueError(f"No private key for sender {sender}") | |
def model_post_init(self, __context): | |
"""Ensure all transactions have private keys.""" | |
super().model_post_init(__context) | |
senders: Dict[Address, EOA] = {} | |
for addr, account in self.accounts.items(): | |
if account.private_key is None: | |
continue | |
sender = EOA(key=account.private_key) | |
assert Address(sender) == addr, ( | |
f"Private key for account {addr} does not match derived address {sender.address}" | |
) | |
senders[addr] = sender | |
for tx in self.transactions: | |
assert tx.from_ in senders, f"Sender {tx.from_} not found in accounts" | |
tx.sender = senders[tx.from_] |
ποΈ Description
π Related Issues or PRs
N/A.
β Checklist
tox
checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:uvx --with=tox-uv tox -e lint,typecheck,spellcheck,markdownlint
type(scope):
.mkdocs serve
locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.@ported_from
marker.