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

76540 bc medium witnessexecutor proves root without proving claimed l2 block number was reached

Submitted on May 4th 2026 at 19:28:57 UTC by @jesse03 for Audit Comp | Base Azul

  • Report ID: #76540

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

    • A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk

Description

WitnessExecutor::run proves a root, but does not prove it reached the claimed L2 block number.

base-0.8.0-rc.28/crates/succinct/utils/client/src/witness/executor.rs

async fn run<O, DP, P>(
    &self,
    boot: BootInfo,
    pipeline: DP,
    cursor: Arc<RwLock<PipelineCursor>>,
    l2_provider: OracleL2ChainProvider<O>,
    intermediate_root_interval: u64,
) -> Result<(BootInfo, Vec<alloy_primitives::B256>)>

WitnessExecutor::run calls advance_to_target with the claimed L2 block number:

It then validates only the output root:

But it never checks that:

That means the proof can succeed even if derivation stopped before the claimed target block.

The issue becomes critical because run returns the original boot_clone:

Then run_range_program commits this original boot info publicly:

And BootInfoStruct::new uses the claimed block number from BootInfo:

So the public proof output can claim:

“I proved output root R at L2 block N”

even though the program only derived up to an earlier block M < N.

The driver explicitly allows early termination when the derivation pipeline runs out of data.

In base-0.8.0-rc.28/crates/proof/driver/src/core.rs:

If target = Some(boot.claimed_l2_block_number) and the pipeline reaches EndOfSource, the driver silently changes the target to the current safe head.

Then it returns successfully:

This is dangerous for a proof program. A proving circuit/program must fail if it cannot reach the exact public target it claims to prove.

Exploit

Assume the real derivation can only reach block 110.

The attacker supplies boot data like this:

The witness contains enough data to derive blocks 101..110, but not enough data to derive up to 1_000_000.

1

WitnessExecutor::run asks the driver to derive to block 1_000_000

WitnessExecutor::run requests derivation to the claimed block number.

2

The driver derives only to block 110

Derivation stops at the last available block.

3

The pipeline hits EndOfSource

The data source is exhausted.

4

The driver silently changes the target to block 110

The target is adjusted to the current safe head.

5

The driver returns (safe_head = block 110, output_root = R110)

The returned output root matches the derived head.

6

WitnessExecutor::run checks only output_root == boot.claimed_l2_output_root

This passes because the attacker set:

7

The function returns the original boot_clone, which still says claimed_l2_block_number = 1_000_000

The original claimed block number is preserved.

8

run_range_program commits a public BootInfoStruct saying l2BlockNumber = 1_000_000 and l2PostRoot = R110

The proof has now falsely attested that R110 is the output root at block 1_000_000.

Impact

This is a proof soundness failure.

A malicious prover can generate a valid zkVM proof for a false L2 range statement. Specifically, they can claim that an output root corresponds to a later L2 block number without actually deriving or executing blocks up to that number.

This can allow:

The aggregation program does not fix this. It verifies the proof digest of each BootInfoStruct and checks sequencing between public boot infos, but it has no independent knowledge of the actual block number reached inside WitnessExecutor::run.

So once the range proof commits the wrong l2BlockNumber, aggregation trusts it.

Root cause

WitnessExecutor::run treats root equality as sufficient:

But the proven statement includes both:

The function must prove the pair:

not just the output root.

Proof of Concept

Was this helpful?