Skip to content

id(waiting): the waiting pool has no total cap — the per-author cap is Sybil-defeated, so one peer can make a node hold (and persist) unbounded waiting events #1567

Closed

Description

@dezren39

Bug / robustness gap

pkgs/id/src/waiting.rs — the waiting pool has no total cap. The only bounds are per-author and per-event age, so the module doc's claim is not true as written:

"Two caps bound what a peer can make a node hold: at most WaitingLimits::per_author waiting events per author, and a drop once an event has waited WaitingLimits::max_age_ms." (waiting.rs:5-7)

Evidence on main db6f7d0:

  • WaitingLimits carries exactly two fields, per_author (default 256) and max_age_ms (default 7 days) — waiting.rs:44-61, defaults :33-38.
  • The single hold path, WaitingPool::hold, checks only the per-author count and nothing else (waiting.rs:296-322):
    if self.waiting_count(author) >= self.limits.per_author() {
        return Err(WaitRefusal::AuthorFull);
    }
    insert, park, and restore all funnel through it; no path ever consults self.waiting.len() against a bound.
  • Author keys are self-certifying: an event's author is the Ed25519 public key embedded in its signed header, and chain::decode (src/chain.rs:321+) verifies the signature against that embedded key. Minting a new author identity is free — generate a keypair, sign events whose after/backlink dependencies never arrive, and each new author gets a fresh 256-event allowance.
  • Held events are not memory-only: the event store mirrors every held event into redb waiting rows on disk (src/event_store/waiting_rows.rs:1-12, "the events held on disk"), and restores them on open. So the unbounded total is a disk bound failure as well as a memory one, and it survives restarts until each event individually ages out (up to 7 days at the default).

Why it matters

A single hostile or buggy peer syncing events with unsatisfiable dependencies can grow a node's waiting pool — and its on-disk waiting table — without limit: 256 events × N minted authors × up to 7 days each. The per-author cap gives the appearance of a bound (and the doc asserts one) while bounding nothing an attacker controls. Every other ingest-side resource in this codebase is capped (wants entries/bytes, schedule count/horizon, tick intervals — src/world_caps.rs); the waiting pool is the outlier.

Acceptance criteria

  • WaitingLimits gains a total cap across all authors (a named constant default, e.g. DEFAULT_TOTAL), enforced in hold so insert/park/restore all respect it; refusal reuses or extends WaitRefusal so callers can distinguish total-full from author-full (or documents why one variant serves both).
  • The cap interacts sanely with restore-on-open: a store whose on-disk rows exceed a newly lowered total does not brick on open (excess handling is defined — e.g. oldest-first expiry — and tested).
  • waiting.rs module doc updated so the stated bound is the real bound.
  • Tests: a pool under a small total refuses the (total+1)-th held event spread across distinct authors while per-author counts are all below their cap; existing per-author and age-cap tests keep passing.
  • No change to release order, expiry semantics, or the sync contract (a refused event is, as today, dropped and not acknowledged, so sync re-sends it if its dependency ever exists).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/mMedium: expected under 1 day of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-b (Lane B, developing-today/code)
    • Session / ID: cron job new-issue-creator-code-b, run 2026-10-27 21:20 CDT
    • Found via: filed this run
    • Task: Filed after code verification on main db6f7d0: the waiting pool (pkgs/id/src/waiting.rs) has no total cap. WaitingLimits holds only per_author (256) and max_age_ms (7d) (:44-61); the single hold path checks only the per-author count (:296-322) and insert/park/restore all funnel through it. Author keys are self-certifying (chain::decode, src/chain.rs:321+, verifies the signature against the header-embedded key), so minting authors is free and each gets a fresh 256 allowance — contradicting the module doc's claim (:5-7) that the two caps "bound what a peer can make a node hold". Held events are also mirrored to redb waiting rows on disk (event_store/waiting_rows.rs:1-12), so the unbounded total is a disk bound too. Enqueued as a triage packet to repo-health-code-pilot.
    • Expectation: triage (effort m) confirms the reachability framing and routes the fix: a total cap in WaitingLimits enforced in hold, restore-on-open behaviour defined for over-cap rows, doc corrected, cross-author total test added.
    • Links: packet hidden_files/packet-1567.json · body hidden_files/issue-code-id-waiting-total-cap-body.md
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 28, 2026
  5. 12 remaining items

  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), lane D
    • Session / ID: task 1836
    • Found via: queue claim
    • Task: Implement the waiting-pool total cap: WaitingLimits gains a total cap (named default constant) enforced in hold(), refusal distinguished/documented, restore-on-open behaviour for over-cap rows defined, module doc corrected, cross-author total test added.
    • Expectation: effort m; done = PR opened with cargo tests for waiting + event_store passing, issue closes via the PR.
    • Links: branch queue/1836-1567-waiting-total-cap (to be created)
  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

    • Agent: queue worker (repo-worker-code), lane D
    • Session / ID: task 1836
    • Found via: queue claim
    • Task: Implemented the total cap and opened PR 2940 @c1f791db5: DEFAULT_TOTAL 8,192 in WaitingLimits (total() / with_total), hold() refuses WaitRefusal::PoolFull (checked before per-author), restore-on-open drops over-cap rows like per-author refusals so a lowered total never bricks the store, module doc corrected to three caps. Laptop: cargo test --lib waiting 29/29 and --lib ingest 102/102 pass, including the cross-author total test and the reopen-under-lowered-total store test.
    • Expectation: parked 20m for CI + Codex review on PR 2940; resume = CI green + Codex dispositioned → merge (closes this issue), complete task 1836.
    • Links: PR id(waiting): add a total cap to the waiting pool (#1567) #2940 · branch queue/1836-1567-waiting-total-cap
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · correction

    • Agent: queue worker (repo-worker-code), lane D
    • Session / ID: task 1836
    • Found via: queue claim
    • Task: Correction to the parked note on PR 2940: the laptop counts it cited (waiting 29/29, ingest 102/102) came from an unreliable background delivery. A foreground re-run found the new reopen test failing (InUse: the middle store was still open at the third reopen); fixed in 9738686. True foreground counts: ingest 55/55, waiting 37/37. The task stays parked for CI + Codex on the new head.
    • Expectation: resume unchanged = CI green + Codex dispositioned on PR 2940 @97386867d → merge (closes this issue), complete task 1836.
    • Links: PR id(waiting): add a total cap to the waiting pool (#1567) #2940 · branch queue/1836-1567-waiting-total-cap @ 9738686
  10. added 2 commits that reference this issue on Oct 31, 2026
  11. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Oct 31, 2026
  12. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions