Skip to content

store: select_nullifiers_by_prefix stalls sync when single block exceeds MAX_ROWS for a prefix set #2460

Description

@Sertug17

Version

main branch, commit 597e354 (2026-08-09)

Other packages versions

N/A store crate internal bug

What happened?

When querying nullifiers via select_nullifiers_by_prefix, if a single block produces more nullifiers matching the requested prefix set than MAX_ROWS, the sync client enters an infinite loop.

Current code (crates/store/src/db/models/queries/nullifiers.rs:100-115):

if let Some(last) = raw.last() && raw.len() > MAX_ROWS {
let last_block_num_i64 = last.block_num;
let nullifiers = vec_raw_try_into(
raw.into_iter().take_while(|row| row.block_num != last_block_num_i64),
)?;
let last_block_included = BlockNumber::from_raw_sql(last_block_num_i64.saturating_sub(1))?;
Ok((nullifiers, last_block_included))
}

If ALL returned rows belong to the same block N (≥ MAX_ROWS+1 nullifiers):

  • take_while drops everything → empty nullifiers vector
  • last_block_included = N - 1
  • Client thinks: "synced to N-1, no nullifiers"
  • Client re-requests [N..chain_tip]
  • Same result → infinite loop. Client permanently stalled.

What should have happened?

The function should either:

  1. Return a non empty response with progress guarantee, OR
  2. Return an explicit error (e.g. BlockTooLargeToPage) so the client knows it cannot page through that block with the current protocol.

The client should never be told "you are synced to N-1" when block N actually contains matching nullifiers.

How can this be reproduced?

Trigger condition: A single block must contain > MAX_ROWS nullifiers for the queried prefix set.

MAX_RESPONSE_PAYLOAD_BYTES / 36 ≈ 116,508 nullifiers per block for a prefix set.
With 16-bit prefixes (65,536 space) and up to 1,000 prefixes requested, a wide-range scanning client can easily hit this.

Test that should fail on current code:

#[test]
fn supersaturated_single_block_nullifier_does_not_stall() {
// Insert MAX_ROWS + 100 nullifiers all in block N for prefix P
// Query select_nullifiers_by_prefix for P
// Must return non-empty AND allow progress to N+1
}

No such test exists in tests.rs today.

Relevant log output

// No runtime log this is a silent correctness bug.
// The buggy path returns Ok(([], N-1)) which looks valid to the caller.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions