For the complete documentation index, see llms.txt. This page is also available as Markdown.

74678 bc medium missing requests hash validation in the raw import path can persist invalid post isthmus blocks and cause chain split

Submitted on Apr 24th 2026 at 07:28:38 UTC by @OxPrince for Audit Comp | Base Azul

  • Report ID: #74678

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • Target: https://github.com/base/base/releases/tag/v0.8.0-rc.15

  • Impacts:

    • Unintended chain split (network partition)

Description

Brief/Intro

After Isthmus, Base requires every block header to carry requestsHash = sha256(""). The Engine/spec-facing path enforces that rule, but the raw sync/import path does not. As a result, the same malformed post-Isthmus block can be rejected by one execution path and accepted, persisted, and later served as canonical by another. This creates a real block-validity differential inside the execution client and can lead to unintended chain split if different nodes or code paths disagree on the same block.

Vulnerability Details

The local Isthmus spec is explicit:

  • before Isthmus, requestsHash must be omitted

  • after Isthmus, requestsHash must equal sha256("")

See exec-engine.md and exec-engine.md.

The Engine/spec-facing path follows that rule. In payload/mod.rs, non-empty execution requests are rejected and header.requests_hash is only accepted as EMPTY_REQUESTS_HASH.

The raw sync/import path does not enforce the same invariant:

  • OpBeaconConsensus::validate_header validates nonce, ommers, extra-data, gas, and base fee, but never inspects requests_hash.

  • validate_header_against_parent validates parent linkage, timestamp, base fee, and blob fields, but not requests_hash.

  • validate_block_post_execution validates receipts, gas usage, and Jovian blob gas, but never compares executed requests to the header hash.

I first confirmed the acceptance gap with a focused regression test at lib.rs, test_isthmus_sync_validation_accepts_non_empty_requests_hash, which proves a post-Isthmus block with an arbitrary non-empty requests_hash still passes all sync-facing consensus checks.

I then upgraded the proof to an end-to-end import/boot PoC see below

PoC does the following:

  1. Launches a real local Base execution node with an Isthmus-active chain spec.

  2. Mines a valid post-Isthmus block and confirms it carries the correct empty requests_hash.

  3. Mutates only header.requests_hash to 0x1111...1111 in import.rs.

  4. Confirms the Engine/spec-facing path rejects that same malformed block via BasePayloadError::NonEmptyELRequests in import.rs.

  5. Writes the malformed block to raw RLP and feeds it into the real historical import path via import_blocks_from_file(...) in import.rs.

  6. Verifies the import path accepts and persists the malformed block in import.rs.

  7. Reboots a fresh node from the imported datadir and verifies the malformed header is served as canonical in import.rs.

Impact Details

This proves the issue is not just a missing assertion in isolation. A malformed post-Isthmus block is rejected on one execution path but accepted, persisted, and later re-served as canonical on another.

Why this fits:

  • Post-Isthmus block validity depends on whether a node/path enforces requestsHash == sha256("").

  • The Engine/spec-facing path rejects the malformed block, while the raw import path accepts it.

  • The e2e PoC proves that acceptance is durable: the malformed block is persisted and a rebooted node serves it as canonical state.

Once different nodes or execution paths disagree on whether the same block is valid, they can follow different histories. Because this is a rule disagreement rather than a temporary networking issue, recovery can require coordinated operator action, rollback, or hard-fork-style reconciliation.

This is stronger than a mere informational inconsistency because:

  • it affects a real sync/import path

  • it changes canonical state

  • it survives restart

  • it can create incompatible views of the chain

References

  • Isthmus header validity rules: exec-engine.md

  • Isthmus block sealing rule that requestsHash is always sha256(""): exec-engine.md

  • Engine/spec-facing enforcement: payload/mod.rs

  • Missing sync/import validation in header checks: lib.rs

  • Missing sync/import validation against parent: lib.rs

  • Missing post-execution validation: validation/mod.rs

  • Focused regression proof: lib.rs

Proof of Concept

Was this helpful?