Skip to content

fix(purchases): idempotency token defeated by retry/fan-out — derive from stable lineage key (double-buy risk) #1012

Closed

Description

@cristim

Problem. The per-rec idempotency token is sha256(executionID + ":" + recIndex) (pkg/common/tokens.go:41, derived in internal/purchase/execution.go:428). But the Retry path mints a fresh uuid.New() execution ID (internal/api/handler_purchases.go:840), and the multi-account path mints a fresh per-account UUID (internal/purchase/execution.go:151-160). Both therefore derive a different token than the original attempt, so provider-level idempotency (AWS SP ClientToken, Azure reservationOrderID, GCP RequestId/name) cannot match → double-buy when an executor died after the provider created the commitment but before the row persisted as success.

Evidence.

  • internal/purchase/execution.go:428 (token derivation), :151-160 (per-account UUID)
  • pkg/common/tokens.go:41 (DeriveIdempotencyToken)
  • internal/api/handler_purchases.go:840 (persistRetryExecution, uuid.New())
  • internal/purchase/reaper.go:110-113 + recovery sweep comments assert "the operator's retry hits the idempotency path" — false per above.

Impact. Direct financial loss (duplicate RI/SP/CUD purchase) on the exact recovery scenario the whole design targets. Highest-dollar case is a multi-account plan fan-out.

Suggested fix. Make idempotency identity independent of the execution ID: either (a) add an IdempotencyKey column to purchase_executions, generated once at first creation and copied verbatim into every retry/per-account successor; or (b) derive from the root lineage execution ID + account ID + rec index, propagated onto every successor row. Update reaper/recovery comments once true. Regression test: retry of a "failed" execution whose commitment landed must derive the SAME token and short-circuit.

References. Source: report 05 (C1, H1) + architecture notes. Blocks #639 (its "re-drive is double-buy-safe" premise is invalid until this lands). Related #636/#638 (idempotent creation), #197/#838 (FE multi-account seeding — UI only).


Filed from automated adversarial code review (see docs/code-review/). Source finding(s): 05-C1, 05-H1.

Activity

  1. added
    pr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)
    pr-mergedThe PR for this issue has been merged
    on Jun 7, 2026
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

    bugSomething isn't workingeffort/lWeeksimpact/all-usersAffects every userpr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)pr-mergedThe PR for this issue has been mergedpriority/p0Drop everything; same-day fixseverity/criticalMajor harm when it happenstriagedItem has been triagedtype/bugDefecttype/securitySecurity findingurgency/nowDrop other things

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions