crates/execution/engine-tree/src/cached_execution.rs line 58–66:
// cached_execution.rs:58-66 (literal)ifletSome(prev_cached_hash)=prev_cached_hash{// all previous transactions from start of block to prev_cached_hash are cached,// so only check if the previous transaction is cachedif!pending_blocks.has_transaction_hash(prev_cached_hash){warn!(prev_cached_hash=?prev_cached_hash,"Not using cached results - previous transaction not cached",);returnNone;}}
has_transaction_hash is HashMap::contains_key (pending_blocks.rs:384-386):
It checks whether the hash exists anywhere in the cache — it never checks whether the requested transaction is the immediate successor of that hash in the cached pending order.
The comment at line 59 reveals the developer's intent: they assumed payloads always arrive in the same order as the pending cache. But nothing in the code enforces that assumption — has_transaction_hash is a membership test, not a position test.
A payload body [deposit, nonce1] that omits nonce0:
Step
Cached node
Vanilla node
Lookup deposit in cache
Found → guard passes
N/A (no cache)
Execute nonce1
Returns cached result (computed when nonce0 was present)
Re-executes from state → nonce 1 too high, expected 0
Verdict
Valid
Invalid
Same parent state, same payload, different verdict. Hard consensus split.
Root Cause
CachedExecutor::execute_transaction_without_commit at line 183 derives prev_tx_hash from the incoming payload body's ordering (self.txs), not from the pending flashblocks order:
This prev_tx_hash is passed to FlashblocksCachedExecutionProvider::get_cached_execution_for_tx, which only checks if that hash exists somewhere in the cache (lines 58–66 above). If it does, the provider returns the cached ResultAndState unconditionally (lines 80–81):
The cached result for nonce1 was computed in a world where nonce0 had already executed and incremented the sender's nonce from 0 to 1. When the malicious payload skips nonce0, the EVM state hasn't seen that increment — but the cached node trusts the stale result and commits it anyway.
A sequencer or unsafe_block_signer crafts an engine_newPayloadV4 body that omits one or more transactions present in the cached pending block. Every flashblocks-cached node accepts it as Valid and advances its chain tip. Every non-cached node rejects it as Invalid.
Hard consensus split between cached and non-cached validators. Irreconcilable chain tips.
Invalid state commitment. The cached node accepts a block whose execution results were computed under assumptions that don't hold (the skipped transaction's state effects are missing). The PoC proves the cached node returns Valid for this block while the vanilla node returns Invalid — the two nodes disagree on whether the block belongs to the canonical chain.
Precondition: the victim node runs with FlashblocksConfig.cached_execution = true. This flag defaults to false (see config.rs:24) and must be explicitly enabled. However, cached execution is the performance optimization that makes flashblocks viable at production throughput — operators running the flashblocks extension are expected to enable it.
Severity
High — Unintended chain split (network partition).
The PoC launches three real Base nodes from the same genesis, mines a shared parent block, then submits the same engine_newPayloadV4 payload to both the cached-flashblocks node and the independent vanilla node. The cached node returns Valid; the vanilla node returns Invalid. Same parent state, same payload, opposite verdicts. This is a network partition by definition.
The attacker model is the sequencer or unsafe_block_signer — the entity that constructs engine_newPayloadV4 payloads in Base's architecture. No external network access or privilege escalation is required beyond the ability to author payload bodies, which is the sequencer's core function.
Importantly, the divergence occurs between honest validators — the cached node and the vanilla node are both behaving correctly according to their own execution logic. A sequencer bug that drops or reorders transactions (without any malicious intent) would trigger the same split. The vulnerability is in the cache validation logic, not in the sequencer's behavior.
Launches three real Base v0.8.0-rc.15 nodes — one reference, one cached-flashblocks victim, one independent vanilla. All start from the same genesis. The PoC mines a shared parent block, primes the victim's cache with [deposit, nonce0, nonce1], then submits [deposit, nonce1] (skipping nonce0) to both victim and independent node.
Build & run
Or use the provided run_poc.sh which handles all of this automatically(can be found in github gist that is attached to the report)