Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Priority: P1 — fund-safety program: map every incident class against CDK's saga model; land safe wins pre-release, deep-dive after (not a release blocker)

Aperta
#703 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
25/100
Tipo di issue
Refactoring
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
go
Ambito
backend, payments

Direzione di ricerca

Start with AGENTS.md, then read the referenced incident issues (#497, #498, #500) and the saga entry points named in cashubtc/cdk, especially crates/cdk/src/wallet/{swap,send,melt,issue,receive}/saga/ and crates/cdk/src/wallet/saga/mod.rs. Compare TollGate’s current recovery and transaction handling with CDK’s prepare, persistence, and resume model; the issue calls for a post-v0.6.0 deep dive and a separate list of strictly safer pre-release actions. Done means a complete incident-class map, comparison, and decision list—not implementing the structural fix.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area: payments important release:post-0.6.0

Priority: P1 — the fund-safety program: every incident class mapped against CDK's saga model; land the safe wins before v0.6.0, run the deep dive after. Explicitly NOT a release blocker by itself — items that carry their own P0 keep it.

Framing

TollGate's incident history is one structural story told five ways: irreversible Cashu operations (swap/mint/melt) whose supporting state — derivation counters, secrets, produced tokens, business outcome — is not durable at the right moments, and whose ambiguous outcomes are retried instead of reconciled. AGENTS.md encodes the rules earned from these incidents; this issue turns them into a program: a complete map of what happened and why, a comparison against CDK's saga model (the reference design for crash-safe Cashu), a short pre-release decision list (only strictly-better-than-status-quo items), and a post-0.6.0 deep dive that designs the real fix instead of patching symptoms.

This issue does not lower anyone's priority: #497 keeps its P0; pin audits descending from #494–#496 stay mandatory before any tag. What it does say: the deep-dive work must not be rushed in under a release deadline — that is exactly how the half-fixes below were born.

The incident map

Class A — deterministic-derivation reuse (the 10002 brick family)
Issue What happened Why it happened
#257 Production v0.5.0: every payment fails outputs have already been signed before; wallet bricked Counter incremented only after a successful swap; a transient mint failure (timeout/5xx/429) left the counter put, so the retry re-derived the same blinded messages → NUT-02 error 10002. Latent in tests because mock mints don't fail
#266 The fix: IncrementKeysetCounter() moved before the swap call (gonuts v0.7.4) Correct for the first attempt — but a patch on one path, not an invariant; see #495
#480 Any service restart permanently broke swaps for every mint with prior history (Duplicate outputs) Two URL spellings of one mint created two keyset buckets; on restart the in-memory map lost the alias, AddMint re-registered the second spelling, and a freshly-fetched keyset record (counter 0) overwrote the persisted higher counter — SaveKeyset had no monotonic guard. Re-derivation from counter 0 → brick
#496 The #480 fix existed on a branch but was not in the pinned dependency — production stayed vulnerable Pin discipline gap; lesson: the class (any writer that can regress the counter) needs an audit, not just the one writer that fired
#494 Melt-path variant: change outputs sent to the mint before any counter increment; response lost → TollGate's retry (fresh invoice, fresh quote) re-derived the same range → 10002 Same increment-after-success pattern as #257, on the melt path — the class was fixed where it burned first, not everywhere
#495 After any 10002-retry the persisted counter sits below the ranges actually exposed; every later first attempt eats a guaranteed 10002 and only the regenerated retry works The #266 fix covered the first attempt; swapWithRetry's regenerated retry range was never persisted. Masked (payments succeed) until the retry itself collides or a mint rate-limits the double request
#640 The #535 conformance lane measured the swap-timeout-retry scenario re-exposing outputs on 2026-10-05 main — same class, alive post-#480-fixes An ambiguous outcome (response dropped after the mint processed it) was retried blindly with regenerated outputs instead of reconciling proof state first. Issue since closed by the gonuts pin — the pre-release checklist re-verifies that

Root cause, structural: in gonuts the derivation counter is keyset state nested under a mint-URL-named bbolt bucket, with no transaction spanning "reserve counter → POST → save proofs". Every writer of keyset metadata is a counter writer, and discipline is manual. The brick family is what happens each time a writer or a retry path breaks that discipline.

Class B — crash windows that destroy created value
Issue What happened Why it happened
#497 (P0, open) Swap secrets/blinding factors exist only in memory; process death after PostSwap succeeds but before SaveProofs makes the mint-issued outputs unrecoverable — customer's token gone, no local trace Nothing durable is written between the irreversible POST and the save. The exact gap CDK's wallet saga exists to close
#375 wallet drain cashu aborted on the first per-mint failure, discarding tokens already produced by earlier successful mints; wallets with two spellings of one mint showed phantom balances No partial-result contract, no journal, non-canonical mint identity. Fixed by #433 (journal-before-next-mint, partial results, canonicalization) — the journal is a hand-rolled, one-purpose saga record
Class C — the business transaction spanning wallet + memory + NDS
Issue What happened Why it happened
#258 / #403 Payment consumed (Receive + swap irreversible), then ndsctl auth fails → customer's value sits in the operator wallet, no session, no refund path; restoreSession rewinds only the in-memory session map Wallet-internal atomicity ≠ business-transaction atomicity. Receive → session → gate-open → response has no compensating action and no record; the refund-vs-late-grant decision was never made. #403 closed; #502 generalizes it into a TollGate-level transaction record
#498 (P1, open) The ambiguous-outcome class at the TollGate layer: timeouts treated as failure, retries launched without reconciling what the mint already did Same anti-pattern as #640 one layer up
Class D — stuck value nobody sweeps
Issue What happened Why it happened
#500 (P1, open) Proofs entering pending (failed send, ambiguous melt, reseller hand-off) stay reserved forever: invisible in balance, reclaim machinery (ReclaimUnspentProofs, GetPendingMeltQuotes) exists in gonuts but the daemon never calls it Recovery was built as tools (nutw CLI, scripts/token-recovery), never wired into the service lifecycle
#423 clientd burns tokens on rejected payments; no reclaim path Same gap on the client side
#417 Keyset final_expiry silently kills held balances (proven in cloud lab: Keyset has expired) Spec-correct mint behavior (NUT-02); wallets must rotate off inactive keysets — hygiene, not a crash bug, but the same "value strands silently" outcome
Class E — failure-path liveliness
Issue What happened Why it happened
#525 / #532 A wedged mint (accepts TCP, never answers) parked the fee precheck for 5+ minutes with no deadline; goroutines stacked The wallet client's retry ladder (30 s × 5 attempts × 2 endpoints) has no context/deadline. Fixed by budget-bounding the fetch; the same idiom now guards #549's checkstate (10 s budget — a dead mint still costs ~8 s on the retry ladder, so the budget is load-bearing)

What CDK does differently (the saga model)

Verified against cashubtc/cdk main on 2026-10-07: the saga layout #497/AGENTS.md cite exists exactly as described — crates/cdk/src/wallet/{swap,send,melt,issue,receive}/saga/ plus shared crates/cdk/src/wallet/saga/mod.rs, mirrored on the mint side (crates/cdk/src/mint/{swap/swap_saga,melt/melt_saga}/). The design, from source:

  • Type-state sagas — operation states are distinct types (swap/saga/state.rs: Initial {operation_id, keyset_policy} → prepare() → Prepared {input_ys, pre_swap, saga}); invalid transitions do not compile.
  • Prepare persists the intent — swap/saga/mod.rs reserves the input proofs, reserves the derivation-counter range in one atomic call (increment_keyset_counter; counter_start = counter_end − derived_secret_count), and persists the saga record (add_saga) for crash recovery — inputs, counter range and premint secrets are durable before the irreversible POST. Registered compensating actions revert the reservation and delete the saga on failure.
  • Compensating actions, LIFO — wallet/saga/mod.rs: each step registers a CompensatingAction; rollback runs them in reverse order (errors logged, execution continues) — the compensation our Class C lacks.
  • Resume is replay-first — receive/saga/resume.rs: for SwapRequested, replay the original post_swap (a NUT-19-cached mint response returns the signatures immediately); only on replay failure check whether inputs are spent and fall back to NUT-09 /restore. ProofsPending → compensate by removing pending proofs. Boot walks every persisted saga and resumes or compensates it.
  • Proof ownership markers — reserved proofs carry used_by_operation: operation_id, and parent sagas (send/melt/receive) compose the swap saga with ProofReservation::Skip so exactly one layer owns each proof lifecycle — the ownership-marker design #500 item 4 and #702 ask for, already shipped there.
  • Handed-out tokens are first-class — send outputs persist as State::Reserved (not spent, not unspent — the mint decides); reclaim.rs sync_proofs_state batch-reconciles against NUT-07 and updates locally per verdict.

Their maintenance history is itself study material: cdk#1278 fixed a swap path missing its reconciliation wrapper (our #640 exactly); #1456 then removed the wrapper from receive ops (receive has no inputs of ours to reclaim) and #1457 slimmed the saga table; #1446 added mint-side compile-time row locks. Lesson: wrapper idioms rot when one call site is forgotten — the saga makes the safe path the only path that compiles.

One sentence: CDK persists the intent (inputs, counter range, blinded messages) before the irreversible call, marks ownership on every proof, and resumes by replay; gonuts persists only the counter and hopes the rest fits in memory between two lines of code. Every Class A and B incident lives in that gap. The structural fix direction is why AGENTS.md flags migration (#305/#277), with the MIPS/CGO_ENABLED=0 constraint pushing cdk-go behind the sidecar (src/tollwallet/sidecar.go) rather than in-process.

External study material — where to study what, and why

cashu-audit (ours — Amperstrand/cashu-audit)

The 3-layer framework (greatspectations spec-quote anchors → cross-implementation divergence database → AI audit prompts; #549's verbatim NUT quotes already come from this machinery). Fund-safety-relevant holdings:

  • issues/interrupted-operation-recovery.md (GH-tracked, HIGH, "biggest underspecified area"): recovery from interrupted operations is unspecified — every implementation solves it privately, so interoperable wallets cannot share recovery assumptions; proposes a NUT for operation-recovery semantics (state machines across crash points, idempotency keys, restore obligations). This issue's deep dive should feed that upstream draft — TollGate is the interoperable payment-router case study the gap hurts most.
  • divergences/RESOLVED-ISSUE041-concurrent-double-melt.md (CRITICAL, resolved 2026-07-29): TOCTOU on the melt-quote UNPAID→PENDING transition — read-check, many await points, unconditional write 650 lines later; two concurrent melts interleaved and the loser's proofs were burned for nothing. Fix: atomic compare-and-set at the write site; the race loser is rejected before any proof is touched. Direct lesson for our #639 (concurrent duplicate token POST double-grants) and the #494 melt paths: CAS the state transition where it is enforced, don't check it earlier and write later.
  • issues/keyset-final-expiry-enforcement.md: cdk enforces final_expiry at swap (error 12003), nutshell never expires; inactive ≠ expired — inactive keysets MUST still accept swap inputs. Cross-checked against our cloud-lab rotation lane; downstream tracking = #417.
  • divergences/CDK-VS-NUTSHELL-ALIGNMENT-2026-07-29.md: the triage method worth adopting (both references pass → your bug; references differ → genuine divergence; "default to CDK unless the NUT spec is explicit"), plus open items including proofs stuck PENDING ~30 min on payment-timeout deferral — our Class D manifesting in another implementation.
  • LEARNINGS-FROM-ISSUES.md (patterns from 36+ fixed issues): five catch-patterns, notably "state-machine bugs only manifest with specific proof configurations → E2E tests with all state combinations" — the design brief for extending the #535 fault-injection lane.
  • signoffs/gonuts-tollgate/ (NUT-by-NUT audits of our fork, incl. POST-FIX-VERIFICATION-20260728): re-run these against the release pin as part of the pre-release pin audit; record outcomes back into cashu-audit.
  • signoffs/comparisons/NUT-{05,07,08,09}-comparison.md: per-NUT wallet-relevant differences (melt change, checkstate shape, fees, restore).
  • issues/nut05-blank-change-outputs.md (HIGH, production incident exists): melt/swap change outputs are blank — pairing is positional, declared amounts are ignored, take amounts from the returned signature. The wallet-side "I declared denominations" mental model is wrong; relevant to #494 (change-output handling) and #414 (input fees).
Conduition — NUT-13 deterministic-secrets disclosure (mandatory reading for Class A)

https://conduition.io/code/cashu-disclosure/ (2026-01, bounty-paid): NUT-13 secrets derived via BIP32 paths keyed on a 31-bit integer residue of the keyset ID let a malicious mint force cross-mint secret/preimage reuse, then use the target mint's NUT-09 /restore as an oracle to harvest blind signatures. Two consequences: the derivation counter is not just a fund-safety component but an attack surface ("wallets must manage the stateful counter correctly and verify keyset IDs"), and /restore — which recovery designs lean on — doubles as an attack oracle. Fixes: HMAC over the full keyset ID + counter (long-term), residue-collision guards (short-term), 256-bit keyset-ID v2 (protocol level). Most Cashu wallets were affected.

Nutshell (cashubtc/nutshell)
  • Mint Ledger marks input proofs pending in a DB table as a distributed lock for the duration of processing — double-spend protection as a persisted state machine (witness fields valid only in SPENT, enforced at the model layer).
  • Wallet restore-from-mnemonic then invalidate: reconcile restored proofs against NUT-07 checkstate before trusting them (cashu/wallet/wallet.py ~L1120) — the same authority-ordering as #549; the keysets table keeps per-keyset derivation counters as first-class wallet state.
  • Proof selection filters to unreserved proofs on active keysets (wallet.py ~L1175) — rotation hygiene for #417; noted divergence: nutshell never expires keysets.
  • Far-field: nutshell#1046 Proof-of-Liabilities (MS-SMT epoch trees, cashu pol audit letting wallets trustlessly audit spent/issued trees) — the "mint as authority" endgame.

Pre-v0.6.0 decision list (strictly better than status quo, small blast radius)

  1. Ship #549 — read-only liveness report; no wallet state touched; already battery-green.
  2. Pin audit (cheap, mandatory-feeling): verify the pinned gonuts tag carries the #494/#495/#496-class fixes and the #640 fix; re-run the #535 conformance fast-subset lane on the release candidate and attach the evidence to the release issue (#665/#339).
  3. #500 boot-time reconciliation — pre-release candidate only: bounded, operates within our own wallet, and turns stuck value into self-healing. Include if the conformance lane is green on it; otherwise it slips with the deep dive.
  4. Do NOT rush #497 (pre-persisted swap intents) pre-release. It is research-first by its own framing; every half-fix in Class A was a deadline patch.
  5. #502 (business-transaction record) stays post-release unless already stacked and reviewed.
  6. Informational read-audit of the NUT-13 derivation question (Conduition disclosure vs gonuts) — zero code, feeds deep-dive item 7; if it surfaces a live vulnerability, that gets its own P0 rather than blocking on this issue's say-so.

Post-0.6.0 deep dive — scope

  1. Enumerate every crash window (swap/mint/melt × before-POST/after-POST-before-save, plus send/receive hand-offs) — extend #497's research doc into the canonical table; re-pin the exact upstream saga sources (commit + paths) into #497's research doc so citations stop drifting.
  2. Design decision with evidence: (a) bbolt pending_ops intents inside gonuts-tollgate (counter range + secrets + rs persisted with the counter increment in one tx), vs (b) CDK behind the AF_UNIX sidecar, vs (c) full migration (#305/#277). Constraints: MIPS, flash wear (#505), multi-process access (#504), the 16-module replace-directure dance.
  3. TollGate-layer transaction record (#502) with the explicit refund-vs-late-grant policy #258/#403 never got.
  4. Reconciliation daemon (#500) + recovery classes — owned vs handed-out clawback as separate verbs with policy (companion issue, filed alongside this one).
  5. Conformance/fault-injection matrix as the gate (#503, the #535 lane, cloud-lab kill-at-boundary lanes) — no saga-style change lands without a SIGKILL-at-every-boundary test reproducing the incident first.
  6. CDK parity checklist derived from the saga sources — the invariant list this repo must satisfy regardless of which implementation backs it.
  7. NUT-13 derivation audit (Class A attack surface): audit gonuts-tollgate's deterministic derivation (BIP32 path over keyset_id_int vs HMAC over the full keyset ID) against the Conduition disclosure; verify residue-collision guards and how much NUT-09 /restore exposure we have. Outcome: a clean bill recorded in cashu-audit, or a gonuts issue + pin bump.
  8. Upstream recovery-semantics NUT: contribute TollGate's incident taxonomy to cashu-audit's interrupted-operation-recovery draft (venue: cashubtc/nuts) — crash-point state machines, idempotency keys, restore obligations.
  9. Port used_by_operation proof-ownership markers into the #500/#702 design — CDK's swap-saga tests assert them; they are the coordination primitive between the sweeper, in-flight reseller hand-offs, and clawback.

Acceptance criteria (deep dive, post-release)

  • The crash-window table exists, is exhaustive, and each row cites a failing test that proves the window (red) before the fix (green).
  • A written decision on (a)/(b)/(c) with measured trade-offs, checked into docs/decisions/ per ADR practice.
  • The #535 lane runs the full brick-family scenario set (10002-retry, restart-alias, timeout-retry, kill-between-increment-and-swap, kill-between-swap-and-save) against the chosen design and measures zero re-exposures.

Out of scope

Release management itself (#665, #339); individual P0 execution (tracked in their own issues); NUT-07 policy work (companion recovery-semantics issue).

Lingua principale
Go
Stelle
12
Fork
14
Merge medio
1g 6h
PR unite (30g)
211

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di OpenTollGate/tollgate-module-basic-go

Tutte le issue di OpenTollGate/tollgate-module-basic-go

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.