KB-4912

RS3B-05 — Registrar Replay / Idempotency / Attempt State Machine v0.1 — 2026-06-21

10 min read Revision 1
rs3breplayidempotencynonceattempt-idfail-closedcodex-c1-c32026-06-21

RS3B-05 — Registrar Replay / Idempotency / Attempt State Machine v0.1 — 2026-06-21

Macro: RS3B-REGISTRAR-HARDENING-DESIGN (read-only / KB-design) Deliverable: 05 of 10 · Replay / idempotency / attempt (Mục tiêu E) · consumes Codex RS3-PATCH2 C1–C3 Date: 2026-06-21 · 0 mutations · NO_CODEX_LIVE_READ Deliverable status: REPLAY_DOMAIN_FAIL_CLOSED_UNTIL_SURFACE_FIT_PROVEN · REPLAY_SURFACE_NOT_FIT — no existing surface fits; required surface defined as criteria, not created.


1. Three separated identities (Codex C1)

The conflated nonce | idempotency_key is split into three independent identities. One physical surface may carry all three only if it proves separate constraints and state transitions for each.

Identity Definition Uniqueness rule Must NOT
logical_request_key stable across exact client retries; unique for one intended registration effect (one DOT artifact, one operation/target/run) single-use axis: one durable consumed record per logical key be re-satisfied by a new attempt or a fresh nonce
authorization_nonce single-use authority credential, bound to the exact authority envelope (owner/approval row) + validity window single-use; consumed on use; bound to envelope, not free-floating authorize a duplicate logical effect just by being fresh
attempt_id execution/retry identity for one logical key never part of the single-use uniqueness key bypass logical_request_key (the REPLAY_ATTEMPT_NO_BYPASS defect)

Canonical binding digest (RS3-PATCH2 §6.1, carried as the authority-binding value, not the sole identity):

replay_key = H(protocol_version, logical_request_key, canonical_operation, canonical_target,
               deployed_artifact_hash, owner_or_approval_binding, run_id)   # attempt_id NON-keying

A fresh authorization_nonce must not permit a duplicate logical registration effect: the nonce authorizes; the logical_request_key (via replay_key) is what is single-use.

2. Why the live surface is NOT fit (source-fact, 2026-06-21)

iu_route_attempt (68 rows): columns idempotency_key (text NN), attempt_no (int NN, default 1, CHECK ≥ 1), route_kind ∈ {inbound,outbound}, status ∈ {pending,dry_run,sent,skipped,failed,disabled}; constraint UNIQUE(idempotency_key, attempt_no); no replay_key/nonce/run_id/operation/canonical_target/deployed_artifact_hash/owner-approval columns; no triggers.

  • UNIQUE(idempotency_key, attempt_no) is not single-use: (key,1),(key,2),… all insert → incrementing attempt_no reuses the same key (REPLAY_ATTEMPT_NO_BYPASS).
  • Wrong domain: IU message routing (inbound/outbound), not DOT registration.
  • Writer authority unproven; no atomic-consume semantics for registration.

