[Critical] Scroll Chain DoS via CCC Overflows in Single User Transactions

(click toggle to expand) Report Triage Timeline + Full Conversation

Scroll Technical Description:

In Scroll zkEVM Rollups, transaction execution occurs in two main steps:

  1. The standard EVM executes all transactions, performs state transitions, and then sends transaction traces to the zkEVM provers.
  2. The zkEVM provers process these traces, but due to their limited capacity, they may be unable to handle all of them. To prevent this, Scroll imposes a row consumption limit (circuit capacity) of transactions per block, rejects and reorganizes transactions before finalizing the block at the EVM execution stage.

Scroll follows a strict threat model for zkEVM provers: if transaction traces exceed row capacity, the zk provers may fail. An unprovable block prevents the entire chain from finalizing and wastes valuable computational resources. To mitigate this, Scroll employs the Circuit Capacity Checker (CCC) in l2-geth to validate transactions before they enter the zkEVM circuits.

Vulnerability Description:

The mining process for transactions follows these high-level steps:

  1. A transaction enters the mempool and is then promoted to the worker.

  2. Transactions are processed within the mainLoop() function, as handled by the following code section:

  3. The ProcessTxs() function processes transactions one by one according to priority via ProcessTx().

    Important factor: In scroll-geth, all incoming transactions are processed first, executing EVM operations and state transitions. The Circuit Capacity Checker (CCC) is then executed asynchronously just before block sealing.

  4. Once the block is ready for sealing, the mainLoop() function proceeds to this code section to commit the block.

In the current design, the CCC functions primarily act as a post-sealing validation rather than a pre-sealing check. This approach enables faster block production while still enforcing circuit capacity constraints through the reorg mechanism.

The CCC verifies each transaction individually, accumulating row consumption within the Rust library. When the CCC limit is reached, it returns an error, TxIdx, and ShouldSkip, then triggers the failingCallback() function to initiate a reorg for the CCC case. Example of this code snippet:

https://github.com/scroll-tech/go-ethereum/blob/ac8164f5a4190ff9e536f296195013ea7a4e3e3d/rollup/ccc/async_checker.go#L179-L198

If a single transaction alone could exceed the CCC limit, Scroll removes it entirely from both the mempool and the mined block, as it would not be processed by the zkProvers anyway. If a single transaction is capable of exceeding the CCC limit, the ShouldSkip flag is set to true, and during chain reorganization, the transaction is removed from both the block and the mempool.