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

75096 bc medium da backlog bytes incorrectly reports the backlog

#75096 [BC-Medium] da_backlog_bytes() incorrectly reports the backlog

Submitted on Apr 27th 2026 at 08:04:40 UTC by @shadowHunter for Audit Comp | Base Azul

  • Report ID: #75096

  • Report Type: Blockchain/DLT

  • Report severity: Medium

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

  • Impacts:

    • Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours

Description

Brief/Intro

In Span mode, da_backlog_bytes() incorrectly reports the backlog as 0 while blocks are staged in span_accumulator awaiting channel flush.

The encoder maintains a da_backlog_bytes cache field that is incremented when a block is added and decremented when a block is considered "encoded." In Span mode, the decrement happens when a block enters span_accumulator in step(). However span_accumulator is an in-memory staging area, blocks staged there have not been written to any channel, produced any frames, or been submitted to L1 in any form. The decrement is applied at the wrong lifecycle point.

In Single mode the decrement is correct because a block moves directly into an open channel during step() with no intermediate staging area. The bug is specific to Span mode where the two-phase accumulate-then-flush design means step() and actual channel encoding are decoupled.

Vulnerability Details

1

Operator starts batcher with Span mode and throttling enabled per the below config.

OP_BATCHER_BATCH_TYPE=1
OP_BATCHER_THROTTLE_THRESHOLD=1000000
2

Sequencer produces L2 blocks. Driver calls add_block for each, incrementing the backlog cache by the block's user tx bytes.

// encoder.rs:378-380
let backlog_bytes = Self::block_da_backlog_bytes(&block);
self.da_backlog_bytes += backlog_bytes;
self.blocks.push_back(block);
3

Driver calls step() to encode each block. In Span mode each block is pushed into span_accumulator and da_backlog_bytes is immediately subtracted before any channel is opened or any frame is produced.

// encoder.rs:421-423
self.span_accumulator.push((single_batch, seq_num));
self.block_cursor += 1;
self.da_backlog_bytes = self.da_backlog_bytes.saturating_sub(block_da_backlog_bytes);
4

span_accumulator now holds unshipped blocks but da_backlog_bytes reads 0. No channel has been opened. No frames exist. Nothing has been submitted to L1.

// encoder.rs:748
fn da_backlog_bytes(&self) -> u64 {
    self.da_backlog_bytes  // returns 0
}
5

Driver calls throttle.apply() with 0 as the backlog signal on every step loop iteration.

// driver.rs:155
self.throttle.apply(self.pipeline.da_backlog_bytes()).await;
6

Throttle compares 0 against threshold_bytes of 1_000_000. Condition is never true. Sequencer is never told to slow down.

// throttle.rs:148
if da_backlog_bytes >= self.config.threshold_bytes {
7

Blocks only leave span_accumulator when close_current_channel is called on timeout or size trigger. Until then da_backlog_bytes stays 0 regardless of how many blocks accumulate — span_raw_bytes which tracks the exact byte count is never included.

// encoder.rs:196-198
if add_ok {
    self.span_accumulator.clear();
    self.span_raw_bytes = 0;  // span_raw_bytes zeroed here but da_backlog_bytes already 0

Impact Details

The throttle compares da_backlog_bytes() value against threshold_bytes to decide whether to instruct the sequencer to slow down block production. With da_backlog_bytes() returning 0 while real backlog exists in span_accumulator, the throttle never fires during the accumulation window. The sequencer receives no slowdown signal and continues producing blocks at full speed, causing unbounded growth of span_accumulator in memory until the next channel flush.

Recommendation

Do not subtract from da_backlog_bytes when a block enters span_accumulator in step(). Remove this subtraction:

Instead subtract in close_current_channel() at the same point span_raw_bytes is zeroed on successful flush:

Proof of Concept

Add in crates/batcher/encoder/src/encoder.rs, inside the #[cfg(test)] mod tests { } block, right after the existing test_da_backlog_excludes_deposits test (around line 852).

Output

da_backlog_bytes() returns 0 while 225 bytes of real unshipped backlog sit in span_accumulator. The throttle receives 0, never fires, sequencer is never told to slow down.

Was this helpful?