Recovery semantics: owned-stranded vs handed-out (clawback) — encode the class in code, give clawback its own verb and policy

Abierto
#702 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
25/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
go
Área
backend, payments

Línea de trabajo

Start by reading src/tollwallet/port.go and src/cli/server.go for the existing recovery contract, then compare the manual clawback flow in scripts/token-recovery/main.go with src/upstream_session_manager/token_recovery.go. The issue depends on #549, #500, #502, and #423 and calls for new policy, ownership coordination, dispute handling, and crash consistency. Done means the new clawback path is distinct from wallet recover and passes the specified unit, integration, and failure-injection tests.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

area: payments release:post-0.6.0

Priority: P2 — recovery semantics: owned-stranded vs handed-out (clawback). Encode the distinction in code; give clawback its own verb and policy. Companion to #500, downstream of #549. Explicitly post-0.6.0.

Problem

NUT-07 liveness checks serve two kinds of recovery with opposite verdict semantics, and the codebase already implements both without naming the split:

  1. Owned-stranded recovery (type 1) — tokens that are ours, stuck mid-operation by a failure: drain-journal entries (#375/#433, recovered by #549's wallet recover), proofs reserved in the gonuts pending bucket (#500's ReclaimUnspentProofs).
  2. Handed-out clawback (type 2) — tokens we gave to someone else whose payment then failed: scripts/token-recovery over tokens-to-recover.txt (written by upstream_session_manager/token_recovery.go on autopay failures), clientd rejections (#423), reseller interrupted hand-offs.

The mint cannot tell these apart — same checkstate call — but the meaning of each verdict inverts:

Verdict Owned-stranded (type 1) Handed-out (type 2)
UNSPENT stranded value; re-import, races nobody recipient has not redeemed — clawback window open; re-spending is a race against their redemption
SPENT already secured; nothing to do the counterparty took the value while we got nothing — a dispute, not a closed case
PENDING re-check later (in-flight op) their redemption is literally in flight — cannot and should not act
unknown/error re-run later must not sweep; after a partial sweep, ambiguity is monetary (ErrSidecarAmbiguous class), not an incomplete report

#549 documents this taxonomy on the WalletPort.CheckTokenSpendable seam and keeps wallet recover strictly type-1. But only type 1 has an in-tree, policy-governed path. Type 2 today is an out-of-tree script run by hand, with no grace window, no ownership coordination with #500's sweeper, no dispute record, and no crash story of its own — even though Receive()-back-to-wallet is itself an irreversible operation.

Why it matters

Conflating the two classes produces one of two failures: treating a clawback verdict as simple recovery (double-pay — voiding a slow-but-honest recipient's payment), or treating "spent" as "case closed" when it means the counterparty kept the money. Type-2 actions are spends; running them under a read-only report's exit-code contract ("unknown exits non-zero") is category confusion — a partial sweep must surface as monetary ambiguity, not as a re-runnable report.

Current behavior (source refs)

  • src/tollwallet/port.go — CheckTokenSpendable doc comment states the two-semantics contract (#549, 0cbe21d).
  • src/cli/server.go handleWalletRecover — type-1 only, ownership assumption explicit.
  • scripts/token-recovery/main.go — manual type-2 tool: bounded checkstate, Wallet.Receive() on UNSPENT, dry-run mode.
  • src/upstream_session_manager/token_recovery.go — writes tokens-to-recover.txt on autopay failure.
  • gonuts ReclaimUnspentProofs / GetPendingMeltQuotes — type-1 machinery, never invoked by the daemon (#500).
  • #423 — clientd burns tokens on rejected payments; no reclaim path at all.

Desired invariant

Every recovery action declares its class. Owned recovery may report (and, per #500, reclaim automatically within our own wallet). Handed-out clawback is a separate verb — a spend that races the recipient — governed by an explicit policy: grace window, ownership markers, dispute surfacing, and its own crash-consistency (the clawback swap itself needs durable intent before the irreversible step).

Proposed scope

  1. Name the class at the seams: RecoveryClass (owned vs handed-out) or distinct verbs — wallet recover (report, type 1) vs wallet reclaim (sweep, type 2). recover must never sweep handed-out tokens.
  2. In-process type-2 path replacing the ad-hoc script for tokens-to-recover.txt: checkstate → still unspent past the grace window → swap-to-self → record; spent → mark dispute/owed (feeds the business-transaction record #502).
  3. Policy state: per-token grace window; ownership markers so the #500 sweeper and an in-flight reseller hand-off never fight over the same proofs (extends #500 scope item 4).
  4. clientd alignment (#423): same class distinction client-side.
  5. Exit-code/output contract for sweeps: any unknown after a partial sweep is reported as monetary ambiguity with per-token state, not a generic non-zero.

Acceptance criteria

  • Fault test: token handed out, recipient never redeems → after grace, reclaim succeeds and the recipient's later redemption fails cleanly with a surfaced dispute record.
  • Fault test: recipient redeems during the grace window → no double effect; state becomes dispute.
  • Unknown at sweep time → no sweep attempted, retried next interval, no crash-loop.

Required tests

  • Unit: class routing (recover never sweeps); grace-window expiry; ownership-marker skip shared with #500.
  • Integration (cloud-lab): interrupted autopay lane → automated reclaim; dispute state when upstream did redeem.

Failure-injection tests

  • Wedged mint during clawback check (bounded, #525/#532 idiom) — no sweep on unknown.
  • Recipient redemption racing the reclaim swap (-race).
  • SIGKILL between clawback intent persistence and the swap (the clawback needs its own crash story — journal or pending-bucket entry before the irreversible step, per the AGENTS.md fund-safety rules).

Compatibility

New verbs/state only; wallet recover behavior unchanged.

Dependencies

#549 (seam + taxonomy docs, merged first), #500 (sweeper interplay), #502 (dispute records), #423 (clientd).

Out of scope

Changing when tokens enter tokens-to-recover.txt (each flow's own fix); NUT-07 protocol changes; the #497 swap-crash-window saga work (separate track).

Lenguaje dominante
Go
Estrellas
12
Forks
14
Merge medio
3 d 20 h
PR fusionados (30 d)
261

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de OpenTollGate/tollgate-module-basic-go

Todos los issues de OpenTollGate/tollgate-module-basic-go

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.