paymentsdb: add inflight recovery regression tests
What changed, and why it matters
This commit only adds new regression tests to LND's payment database. The tests document that the SQL database backend previously failed to return certain kinds of non-finished (retryable or just-started) payments when asked for in-flight payments, while the older KV backend returned them correctly. The commit itself does not contain the actual fix; it only adds tests that would have failed before the fix. The practical risk is that recovery or routing logic relying on FetchInFlightPayments could miss payments it should retry or monitor, but no direct funds-loss vulnerability is shown in the diff.
Review the companion fix commit that makes these new tests pass on the SQL backend, and verify that FetchInFlightPayments in the SQL implementation now returns payments in StatusInitiated and StatusInFlight with only failed attempts. Ensure recovery and retry logic does not skip these payments in production.
Security signals we found
Behavioral discrepancy between SQL and KV database backends for payment lifecycle state
Potential omission of retryable/initiated payments from in-flight recovery queries
Regression-test-only commit; actual fix is elsewhere
No direct cryptographic, network, or access-control weakness in the diff
Evidence from the diff
The change adds two test cases in payments/db/payment_test.go: TestFetchInFlightPaymentsIncludesRetryablePayments and TestFetchInFlightPaymentsIncludesInitiatedPayments. They assert that FetchInFlightPayments must include (1) payments whose HTLC attempts have all failed but the payment itself has no terminal failure reason (StatusInFlight), and (2) payments that were initialized but have not yet registered any HTLC (StatusInitiated). The commit message states these tests pass on the KV backend and fail on the SQL backend before the fix, confirming a behavioral discrepancy between backends. The diff does not include the SQL fix, only the tests.
Changed components
payments/db/payment_test.goFetchInFlightPayments SQL backend implementation (referenced but not changed)LND payment state machine / router recovery logicInspect captured patch +74 / −0
diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go
index 2c547a4..e304b13 100644
--- a/payments/db/payment_test.go
+++ b/payments/db/payment_test.go
@@ -3008,6 +3008,80 @@ func TestFetchInFlightPaymentsMultipleAttempts(t *testing.T) {
require.Len(t, inFlightPayments[0].HTLCs, 2)
}
+// TestFetchInFlightPaymentsIncludesRetryablePayments tests that payments with
+// only failed HTLCs but no payment-level failure reason are still returned as
+// in-flight. This matches the shared payment state machine used by the router.
+func TestFetchInFlightPaymentsIncludesRetryablePayments(t *testing.T) {
+ t.Parallel()
+
+ ctx := t.Context()
+
+ paymentDB, _ := NewTestDB(t)
+
+ preimg := genPreimage(t)
+ rhash := sha256.Sum256(preimg[:])
+ info := genPaymentCreationInfo(t, rhash)
+
+ err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info)
+ require.NoError(t, err)
+
+ attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash)
+ _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt)
+ require.NoError(t, err)
+
+ _, err = paymentDB.FailAttempt(
+ ctx, info.PaymentIdentifier, attempt.AttemptID,
+ &HTLCFailInfo{Reason: HTLCFailUnreadable},
+ )
+ require.NoError(t, err)
+
+ payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier)
+ require.NoError(t, err)
+ require.Equal(t, StatusInFlight, payment.Status)
+
+ inFlightPayments, err := paymentDB.FetchInFlightPayments(ctx)
+ require.NoError(t, err)
+
+ inFlightHashes := make(map[lntypes.Hash]struct{}, len(inFlightPayments))
+ for _, p := range inFlightPayments {
+ inFlightHashes[p.Info.PaymentIdentifier] = struct{}{}
+ }
+
+ require.Contains(t, inFlightHashes, info.PaymentIdentifier)
+}
+
+// TestFetchInFlightPaymentsIncludesInitiatedPayments tests that payments which
+// have been initialized but have not yet registered an HTLC are still returned
+// as non-terminal payments.
+func TestFetchInFlightPaymentsIncludesInitiatedPayments(t *testing.T) {
+ t.Parallel()
+
+ ctx := t.Context()
+
+ paymentDB, _ := NewTestDB(t)
+
+ preimg := genPreimage(t)
+ rhash := sha256.Sum256(preimg[:])
+ info := genPaymentCreationInfo(t, rhash)
+
+ err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info)
+ require.NoError(t, err)
+
+ payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier)
+ require.NoError(t, err)
+ require.Equal(t, StatusInitiated, payment.Status)
+
+ inFlightPayments, err := paymentDB.FetchInFlightPayments(ctx)
+ require.NoError(t, err)
+
+ inFlightHashes := make(map[lntypes.Hash]struct{}, len(inFlightPayments))
+ for _, p := range inFlightPayments {
+ inFlightHashes[p.Info.PaymentIdentifier] = struct{}{}
+ }
+
+ require.Contains(t, inFlightHashes, info.PaymentIdentifier)
+}
+
// TestRouteFirstHopData tests that Route.FirstHopAmount and
// Route.FirstHopWireCustomRecords are correctly stored and retrieved.
func TestRouteFirstHopData(t *testing.T) {
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.