Optional defense-in-depth: idempotency-token guard for leaderboard submit (deferred) #82

Closed
opened 2026-06-22 12:01:51 +02:00 by bosun · 1 comment
Owner

Background

Filed as deferred-optional defense-in-depth per operator ratification on 2026-06-22, following the #80 design conversation.

Context

The #80 client-only fix (deactivate input on Enter-commit, kills double-submit at source) is the load-bearing fix and sufficient for jam-scope. The server side has been substrate-grounded: leaderboard.go submit() has no dedup; duplicate (initials, score) rows are semantically valid (legit ties — two real players, same initials AAA / same score 1000 — are plausible).

So a naive server-side content-dedup ("reject if (initials, score) already present") would FALSE-POSITIVE on legit ties and is the WRONG mechanism. This tracker captures the CORRECT optional belt-and-suspenders mechanism: per-run idempotency-token.

Proposed mechanism

Client-provided per-run idempotency-token (e.g. ULID or UUID per game session). Each /submit POST carries the token. Server keeps a short-window cache (e.g. 10 min) of recently-seen submit-tokens + rejects repeats with same token.

Distinguishes "same player re-submitting via Re-Enter" (same token → reject) from "two players genuinely colliding" (different tokens → both accept). Preserves the legit-tie invariant while preventing same-player double-submits even if the client guard fails.

Priority + status

Deferred: client-only fix in #80 is sufficient for jam-scope. Filed for board-visibility of the optional future-work; NOT a commitment to build.

Trigger to promote from deferred → active:

  • Empirical signal: Re-Enter dups appear in prod post-#80-fix (would suggest client guard insufficient for some edge case)
  • OR operator/crew choice: belt-and-suspenders desired before a more visible launch (jam audience, public competition, etc.)

Lane + size

Engineer lane (server-side protocol field + cache + reject logic) + Shipwright (client-side token generation + send). Moderate scope; new protocol field, new server-state. Likely size/M.

Acceptance criteria

  1. Client generates a per-run idempotency-token (once per game-session)
  2. Client sends the token with each /submit POST
  3. Server caches recently-seen submit-tokens (sliding window appropriate to session duration)
  4. Server rejects duplicate-token submits with appropriate status (probably 409 Conflict)
  5. Server ACCEPTS different-token submits with same (initials, score) — legit ties preserved
  6. Verified via test: two clients with different tokens but same (initials, score) both place; one client with same token submitted twice → second rejected

Cross-refs

  • cellblock#80 — the original behavior gap where this option was surfaced
  • Engineer's substrate probe (f59b + 6df4 to Shipwright): leaderboard.go:77-95 has no dedup
  • Operator ratification 2026-06-22: bundle ratified (M + S + S) + this tracker filed as deferred per separate-question disposition

Anchor

2026-06-22 cellblock next-sprint design conversation. Engineer flagged that the original #80 AC#4 encoded a content-dedup mechanism-assumption that would break legit-ties invariant; reshape-to-match-observable-outcome-via-different-mechanism produced this idempotency-token form as the correct optional belt-and-suspenders.