Verdict: iu_route_attempt is rejected as the registrar replay store (must-not-do #31). It is a retry-ledger precedent only.

3. Transaction / rollback state machine (Codex C2)

Single atomic Phase-1 transaction consumes the logical key and writes the inert registration result together. No "committed consume + failed same-txn registration" split (that contradiction is rejected, RS3-PATCH2 C2).

            ┌──────────────────────────── Phase-1 atomic txn ───────────────────────────┐
 issue req  │  check logical_request_key consumed?  ── yes ─► return PRIOR durable result │  (exact retry)
   (nonce,  │            │ no                                                              │
   attempt) │            ▼                                                                 │
            │  validate nonce single-use + bound + fresh ── fail ─► REJECT (no consume)    │
            │            │ ok                                                              │
            │            ▼                                                                 │
            │  INSERT consume(logical_request_key) + INSERT inert registration row         │
            │            │   ON CONFLICT(logical_request_key) -> REPLAY_DUPLICATE (reject)  │
            │            ▼                                                                 │
            │        COMMIT  ── fail ─► (S1) PRE-COMMIT FAILURE: both roll back together     │
            └────────────┬────────────────────────────────────────────────────────────────┘
                         │ commit ok
                         ▼
              (S2) COMMIT SUCCESS: consume + inert registration durable together
                         │
                         ▼
               post-commit paired verifier (RS3B-03 §5)
                         │
              ┌──────────┴───────────┐
        verify PASS            verify FAIL
              │                       │
        registered-closed     (S3) POST-COMMIT VERIFY FAIL:
                                inert row exists; logical key STAYS consumed;
                                returns failed/compensating state; a NEW nonce may
                                authorize COMPENSATION but MUST NOT recreate the effect

State table

State Condition Consume row Registration row Retry behavior
S0 issue request arrives check consumed first
S1 pre-commit failure commit fails / crash before commit rolled back rolled back uncertain-commit recovery: client must re-resolve commit status before issuing a new logical request (do not blindly re-issue)
S2 commit success atomic commit durable durable inert exact retry returns the durable prior result (idempotent)
S3 post-commit verify fail verifier rejects committed row durable (stays consumed) durable inert (flagged) same logical key returns failed/compensating; new nonce → compensation only, no new registration effect
S4 concurrent attempt second attempt_id on same logical key during S0–S2 one wins via ON CONFLICT one row loser → ATTEMPT_COLLISION, reads winner's result

4. Freshness vs consumed-state (Codex C3)

Request/envelope TTL governs whether a new request is admissible; it does not make an already-consumed logical_request_key reusable. The consumed record is retained / tombstoned for the replay horizon (not deleted when a request goes stale). An old consume row still proves prior consumption.

  • P2-RP-07 REPLAY_STALE_ROW is reinterpreted: a stale request is rejected as inadmissible; a stale consume row still blocks replay.
  • No authority-approved key-reuse policy exists here, so consumed = permanently blocking for the horizon. CONSUMED_STATE_ERASED_BY_STALE_REQUEST is a fail-open condition (must-not-do #33).

5. Failure / adversarial table

ID Bad input/state Expected Layer Fail-open condition (forbidden)
RP-01 same logical_request_key, new attempt_id REPLAY_ATTEMPT_NO_BYPASS / return prior R accepting because attempt differs
RP-02 fresh authorization_nonce, same logical effect reject duplicate effect R nonce freshness re-authorizing effect
RP-03 reused (already-consumed) nonce REPLAY_NONCE_CONSUMED R accepting consumed nonce
RP-04 nonce not bound to envelope/window NONCE_UNBOUND V/R accepting free-floating nonce
RP-05 exact client retry after S2 return durable prior result R creating a 2nd effect
RP-06 crash between insert and commit (S1) both roll back; uncertain-commit recovery R re-issuing without recovery → double effect
RP-07 post-commit verify fail (S3) logical key stays consumed; compensation only R recreating registration under new nonce
RP-08 stale request, consume row exists request inadmissible; replay still blocked V/R erasing consumed meaning on staleness
RP-09 two concurrent attempts (S4) one wins (ON CONFLICT), other ATTEMPT_COLLISION R both commit
RP-10 use iu_route_attempt as the store REPLAY_SURFACE_NOT_FIT (reject surface) R treating attempt-ledger as single-use

6. Required future surface (defined, NOT created)

To reach REPLAY_DOMAIN_READY_AS_CRITERIA, the selected surface must prove ALL: (a) single-use UNIQUE on replay_key (one row per logical effect; attempt_no/attempt_id non-keying); (b) columns binding operation, canonical_target, run_id, deployed_artifact_hash, owner_or_approval_binding; (c) registration domain (not IU routing); (d) atomic in-Phase-1 consume with ON-CONFLICT reject; (e) proven registrar writer + append-only/immutability; (f) durable failure audit outside the rolled-back txn (RS3B-06); (g) exact-retry returns prior decision; (h) retention/tombstone for the replay horizon (C3). Surface selection is fail-closed; no table is created (reuse-first; evaluate existing surfaces before any new ledger).

7. Status block

  • Deliverable status: REPLAY_DOMAIN_FAIL_CLOSED_UNTIL_SURFACE_FIT_PROVEN · REPLAY_SURFACE_NOT_FIT
  • Consumes Codex C1 (3 identities), C2 (txn state model), C3 (freshness ≠ consumed-erasure)
  • iu_route_attempt rejected as store; pure validator is not the replay-state owner (must-not-do #28)
  • Registration gate: REGISTRATION_HOLD · REGISTRATION_CAN_PROCEED = NO · 0 mutations
Back to Knowledge Hub knowledge/dev/laws-new/reports/rs3b/05-replay-idempotency-attempt-state-machine-v0-1-2026-06-21.md