Skip to content

security(rollup): _commitBatch does not validate lastBlockNumber > parent (empty block span → node DoS) #996

@curryxbo

Description

@curryxbo

Summary

Rollup.sol::_commitBatch accepts an unconstrained lastBlockNumber from calldata. There is no on-chain check that the committed batch's lastBlockNumber is greater than the parent batch's lastBlockNumber (i.e. that the batch contains at least one block / blockCount >= 1).

Because the BLS signature path is still a stub (_getBLSMsgHash returns bytes32(0)), this field is not signature-bound either, so a single active staker (byzantine or merely buggy) can commit a malformed lastBlockNumber on-chain.

This is the root cause of the node-side batch-parse DoS fixed in #994 / #995. Those PRs harden the derivation node so a malformed blockCount becomes a clean parse error instead of a panic — but the malformed batch is still accepted on L1, which leaves every verifier node permanently stalled on that batch (it cannot be skipped without diverging from the L1-canonical chain). The only complete fix is to reject the malformed value at the source, on-chain.

Impact

Affected code

  • contracts/contracts/l1/rollup/Rollup.sol::_commitBatch (L888) — stores lastBlockNumber into the data hash with no validation.
  • contracts/contracts/l1/rollup/Rollup.sol::_commitBatchWithBatchData — the shared path for commitBatch / commitState / commitBatchWithProof.
  • Related: _getBLSMsgHash (L788) is a return bytes32(0) stub, so the field is not signature-bound (tracked separately).

Proposed fix (this issue)

In _commitBatchWithBatchData, after loading the parent header and before computing the data hash, enforce a non-empty, monotonic block span when the parent header carries a lastBlockNumber (V1+):

if (BatchHeaderCodecV0.getVersion(_batchPtr) >= 1) {
    require(
        batchDataInput.lastBlockNumber > BatchHeaderCodecV1.getLastBlockNumber(_batchPtr),
        "empty block span"
    );
}

This mirrors the node's blockCount = lastBlockNumber - parentLastBlockNumber derivation. The V0->V1 transition (parent version 0) carries no lastBlockNumber and is a historical one-time event, so it is not guarded here.

Out of scope (follow-up)

  • Implementing _getBLSMsgHash so batch fields (incl. lastBlockNumber) are bound to a real BLS signature over the sequencer set (_getValidSequencerSet is also a TODO). This is the deeper, systemic fix and is tracked separately.
  • An upper-bound span cap (MAX_BLOCKS_PER_BATCH) as additional defense-in-depth; the node side (fix(derivation): reject invalid blockCount to prevent batch-parse DoS #995) already bounds allocation by payload length.

Notes

This is an L1 contract change and requires the normal upgrade path (audit + timelock/governance). #995 (node-side mitigation) ships independently and faster; this issue is the source-level closure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions