Skip to content

id(tag_claims): check_op accepts a tag.clear naming the same claim twice — every sibling hash-set field in a chain extension refuses duplicates #4765

Closed

Description

@dezren39

Bug

pkgs/id/src/tag_claims.rs — check_op for tag.clear is the only hash-set field in a chain extension whose duplicates are not refused. Every sibling set-field validated through ChainExt::validate rejects a repeated hash; a clear naming the same claim twice (or 256 times) passes both the builder and the decoder check, so any node accepts and folds it on receipt.

Evidence (verified this run, origin/main a8aa52d)

  • tag_claims.rs:132-141 — the Clear branch of check_op checks only claims.len() > MAX_CLEAR_CLAIMS (256). No duplicate check.
  • Sibling rules in the same validation tree:
    • chain.rs:219-223 — a hash repeated in after is ChainError::DuplicateAfter.
    • notes.rs:124-130 — a revision naming a parent twice is a NoteError::Field ("a revision names a parent twice").
    • session_checkpoint.rs:235-242 — a session naming a participant twice is a CheckpointError::Field.
  • Laptop probe (worktree probe-2026-11-02-laneB-scan6, test against the real id crate, since removed):
    • check_op(CLEAR_KIND, Clear{claims:[claim, claim]}) = Ok(()) — duplicates accepted.
    • check_op(CLEAR_KIND, Clear{claims: vec![claim; 257]}) = Err("257 claims, the most is 256") — the cap counts copies, so a clear naming ONE distinct claim 257 times is refused while one naming 256 distinct claims passes.
    • check_op(CLEAR_KIND, 256 distinct claims) = Ok(()).
  • The folds (live_claims / compacted_claims, tag_claims.rs) treat the claims as a set, so duplicates change no outcome — the gap is purely at the admission check every other set-field enforces. Local producers never emit duplicates (LocalEvents::clear_tag builds its lists from distinct live claims and chunks them, id(local_events): clear_tag can never clear a name with more than 256 live claims — it builds one tag.clear over the 256-claim cap and every attempt fails #1720), so only a foreign or buggy peer's signed event exercises it — exactly the input check_op exists to police on decode (ChainExt::validate is applied by both chain::sign and chain::decode).

Why it matters

  • The header is the wire contract: a non-canonical clear (the same claim spelled many times) is admitted, stored, and synced to other peers instead of being refused at the choke point, mirroring the canonical-form discipline the tuple decoder and the other extension checks enforce.
  • The 256-claim cap's meaning is currently "256 entries", not "256 claims": padding a clear with copies of one claim trips the cap, and a legitimate-looking count cannot be trusted when auditing a clear's size.

Acceptance criteria

  • check_op(CLEAR_KIND, …) refuses a Clear whose claims repeat a hash, with a TagError::Field in the style of the sibling messages (e.g. "a clear names a claim twice").
  • Regression tests alongside test_a_clear_naming_too_many_claims_is_refused: duplicate claims refused (adjacent and non-adjacent); 256 distinct claims still pass; 257 copies of one claim refused as duplicates (either error accepted, but the duplicate case with ≤256 entries must fail specifically on duplication).
  • No fold behavior change: live_claims/compacted_claims results for duplicate-free clears are unchanged, and existing tag_claims/chain tests stay green.

Dedup

Searched open + closed for "tag.clear duplicate claims" and "check_op clear": only #1720 (closed — clear_tag chunking over the cap, a different defect, fixed). No issue covers duplicate claim IDs inside one tag.clear.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-11-02 23:20 CST (2026-11-03T05:20Z)
    • Found via: filed this run
    • Task: check_op for tag.clear accepts duplicate claim IDs, unlike every sibling hash-set field in a chain extension; verified with a laptop probe against the real id crate at origin/main a8aa52d
    • Expectation: effort s — small-fix packet enqueued for the queue lane; done when check_op refuses duplicate claims and regression tests pass
    • Links: packet packet-4765-fix.json (repo-health-code-pilot)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Nov 3, 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

No one assigned

    Labels

    app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions