Skip to content

fix(queue): reject duplicate per-identity enrollment (Closes #99) - #166

Open
Fabiana1504 wants to merge 1 commit into
Stellar-Deejah:mainfrom
Fabiana1504:fix/99-queue-duplicate-identity-guard
Open

fix(queue): reject duplicate per-identity enrollment (Closes #99)#166
Fabiana1504 wants to merge 1 commit into
Stellar-Deejah:mainfrom
Fabiana1504:fix/99-queue-duplicate-identity-guard

Conversation

@Fabiana1504

Copy link
Copy Markdown

Closes #99.

Adds a per-identity duplicate guard to the queue contract so a single identity can no longer occupy multiple positions (the one-position-per-identity fairness guarantee).

Feature (issue #99)

  • Per-identity index — storage key ("identity_pos", identity) -> u32 (the position id), mirroring the existing ("pos", id) tuple layout so it never collides with position or scalar keys.
  • enroll_position() — checks the index before creating a position and panics with "identity_already_enrolled" on a duplicate; writes the index entry after the position is created.
  • cancel_position() — removes the index entry, so a cancelled identity may re-enroll. total_enrolled is unaffected (it counts next_id - 1, including cancelled positions).
  • get_position_by_identity(env, identity) -> Option<u32> — added to the Queue trait and implemented; O(1) lookup instead of unbounded iteration.
  • Tests (4) — duplicate enroll panics; distinct identities both succeed; index cleared on cancel (+ re-enroll works); get_position_by_identity after enroll and after cancel.
  • SDKQueueClient.getPositionByIdentity() read method (mirrors getPosition).

Why this key format & concurrency: the ("identity_pos", identity) tuple matches the contract's existing ("pos", id) convention, namespacing the entry via a Symbol discriminator. Each enroll_position() is a full transaction; Soroban serializes transactions at the ledger level and storage writes are atomic within a transaction, so the check-then-write index update cannot race.

Pre-existing fixes (required to build/run the tests)

cargo test -p lineproof-queue did not compile on main (an earlier merge left the test target broken). These were unrelated to #99 but necessary to satisfy the "tests pass" criterion:

  • test.rs: removed a duplicate use import and a duplicate name: struct field; changed |&id| closures to |id| (soroban Vec::iter() yields owned values).
  • initialize(): removed an extend_ttl() on the contract address as a persistent key that never exists — it panicked with Storage/MissingValue and broke nearly every test.
  • Made expire_position() / expire_positions_batch() pub so the generated client exposes them (their tests call client.expire_position(...)).
  • Two expire tests used advance(&admin, &0) (a no-op that never transitions state), so expire panicked; switched them to close(), a valid state for expiry.

Testing

cargo test -p lineproof-queue29 passed; 0 failed. SDK queue.ts typechecks clean. (Auto-generated test_snapshots/ are gitignored, so not included.)

Closes #99.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

enroll_position() created a new position for any identity without checking
whether that identity already held one, so a single identity could occupy
multiple slots and break the one-position-per-identity fairness guarantee.

Feature (issue Stellar-Deejah#99):
- Per-identity index: storage key ("identity_pos", identity) -> u32 position id,
  mirroring the existing ("pos", id) tuple layout so it never collides.
- enroll_position(): panics "identity_already_enrolled" if the identity already
  holds a position; writes the index after creating the position.
- cancel_position(): removes the index so a cancelled identity can re-enroll.
  total_enrolled is unaffected (counts next_id - 1, incl. cancelled positions).
- get_position_by_identity(env, identity) -> Option<u32> added to the Queue trait
  and implemented (O(1) lookup instead of unbounded iteration).
- 4 tests: duplicate enroll panics, distinct identities succeed, index cleared on
  cancel (+ re-enroll), get_position_by_identity after enroll and after cancel.
- SDK QueueClient.getPositionByIdentity() read method.

Concurrency: each enroll_position() is a full transaction; Soroban serializes
transactions at ledger level and storage writes are atomic within a tx, so the
check-then-write index update cannot race.

Pre-existing fixes required for `cargo test -p lineproof-queue` to compile & pass
(the test target was broken on main by an earlier merge, unrelated to Stellar-Deejah#99):
- test.rs: removed a duplicate `use` import and a duplicate `name:` field; fixed
  `|&id|` closures to `|id|` (soroban Vec::iter yields owned values).
- initialize(): removed an extend_ttl() on the contract address (a persistent key
  that never exists) which panicked Storage/MissingValue and broke ~every test.
- Made expire_position()/expire_positions_batch() `pub` so the generated client
  exposes them (tests call client.expire_position(...)).
- Two expire tests used advance(&admin, &0) (a no-op that never transitions) so
  expire panicked on state; switched them to close(), a valid state for expiry.

Result: cargo test -p lineproof-queue -> 29 passed; 0 failed.

Closes Stellar-Deejah#99.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Deejah Team on Vercel.

A member of the Team first needs to authorize it.

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: Queue enroll_position() allows the same identity to hold multiple positions — no duplicate guard

1 participant