## Background Filed as deferred-optional defense-in-depth per operator ratification on 2026-06-22, following the #80 design conversation. ## Context The #80 client-only fix (deactivate input on Enter-commit, kills double-submit at source) is the load-bearing fix and sufficient for jam-scope. The server side has been substrate-grounded: leaderboard.go `submit()` has no dedup; duplicate (initials, score) rows are semantically valid (legit ties — two real players, same initials AAA / same score 1000 — are plausible). So a naive server-side content-dedup ("reject if (initials, score) already present") would FALSE-POSITIVE on legit ties and is the WRONG mechanism. This tracker captures the CORRECT optional belt-and-suspenders mechanism: per-run idempotency-token. ## Proposed mechanism Client-provided per-run idempotency-token (e.g. ULID or UUID per game session). Each /submit POST carries the token. Server keeps a short-window cache (e.g. 10 min) of recently-seen submit-tokens + rejects repeats with same token. Distinguishes "same player re-submitting via Re-Enter" (same token → reject) from "two players genuinely colliding" (different tokens → both accept). Preserves the legit-tie invariant while preventing same-player double-submits even if the client guard fails. ## Priority + status **Deferred**: client-only fix in #80 is sufficient for jam-scope. Filed for board-visibility of the optional future-work; NOT a commitment to build. **Trigger to promote from deferred → active**: - Empirical signal: Re-Enter dups appear in prod post-#80-fix (would suggest client guard insufficient for some edge case) - OR operator/crew choice: belt-and-suspenders desired before a more visible launch (jam audience, public competition, etc.) ## Lane + size Engineer lane (server-side protocol field + cache + reject logic) + Shipwright (client-side token generation + send). Moderate scope; new protocol field, new server-state. Likely size/M. ## Acceptance criteria 1. Client generates a per-run idempotency-token (once per game-session) 2. Client sends the token with each /submit POST 3. Server caches recently-seen submit-tokens (sliding window appropriate to session duration) 4. Server rejects duplicate-token submits with appropriate status (probably 409 Conflict) 5. Server ACCEPTS different-token submits with same (initials, score) — legit ties preserved 6. Verified via test: two clients with different tokens but same (initials, score) both place; one client with same token submitted twice → second rejected ## Cross-refs - cellblock#80 — the original behavior gap where this option was surfaced - Engineer's substrate probe (f59b + 6df4 to Shipwright): leaderboard.go:77-95 has no dedup - Operator ratification 2026-06-22: bundle ratified (M + S + S) + this tracker filed as deferred per separate-question disposition ## Anchor 2026-06-22 cellblock next-sprint design conversation. Engineer flagged that the original #80 AC#4 encoded a content-dedup mechanism-assumption that would break legit-ties invariant; reshape-to-match-observable-outcome-via-different-mechanism produced this idempotency-token form as the correct optional belt-and-suspenders.
Author
Owner

Closing per Shipwright's substrate-grounded re-verification (c08e):

Substrate state on current main:

  • #80 load-bearing client fix shipped 2026-06-22: input deactivates on Enter-commit, kills double-submit at source. Close path.
  • leaderboard.go submit() (lines 77-95) inserts purely by score-rank, NO dedup BY DESIGN: duplicate (initials,score) rows accepted = legit-ties-valid invariant intact.
  • Idempotency-token remains the CORRECT optional mechanism (NOT content-dedup) if ever needed.

Neither promotion-trigger fired:

  1. No empirical Re-Enter dups reported in prod post-#80 ship
  2. No operator/crew request for belt-and-suspenders before public launch

Close-as deferred-won't-build-for-jam-scope per the #105 pattern. Knowledge preserved in issue body (design rationale + re-file triggers) — substrate-of-record survives the close.

Same close-with-substrate-of-record discipline as #105 (verify-only confirmed no-fix-needed) applied here at decision-not-build layer (verify-mootness + close-without-action). Idempotency-token-guard remains the right mechanism for if-and-when triggers fire.

Closing per Shipwright's substrate-grounded re-verification (c08e): **Substrate state on current main**: - **#80 load-bearing client fix shipped 2026-06-22**: input deactivates on Enter-commit, kills double-submit at source. Close path. - **leaderboard.go submit() (lines 77-95) inserts purely by score-rank, NO dedup BY DESIGN**: duplicate (initials,score) rows accepted = legit-ties-valid invariant intact. - **Idempotency-token remains the CORRECT optional mechanism** (NOT content-dedup) if ever needed. **Neither promotion-trigger fired**: 1. No empirical Re-Enter dups reported in prod post-#80 ship 2. No operator/crew request for belt-and-suspenders before public launch **Close-as deferred-won't-build-for-jam-scope** per the #105 pattern. Knowledge preserved in issue body (design rationale + re-file triggers) — substrate-of-record survives the close. Same close-with-substrate-of-record discipline as #105 (verify-only confirmed no-fix-needed) applied here at decision-not-build layer (verify-mootness + close-without-action). Idempotency-token-guard remains the right mechanism for if-and-when triggers fire.
bosun closed this issue 2026-06-23 19:44:15 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock#82
No description provided.