Skip to content

swaps: cover issue 881 settlement replay - #1171

Merged
bhandras merged 3 commits into
mainfrom
agent/issue-881-lightning-reconcile
Aug 21, 2026
Merged

swaps: cover issue 881 settlement replay#1171
bhandras merged 3 commits into
mainfrom
agent/issue-881-lightning-reconcile

Conversation

@bhandras

@bhandras bhandras commented Aug 21, 2026

Copy link
Copy Markdown
Member

What this changes

The production bug was fixed by #1061. This PR adds the incident-shaped regression that was still missing.

  • Extends TestPaySessionFundingReplayAfterLostResponse through authoritative claim observation, durable completion, and a second daemon restart.
  • Proves the original payment-scoped OOR funding intent and recovery actor run exactly once.
  • Replays an authoritative funding miss as the failed-payment negative control.
  • Projects the reported payment shape through the real activity store.
  • Replays terminal summaries through the actual startup backfill and proves both COMPLETE and FAILED rows are idempotent.

There is no production-code change.

Incident path

  1. Waved accepted the OOR funding transfer, but Wavelength lost the RPC response before it persisted the funding session and vHTLC outpoint.
  2. After restart, keyed reconciliation recovered the original transfer and advanced the durable pay state to VHTLCFunded.
  3. Before sdk/swaps: prevent duplicate in-swap vHTLC funding #1061, the same FundingInitiated action kept polling after changing its own state. The live-vHTLC lookup then emitted OnVHTLCFunded again.
  4. The duplicate event caused the invalid transition VHTLCFunded -> OnVHTLCFunded, so the pay row became FAILED even though the server later claimed the vHTLC.
  5. sdk/swaps: prevent duplicate in-swap vHTLC funding #1061 returns control to the FSM as soon as replay durably reaches VHTLCFunded. The FSM advances to WaitingForClaim, observes the indexed preimage, and persists Completed.

The attached #881 daemon log records the separate OOR accounting boundary: the 3,000-sat input was spent and replaced by a 1,999-sat live change VTXO for the 1,001-sat send. The activity mismatch came from the pay FSM's incorrect terminal state, not from a missing VTXO debit.

The wallet regression does not feed a fake balance into the assertion. The incident log is the authoritative evidence for the debit; the test covers the activity projection that was wrong.

Tests

  • go test ./sdk/swaps -count=1
  • go test -tags='wavewalletrpc swapruntime' ./swapwallet -count=1
  • Focused SQLite and PostgreSQL wallet regressions
  • Focused race tests for both changed packages
  • make lint-changed-local base=origin/main
  • make fmt-changed-check base=origin/main
  • make tidy-module-check
  • make sqlc-check
  • make doc-check schema-check sample-conf-check
  • make commitmsg-lint range=origin/main..HEAD

Related to #881. Production fix: #1061.

Extend the lost-response regression through authoritative claim
observation and terminal restart. Assert the recovered payment completes
once without duplicate funding or recovery side effects.

Also replay an authoritative funding miss after restart to prove the
negative control remains terminal.
Project the reported incident shape through the real activity store and
daemon balance adapter. Assert a completed pay produces one terminal
event, the activity amount matches the VTXO debit, and a failed negative
control does not alter balance.

Copy link
Copy Markdown
Member Author

/gateway review

Review this test-only PR for merge readiness.

Incident invariant:

  • One payment-scoped OOR transfer may be accepted even if the SDK loses its RPC response.
  • After restart, keyed reconciliation must recover that transfer, advance to WaitingForClaim, observe the authoritative claim preimage, and persist Completed.
  • A later restart must return the completed result without another funding send or recovery side effect.
  • Wallet activity must project COMPLETE once, and its signed amount must match the daemon's authoritative VTXO balance debit.
  • An authoritative funding miss must remain a terminal failed/expired negative control without creating funds movement.

Focus on concrete, reachable defects introduced by this PR. Check assertion validity, whether the fakes model the production boundaries faithfully, test determinism, and whether the tests could pass while the #881 path is still broken. Treat style-only or pre-existing debt as non-blocking.

Return READY, READY WITH FOLLOW-UPS, or NOT READY. For each blocker, give the trigger, execution path, consequence, existing guard, smallest fix, and regression risk. Stop once the invariant is proven and no concrete PR-introduced blocker remains.

@lightninglabs-gateway lightninglabs-gateway Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gateway review — 5 findings

🔴 0 Blocker · 🟠 0 Major · 🟡 5 Minor · 🔵 0 Nit

Summary

Test-only PR that extends the #881 regression coverage: the sdk/swaps test now carries the recovered pay session through authoritative claim observation, durable completion, and a second restart, and the negative-control test gains a terminal-reload assertion. The exactly-once evidence in sdk/swaps/in_swap_test.go is the strongest part of the change — sendPolicyCalls/armRecoveryCalls/cancelCalls pinned across the second restart, plus GetSwapSummary re-read from the store, do genuinely fail if the FSM re-enters funding or re-derives the claim after terminal persistence.

The weaker half is the wallet-projection test. Its two headline invariants — "the SEND amount matches the VTXO balance debit" and "an authoritative failed pay implies no debit" — are asserted against balances the test itself hardcodes through the fake daemon, so neither assertion can fail for a reason connected to the projection under test. It also models "startup replay" by calling the live fan-out twice rather than through the startup backfill path that the sibling tests in the same file use for restart shapes.

