Skip to content

fix(l1): validate block header logs_bloom against executed receipts#6766

Merged
iovoid merged 12 commits into
mainfrom
fix/l1-validate-logs-bloom
Jun 5, 2026
Merged

fix(l1): validate block header logs_bloom against executed receipts#6766
iovoid merged 12 commits into
mainfrom
fix/l1-validate-logs-bloom

Conversation

@ElFantasma

@ElFantasma ElFantasma commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Motivation

During block import, ethrex validates gas_used, receipts_root, requests_hash, and the BAL hash, but never checks the block header's logs_bloom against the bloom of the executed receipts. The header bloom is only computed when building a payload, never re-validated on import. The receipts root commits to the per-receipt blooms, not to the header's aggregate logs_bloom field, so a block with correct roots but an arbitrary logs_bloom passes every existing check. geth/reth (and the execution spec) recompute and compare the bloom, so ethrex would import/attest a block the rest of the network rejects — a consensus divergence at the Engine API — and serve a wrong bloom to RPC consumers.

Description

The receipts root and the aggregate logs_bloom are now validated together in a single pass over the receipts, so each receipt's bloom is hashed only once (it feeds both the receipts trie and the OR-ed header bloom):

  • Adds compute_receipts_root_and_logs_bloom(receipts, crypto) -> (H256, Bloom) (crates/common/types/block.rs) and Receipt::encode_inner_with_precomputed_bloom so the bloom computed for the trie is reused for the aggregate instead of being recomputed.
  • Adds validate_receipts_root_and_logs_bloom(header, receipts, crypto) (crates/common/validation.rs), returning InvalidBlockError::ReceiptsRootMismatch or the new InvalidBlockError::LogsBloomMismatch. This replaces the separate validate_receipts_root / validate_logs_bloom calls.
  • Wires it into every post-execution validation site: the three L1 import paths in crates/blockchain (execute_block, execute_block_pipeline, execute_block_from_state) and the stateless/zkVM guest path (crates/guest-program/src/common/execution.rs), so proof-based execution enforces the same bloom check as full-node import. Because the bloom is already hashed for the receipts root, adding the check costs no extra keccak — which is what made it cheap enough to include in the cycle-counted guest.

Tests:

  • test/tests/blockchain/logs_bloom_tests.rs — end-to-end via add_block; now asserts the specific LogsBloomMismatch rejection (not just any error), so the check can't silently rot behind an unrelated failure.
  • test/tests/common/logs_bloom_validation_tests.rs — exercises the validator with receipts that carry logs (non-trivial bloom): matching header passes; a flipped bloom bit → LogsBloomMismatch; a wrong receipts root → ReceiptsRootMismatch; and the single-pass root equals the standalone compute_receipts_root (guards the dedup refactor).

Updated from the original single-validate_logs_bloom approach per review (@iovoid, greptile): the bloom was being computed twice per receipt (once inside compute_receipts_root), so the two checks are folded into one pass. That also makes the check cheap in the guest, so the consensus gap there (flagged by the Codex review) is closed rather than deferred.

@ElFantasma
ElFantasma requested a review from a team as a code owner June 1, 2026 18:16
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-7 semantics that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

@github-actions github-actions Bot added the L1 Ethereum client label Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

The PR correctly addresses a consensus vulnerability where the logs_bloom header field was not being validated against the executed receipts. This would have allowed ethrex to accept blocks that other clients (geth, reth) reject, causing a chain split.

Security/Consensus: Correct

  • The validation logic (computed_bloom |= bloom_from_logs) correctly implements the bitwise OR aggregation specified in the Yellow Paper (Item 4).
  • The error is properly propagated in all three block processing paths (validation, batch processing, and import).

Code Quality: Minor suggestions

  1. Performance optimization (crates/common/validation.rs, lines 102-109):
    If the Receipt struct stores the pre-computed logs_bloom (which it typically does for RLP encoding), you should use receipt.logs_bloom directly instead of recomputing via bloom_from_logs. This avoids redundant keccak256 operations and ensures consistency with the receipts root validation:

    // If Receipt has logs_bloom field:
    for receipt in receipts {
        computed_bloom |= receipt.logs_bloom;
    }
  2. Error diagnostics (crates/common/errors.rs, line 20):
    Consider including the computed vs expected bloom values in the error variant to aid debugging, though this is optional if it conflicts with the codebase's error handling patterns.

