Repository navigation
fix(wallet): drain safety — canonical mint identity, partial results, fsync'd journal (#375) - #433
Conversation
… fsync'd journal (#375) `wallet drain cashu` aborted on the first per-mint failure, silently discarding tokens already produced by earlier successful (and irreversible) mints; duplicate URL spellings of one mint made wallets drain the same logical mint twice. Three layers: - canonicalize mint URLs (scheme/host case, trailing slash) at registration and merge on read, so one logical mint can never appear as two phantom-balanced entries - collect per-mint failures into an explicit partial result (success:false, partial:true, tokens, per-mint errors) instead of discarding earlier tokens; plain and JSON output both report it - append every produced token to an fsync'd /etc/tollgate/wallet-drain-journal.jsonl before the next mint is attempted; a journal failure stops further draining Also adds --yes/-y for non-interactive drains and meaningful exit codes (cancel, full and partial failure, JSON-mode success:false). CHANGELOG [Unreleased] normalized while inserting the entries: merged the duplicated Fixed/Added/Changed sections that had accumulated from the merge-night conflicts and removed one literal duplicate (#381 tester guide); entries themselves unchanged. Verification: go vet + tests green in src/cli, src/tollwallet, src/cmd/tollgate-cli; root gofmt/vet/build + go test -race -count=1 -tags testenv green.
Amperstrand
left a comment
There was a problem hiding this comment.
Code: verified green — this is the right fix, well-shaped
Ran the full check battery locally on 45fce6d:
src/tollwallet,src/cli,src/cmd/tollgate-cli:go test -count=1 -tags testenv ./...all pass;-race -p 1green on the drain + wallet suites;go vetclean;gofmtclean.- The six drain tests cover exactly the #375 repro scenarios — partial-failure token retention, duplicate-alias shared balance, journal-before-next-mint ordering, journal-failure-stops-further-drains — plus all-green path.
- All three reported bug layers map to real changes: canonical identity fixed write-side at
AddMint(the actual duplicate-creation mechanism — good catch registering the canonical form, not just comparing it), partial results with the historical error-string shape preserved for automation, and the CLI confirm/exit-code semantics (silent no-op → error with--yesguidance; JSON-mode exit 0 → non-zero onsuccess:false). - Journal design is right: fsync before the next irreversible swap, stop-on-journal-failure direction is the safe one, 0600/0700, bearer-token retention documented.
Release fit: given #375 is live-repro'd fund loss on main (end-to-end, Sep-17 build), this should land before the v0.6.0 stable tag. The changelog entry text itself is accurate.
One blocking nit: the CHANGELOG hunk drops existing [Unreleased] entries
The rewrite keeps this PR's entries and "Reproducible builds", but loses (present on main today, absent after this diff):
- "Release pipeline runs on Nostr CI (no GitHub dependency)" — the #410 ngit-pipeline entry (the largest, ~28 lines),
- "gonuts re-pin to v0.11.2" (#394),
- the
.gitignoreCLI-binary fix (#411).
Looks like a merge-conflict resolution that took this branch's side. Since [Unreleased] feeds the v0.6.0 release notes directly, please restore those three entries (rebase onto current main and take main's side for the unrelated entries is the easiest route). Happy to re-verify — everything else is 👍 from me.
|
Correction to my review above (entry-level re-diff, sorry for the noise): the ngit-pipeline / gonuts re-pin / .gitignore entries do survive the rewrite — my keyword-count check was misled by reworded fragments. The only genuinely dropped entry is:
Restore that one entry (Added section) and the changelog is whole. Everything else in the review stands as written: code verified green locally (tests ×12 incl. the #375 scenarios, race, vet, gofmt on |
|
Changelog fix pushed ( Recap for the reviewer: code verified locally on |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Reviewed head: f6a64285393b — merge-base 125f6a06; the merge-base is the live upstream/main tip and git merge-tree is clean (exit 0, one tree OID), so mergeable_state: blocked is neither a conflict nor the dead CI lane: ruleset 18235130 on main requires one approving review and carries no required-status-check rule.
What it does. wallet drain cashu is the pre-wipe command on a gateway, and it was lossy: the old loop returned on the first per-mint failure with Success:false, discarding every token minted that run (src/cli/server.go:291-304), and a declined confirmation exited 0 (main.go:101-104). Now failures accumulate, successful tokens reach the token file before the error prints, each is fsynced to the drain journal before the next mint, mint URLs are canonicalised (#375), and failures exit non-zero. My findings are the edges it leaves open.
1. Mint identity is not total, though the docs claim it is. normalizePath (tollwallet.go:354-362) strips at most one trailing slash; default ports, userinfo, query/fragment and IDN are untouched. So …/Bitcoin/ collapses onto …/Bitcoin (the #375 fix), but …/Bitcoin//, …:443/Bitcoin, user:pass@…/Bitcoin and …?v=1 stay distinct keys for one physical mint: each reports the same keyset balance, so server.go:287 drains it twice, the retry failing on zero. …/Bitcoin%2F also normalises onto the unescaped path, an over-merge in the direction that loses funds. Fix: strings.TrimRight(p, "/") and port/userinfo handling.
2. The read-side alias merge is load-bearing yet untested. Neutralising GetAllMintBalances (tollwallet.go:406-430 → return w.wallet.GetBalanceByMints()) leaves src/tollwallet and src/cli green, while my probe on a wallet holding one mint under two spellings goes from one 50-sat entry to two phantom ones: registerMint normalises first, so the branch credited with repairing existing installs is unexercised. Reuse the probe (tw.wallet.AddMint(aliasWithSlash); one entry / 50 sats). max is right, not a sum: gonuts sums by keyset id.
3. The journal is write-only, unbounded, and its crash claim is overstated. Ordering is genuine (appendDrainJournal in-loop at server.go:323, Write→Sync at drain_journal.go:63-66), but nothing reads it back: grep finds only its definition, call site and test. Drained bearer tokens accumulate forever in /etc/tollgate/wallet-drain-journal.jsonl unless swept by hand (0600/0700 apply only at creation). drain_journal.go:15-22 also says journaling "closes that window" for a crash between swap and response, but gonuts' Send has already put the proofs in pending_proofs, leaving the in-Send window uncovered. Reword: a second copy, not the recovery path (scripts/token-recovery exists).
4. TOLLGATE_TEST_CONFIG_DIR is honoured by shipped code and moves a token sink. drainJournalPath() (drain_journal.go:27-33) and socketPath() (main.go:34-40) branch on it unconditionally in the production binary: set it in a drop-in or wrapper and drained tokens go to an arbitrary directory while the CLI talks to another socket.
5. Test-only: the CLI drain tests are not hermetic w.r.t. TMPDIR. With this host's TMPDIR the package is 29 PASS / 2 FAIL — the partial-failure and JSON-mode failure tests die with bind: invalid argument, as t.TempDir()'s name-derived path exceeds AF_UNIX's 108-byte sun_path; TMPDIR=/tmp gives 31/31.
Acquittals worth recording. Real cliff targeted (drained proofs sit in pending_proofs, so the CLI cannot re-emit them); partial JSON is additive with the old error prefix kept; plain text shows tokens/file before the error; --yes skips exactly one prompt; packaging/ and the feed payload are untouched.
CI: no GitHub CI evidence — gh pr checks 433 returns "no checks reported on the 'fix/375-wallet-drain-safety' branch" (exit 1) because Actions is queued-dead org-wide since 2026-08-27; the ngit lane builds main only, so there is no ngit result here. Locally: src/cli ok 0.130s, src/tollwallet ok 0.233s, tollgate-cli 31/31 under TMPDIR=/tmp. Four mutations: reverting token retention, deferring the journal and reverting normalizeMintURL each went red; neutralising the alias merge did not (finding 2).
RC RECOMMENDATION: LAND BEFORE THE ALPHA RC. It touches src/cli, src/tollwallet and src/cmd/tollgate-cli, so it is in the RC binary either way; the question is only whether testers run it. Their verdicts turn on a multi-mint drain with real sats, exactly where the old code stranded funds in a bucket the CLI cannot re-emit. Landing after the freeze instead costs a re-cut of the feed artifact (.ipk and .apk), a re-install on testers' boards and invalidating results already gathered against the frozen commit, for a fix whose purpose is to stop that drain stranding money. Ship F1/F2 as fix-ups; until F1 lands, testers need one canonical mint spelling.
Amperstrand
left a comment
There was a problem hiding this comment.
Ran the affected modules on this branch: go vet + go test -race -count=1 -tags testenv ./... green in src/cli, src/cmd/tollgate-cli, src/tollwallet; root gofmt -l empty. Read the diff in full — the ordering guarantee (journal fsync before the next mint, break-on-journal-failure with the token still in the response), the canonical-identity single-source (registry keys and MintURLMatches both derive from normalizeMintURL, so they cannot disagree), and the backward-compatible error phrasing for string-matching automation all check out. Pairwise merge check vs #432 (the other CHANGELOG-touching PR): no conflicts.
|
Small branch fixup pushed ( |
|
Second small branch fixup ( |
felixfelix-bot
left a comment
There was a problem hiding this comment.
UPDATE at head eb7050bf49 (live, re-derived before posting) — the head moved one commit since my review of this PR (f6a6428539, id 5261470806, artifact round4/post-433.md). I re-derived the delta rather than assuming it: gh api repos/OpenTollGate/tollgate-module-basic-go/compare/f6a64285393bf47c4b0ef02e151e7c10c28b0cfa...eb7050bf49aebbad1bc6621abc13f3d2f29cc306 → ahead_by: 1, behind_by: 0, one file — docs(changelog): drop the duplicated [Unreleased] heading, CHANGELOG.md +0/−1. No code, test, packaging or workflow byte changed, so every finding and the verdict in that review stand unchanged; read them there rather than expecting a second full pass:
- F1 mint identity is not total —
normalizePathstrips only one trailing slash, so//,:443, userinfo and?v=1remain distinct registry keys and%2Fover-merges in the loss direction (src/tollwallet/tollwallet.go:354-362); - F2 the read-side alias merge in
GetAllMintBalancesis load-bearing and pinned by no test (mutation M3 left the packages green while a probe went 1×50 sat → 2 phantom 50 sat) (tollwallet.go:406-430); - F3 the journal is write-only, orphanable and unbounded, its
0600/0700apply only at creation, and its "closes that window" claim is overstated (src/cli/drain_journal.go:15-22,41-70); - F4
TOLLGATE_TEST_CONFIG_DIRis honoured by shipped production code (moves the token sink and the control socket) (drain_journal.go:27-33,src/cmd/tollgate-cli/main.go:34-40); - F5 test-only: the new CLI drain tests are not hermetic w.r.t.
TMPDIR(29 PASS/2 FAIL with the hostTMPDIR, 31/31 withTMPDIR=/tmp— AF_UNIX 108-bytesun_path).
Rebase note (new). This PR's base is 125f6a0645; main is now 52b8481a3b (#435 merged), so it is 1 behind, and the commit above edits CHANGELOG.md — the same file #435's merge touched. Rebase before merging and expect a one-hunk conflict there.
CI: no GitHub CI evidence — gh pr checks 433 --repo OpenTollGate/tollgate-module-basic-go → no checks reported on the 'fix/375-wallet-drain-safety' branch (exit 1); actions/runs?head_sha=eb7050bf49 → total_count: 0 (Actions queued-dead org-wide since 2026-08-27). The ngit lane builds main only → no ngit result for this branch, asserted rather than implied green.
RC RECOMMENDATION: LAND BEFORE THE ALPHA RC (unchanged). It ships in the RC binary either way — src/cli, src/tollwallet and src/cmd/tollgate-cli are all packaged — so the only question is whether testers run it, and their verdicts turn on a multi-mint drain with real sats, which is exactly where the old code strands funds in a bucket the CLI cannot re-emit. Landing after the freeze costs a re-cut of the feed artifact (.ipk and .apk), a re-install on tester boards and invalidating results already gathered against the frozen commit. The one-commit delta is a CHANGELOG.md dedup: zero byte risk, nothing to retest. Ship F1/F2 as fix-ups per the earlier review — until F1 lands, testers need one canonical mint spelling.
felixfelix-bot
left a comment
There was a problem hiding this comment.
CORRECTION + UPDATE at head 5756d9ca5a (live, re-derived immediately before posting this). My immediately preceding review of this PR (id 5261613217) was pinned at this same head but its body named eb7050bf49 as the head — that was true when the body was written and false by the time it was posted, because the author pushed again in between. This review supersedes it on the head fact; the findings and the verdict are unchanged from it and from my full review at f6a6428539 (id 5261470806, artifact round4/post-433.md).
The full delta since my reviewed head f6a6428539. gh api repos/OpenTollGate/tollgate-module-basic-go/compare/f6a64285393bf47c4b0ef02e151e7c10c28b0cfa...5756d9ca5aaf571bf0082f62bc6b095cff88b33d → ahead_by: 2, behind_by: 0, one file: CHANGELOG.md +0/−13 across two commits — eb7050bf49 docs(changelog): drop the duplicated [Unreleased] heading and 5756d9ca5a docs(changelog): remove the literal #381 duplicate entry. (Cross-checked the intermediate step separately: eb7050bf49...5756d9ca5a is ahead_by: 1, CHANGELOG.md +0/−12.) No src/, test, packaging/ or workflow byte has changed since the full review, so nothing in it needs re-deriving — read the findings there:
- F1 mint identity is not total —
normalizePathstrips only one trailing slash, so//,:443, userinfo and?v=1stay distinct registry keys and%2Fover-merges in the loss direction (src/tollwallet/tollwallet.go:354-362); - F2 the read-side alias merge in
GetAllMintBalancesis load-bearing and pinned by no test (mutation M3 leftsrc/tollwallet+src/cligreen while a probe went 1×50 sat → 2 phantom 50 sat) (tollwallet.go:406-430); - F3 the journal is write-only, orphanable and unbounded,
0600/0700apply only at creation, and its "closes that window" claim is overstated (in-Sendwindow uncovered) (src/cli/drain_journal.go:15-22,41-70); - F4
TOLLGATE_TEST_CONFIG_DIRis honoured by shipped production code — it moves the token sink and the control socket (drain_journal.go:27-33,src/cmd/tollgate-cli/main.go:34-40); - F5 test-only: the new CLI drain tests are not hermetic w.r.t.
TMPDIR(29 PASS/2 FAIL with the hostTMPDIR, 31/31 withTMPDIR=/tmp— AF_UNIX 108-bytesun_path).
Rebase note. The PR base is 125f6a0645; main is now 52b8481a3b (#435 merged), so the branch is 1 behind and all three of its most recent commits edit CHANGELOG.md, the file #435's merge also touched. Rebase before merging and expect a conflict in that section; the dedup commits themselves are the answer there (the duplicate entries they remove are exactly what a stale-base merge would resurrect).
CI: no GitHub CI evidence — gh pr checks 433 --repo OpenTollGate/tollgate-module-basic-go → no checks reported on the 'fix/375-wallet-drain-safety' branch (exit 1); actions/runs?head_sha=5756d9ca5a → total_count: 0 (Actions queued-dead org-wide since 2026-08-27; the newest run anywhere in the repo is a 2026-09-17 "Graph Update" still queued). The ngit lane builds main only → no ngit result for this branch, asserted rather than implied green.
RC RECOMMENDATION: LAND BEFORE THE ALPHA RC (unchanged). It ships in the RC binary either way — src/cli, src/tollwallet and src/cmd/tollgate-cli are packaged — so the only question is whether testers run it, and their verdicts turn on a multi-mint drain with real sats, exactly where the old code strands funds in a bucket the CLI cannot re-emit. Landing after the freeze costs a re-cut of the feed artifact (.ipk and .apk), a re-install on tester boards and invalidating results already gathered against the frozen commit. The two newest commits are CHANGELOG-only: zero byte risk, nothing to retest, and they remove duplicated release notes that would otherwise be published verbatim. Ship F1/F2 as fix-ups per the full review — until F1 lands, testers need one canonical mint spelling.
… test, honest journal claim, hermetic socket tests #433 follow-ups on the review's F1/F2/F3/F5: - F1: normalizeMintURL is now total over the URL components that can spell one physical mint: any number of trailing slashes collapse, scheme-default ports (443/https, 80/http) drop, and userinfo, query and fragment are discarded — '…/Bitcoin//', '…:443/Bitcoin', 'user:pass@…/Bitcoin' and '…?v=1' no longer drain one mint twice. Escaped slashes stay distinct (over-merging loses funds), pinned by new rows in the canonicalization tables. - F2: the read-side alias merge in GetAllMintBalances — the branch credited with repairing existing installs — is now exercised: TestGetAllMintBalances_MergesLegacyAliasEntries injects a second spelling below the canonicalizing registration layer (as an older version left them), proves the underlying wallet really holds two entries, and requires one merged max-balance entry out. Mutation check: neutralizing the merge fails the test. - F3: the drain journal's doc no longer claims to 'close that window' for a crash inside the swap: proofs accepted by the mint sit in wallet.db's pending bucket (scripts/token-recovery's domain); the journal is a second copy covering tokens the CLI already holds. - F5: the CLI drain tests bind their fake Unix socket under t.TempDir(); on hosts with deep TMPDIR paths that exceeds AF_UNIX's 108-byte sun_path and fails with 'bind: invalid argument'. shortTestConfigDir() falls back to /tmp when the path is too long — suite now passes under a deliberately deep TMPDIR. - F4 recorded as #443 (TOLLGATE_TEST_CONFIG_DIR honoured by shipped code). Verification: gofmt/vet clean; -race suites green in tollwallet, cli, cmd/tollgate-cli and the root package; cmd/tollgate-cli green under a 108+-byte TMPDIR; canonicalization tables cover the new equivalences and distinctions.
5756d9c to
54b043e
Compare
The normalization hunk in this branch emitted the [Unreleased] heading twice in a row (lines 11-12); keep one.
The normalization claims a removed #381 duplicate in the PR description; the hunk did not actually deliver it — both byte-identical copies survived. Keep the first (Added section) copy only.
|
Review follow-ups landed in
Verification on Heads-up for coordination: this branch received two parallel pushes during the follow-up work (a fetch+force-with-lease momentarily clobbered the changelog hygiene pair cc @felixfelix-bot — F1/F2/F3/F5 fixed in-tree, F4 filed as #443; ready for another pass. |
|
Verification pass on the current revision (f6a6428).
No changes requested from our side; merge-ready pending review sign-off. |
# Conflicts: # CHANGELOG.md
…ion paths (#443) The drain journal (bearer tokens) and the CLI socket honor the test env var unconditionally in shipped binaries. A stray service drop-in or profile export silently splits state: tokens journal to an arbitrary directory while everything else uses the stock paths. Disposition chosen over a testenv build-tag gate: the harness sets the variable without the tag in dozens of subpackage tests, so a gate would break tagless runs for hygiene-only value. Instead both honor sites log a WARNING naming the redirect target, and the operator guide documents the footgun. Chose observability because no known exploit path exists (needs service environment control); the tag gate remains an option if that changes.
|
#443 addressed on this branch ( Reasoning for the choice: the harness sets What landed instead:
If an exploit class ever materializes, the tag-gate remains the escalation path (the commit message records the tradeoff). Touched-module battery green: |
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
|
The |
Fixes #375.
wallet drain cashuaborted on the first per-mint failure, silently discarding tokens already produced by earlier successful (and irreversible) mints — on wallets whose registry held two URL spellings of one mint this could destroy real funds. Three layers:success:false,partial:true,tokens, per-minterrors) in both plain and JSON output; earlier tokens are never discarded. On the CLI side the partial data is shown before the error so real funds stay visible./etc/tollgate/wallet-drain-journal.jsonl(0600, 0700 dir) before the next mint is attempted; a journal failure stops further draining.Also:
--yes/-yfor non-interactive drains, and meaningful exit codes (cancellation, full and partial failure, JSON-modesuccess:false).Tests: partial-failure retention, journal-before-next-mint ordering, canonicalization/duplicate-registry, drain confirm flow (
src/cli/server_drain_test.go,src/cmd/tollgate-cli/drain_*_test.go,src/tollwallet/{mint_canonicalization,duplicate_mint_registry}_test.go).Verification:
go vet+ tests green insrc/cli,src/tollwallet,src/cmd/tollgate-cli; rootgofmt -l/go vet/go build+go test -race -count=1 -tags testenv ./...green. Live regression validated in PRTA #107 (closed).Note: the CHANGELOG hunk also normalizes
[Unreleased](merges the duplicated Fixed/Added/Changed sections accumulated from the merge-night conflicts, removes one literal #381 duplicate); no entry content changed.