Skip to content

Conversation

bshastry
Copy link
Contributor

πŸ—’οΈ Description

πŸ”— Related Issues or PRs

N/A.

βœ… Checklist

  • All: Ran fast 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
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered adding an entry to CHANGELOG.md.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).
  • Tests: Ran mkdocs serve locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.
  • Tests: For PRs implementing a missed test case, update the post-mortem document to add an entry the list.
  • Ported Tests: All converted JSON/YML tests from ethereum/tests or tests/static have been assigned @ported_from marker.

Copy link
Member

@danceratopz danceratopz left a 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",
Copy link
Member

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 :)


## Production Test Suite

See `production_test.py` for a complete example that:
Copy link
Member

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.

@danceratopz danceratopz self-assigned this Sep 29, 2025
bshastry and others added 11 commits September 29, 2025 13:37
- 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]>
bshastry and others added 5 commits September 29, 2025 14:35
- 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]>
@marioevz marioevz self-requested a review October 2, 2025 16:50
Copy link
Member

@marioevz marioevz left a 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
Copy link
Member

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],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fuzzer_output: Dict[str, Any],
fuzzer_output: FuzzerOutput,

And parse when calling.

Comment on lines +69 to +91
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
Copy link
Member

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.

Comment on lines +12 to +27
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()}
Copy link
Member

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:

Suggested change
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")
Copy link
Member

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")
Copy link
Member

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:

Suggested change
chainId: int = Field(1, alias="chainId")
chain_id: HexNumber = Field(HexNumber(1))

which will accept both.

Comment on lines +46 to +66
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
Copy link
Member

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:

Suggested change
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")

Comment on lines +113 to +122
@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()
}
Copy link
Member

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.

Suggested change
@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()
}

Comment on lines +133 to +140
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
Copy link
Member

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.

Suggested change
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

Comment on lines +124 to +131
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}")
Copy link
Member

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:

Suggested change
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_]

@danceratopz danceratopz removed their assignment Oct 3, 2025
@danceratopz
Copy link
Member

Thanks a lot for your review @marioevz. @bshastry I'm unassigning myself as I'll be afk for the next couple of weeks. Thanks for addressing the comments!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants