Skip to content

Priority: P1 — ambiguous-outcome class at the TollGate layer. Independent of gonuts fixes. #498

Open

Description

@Amperstrand

Priority: P1 — ambiguous-outcome class at the TollGate layer. Independent of gonuts fixes.

Problem

PurchaseSession wraps Receive in a 30s timeout, but the goroutine keeps running. When Receive completes after the timeout response was sent: funds land in the operator wallet, no session is granted, no record exists, and the customer was told "timed out, try again" — so they pay again. Duplicate value consumed for one session; every timeout has this tail risk.

Why it matters

Timeouts are routine (mobile-network mints). The current code converts each one into a potential silent confiscation with a lying error message. Same family as #403 but via the timeout path, and invisible in logs beyond a Receive-completed line nobody correlates.

Current behavior (source refs)

  • src/merchant/merchant.go:506-538 — goroutine + select timeout; on timeout the notice returns immediately, goroutine abandoned.
  • src/merchant/merchant.go:519-529 — late result (success or error) is logged and dropped.
  • No pending-payment record anywhere (sessions are created only post-timeout-window).

Desired invariant

No Receive outcome is discarded: a late success is reconciled into a granted session (or refund) durably; a late failure clears the pending record; the customer-visible timeout message states that the payment may still complete and how to check (GET /usage).

Proposed scope

  1. Durable pending-payment record keyed by token Ys hash, written before launching Receive (this is also the idempotency key — coordinate with the payment-saga design for Rust parity, Rust issues Refs/heads/x1860 #5/Build/Deployment improvements #6).
  2. Late-completion handler: on late success → same grant path as normal completion (session + gate + record COMPLETE); on late failure → record FAILED.
  3. Startup + periodic reconciliation: records in RECEIVE state older than timeout → query mint proof state (NUT-07) → resolve.
  4. Timeout notice copy updated to reflect "may complete; check usage" (wire-compatible text change only).

Areas / files

src/merchant/merchant.go, new small store (JSONL or bbolt sub-bucket; flash-aware), startup reconciliation in merchant init.

Acceptance criteria

  • Fault test: Receive completes at t=35s, timeout returned at t=30s → session granted at t=35s, /usage reflects it, record COMPLETE.
  • Kill between receive-start and completion → restart reconciliation resolves by mint state.

Required tests

  • Unit: late-completion grant path; reconciliation decision table.
  • Integration (cloud-lab): slow-mint proxy lane.

Failure-injection tests

  • Proxy delays swap response 35s (payment completes late).
  • Proxy drops response entirely; mint did consume inputs (mint-side state says spent) → reconciliation grants session.

Compatibility

Notice text change only.

Dependencies

Conceptually pairs with the Rust payment-saga issue (Amperstrand/tollgate-module-basic-rust#5) — implement to the same state names for test parity.

Out of scope

Activity

  1. Amperstrand commented on Sep 22, 2026

    @Amperstrand
    CollaboratorAuthor

    Verification of PR #511 (late-Receive reconciliation) is up: #511 (comment)

    Short version: the in-process fix is correct and race-verified — a late Receive success now grants the session through the same path as an inline success, exactly-once per completion, with the #507 reclaimer interplay confirmed clean. Two stack-level blockers (missing src/cli go.mod tidy inherited from the dep-bump base, missing CHANGELOG entry) hold the PR, and note the PR's "Fixes #498" would auto-close this issue while the durable-record/crash-restart criteria (durable payment record, startup reconciliation, slow-mint cloud-lab lane) remain open — tracked in #502. Suggest keeping this issue open (or re-scoping it) until the durable saga lands.

  2. Amperstrand commented on Oct 7, 2026

    @Amperstrand
    CollaboratorAuthor

    Pointer: #703's incident map folds this class in (Class C rows, with #640 as the measured evidence on the wallet side) and names the pattern the TollGate layer should adopt from CDK for ambiguous outcomes: replay-first reconciliation.

    Reference: crates/cdk/src/wallet/receive/saga/resume.rs — for an ambiguous swap, re-issue the original request first (NUT-19-cached responses make replay idempotent at compliant mints, so a mint that already processed it returns the signatures instead of a duplicate-output error), and only on replay failure query mint-side state (checkstate / quote state) before regenerating anything. Regenerating fresh outputs on timeout is precisely the move that re-exposes derivation ranges.

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

    release:post-0.6.0Deferred until after the v0.6.0 release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions