75220 bc medium isthmus withdrawalsroot validation skipped when parent state unavailable allowing invalid blocks to pass post execution validation
Submitted on Apr 27th 2026 at 21:24:59 UTC by @Planarian89120 for Audit Comp | Base Azul
Report ID: #75220
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/base/base/tree/v0.8.0-rc.28
Impacts:
Unintended chain split (network partition)
Description
Summary
BaseEngineValidator::validate_block_post_execution_with_hashed_state skips the withdrawalsRoot verification under Isthmus rules when self.provider.state_by_block_hash(block.parent_hash()) fails. This allows blocks with non-canonical in-memory parents to pass post-execution validation without accurate withdrawalsRoot commitments.
Vulnerability Details
When Isthmus rules activate, validate_block_post_execution_with_hashed_state checks for withdrawals root but falls back to Ok(()) if the parent hash cannot be resolved in the database:
// base/crates/execution/node/src/engine.rs (around line 130)
if self.chain_spec().is_isthmus_active_at_timestamp(block.timestamp()) {
let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else {
return Ok(());
};
isthmus::verify_withdrawals_root_prehashed(...)?;
}The database-backed provider cannot load parent states that are only present in the in-memory tree. This contrasts with the tree_state resolution path used when executing payloads (see base/crates/execution/payload/src/builder.rs lines 197 and 231, and base/crates/execution/trie/src/live.rs line 86, which all use state_by_block_hash against parent hashes).
Furthermore, the validation flow does not re-check the Base-specific withdrawalsRoot after execution but assumes payload status VALID. Thus, a malformed header with a correct state root but incorrect withdrawalsRoot can be inserted and marked VALID in the tree state.
This behavior violates the post-Isthmus requirement that withdrawalsRoot matches the L2ToL1MessagePasser account storage root committed by the block state root.
Root Cause
The fallback return Ok(()) on a missing parent state silently waives a consensus-critical validation rule. In any execution path where:
Isthmus is active (timestamp-gated),
The post-execution validator is invoked with a block whose parent state is not in the database-backed provider (in-memory-only / non-canonical / freshly-promoted side chain),
the withdrawalsRoot field is not checked at all. The correct behavior under Isthmus is mandatory verification of withdrawalsRoot against the L2ToL1MessagePasser storage root commitment.
Impact
Severity: High
An invalid withdrawalsRoot accepted by the validator can:
Skew downstream query results (e.g., withdrawal proofs derived from an inconsistent root).
Circumvent validity rules intended to prevent unsafe blocks from propagating.
Concrete risks:
Unsafe but chain-consistent blocks reaching canonical status under a permissive parent during reorgs or side-chain promotions.
Adversaries exploiting non-canonical chains or induced reorgs to submit headers with invalid withdrawals roots, with the validator unable to reject them.
Inconsistent
withdrawalsRootvalues create the necessary precondition for L1-bridge-side proof issues if the malformed header is referenced upstream.
Recommended Fix
The post-execution hook should:
Include in-memory parent resolution (mirror the
tree_statelookup path used in payload execution) so that non-canonical / in-memory parents can be validated.Reject blocks with invalid withdrawals roots (or unresolvable parents under Isthmus) rather than default to
Ok(()).Revalidate
withdrawalsRootexplicitly after the execution phase, ensuring in-memory parents are fully checked.
Concretely: replace the silent Ok(()) fallback with either a tree-state lookup (matching how the executor itself resolves parent state) or an error return on unresolvable parents under Isthmus.
Proof of Concept
Step-by-Step Reproduction
Identify the vulnerable code
File: base/crates/execution/node/src/engine.rs (around line 130)
The pattern let Ok(state) = ... else { return Ok(()); } causes the validator to silently approve the block when the parent state cannot be loaded by the database-backed provider — even though Isthmus rules require the withdrawalsRoot be verified.
Compare against the executor's parent-state lookup
The executor (payload builder / live trie) resolves parent state through paths that include in-memory / tree-state fallbacks:
base/crates/execution/payload/src/builder.rs:197—self.client.state_by_block_hash(ctx.parent().hash())?base/crates/execution/payload/src/builder.rs:231— same patternbase/crates/execution/trie/src/live.rs:86— same pattern, but propagates the error with?
The validator's behavior is asymmetric: the executor builds blocks against a parent reachable through tree-state, but the validator's database-backed provider can fail on the same parent and silently skip validation.
Construct the test
A focused regression test was added to engine.rs (alongside existing tests in the same module) that:
Defines a
MissingParentProviderwrapper aroundNoopProviderthat always errors onstate_by_block_hash.Constructs an Isthmus-era block (
is_isthmus_active_at_timestamp(timestamp) == true) with an incorrectwithdrawalsRoot(e.g.,B256::ZEROinstead of the realL2ToL1MessagePasserstorage root).Calls
validate_block_post_execution_with_hashed_statewith that block.Asserts the validator returns
Ok(())despite the malformed root.
Test name: test_isthmus_withdrawals_root_validation_skipped_when_parent_state_unavailable
Production reachability
Conditions under which a real validator would hit the silent-skip path:
In-memory-only parent (pre-canonicalization). The validator is invoked on a block whose parent has been processed but not yet committed to the database (e.g., during sync, reorg windows, or sidechain extensions). The database-backed
state_by_block_hashreturnsErr; tree state has the parent, but the validator does not consult it.Reorg / side-chain promotion. A side branch is being evaluated for canonicalization. If validation runs before the parent's state is materialized into the DB, the silent-skip path is taken.
Provider transient failure. Any I/O / lookup error from the underlying state provider (corruption, race during DB compaction, cold-cache miss with stale DB) causes the validator to skip
withdrawalsRootverification entirely.
In all three cases, an honest or adversarial proposer can submit an Isthmus block whose withdrawalsRoot does not match the L2ToL1MessagePasser storage root, and the validator marks it VALID.
Note on PoC scope
This PoC isolates the validator-side defect (the silent fallback) using a controlled test wrapper. The bug exists in production code as written; the test wrapper is used only to deterministically reproduce the conditions (database-side parent unavailability) under which the silent-skip path is taken. No exploit chain against a live mainnet node is included — the report is scoped to the consensus-validation defect itself.
Was this helpful?