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

74638 bc insight missing l1origintooold handler in proposer causes infinite retry loop halting l2 finality and l2 l1 withdrawals

Submitted on Apr 24th 2026 at 00:01:37 UTC by @AlexMyas for Audit Comp | Base Azul

  • Report ID: #74638

  • Report Type: Blockchain/DLT

  • Report severity: Insight

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

Description

Brief/Intro

The Base Azul proposer enters a permanent infinite retry loop when a proof's l1_head expires after ~27h, preventing new dispute games from being created and blocking all L2→L1 withdrawals until a manual restart. This is the sibling of the stall fixed in PR #2221 — L1OriginTooOld was not handled alongside GameAlreadyExists.

Vulnerability Details

l1_head is baked into the proof at dispatch time and cannot be refreshed (pipeline.rs:957, server.rs:171). After 8,191 L1 blocks (~27h), the contract rejects it:

if (blockAge > EIP2935_WINDOW) revert L1OriginTooOld(); // AggregateVerifier.sol:963

estimate_gas catches this before broadcast. The error hits classify_tx_manager_error() (output_proposer.rs:25), which only special-cases GameAlreadyExistsL1OriginTooOld falls through to SubmitAction::Failed (pipeline.rs:1196).

The Failed handler re-queues the same stale proof:

// pipeline.rs:579
state.proved.insert(target_block, proof); // stale proof re-inserted, loop repeats

No escape exists: prune_stale only removes entries ≤ recovered_block; dispatch_proofs skips blocks in state.proved; no retry cap on submission failures.

Fix: route L1OriginTooOld to SubmitAction::Discard — drops the stale proof so a fresh one is requested on the next tick.

Impact Details

  • No new dispute games → L2 finality stalls, all L2→L1 withdrawals blocked.

  • L2 sequencer unaffected; only finality and bridges halt.

  • Recovery: operator restart (minutes); proof regenerates automatically with fresh l1_head.

  • Trigger: prover or L1 RPC outage lasting >27h while a proof is in-flight.

References

  • crates/proof/proposer/src/pipeline.rs L579, L1196

  • crates/proof/proposer/src/output_proposer.rs L25

  • src/multiproof/AggregateVerifier.sol L963

  • PR #2221: fix(proposer): break GameAlreadyExists stall — analogous fix

Proof of Concept

  1. Proposer dispatches proof for L2 block N; l1_head is baked in at this moment (pipeline.rs:957).

  2. Prover/L1 RPC outage lasts >8,191 L1 blocks (~27h). Proof lands in state.proved.

  3. try_submitestimate_gas → reverts L1OriginTooOld (AggregateVerifier.sol:963).

  4. classify_tx_manager_error() has no case for it → SubmitAction::Failed → stale proof re-inserted into state.proved (pipeline.rs:586).

  5. Next tick (12s): same proof, same revert. Loop repeats indefinitely.

  6. prune_stale never clears block N; dispatch_proofs never re-proves it. Proposer halted until manual restart.

Was this helpful?