Testing: Adequate

  • The regression test (test/tests/blockchain/logs_bloom_tests.rs) correctly verifies that a block with a tampered non-zero bloom is rejected when it should be zero (Item 44-107).
  • Note: The test function is async but blockchain.add_block(block) appears to be synchronous (based on the .is_err() call). If add_block is actually async, the test is missing .await.

Verification completeness: Good

  • The validation is correctly placed alongside validate_receipts_root in all three locations (blockchain.rs lines 440, 689, 1332), ensuring coverage for block import, sync, and production paths.

Conclusion: The PR is correct and should be merged after considering the performance optimization regarding pre-computed receipt blooms.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Full-node import is fixed, but the stateless/guest execution path still skips this consensus check. In execution.rs, the guest program still validates gas, receipts root, and requests hash only; it never calls validate_logs_bloom. That leaves proof-based execution able to accept a block header that Blockchain::add_block now rejects, which is exactly the kind of split you want to avoid in consensus code. Add the same post-execution bloom check there, immediately after validate_receipts_root, and map the resulting InvalidBlockError into the guest error surface.

  2. The new check adds an avoidable second full hash pass over all logs on the hot import path. validate_logs_bloom recomputes each receipt bloom from raw logs, but compute_receipts_root already does the same work when encoding receipts-with-bloom. For log-heavy blocks this doubles keccak work unnecessarily. Consider folding aggregate-bloom computation into the receipts-root pass, or reusing ReceiptWithBloom/cached per-receipt blooms.

Aside from those points, the added blockchain-side validation is correct and the new regression test covers the primary import path well.

I couldn’t run cargo test here because the environment blocks rustup toolchain sync/write access and has no network, so this review is from static inspection only.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 30
Total lines removed: 0
Total lines changed: 30

Detailed view
+----------------------------------------+-------+------+
| File                                   | Lines | Diff |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs | 2665  | +8   |
+----------------------------------------+-------+------+
| ethrex/crates/common/errors.rs         | 38    | +2   |
+----------------------------------------+-------+------+
| ethrex/crates/common/types/block.rs    | 994   | +16  |
+----------------------------------------+-------+------+
| ethrex/crates/common/types/receipt.rs  | 389   | +2   |
+----------------------------------------+-------+------+
| ethrex/crates/common/validation.rs     | 245   | +2   |
+----------------------------------------+-------+------+

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes a gap in L1 block validation: the header's logs_bloom field was never re-validated on import, meaning a block with a correct receipts root but an arbitrary bloom value would be accepted, diverging from geth/reth and the execution spec.

  • Adds validate_logs_bloom in crates/common/validation.rs, which ORs the per-receipt blooms from executed logs and compares the result to header.logs_bloom, returning the new InvalidBlockError::LogsBloomMismatch variant on mismatch.
  • Hooks the check at all three post-execution sites in crates/blockchain/blockchain.rs (execute_block, execute_block_pipeline, execute_block_from_state), immediately after validate_receipts_root, and ships a regression test that injects a tampered bloom into an otherwise-valid block and asserts rejection.

Confidence Score: 4/5

Safe to merge; the new check is strictly additive and covers all three L1 import paths. The implementation correctly reproduces the same OR-of-per-receipt-blooms logic used by geth/reth.

The bloom computation is sound and the call-sites are complete. The two observations are: bloom is hashed twice per import (once inside compute_receipts_root, once inside validate_logs_bloom), which adds unnecessary keccak overhead on log-heavy blocks; and the regression test only checks is_err() rather than the specific LogsBloomMismatch variant, so a future regression in the bloom path could go undetected if something else rejects the block first.

crates/common/validation.rs (redundant bloom computation) and test/tests/blockchain/logs_bloom_tests.rs (underspecified error assertion)

Important Files Changed