Nothing here changes production code, and I found no defect that makes the added assertions wrong. The findings below are about evidential value and determinism, not correctness of the diff.

Bot commands
  • /gateway re-review — re-run after pushing changes (maintainers)
  • /gateway dismiss <id> — silence a finding (maintainers)
  • /gateway explain <id> — elaborate on a finding (anyone)

Comment thread swapwallet/reconciler_test.go Outdated
Comment thread swapwallet/reconciler_test.go Outdated
Comment thread sdk/swaps/in_swap_test.go
Comment thread sdk/swaps/in_swap_test.go
Comment thread sdk/swaps/in_swap_test.go Outdated
@lightninglabs-gateway

lightninglabs-gateway Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 gateway audit metadata for this PR — auto-generated, please don't edit.

Exercise activity replay through the startup backfill and remove a fake
balance comparison that only restated its fixture. Bound terminal reload
waits and use txid-shaped identities.

Copy link
Copy Markdown
Member Author

/gateway re-review

Head is now 76e0c14.

Re-check only the five prior findings:

  • F1: fake-balance assertions were removed; the incident log owns debit evidence.
  • F2: terminal replay now uses ListSwaps through the real startup backfill.
  • F3: Resume only reloads state; Wait drives the FSM synchronously, and race tests pass.
  • F4: terminal reloads have one-second contexts.
  • F5: payment and spend identifiers are production-shaped hex strings.

Return READY, READY WITH FOLLOW-UPS, or NOT READY. Stop if no concrete PR-introduced blocker remains.

@lightninglabs-gateway lightninglabs-gateway Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gateway review — 2 findings

🔴 0 Blocker · 🟠 0 Major · 🟡 2 Minor · 🔵 0 Nit

Summary

READY WITH FOLLOW-UPS. Four of the five prior findings are closed by 76e0c14: the tautological balance deltas are gone (F1), the duplicate terminal summary now arrives through ListSwaps + runtime.backfillActivity with a third idempotent pass (F2), both terminal reloads are bounded by a one-second context (F4), and the txid/payment-hash literals are now 64-hex (F5). F3 is retracted on the basis of the maintainer's statement that ResumePayViaLightning only reloads durable state and that the FSM advances synchronously inside Wait — the non-test sdk/swaps sources are not in this review context, so I cannot re-derive it, but the passing focused race test is consistent with it.

Two residual gaps, both in the new assertions rather than in the code paths under test, both minor: the terminal-restart block cannot distinguish "reloaded the persisted Completed row" from "re-observed the still-armed indexer and re-completed", and the failed-pay negative control asserts status only, leaving the "no funds movement" half of the stated invariant uncovered.

The incident invariant itself is proven end to end: one accepted payment-scoped funding intent (accepted length 1, sendPolicyCalls pinned), one recovery arm/cancel pair, authoritative preimage observation, durable Completed, and a second restart with no new funding or recovery side effect. No PR-introduced blocker.


Status of prior findings

  • F1 addressed: Both GetConfirmedSat deltas and the require.Equal(t, after, afterFailure) restatement are gone from swapwallet/reconciler_test.go. What remains — entry.AmountSat == -paymentAmount and entry.FeeSat == 1 at lines 100–101 — is a projection assertion against the projector's output, not a comparison of two fixture-supplied constants. Resolved.
  • F2 addressed: The duplicate now goes through the real startup shape: live fanOutSwapUpdate at swapwallet/reconciler_test.go:77, then swap.listSwapsResp set and runtime.backfillActivity(ctx) at line 92, with require.Len(t, events, 1) at line 105 proving suppression. The added third pass at line 130 also covers the FAILED row. Resolved.
  • F3 withdrawn: Retracting. The finding was conditional on whether the resumed session polls before Wait; the answer is that it does not, so the post-resume field writes at sdk/swaps/in_swap_test.go:26342644 and the exact sendPolicyCalls == 3 at line 2653 are deterministic. This rests on the maintainer's statement plus the focused race run, not on code I can read here — sdk/swaps non-test sources are outside this context.
  • F4 addressed: Both terminal reloads now wrap Wait in a one-second context (sdk/swaps/in_swap_test.go:2681 and :2799), so a regression that re-enters the poll loop fails in a second instead of hanging to the package timeout. Note the fix trades a package timeout for a wall-clock bound inside a t.Parallel() test; if this ever flakes under -race on a loaded runner, widen the bound rather than removing it.
  • F5 addressed: SpentByTxID is now 64 hex characters (sdk/swaps/in_swap_test.go:26372638), matching the sibling TestPayViaLightningReturnsClaimPreimage, and failedHash at swapwallet/reconciler_test.go:5657 is a 64-hex value alongside the equally-shaped paymentHash. Resolved.
Bot commands
  • /gateway re-review — re-run after pushing changes (maintainers)
  • /gateway dismiss <id> — silence a finding (maintainers)
  • /gateway explain <id> — elaborate on a finding (anyone)

Comment thread sdk/swaps/in_swap_test.go
Comment thread swapwallet/reconciler_test.go
@bhandras
bhandras merged commit ac4f2e0 into main Aug 21, 2026
52 checks passed
@bhandras
bhandras deleted the agent/issue-881-lightning-reconcile branch August 21, 2026 11:24
@bhandras bhandras added the backport-v0.1.x-branch Backport this merged PR to v0.1.x-branch label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

Successfully created backport PR for v0.1.x-branch:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-v0.1.x-branch Backport this merged PR to v0.1.x-branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant