Skip to content

fix: advance(batch_size=0) must not brick the queue (#199) - #234

Merged
k-deejah merged 1 commit into
Stellar-Deejah:mainfrom
BigDella:fix/199-advance-zero-batch-size-guard
Sep 3, 2026
Merged

fix: advance(batch_size=0) must not brick the queue (#199)#234
k-deejah merged 1 commit into
Stellar-Deejah:mainfrom
BigDella:fix/199-advance-zero-batch-size-guard

Conversation

@BigDella

Copy link
Copy Markdown

Summary

advance() had no guard against batch_size == 0. With batch_size = 0 the for _ in 0..batch_size loop body never executes — nothing advances — but the function still fell through to write the idx storage key, wasting Soroban fee budget for zero work.

The original issue also described this as bricking the queue permanently in AdvancementActive with no way to advance. That specific consequence is already avoided in the current code — this contract gates the AdvancementActive transition on !advanced.is_empty() — so a zero-result advance() no longer commits the status change. But the contract still silently accepted a call that can never do anything useful, and the issue's acceptance criteria explicitly wants the operator-facing panic plus a status-preserving guarantee proven by test, so:

Changes

  • Added if batch_size == 0 { panic!("batch_size_must_be_positive") } at the very top of advance(), before any storage read or write (before config/admin are even loaded) — a caller that passes 0 finds out immediately, and there's nothing left to accidentally mutate.
  • Added test_advance_zero_batch_size_panics (#[should_panic(expected = "batch_size_must_be_positive")]).
  • Added test_advance_zero_batch_size_does_not_change_queue_status, which catches the panic via std::panic::catch_unwind (the same pattern already used elsewhere in test.rs), asserts the queue is still EnrollmentClosed afterward, and then proves the queue is still advanceable with a real batch size — i.e. advance(0) can't brick it even implicitly.

The contributor note's open question

Whether an all-cancelled batch (loop runs, advanced stays empty, only Skipped events fire) should transition to AdvancementActive: left as-is — it does not, per the existing !advanced.is_empty() guard. A queue where every position in the sampled range was pre-cancelled hasn't actually progressed, so treating it as "still closed, nothing done" rather than silently committing to AdvancementActive on cancellations alone seemed like the safer default; happy to discuss if the intended behavior is different.

Acceptance Criteria

  • advance(env, admin, 0) panics with "batch_size_must_be_positive"
  • Queue status is NOT changed to AdvancementActive when advance() panics on validation
  • #[should_panic(expected = "batch_size_must_be_positive")] test added
  • Status transition to AdvancementActive only occurs when loop produces ≥1 result (pre-existing, verified untouched)
  • All existing advance tests still pass (unmodified — new guard only affects the batch_size == 0 path)
  • cargo test -p lineproof-queue passes with zero failures — could not run locally, see verification note

Verification

No local Rust toolchain is available in this environment (link.exe fails compiling proc-macro2/quote build scripts — unrelated to this change). Verified instead by close manual review: the added guard is a single early-return panic with no interaction with any other code path, the rest of the existing advance() test suite is untouched and calls advance() with batch sizes ≥ 1 throughout, and the new catch_unwind test reuses an already-proven-working pattern from elsewhere in the same file. Would appreciate CI/a reviewer confirming cargo test -p lineproof-queue locally.

Closes #199

advance() previously had no guard against batch_size == 0: the for-loop
body (`for _ in 0..batch_size`) never executed, so nothing advanced, but
the function still fell through to the idx storage write — pure wasted
Soroban fee budget for zero work done.

That specific instance is otherwise harmless in the current code, because
this contract already gates the AdvancementActive transition on
`!advanced.is_empty()` (line ~315) — so an all-zero-result advance() no
longer permanently bricks the queue the way the issue originally
described. But the contract still silently accepted a call that can never
do anything useful, and the acceptance criteria explicitly asks for the
operator-facing panic plus a status-preserving guarantee, so:

- Added `if batch_size == 0 { panic!("batch_size_must_be_positive") }` at
  the very top of advance(), before any storage read or write — a caller
  that mistypes 0 finds out immediately instead of paying to no-op, and
  the panic happens before config/admin are even loaded, so there's
  nothing left to accidentally mutate.
- Added `test_advance_zero_batch_size_panics`
  (`#[should_panic(expected = "batch_size_must_be_positive")]`).
- Added `test_advance_zero_batch_size_does_not_change_queue_status`,
  which catches the panic via `std::panic::catch_unwind` (the same
  pattern already used elsewhere in this test file), asserts the queue is
  still EnrollmentClosed afterward, and then proves the queue is still
  advanceable with a real batch size — i.e. advance(0) can't brick it
  even implicitly.

On the contributor note's open question — whether an all-cancelled batch
(loop runs, `advanced` stays empty, only `Skipped` events fire) should
transition to AdvancementActive: left as-is (it does not, per the
existing `!advanced.is_empty()` guard), since a queue where every
position in the sampled range was pre-cancelled hasn't actually
progressed and re-running with a different admin/idx state should still
be treated as "still closed, nothing done" rather than silently
committing to AdvancementActive on cancellations alone.

Verification: no local Rust toolchain available in this environment
(link.exe fails on proc-macro2/quote build scripts, unrelated to this
change — same limitation hit on other Rust work this session), so
`cargo test -p lineproof-queue` could not be run here. Verified instead
by close manual review: the added guard is a single early-return panic
with no interaction with any other code path, `test_advance_closed_queue_panics`
and the rest of the existing `advance()` test suite are untouched and
call `advance()` with batch sizes >= 1 throughout, and the new
catch_unwind test reuses an already-proven-working pattern from this
same file (see the pre-existing `catch_unwind` test elsewhere in
test.rs).
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Obiajulu-gif is attempting to deploy a commit to the Deejah Team on Vercel.

A member of the Team first needs to authorize it.

@k-deejah
k-deejah merged commit a910be3 into Stellar-Deejah:main Sep 3, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contracts: advance() with batch_size=0 still transitions queue to AdvancementActive and wastes ledger resources

3 participants