Filename Overview
crates/common/validation.rs Adds validate_logs_bloom which recomputes the aggregate bloom per receipt and compares it to the header; logic is correct but bloom is already computed inside validate_receipts_rootcompute_receipts_rootencode_inner_with_bloom, resulting in double hashing.
crates/blockchain/blockchain.rs Correctly inserts validate_logs_bloom at all three post-execution sites (execute_block, execute_block_pipeline, execute_block_from_state), immediately after validate_receipts_root and before validate_requests_hash.
crates/common/errors.rs Adds LogsBloomMismatch variant to InvalidBlockError with a clear error message; no issues.
crates/common/common.rs Re-exports validate_logs_bloom alongside other validation helpers; straightforward and consistent.
test/tests/blockchain/logs_bloom_tests.rs Regression test that builds a manually-crafted block with a tampered logs_bloom and asserts rejection; correct setup but only checks is_err() rather than the specific LogsBloomMismatch variant.
test/tests/blockchain/mod.rs Adds the new test module declaration; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[add_block / execute_block] --> B[validate_block_pre_execution]
    B --> C[Execute transactions]
    C --> D[validate_gas_used]
    D --> E[validate_receipts_root\ncomputes bloom per receipt internally]
    E --> F[validate_logs_bloom NEW\naggregates bloom OR per receipt\ncompares to header.logs_bloom]
    F -->|match| G[validate_requests_hash]
    F -->|mismatch| H[Err LogsBloomMismatch]
    G --> I[validate_block_access_list_hash]
    I --> J[Commit to storage]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/common/validation.rs:100-115
**Redundant bloom computation on every block import**

`validate_receipts_root` (called immediately before this function) already invokes `compute_receipts_root`, which in turn calls `encode_inner_with_bloom``bloom_from_logs` for every receipt. `validate_logs_bloom` then calls `bloom_from_logs` a second time over the same logs. For a block carrying many log-heavy transactions this doubles the keccak hashing load on the import hot-path. A straightforward mitigation is to accept an already-computed `Bloom` rather than raw receipts, letting the call-site pass the value it got from `compute_receipts_root`. Alternatively, the aggregate bloom could be accumulated inside `compute_receipts_root` and returned alongside the root hash.

### Issue 2 of 2
test/tests/blockchain/logs_bloom_tests.rs:103-108
The assertion only checks that `add_block` returns any error, not that it specifically returns `LogsBloomMismatch`. If a future refactor accidentally causes the block to be rejected earlier (e.g. a pre-execution header check), this test would continue to pass while the bloom validation itself silently stopped working. Asserting the specific error variant pins the test to the intended invariant.

```suggestion
    let result = blockchain.add_block(block);
    assert!(
        matches!(
            result,
            Err(ethrex_blockchain::error::ChainError::InvalidBlock(
                ethrex_common::errors::InvalidBlockError::LogsBloomMismatch
            ))
        ),
        "expected LogsBloomMismatch, got: {result:?}"
    );
```

Reviews (1): Last reviewed commit: "fix(l1): validate block header logs_bloo..." | Re-trigger Greptile

