Skip to content

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

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

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