Repository navigation
fix(merchant): boot resume replays journaled swap intents — close the #719 crash window (#497) - #771
Amperstrand wants to merge 2 commits into
Conversation
…ollGate#719 crash window closes at boot (OpenTollGate#497) OpenTollGate#719: the crash-window lane fails on main because only the CUSTOMER half of OpenTollGate#502's business-transaction record landed — the merchant restores receive intents and converges owed service, but nothing ever drives the WALLET half, so value the mint already issued is never recovered: the swap-intent machinery (gonuts-tollgate #31, merged but unreleased) is not in the v0.13.0 pin and the daemon calls no resume. Three coordinated moves: - bump gonuts-tollgate v0.13.0 -> d7591d2 (fork main: #31 pre-persisted swap intents + #30 KeysetFeesForMint) in all four carrying go.mod files. The maintainer tags the fork release (v0.14.0) and repins; the integration-ref pin follows the 7cd1882 precedent. - WalletPort grows ResumePendingSwaps: the gonuts adapter delegates to the fork's replay (exact request bytes re-POSTed; a deterministically signing mint re-issues identical signatures, verified against cdk-mintd 0.17.6 in OpenTollGate#497); cdk is an honest no-op (its saga resumes at load); the sidecar reports its protocol gap as a typed error. - newFullMerchant calls the resume once at boot, before serving payments: recovered value is operator balance, every failure shape is non-fatal (intents stay journaled, next boot retries, a down mint never wedges boot). Verification: unit tests at both seams (delegation + non-fatal boot shapes); full go-battery green across all 16 modules (three documented pre-existing merchant flakes skipped — OpenTollGate#731/OpenTollGate#732/OpenTollGate#733; OpenTollGate#731 reproduced failing on pristine main at the same rate, 4/5, so not introduced here; the OpenTollGate#726 ndsctl-on-PATH requirement worked around with the cloud-lab stub). The docker crash-window lane itself cannot run on this host (no docker access) — it must be run by the lane owner; the success log line it greps for is exactly the one the fork's resume emits.
|
CHANGES-REQUESTED — the mechanism is fund-safe (I verified in the pinned fork source that the replay can never re-expose deterministic derivation outputs), but the PR's central recovery claim is contradicted by the author's own deeper research in #497: beyond the mint's NUT-19 replay-cache TTL the re-POST is refused and the value is not recovered, and the changelog fragment ships the unqualified claim into v0.6.0 release notes. I audited the fork at the pinned commit (
No overlap concerns: this is the only boot-resume PR in the queue, the customer half it complements (#700) is already on main, and the changelog fragment correctly follows the Freeze compatibility: exactly stop-ship-sized — this is the P0 of the batch and the design is right — but the release-notes claim must be honest before this lands, the fork tag + repin must precede the stable cut, and the crash-injection lane needs one green run on the repinned branch. |
|
Rel-verify: cloud-lab crash-injection lane on #771 head The acceptance run the author could not run (no docker on their host) —
Standard cloud-lab suite on this head: 17 passed / 21 skipped / 6 failed. All 6 failures are pre-existing and not from this PR: 5 fee-allotment assertions break on cumulative in-memory sessions per static client MAC (reproduced identically against a main-built upstream image, and the first case passes on a freshly restarted upstream — an ordering/memory artifact of the combined pytest invocation, amplified by the clientd renewal daemon), plus 1 clientd Scratch-tree disclosures (lane-local, none part of the PR): (1) the #728 Not covered by this run: mint/melt crash windows (gonuts-tollgate #33, explicitly out of scope per the PR), the long-running-daemon sweep, QEMU/router-surface behavior, and the gated cloud-lab lanes that skipped (rotation phases, mint matrix, external mints, two-router). |
…e changelog rides the fragment convention (#778 review) Review mechanicals: - build-purity's Tests 1-2 now audit rateLimitTestBypass = true writers with the same guard discipline as TOLLGATE_TEST_CONFIG_DIR: a non- _test.go, non-build-tagged writer fails purity. The PR's "enforced by the build-purity contract" claim was ahead of the script; it is true now. The audit is verified both ways: a spoofed unguarded writer fails 2/2 checks, the clean tree passes 3/3. - The direct CHANGELOG.md edit is reverted in favour of a changelog.d fragment (the #769/#771/#777 convention), carrying the corrected claim — rig-verified FIXED on #748, purity-audited writer. No runtime behavior touched: the shipped 10 RPM default, its semantics and the three rate-limit proof tests are byte-identical (9/9 passes).
|
Review item applied — the recovery claim is scoped — head
No logic, no pin hunks — the repin rides #780's merge order as established. Verified after the edit: |
|
[release support 2026-10-09] Fork tag prerequisite SATISFIED — gonuts-tollgate
|
|
Carrier adjudication (#665 addendum, #771 vs #793): recommendation — supersede this PR in favor of #793. Same standard as the #718-vs-#774 call: one mechanism wins on the merits, the loser closes with an explicit supersession rather than a silent drop. The comparison, all points verified on both current heads:
What transfers to #793: (1) the scoped NUT-19 fragment wording from the reword comment — please re-push that text somewhere durable (it currently exists only as a PR comment); (2) the crash-injection lane green — your PASS run validated the fork mechanism both PRs share, and #793's review asks for one re-run against its head since its wiring differs (recovery concurrent with serving). What is deliberately dropped: the Suggested close: once #793 lands the scoped claim and the lane run, close this PR as superseded by #793 with a one-line note pointing at this comment — same shape as #718→#774. The engineering here wasn't wasted: this PR's review produced the TTL scoping the feature's changelog now needs, and its lane run is the standing evidence the mechanism works. |
|
🏷️ Next action — changes-requested Changes requested — author addresses the blocking item(s) in the review above, then request a re-review. |
…pin to the NUT-09 restore fallback — #866 Draft pin: gonuts-tollgate PR #40 branch commit d42a98cf8825 (pseudo-version v0.14.1-0.20261011150228-d42a98cf8825) carried by all five go.mod carriers AND packaging/build-inputs.json — hand-edited per the #771 branch-pseudo-pin pattern; bump-gonuts.sh (which refuses pseudo-versions by design) is the flip path at tag time. check-gonuts-pin.py: one pin, everywhere, across 5 carriers. Scenario (e') asserts the restore fallback end-to-end (red on current main by design — the fallback arrives with this repin); Dockerfile.mint018 bridges 0.18's DB-authoritative config from the 0.17.6 env-var flow; the research writeup lives at docs/research/ per contributing guidance. Single clean commit rebased onto main f89c879 (v0.6.0-rc2 train, gonuts base v0.13.1) — supersedes the earlier branch state whose review fixes had failed to push (refspec mistake; disclosed in the PR thread). Closes #866
…et on Unknown Keyset with a reachable mint Wallet init on a DB caching keysets the mint has rotated out failed with 'error getting keyset from mint: Unknown Keyset', the merchant degraded, and the degraded path's first-reachable trigger never fired (mint reachable all along) — the router bricked until wallet.db was moved aside, discarding proofs (#818). State machine at the New() seam: init fail → classify with the fork's IsUnknownKeysetError → if unknown-keyset class AND the current mint answers the bounded NUT-02 /v1/keysets probe (mintServesKeysets), re-fetch and retry the load ONCE → proceed or degrade. Everything else (storage errors, unreachable mint, real refusals) degrades exactly as before — the storage-degraded family (#583/#1035) is a separate slice. Pin: gonuts-tollgate v0.14.1-0.20261024073441-2c1154c68048 (pseudo-version integration pin, #771 pattern) — fork PR #44 carries the loadWalletMints tolerance; stacks on #1063 (v0.14.0 + honest fixtures); owner tags the fork, then scripts/bump-gonuts.sh finalizes.
Closes #719. Fund-safety lane: the P0 swap crash window (#497).
Root cause (why the lane is red on main)
The crash-window acceptance (
tests/cloud-lab/run-crash-injection.sh) fails on main because only the customer half of #502's business-transaction record landed:Restored N pending receive intent(s)+ NUT-07 reconcile + owed grants converge the customer;So after SIGKILL between mint-signed and wallet-save: the customer gets owed service, but the operator's value stays destroyed — no
pending swap … recoveredline ever appears, which is exactly what the lane greps for.What this PR does (three coordinated moves)
v0.13.0→d7591d2(fork main = Duplicate tollgate SSIDs #31 pre-persisted swap intents + Create identities.json #30 KeysetFeesForMint) across all four carrying go.mod files (src, src/cli, src/merchant, src/tollwallet). Integration-ref pin per the 7cd1882 precedent; maintainer action: tag the fork release (v0.14.0) and repin — one line ×4, no code change.WalletPort.ResumePendingSwaps:ErrSidecarResumeUnsupported— the daemon owns the DB; "not recovered this pass", never "lost value".newFullMerchantcalls the resume once at boot, before serving payments and beside the merchant-half restores. Recovered value is operator balance. Every failure shape is non-fatal: intents stay journaled, the next boot retries, a down mint never wedges boot. A degraded boot (no wallet) skips it and the intents wait for the upgrade — nothing is deleted, nothing is regenerated.Verification
TestBootResumeReplaysPendingSwapsExactlyOnce(all four result shapes, exactly one call, non-fatal),TestResumePendingSwapsOnUninitializedWallet+TestGonutsWalletDelegatesResume(typed error, no nil-panic, delegation intact).make go-batterygreen across all 16 modules, with three documented pre-existing merchant flakes skipped (merchant suite: TestLateReceiveOutcomeIsRecordedWhenReceiveCompletesAfterTheDeadline fails in every full-package run (passes in isolation) — record picker grabs the owed-grant ERROR line #731/merchant suite: TestSetOnFirstReachableForDegraded_FiredOnce intermittent — recovery callback fires for a mint already reachable from the initial probe #732/merchant suite: TestKilledAttemptConvergesAfterRestart failed once under full-suite load (5.8 s convergence timeout) — not reproducible in isolation #733 — fixes in flight: test(merchant): late-receive outcome selection raced the owed-grant ERROR line — the #681 "regression" was a racy assertion #718 and thefix/merchant-flakes-731-733branch). Attribution for merchant suite: TestLateReceiveOutcomeIsRecordedWhenReceiveCompletesAfterTheDeadline fails in every full-package run (passes in isolation) — record picker grabs the owed-grant ERROR line #731 verified honestly: the flaky test never callsnewFullMerchant, and it fails at the same rate (4/5 single-count runs) on pristine origin/main in a clean worktree. The go-battery needs an ndsctl on PATH: TestPurchaseSessionGuardHoldsThroughTheOutcomeUnknownWindow fails on bare hosts (passes with stub) #726 bare-hostndsctlrequirement was met with the repo's own cloud-lab stub on PATH.tests/cloud-lab/run-crash-injection.shagainst this branch; the success line it greps (wallet: pending swap … recovered) is emitted by exactly the code path wired here, and the lane's killer passes traffic through afterkill.marker.release, which is the state the replay needs.Out of scope (tracked)