Comment thread crates/common/validation.rs Outdated
Comment on lines +100 to +115
pub fn validate_logs_bloom(
block_header: &BlockHeader,
receipts: &[Receipt],
crypto: &dyn Crypto,
) -> Result<(), InvalidBlockError> {
let mut computed_bloom = Bloom::zero();
for receipt in receipts {
computed_bloom |= bloom_from_logs(&receipt.logs, crypto);
}

if computed_bloom == block_header.logs_bloom {
Ok(())
} else {
Err(InvalidBlockError::LogsBloomMismatch)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Redundant bloom computation on every block import

validate_receipts_root (called immediately before this function) already invokes compute_receipts_root, which in turn calls encode_inner_with_bloombloom_from_logs for every receipt. validate_logs_bloom then calls bloom_from_logs a second time over the same logs. For a block carrying many log-heavy transactions this doubles the keccak hashing load on the import hot-path. A straightforward mitigation is to accept an already-computed Bloom rather than raw receipts, letting the call-site pass the value it got from compute_receipts_root. Alternatively, the aggregate bloom could be accumulated inside compute_receipts_root and returned alongside the root hash.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/validation.rs
Line: 100-115

Comment:
**Redundant bloom computation on every block import**

`validate_receipts_root` (called immediately before this function) already invokes `compute_receipts_root`, which in turn calls `encode_inner_with_bloom``bloom_from_logs` for every receipt. `validate_logs_bloom` then calls `bloom_from_logs` a second time over the same logs. For a block carrying many log-heavy transactions this doubles the keccak hashing load on the import hot-path. A straightforward mitigation is to accept an already-computed `Bloom` rather than raw receipts, letting the call-site pass the value it got from `compute_receipts_root`. Alternatively, the aggregate bloom could be accumulated inside `compute_receipts_root` and returned alongside the root hash.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — folded into a single-pass compute_receipts_root_and_logs_bloom that computes each receipt's bloom once and reuses it for both the receipts trie and the aggregate header bloom (via Receipt::encode_inner_with_precomputed_bloom). The duplicate bloom_from_logs per receipt is gone.

Comment on lines +103 to +108
let result = blockchain.add_block(block);
assert!(
result.is_err(),
"a block whose header logs_bloom does not match the executed receipts must be \
rejected, but add_block returned: {result:?}"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The assertion only checks that add_block returns any error, not that it specifically returns LogsBloomMismatch. If a future refactor accidentally causes the block to be rejected earlier (e.g. a pre-execution header check), this test would continue to pass while the bloom validation itself silently stopped working. Asserting the specific error variant pins the test to the intended invariant.

Suggested change
let result = blockchain.add_block(block);
assert!(
result.is_err(),
"a block whose header logs_bloom does not match the executed receipts must be \
rejected, but add_block returned: {result:?}"
);
let result = blockchain.add_block(block);
assert!(
matches!(
result,
Err(ethrex_blockchain::error::ChainError::InvalidBlock(
ethrex_common::errors::InvalidBlockError::LogsBloomMismatch
))
),
"expected LogsBloomMismatch, got: {result:?}"
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/tests/blockchain/logs_bloom_tests.rs
Line: 103-108

Comment:
The assertion only checks that `add_block` returns any error, not that it specifically returns `LogsBloomMismatch`. If a future refactor accidentally causes the block to be rejected earlier (e.g. a pre-execution header check), this test would continue to pass while the bloom validation itself silently stopped working. Asserting the specific error variant pins the test to the intended invariant.

```suggestion
    let result = blockchain.add_block(block);
    assert!(
        matches!(
            result,
            Err(ethrex_blockchain::error::ChainError::InvalidBlock(
                ethrex_common::errors::InvalidBlockError::LogsBloomMismatch
            ))
        ),
        "expected LogsBloomMismatch, got: {result:?}"
    );
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — the test now asserts the specific ChainError::InvalidBlock(InvalidBlockError::LogsBloomMismatch) rather than is_err(), so an unrelated earlier rejection can't mask a regression in the bloom check. Added test/tests/common/logs_bloom_validation_tests.rs covering the validator directly (matching → ok, flipped bloom bit → LogsBloomMismatch, wrong root → ReceiptsRootMismatch).

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have a complete picture. Here's my review:


Review: fix(l1): validate block header logs_bloom against executed receipts

Summary

This is a correct, well-scoped consensus-correctness fix. The core logic is sound and the architectural reasoning (receipts root commits to per-receipt encoding but not to the aggregate header bloom) is accurate.


Implementation — crates/common/validation.rs

Correctness: The algorithm is correct. Bloom::zero() is the proper OR-identity, and the loop faithfully mirrors how other clients compute the header bloom.

Important detail confirmed: Receipt has no stored bloom field — the bloom is computed from logs on the fly during RLP encoding (see receipt.rs:56). This means bloom_from_logs(&receipt.logs, crypto) is the only correct approach, not a design choice. This also means validate_logs_bloom and compute_receipts_root share the same underlying computation path, giving consistent results by construction.

Style nit: The multi-line doc comment is significantly longer than any other validate_* function in the file (most have no comment at all, or at most one line). Consider trimming to match project conventions.


Call sites — crates/blockchain/blockchain.rs

All three execution entry points are covered and the call is inserted in the correct position: after validate_receipts_root (which validates per-receipt blooms via RLP) and before validate_requests_hash. No issues here.


Test — test/tests/blockchain/logs_bloom_tests.rs

The main feedback for this PR:

assert!(
    result.is_err(),
    "a block whose header logs_bloom does not match..."
);

The test only checks is_err(), not that the error is specifically InvalidBlockError::LogsBloomMismatch. If any earlier validation check fails (base fee, pre-execution header validation, etc.), the assertion passes but the bloom validation isn't actually exercised. This makes the test a regression test in name only.

Recommendation: Pattern-match on the concrete error variant:

use ethrex_common::errors::InvalidBlockError;

let err = result.expect_err("should reject block with mismatched logs_bloom");
assert!(
    matches!(err, /* BlockchainError::InvalidBlock( */ InvalidBlockError::LogsBloomMismatch /* ) */),
    "expected LogsBloomMismatch, got: {err:?}"
);

(Adjust for however BlockchainError wraps InvalidBlockError in the return type of add_block.)

Test construction: The empty block is well-constructed. The calculate_base_fee_per_gas call is correct (block_gas_limit = parent_gas_limit = 30_000_000, and with parent_gas_used = 0 and parent_base_fee = 0 the result is Some(0), matching the header). The state_root: parent.state_root shortcut is valid for a no-transaction pre-Shanghai block over an empty genesis alloc.


Error variant — crates/common/errors.rs

LogsBloomMismatch is placed correctly between ReceiptsRootMismatch and InvalidHeader, consistent with validation ordering. The error message matches the style of ReceiptsRootMismatch. No issues.


What's good

  • Closes a real consensus divergence gap (other clients reject tampered blooms, ethrex did not)
  • Narrow, surgical scope — touches exactly what needs to change
  • All three L1 execution paths covered
  • The zkVM exclusion rationale is sound and correctly documented in the PR description

One concrete change requested: Tighten the test assertion to match LogsBloomMismatch specifically.


Automated review by Claude (Anthropic) · sonnet · custom prompt

Comment thread crates/common/validation.rs Outdated
) -> Result<(), InvalidBlockError> {
let mut computed_bloom = Bloom::zero();
for receipt in receipts {
computed_bloom |= bloom_from_logs(&receipt.logs, crypto);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we reuse the bloom_from_logs calculation done for validate_receipts_root?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — good call. The two checks now run in one pass: compute_receipts_root_and_logs_bloom hashes each receipt's bloom once and feeds it to both the receipts trie (via the new Receipt::encode_inner_with_precomputed_bloom) and the OR-ed header bloom, and validate_receipts_root_and_logs_bloom replaces the two separate calls at all four sites. No more double bloom_from_logs.

A nice side effect: since the check now adds zero extra keccak, it was cheap enough to also wire into the zkVM guest path (crates/guest-program/src/common/execution.rs), which previously validated the receipts root but not the bloom — closing that consensus gap in proofs too.

@ElFantasma

Copy link
Copy Markdown
Contributor Author

Heads-up that the implementation changed meaningfully since the last review (re-review appreciated, @azteca1998):

  • Single pass. validate_receipts_root + validate_logs_bloom are folded into validate_receipts_root_and_logs_bloom, backed by compute_receipts_root_and_logs_bloom, which hashes each receipt's bloom once and reuses it for both the receipts trie and the aggregate header bloom (per @iovoid / greptile — no more double bloom_from_logs).
  • Guest path now covered. Because the check adds zero extra keccak, it's also wired into the zkVM guest (crates/guest-program/src/common/execution.rs), which previously validated the receipts root but not the bloom — closing the consensus gap the Codex review flagged, instead of deferring it.
  • Tests. The node test now asserts the specific LogsBloomMismatch; a new test/tests/common/logs_bloom_validation_tests.rs exercises the validator with log-bearing receipts (matching → ok, flipped bloom bit → LogsBloomMismatch, wrong root → ReceiptsRootMismatch, and single-pass root == standalone compute_receipts_root).

Rebased onto current main; full diff is +156/−51.

@ElFantasma
ElFantasma requested a review from azteca1998 June 4, 2026 13:20
@MegaRedHand
MegaRedHand removed the request for review from azteca1998 June 4, 2026 20:17

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@github-project-automation github-project-automation Bot moved this to In Review in ethrex_l1 Jun 4, 2026
@ElFantasma
ElFantasma enabled auto-merge June 4, 2026 20:31
@ElFantasma
ElFantasma added this pull request to the merge queue Jun 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 4, 2026
@iovoid
iovoid enabled auto-merge June 5, 2026 13:00
@iovoid
iovoid added this pull request to the merge queue Jun 5, 2026
Merged via the queue into main with commit 525dee6 Jun 5, 2026
54 checks passed
@iovoid
iovoid deleted the fix/l1-validate-logs-bloom branch June 5, 2026 13:41
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Jun 5, 2026
ilitteri added a commit to emirongrr/ethrex that referenced this pull request Jun 10, 2026
…bloom (lambdaclass#6821)

**Motivation**

The "Benchmark Block execution" job (`pr_perf_blocks_exec.yaml`) started
failing with `LogsBloomMismatch` after lambdaclass#6766, which added header
`logs_bloom` validation to every import path. The job imports
`fixtures/blockchain/l2-1k-erc20.rlp`, a fixture produced by an old
ethrex that never populated the header bloom — so every header stored a
zero `logs_bloom`, while its ERC20 blocks emit `Transfer` logs. The
first such block now fails validation.

**Description**

Regenerates `l2-1k-erc20.rlp` so its headers carry the correct bloom.
The bloom can't be patched in place (the header hash commits to it), so
the chain is re-derived: each block is re-executed against a fresh
`perf-ci.json` state, the aggregate bloom is recomputed from the
executed receipts, written into the header, and every `parent_hash` is
re-linked to the corrected predecessor. 611 of 1110 headers gained a
non-zero bloom; the corrected chain imports cleanly through the normal
validating path. The file size is unchanged (bloom and `parent_hash` are
fixed-length), so the diff is a clean LFS pointer swap.

This was produced by a one-off script (not kept in-tree). The other
ethrex-generated fixtures (`2000-blocks.rlp`, `l2-loadtest.rlp`) were
checked the same way and need **no change** — their blocks are plain ETH
transfers that emit no logs, so a zero bloom is already correct.

Verified with a binary that includes lambdaclass#6766: original fixture → `Invalid
Block: Logs bloom does not match`; regenerated fixture → `Import
completed blocks=1110`. No client code touched.

**Checklist**

- [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR
includes breaking changes to the `Store` requiring a re-sync. — N/A (no
`Store` changes)
emirongrr pushed a commit to emirongrr/ethrex that referenced this pull request Jun 10, 2026
…lambdaclass#6817)

**Motivation**

A transaction's chain id is bound into its signature — EIP-155 folds it
into `v` for legacy txs, and typed transactions
(EIP-1559/2930/4844/7702) carry an explicit `chain_id` field that is
part of the signing payload. This binds a signature to a single chain so
it cannot be replayed elsewhere.

During **block import**, ethrex recovers a typed tx's sender from the
tx's *own* chain-id signing domain and never re-checks that chain id
against the node's chain. The only chain-id check lives in the mempool
(`MempoolError::InvalidChainId`), but blocks arriving via
`engine_newPayload` or p2p block gossip/sync never pass through the
mempool. As a result a block containing a transaction signed for a
*foreign* chain id executes and is **accepted**. geth's signer rejects
the same transaction at block validation (`ErrInvalidChainId`), so
ethrex would import a block the rest of the network rejects — a
consensus divergence reachable on every imported block.

**Description**

- Adds `InvalidBlockError::InvalidTransactionChainId { have, want }`.
- Adds a per-transaction chain-id check to the shared
`validate_block_pre_execution` (`crates/common/validation.rs`): every
transaction whose `chain_id()` is `Some` and does not equal
`chain_config.chain_id` is rejected pre-execution. Because that
validator is called by the full-node import path, the L2 block producer,
**and** the zkVM guest program, the check is enforced everywhere a block
is validated (same approach as lambdaclass#6766 for `logs_bloom`).
- Legacy pre-EIP-155 transactions carry no chain id (`chain_id() ==
None`) and are intentionally left untouched (replay-allowed by design),
mirroring the existing mempool check.
- `PrivilegedL2Transaction` is exempt: its sender comes from an unsigned
`from` field, so chain id is not a signature-binding scalar for it, and
on L2 it may legitimately name a different *source* chain for
cross-chain deposits (`l1_watcher.rs` builds mint txs with `chain_id =
source_chain_id`). On L1 privileged txs are already rejected as an
unsupported type (lambdaclass#6752), so the exemption opens no L1 gap.

**Test**

`test/tests/blockchain/wrong_chain_id_tests.rs` builds a fully-valid
block (correct post-state, roots, gas) whose only defect is an EIP-1559
transaction signed for a foreign chain id, and asserts `add_block`
rejects it. Confirmed **red before the fix** — `add_block` returned
`Ok(())`, i.e. ethrex *accepted* the block — and green after.

**Checklist**

- [x] Regression test added (red before, green after)
- [x] `cargo fmt` + `cargo clippy` clean
- [x] Enforced on full-node import, L2 block producer, and zkVM guest
paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants