# Off-by-one error in get_pending_commitments causes chain halt

Off-by-one exclusive range in Citrea's get_pending_commitments drops the last pending sequencer commitment, halting the L2 commitment and proof pipeline.

- Payout: Contest reward
- Severity: medium
- Program: Citrea
- Credited: Goran
- Disclosed: 2025-10-21
- Write-up quality: 7/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_citrea_jul2025.pdf#3.2.6
- Library page: https://bugbountylibrary.com/r/cantina%3Acitrea_jul2025%3A3.2.6
- Tags: domain:web3 impact:dos impact:integrity kind:contest platform:cantina sev:medium tech:rust technique:source-review vuln:accounting-error vuln:dos

**Target:** Sequencer ledger DB (SequencerLedgerOps::get_pending_commitments)
**Root cause:** get_pending_commitments passes an exclusive Rust range (start..end) to get_data_range, which computes max_items as end-start, so the final pending commitment index is never read.
**Steps:**
1. Note get_pending_commitments builds range `start..end` where end = last pending index (exclusive).
2. get_data_range computes `max_items = end - start`, one too small, and `take(max_items)`.
3. With pending indexes [5,6,7], only 5 and 6 are returned; with a single pending [7], empty vec returned.
4. Repro: add debug log after `put_pending_commitment`, run bitcoind regtest + citrea sequencer, observe `get_pending_commitments: Ok([])` after storing commitment 1.
**How found:** Code review of the pending-commitment retrieval path, spotting the exclusive Rust range combined with max_items arithmetic in get_data_range, then confirming with debug-log PoC on regtest.
**Exploitation:** Not attacker-triggered; occurs naturally on sequencer crash/restart or DA delays. resubmit_pending_commitment never resubmits the lost commitment, and load_next_commitment_index_and_start_height can create duplicate indices, halting the commitment chain so L2 blocks stay uncommitted and no proofs can be generated.
**Insight:** When a helper computes iteration count as `end - start` from a Rust exclusive range, every caller passing `..end` silently loses the last element; audit range-to-count conversions and test the single-element case.
