Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

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

Open
#702 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
backend, payments

Research direction

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.

Written by the indexing model from the issue text.

Description

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).

Dominant language
Go
Stars
12
Forks
14
Avg merge
1d 6h
Merged PRs (30d)
211

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from OpenTollGate/tollgate-module-basic-go

All issues in OpenTollGate/tollgate-module-basic-go

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.