• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 17236841262

26 Aug 2025 11:33AM UTC coverage: 66.228% (+8.9%) from 57.321%
17236841262

Pull #9147

github

web-flow
Merge 39fde1801 into 0c2f045f5
Pull Request #9147: [Part 1|3] Introduce SQL Payment schema into LND

129 of 1847 new or added lines in 13 files covered. (6.98%)

20 existing lines in 7 files now uncovered.

136069 of 205456 relevant lines covered (66.23%)

21357.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/payments/db/sql_store.go
1
package paymentsdb
2

3
import (
4
        "bytes"
5
        "context"
6
        "database/sql"
7
        "errors"
8
        "fmt"
9
        "math"
10
        "sort"
11
        "strconv"
12
        "time"
13

14
        "github.com/lightningnetwork/lnd/lntypes"
15
        "github.com/lightningnetwork/lnd/lnwire"
16
        "github.com/lightningnetwork/lnd/routing/route"
17
        "github.com/lightningnetwork/lnd/sqldb"
18
        "github.com/lightningnetwork/lnd/sqldb/sqlc"
19
        "github.com/ltcsuite/ltcd/btcec"
20
)
21

22
// SQLQueries is a subset of the sqlc.Querier interface that can be used to
23
// execute queries against the SQL payments tables.
24
//
25
//nolint:ll,interfacebloat
26
type SQLQueries interface {
27
        /*
28
                Payment DB read operations.
29
        */
30
        FilterPayments(ctx context.Context, query sqlc.FilterPaymentsParams) ([]sqlc.Payment, error)
31
        CountPayments(ctx context.Context) (int64, error)
32
        FetchHtlcAttempts(ctx context.Context, query sqlc.FetchHtlcAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error)
33
        FetchCustomRecordsForAttempts(ctx context.Context, attemptIndices []int64) ([]sqlc.PaymentHtlcAttemptCustomRecord, error)
34
        FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentRouteHop, error)
35
        FetchFirstHopCustomRecords(ctx context.Context, paymentID int64) ([]sqlc.PaymentFirstHopCustomRecord, error)
36
        FetchCustomRecordsForHops(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentRouteHopCustomRecord, error)
37

38
        FetchPayment(ctx context.Context, paymentHash []byte) (sqlc.Payment, error)
39
        FetchPayments(ctx context.Context, paymentHashes [][]byte) ([]sqlc.Payment, error)
40

41
        /*
42
                Payment DB write operations.
43
        */
44
        DeletePayment(ctx context.Context, paymentHash []byte) error
45
        DeletePayments(ctx context.Context, paymentIDs []int64) error
46
        DeleteFailedAttempts(ctx context.Context, paymentID int64) error
47
        DeleteFailedAttemptsByAttemptIndices(ctx context.Context, attemptIndices []int64) error
48

49
        InsertPayment(ctx context.Context, payment sqlc.InsertPaymentParams) (int64, error)
50
        InsertFirstHopCustomRecord(ctx context.Context, customRecord sqlc.InsertFirstHopCustomRecordParams) error
51
        InsertHtlcAttempt(ctx context.Context, attempt sqlc.InsertHtlcAttemptParams) (int64, error)
52
        InsertHtlAttemptFirstHopCustomRecord(ctx context.Context, customRecord sqlc.InsertHtlAttemptFirstHopCustomRecordParams) error
53
        InsertHop(ctx context.Context, hop sqlc.InsertHopParams) (int64, error)
54
        InsertHopCustomRecord(ctx context.Context, customRecord sqlc.InsertHopCustomRecordParams) error
55

56
        UpdateHtlcAttemptSettleInfo(ctx context.Context, settleInfo sqlc.UpdateHtlcAttemptSettleInfoParams) (int64, error)
57
        UpdateHtlcAttemptFailInfo(ctx context.Context, failInfo sqlc.UpdateHtlcAttemptFailInfoParams) (int64, error)
58
        UpdatePaymentFailReason(ctx context.Context, failReason sqlc.UpdatePaymentFailReasonParams) (int64, error)
59
}
60

61
// BatchedSQLQueries is a version of the SQLQueries that's capable
62
// of batched database operations.
63
type BatchedSQLQueries interface {
64
        SQLQueries
65
        sqldb.BatchedTx[SQLQueries]
66
}
67

68
// SQLStore represents a storage backend.
69
type SQLStore struct {
70
        cfg *SQLStoreConfig
71
        db  BatchedSQLQueries
72

73
        // keepFailedPaymentAttempts is a flag that indicates whether we should
74
        // keep failed payment attempts in the database.
75
        keepFailedPaymentAttempts bool
76
}
77

78
// A compile-time constraint to ensure SQLStore implements DB.
79
var _ DB = (*SQLStore)(nil)
80

81
// SQLStoreConfig holds the configuration for the SQLStore.
82
type SQLStoreConfig struct {
83
        // QueryConfig holds configuration values for SQL queries.
84
        QueryCfg *sqldb.QueryConfig
85
}
86

87
// NewSQLStore creates a new SQLStore instance given a open
88
// BatchedSQLPaymentsQueries storage backend.
89
func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries,
NEW
90
        options ...StoreOptionModifier) (*SQLStore, error) {
×
NEW
91

×
NEW
92
        opts := DefaultOptions()
×
NEW
93
        for _, applyOption := range options {
×
NEW
94
                applyOption(opts)
×
NEW
95
        }
×
96

NEW
97
        if opts.NoMigration {
×
NEW
98
                return nil, fmt.Errorf("the NoMigration option is not yet " +
×
NEW
99
                        "supported for SQL stores")
×
NEW
100
        }
×
101

NEW
102
        return &SQLStore{
×
NEW
103
                cfg:                       cfg,
×
NEW
104
                db:                        db,
×
NEW
105
                keepFailedPaymentAttempts: opts.KeepFailedPaymentAttempts,
×
NEW
106
        }, nil
×
107
}
108

109
// QueryPayments queries the payments from the database.
110
//
111
// This is part of the DB interface.
112
func (s *SQLStore) QueryPayments(ctx context.Context,
NEW
113
        query Query) (Response, error) {
×
NEW
114

×
NEW
115
        if query.MaxPayments == 0 {
×
NEW
116
                return Response{}, fmt.Errorf("max payments must be non-zero")
×
NEW
117
        }
×
118

NEW
119
        var (
×
NEW
120
                allPayments   []*MPPayment
×
NEW
121
                totalCount    int64
×
NEW
122
                initialCursor int64
×
NEW
123
        )
×
NEW
124

×
NEW
125
        extractCursor := func(
×
NEW
126
                row sqlc.Payment) int64 {
×
NEW
127

×
NEW
128
                return row.ID
×
NEW
129
        }
×
130

NEW
131
        processPayment := func(ctx context.Context,
×
NEW
132
                dbPayment sqlc.Payment) error {
×
NEW
133

×
NEW
134
                // Now we need to fetch all the additional data for the payment.
×
NEW
135
                mpPayment, err := s.fetchPaymentWithCompleteData(
×
NEW
136
                        ctx, s.db, dbPayment,
×
NEW
137
                )
×
NEW
138
                if err != nil {
×
NEW
139
                        return fmt.Errorf("failed to fetch payment with "+
×
NEW
140
                                "complete data: %w", err)
×
NEW
141
                }
×
142

143
                // If the payment is succeeded and we are not interested in
144
                // incomplete payments, we skip it.
NEW
145
                if mpPayment.Status == StatusSucceeded &&
×
NEW
146
                        !query.IncludeIncomplete {
×
NEW
147

×
NEW
148
                        return nil
×
NEW
149
                }
×
150

NEW
151
                if len(allPayments) >= int(query.MaxPayments) {
×
NEW
152
                        return ErrMaxPaymentsReached
×
NEW
153
                }
×
154

NEW
155
                allPayments = append(allPayments, mpPayment)
×
NEW
156

×
NEW
157
                return nil
×
158
        }
159

NEW
160
        err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
×
NEW
161
                // We first count all payments to determine the total count
×
NEW
162
                // if requested.
×
NEW
163
                if query.CountTotal {
×
NEW
164
                        totalPayments, err := db.CountPayments(ctx)
×
NEW
165
                        if err != nil {
×
NEW
166
                                return fmt.Errorf("failed to count "+
×
NEW
167
                                        "payments: %w", err)
×
NEW
168
                        }
×
NEW
169
                        totalCount = totalPayments
×
170
                }
171

NEW
172
                queryFunc := func(ctx context.Context, lastID int64,
×
NEW
173
                        limit int32) ([]sqlc.Payment, error) {
×
NEW
174

×
NEW
175
                        filterParams := sqlc.FilterPaymentsParams{
×
NEW
176
                                NumLimit: limit,
×
NEW
177
                                Reverse:  query.Reversed,
×
NEW
178
                        }
×
NEW
179

×
NEW
180
                        if query.Reversed {
×
NEW
181
                                filterParams.IndexOffsetLet = sqldb.SQLInt64(
×
NEW
182
                                        lastID,
×
NEW
183
                                )
×
NEW
184
                        } else {
×
NEW
185
                                filterParams.IndexOffsetGet = sqldb.SQLInt64(
×
NEW
186
                                        lastID,
×
NEW
187
                                )
×
NEW
188
                        }
×
189

190
                        // Add potential date filters if specified.
NEW
191
                        if query.CreationDateStart != 0 {
×
NEW
192
                                filterParams.CreatedAfter = sqldb.SQLTime(
×
NEW
193
                                        time.Unix(query.CreationDateStart, 0).
×
NEW
194
                                                UTC(),
×
NEW
195
                                )
×
NEW
196
                        }
×
NEW
197
                        if query.CreationDateEnd != 0 {
×
NEW
198
                                filterParams.CreatedBefore = sqldb.SQLTime(
×
NEW
199
                                        time.Unix(query.CreationDateEnd, 0).
×
NEW
200
                                                UTC(),
×
NEW
201
                                )
×
NEW
202
                        }
×
203

204
                        // If we are only interested in settled payments (no
205
                        // failed or in-flight payments), we can exclude failed
206
                        // the failed one here with the query params. We will
207
                        // still fetch INFLIGHT payments which we then will
208
                        // drop in the `handlePayment` function. There is no
209
                        // easy way currently to also not fetch INFLIGHT
210
                        // payments.
211
                        //
212
                        // NOTE: This is set in place to keep compatibility with
213
                        // the KV implementation.
NEW
214
                        if !query.IncludeIncomplete {
×
NEW
215
                                filterParams.ExcludeFailed = sqldb.SQLBool(true)
×
NEW
216
                        }
×
217

NEW
218
                        return db.FilterPayments(ctx, filterParams)
×
219
                }
220

NEW
221
                if query.Reversed {
×
NEW
222
                        initialCursor = int64(math.MaxInt64)
×
NEW
223
                } else {
×
NEW
224
                        initialCursor = int64(-1)
×
NEW
225
                }
×
226

NEW
227
                return sqldb.ExecutePaginatedQuery(
×
NEW
228
                        ctx, s.cfg.QueryCfg, initialCursor, queryFunc,
×
NEW
229
                        extractCursor, processPayment,
×
NEW
230
                )
×
NEW
231
        }, func() {
×
NEW
232
                allPayments = nil
×
NEW
233
        })
×
234

235
        // If make sure we don't return an error if we reached the maximum
236
        // number of payments. Which is the pagination limit for the query
237
        // itself.
NEW
238
        if err != nil && !errors.Is(err, ErrMaxPaymentsReached) {
×
NEW
239
                return Response{}, fmt.Errorf("failed to query payments: %w",
×
NEW
240
                        err)
×
NEW
241
        }
×
242

243
        // Handle case where no payments were found
NEW
244
        if len(allPayments) == 0 {
×
NEW
245
                return Response{
×
NEW
246
                        Payments:         allPayments,
×
NEW
247
                        FirstIndexOffset: 0,
×
NEW
248
                        LastIndexOffset:  0,
×
NEW
249
                        TotalCount:       uint64(totalCount),
×
NEW
250
                }, nil
×
NEW
251
        }
×
252

NEW
253
        return Response{
×
NEW
254
                Payments:         allPayments,
×
NEW
255
                FirstIndexOffset: allPayments[0].SequenceNum,
×
NEW
256
                LastIndexOffset:  allPayments[len(allPayments)-1].SequenceNum,
×
NEW
257
                TotalCount:       uint64(totalCount),
×
NEW
258
        }, nil
×
259
}
260

261
// fetchPaymentWithCompleteData fetches a payment and all its associated data
262
// (HTLC attempts, hops, custom records) to create a complete internal
263
// representation of the payment.
264
//
265
// NOTE: This does also add the payment status and payment state which is
266
// derived from the db data.
267
func (s *SQLStore) fetchPaymentWithCompleteData(ctx context.Context,
NEW
268
        db SQLQueries, dbPayment sqlc.Payment) (*MPPayment, error) {
×
NEW
269

×
NEW
270
        // We fetch all the htlc attempts for the payment.
×
NEW
271
        dbHtlcAttempts, err := db.FetchHtlcAttempts(
×
NEW
272
                ctx, sqlc.FetchHtlcAttemptsParams{
×
NEW
273
                        PaymentID: dbPayment.ID,
×
NEW
274
                },
×
NEW
275
        )
×
NEW
276
        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
277
                return nil, fmt.Errorf("unable to fetch htlc attempts for "+
×
NEW
278
                        "payment(id=%d): %w", dbPayment.ID, err)
×
NEW
279
        }
×
280

281
        // Sort attempts by ID to ensure correct order so that the attempts are
282
        // processed in the correct order.
NEW
283
        sort.Slice(dbHtlcAttempts, func(i, j int) bool {
×
NEW
284
                return dbHtlcAttempts[i].ID < dbHtlcAttempts[j].ID
×
NEW
285
        })
×
286

NEW
287
        if len(dbHtlcAttempts) == 0 {
×
NEW
288
                payment, err := unmarshalPaymentWithoutHTLCs(dbPayment)
×
NEW
289
                if err != nil {
×
NEW
290
                        return nil, fmt.Errorf("unable to create payment "+
×
NEW
291
                                "without HTLCs: %w", err)
×
NEW
292
                }
×
293

294
                // We get the state of the payment in case it has inflight
295
                // HTLCs.
296
                //
297
                // TODO(ziggie): Refactor this code so we can determine the
298
                // payment state without having to fetch all HTLCs.
NEW
299
                err = payment.setState()
×
NEW
300
                if err != nil {
×
NEW
301
                        return nil, fmt.Errorf("unable to set state: %w", err)
×
NEW
302
                }
×
303

NEW
304
                return payment, nil
×
305
        }
306

307
        // Get all hops for all attempts.
NEW
308
        attemptIndices := extractAttemptIndices(dbHtlcAttempts)
×
NEW
309
        dbHops, err := db.FetchHopsForAttempts(ctx, attemptIndices)
×
NEW
310
        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
311
                return nil, fmt.Errorf("unable to fetch hops: %w", err)
×
NEW
312
        }
×
313

314
        // Get all custom records for all attempts.
NEW
315
        dbRouteCustomRecords, err := db.FetchCustomRecordsForAttempts(
×
NEW
316
                ctx, attemptIndices,
×
NEW
317
        )
×
NEW
318
        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
319
                return nil, fmt.Errorf("unable to fetch route custom "+
×
NEW
320
                        "records: %w", err)
×
NEW
321
        }
×
322

323
        // Get all custom records for all hops.
NEW
324
        hopIDs := extractHopIDs(dbHops)
×
NEW
325
        dbHopCustomRecords, err := db.FetchCustomRecordsForHops(ctx, hopIDs)
×
NEW
326
        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
327
                return nil, fmt.Errorf("unable to fetch custom records: %w",
×
NEW
328
                        err)
×
NEW
329
        }
×
330

NEW
331
        dbFirstHopCustomRecords, err := db.FetchFirstHopCustomRecords(
×
NEW
332
                ctx, dbPayment.ID,
×
NEW
333
        )
×
NEW
334
        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
335
                return nil, fmt.Errorf("unable to fetch first hop "+
×
NEW
336
                        "custom records for payment(id=%d): %w",
×
NEW
337
                        dbPayment.ID, err)
×
NEW
338
        }
×
339

NEW
340
        payment, err := unmarshalPaymentData(
×
NEW
341
                dbPayment, dbHtlcAttempts, dbHops, dbRouteCustomRecords,
×
NEW
342
                dbHopCustomRecords, dbFirstHopCustomRecords,
×
NEW
343
        )
×
NEW
344
        if err != nil {
×
NEW
345
                return nil, fmt.Errorf("unable to process payment data: %w",
×
NEW
346
                        err)
×
NEW
347
        }
×
348

349
        // We get the state of the payment in case it has inflight HTLCs.
350
        //
351
        // TODO(ziggie): Refactor this code so we can determine the payment
352
        // state without having to fetch all HTLCs.
NEW
353
        err = payment.setState()
×
NEW
354
        if err != nil {
×
NEW
355
                return nil, fmt.Errorf("unable to set state: %w", err)
×
NEW
356
        }
×
357

NEW
358
        return payment, nil
×
359
}
360

361
// extractAttemptIndices extracts the attempt indices from a slice of
362
// sqlc.PaymentHtlcAttempt.
NEW
363
func extractAttemptIndices(attempts []sqlc.PaymentHtlcAttempt) []int64 {
×
NEW
364
        indices := make([]int64, len(attempts))
×
NEW
365
        for i, attempt := range attempts {
×
NEW
366
                indices[i] = attempt.AttemptIndex
×
NEW
367
        }
×
368

NEW
369
        return indices
×
370
}
371

372
// extractHopIDs extracts the hop IDs from a slice of sqlc.PaymentRouteHop.
NEW
373
func extractHopIDs(hops []sqlc.PaymentRouteHop) []int64 {
×
NEW
374
        ids := make([]int64, len(hops))
×
NEW
375
        for i, hop := range hops {
×
NEW
376
                ids[i] = hop.ID
×
NEW
377
        }
×
378

NEW
379
        return ids
×
380
}
381

382
// FetchPayment fetches a payment from the database given its payment hash.
383
//
384
// This is part of the DB interface.
NEW
385
func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) {
×
NEW
386
        var (
×
NEW
387
                ctx        = context.Background()
×
NEW
388
                mppPayment *MPPayment
×
NEW
389
        )
×
NEW
390

×
NEW
391
        err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
×
NEW
392
                dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
393
                if err != nil {
×
NEW
394
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
NEW
395
                }
×
396

NEW
397
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
398
                        ctx, db, dbPayment,
×
NEW
399
                )
×
NEW
400
                if err != nil {
×
NEW
401
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
402
                                "data: %w", err)
×
NEW
403
                }
×
404

NEW
405
                mppPayment = payment
×
NEW
406

×
NEW
407
                return nil
×
NEW
408
        }, func() {
×
NEW
409
                mppPayment = nil
×
NEW
410
        })
×
411

NEW
412
        if err != nil {
×
NEW
413
                return nil, fmt.Errorf("unable to fetch payment: %w", err)
×
NEW
414
        }
×
415

NEW
416
        return mppPayment, nil
×
417
}
418

419
// FetchInFlightPayments fetches all payments which are INFLIGHT.
420
//
421
// This is part of the DB interface.
NEW
422
func (s *SQLStore) FetchInFlightPayments() ([]*MPPayment, error) {
×
NEW
423
        var (
×
NEW
424
                ctx              = context.Background()
×
NEW
425
                inFlightPayments []*MPPayment
×
NEW
426
        )
×
NEW
427

×
NEW
428
        err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
×
NEW
429
                dbInflightAttempts, err := db.FetchHtlcAttempts(
×
NEW
430
                        ctx, sqlc.FetchHtlcAttemptsParams{
×
NEW
431
                                InFlightOnly: true,
×
NEW
432
                        },
×
NEW
433
                )
×
NEW
434
                if err != nil {
×
NEW
435
                        return fmt.Errorf("unable to fetch inflight htlc "+
×
NEW
436
                                "attempts: %w", err)
×
NEW
437
                }
×
438

439
                // This makes sure we remove all duplicate payment hashes in
440
                // cases where multiple attempts for the same payment are
441
                // INFLIGHT.
NEW
442
                paymentHashes := make([][]byte, len(dbInflightAttempts))
×
NEW
443
                for i, attempt := range dbInflightAttempts {
×
NEW
444
                        paymentHashes[i] = attempt.PaymentHash
×
NEW
445
                }
×
446

NEW
447
                dbPayments, err := db.FetchPayments(ctx, paymentHashes)
×
NEW
448
                if err != nil {
×
NEW
449
                        return fmt.Errorf("unable to fetch payments: %w", err)
×
NEW
450
                }
×
451

452
                // pre-allocate the slice to the number of payments.
NEW
453
                inFlightPayments = make([]*MPPayment, 0, len(dbPayments))
×
NEW
454

×
NEW
455
                for i, dbPayment := range dbPayments {
×
NEW
456
                        // NOTE: There is a small inefficency here as we fetch
×
NEW
457
                        // the payment attempts for each payment again, this
×
NEW
458
                        // could be improved by reusing the data from the
×
NEW
459
                        // previous fetch.
×
NEW
460
                        mppPayment, err := s.fetchPaymentWithCompleteData(
×
NEW
461
                                ctx, db, dbPayment,
×
NEW
462
                        )
×
NEW
463
                        if err != nil {
×
NEW
464
                                return fmt.Errorf("unable to fetch payment: %w",
×
NEW
465
                                        err)
×
NEW
466
                        }
×
467

NEW
468
                        inFlightPayments[i] = mppPayment
×
469
                }
470

NEW
471
                return nil
×
NEW
472
        }, func() {
×
NEW
473
                inFlightPayments = nil
×
NEW
474
        })
×
475

NEW
476
        if err != nil {
×
NEW
477
                return nil, fmt.Errorf("unable to fetch in-flight "+
×
NEW
478
                        "payments: %w", err)
×
NEW
479
        }
×
480

NEW
481
        return inFlightPayments, nil
×
482
}
483

484
// DeletePayment deletes a payment from the database given its payment hash.
485
// It will only delete the failed attempts if the failedAttemptsOnly flag is
486
// set.
487
//
488
// This is part of the DB interface.
489
func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash,
NEW
490
        failedAttemptsOnly bool) error {
×
NEW
491

×
NEW
492
        ctx := context.Background()
×
NEW
493

×
NEW
494
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
495
                payment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
496
                if err != nil {
×
NEW
497
                        if errors.Is(err, sql.ErrNoRows) {
×
NEW
498
                                return fmt.Errorf("payment not found: %w",
×
NEW
499
                                        ErrPaymentNotInitiated)
×
NEW
500
                        }
×
501

NEW
502
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
503
                }
504

505
                // We need to fetch the complete payment data to check if the
506
                // payment is removable.
NEW
507
                completePayment, err := s.fetchPaymentWithCompleteData(
×
NEW
508
                        ctx, db, payment,
×
NEW
509
                )
×
NEW
510
                if err != nil {
×
NEW
511
                        return fmt.Errorf("unable to fetch complete "+
×
NEW
512
                                "payment data: %w", err)
×
NEW
513
                }
×
NEW
514
                if err := completePayment.Status.removable(); err != nil {
×
NEW
515
                        return fmt.Errorf("payment is still in flight: %w", err)
×
NEW
516
                }
×
517

518
                // If we selected to only delete failed attempts, we only
519
                // delete the failed attempts rather than the payment itself.
NEW
520
                if failedAttemptsOnly {
×
NEW
521
                        err = db.DeleteFailedAttempts(ctx, payment.ID)
×
NEW
522
                        if err != nil {
×
NEW
523
                                return fmt.Errorf("unable to delete failed "+
×
NEW
524
                                        "attempts: %w", err)
×
NEW
525
                        }
×
NEW
526
                } else {
×
NEW
527
                        err = db.DeletePayment(ctx, paymentHash[:])
×
NEW
528
                        if err != nil {
×
NEW
529
                                return fmt.Errorf("unable to delete "+
×
NEW
530
                                        "payment: %w", err)
×
NEW
531
                        }
×
532
                }
533

NEW
534
                return nil
×
NEW
535
        }, func() {})
×
536

NEW
537
        if err != nil {
×
NEW
538
                return fmt.Errorf("unable to delete payment: %w", err)
×
NEW
539
        }
×
540

NEW
541
        return nil
×
542
}
543

544
// DeletePayments deletes all completed and failed payments from the DB. If
545
// failedOnly is set, only failed payments will be considered for deletion. If
546
// failedHtlcsOnly is set, the payment itself won't be deleted, only failed HTLC
547
// attempts. The method returns the number of deleted payments, which is always
548
// 0 if failedHtlcsOnly is set.
549
//
550
// TODO(ziggie): Consider doing the deletion in a background job so we do not
551
// interfere with the main task of LND.
552
//
553
// This is part of the DB interface.
554
func (s *SQLStore) DeletePayments(failedOnly, failedAttemptsOnly bool) (int,
NEW
555
        error) {
×
NEW
556

×
NEW
557
        var (
×
NEW
558
                ctx            = context.Background()
×
NEW
559
                totalDeleted   int
×
NEW
560
                paymentIDs     []int64
×
NEW
561
                attemptIndices []int64
×
NEW
562
        )
×
NEW
563
        extractCursor := func(
×
NEW
564
                row sqlc.Payment) int64 {
×
NEW
565

×
NEW
566
                return row.ID
×
NEW
567
        }
×
568

NEW
569
        processPayment := func(ctx context.Context,
×
NEW
570
                dbPayment sqlc.Payment) error {
×
NEW
571

×
NEW
572
                // Now we need to fetch all the additional data for the payment.
×
NEW
573
                mpPayment, err := s.fetchPaymentWithCompleteData(
×
NEW
574
                        ctx, s.db, dbPayment,
×
NEW
575
                )
×
NEW
576
                if err != nil {
×
NEW
577
                        return fmt.Errorf("failed to fetch payment with "+
×
NEW
578
                                "complete data: %w", err)
×
NEW
579
                }
×
580

581
                // We skip payments which are still INFLIGHT.
NEW
582
                if err := mpPayment.Status.removable(); err != nil {
×
NEW
583
                        return nil
×
NEW
584
                }
×
585

586
                // We skip payments which are settled.
NEW
587
                if failedOnly && mpPayment.Status != StatusFailed {
×
NEW
588
                        return nil
×
NEW
589
                }
×
590

591
                // If we are only interested in failed attempts, we only add
592
                // the attempt indices to the slice.
NEW
593
                if failedAttemptsOnly {
×
NEW
594
                        for _, attempt := range mpPayment.HTLCs {
×
NEW
595
                                if attempt.Failure != nil {
×
NEW
596
                                        attemptIndices = append(
×
NEW
597
                                                attemptIndices,
×
NEW
598
                                                int64(attempt.AttemptID),
×
NEW
599
                                        )
×
NEW
600
                                }
×
601
                        }
602

NEW
603
                        return nil
×
604
                }
605

606
                // Otherwise we add the whole payment to the slice.
NEW
607
                paymentIDs = append(paymentIDs, int64(mpPayment.SequenceNum))
×
NEW
608

×
NEW
609
                return nil
×
610
        }
611

NEW
612
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
613
                queryFunc := func(ctx context.Context, lastID int64,
×
NEW
614
                        limit int32) ([]sqlc.Payment, error) {
×
NEW
615

×
NEW
616
                        filterParams := sqlc.FilterPaymentsParams{
×
NEW
617
                                NumLimit: limit,
×
NEW
618
                                IndexOffsetGet: sqldb.SQLInt64(
×
NEW
619
                                        lastID,
×
NEW
620
                                ),
×
NEW
621
                        }
×
NEW
622

×
NEW
623
                        return db.FilterPayments(ctx, filterParams)
×
NEW
624
                }
×
625

626
                // We start at the first payment.
NEW
627
                initialCursor := int64(-1)
×
NEW
628

×
NEW
629
                err := sqldb.ExecutePaginatedQuery(
×
NEW
630
                        ctx, s.cfg.QueryCfg, initialCursor, queryFunc,
×
NEW
631
                        extractCursor, processPayment,
×
NEW
632
                )
×
NEW
633
                if err != nil {
×
NEW
634
                        return fmt.Errorf("unable to paginate payments: %w",
×
NEW
635
                                err)
×
NEW
636
                }
×
637

638
                // Now that we have all the payments or attempt indices, we
639
                // can delete them in batches.
NEW
640
                if len(paymentIDs) > 0 {
×
NEW
641
                        // First we set the deleted count to the number of
×
NEW
642
                        // payments we are going to delete.
×
NEW
643
                        totalDeleted = len(paymentIDs)
×
NEW
644

×
NEW
645
                        //nolint:ll
×
NEW
646
                        return sqldb.ExecuteBatchQuery(
×
NEW
647
                                ctx, s.cfg.QueryCfg, paymentIDs,
×
NEW
648
                                func(id int64) int64 {
×
NEW
649
                                        return id
×
NEW
650
                                },
×
NEW
651
                                func(ctx context.Context, ids []int64) ([]any, error) {
×
NEW
652
                                        return nil, db.DeletePayments(ctx, ids)
×
NEW
653
                                },
×
654
                                nil,
655
                        )
656
                }
657

658
                //nolint:ll
NEW
659
                if len(attemptIndices) > 0 {
×
NEW
660
                        return sqldb.ExecuteBatchQuery(
×
NEW
661
                                ctx, s.cfg.QueryCfg, attemptIndices,
×
NEW
662
                                func(id int64) int64 {
×
NEW
663
                                        return id
×
NEW
664
                                },
×
NEW
665
                                func(ctx context.Context, ids []int64) ([]any, error) {
×
NEW
666
                                        return nil, db.DeleteFailedAttemptsByAttemptIndices(
×
NEW
667
                                                ctx, ids,
×
NEW
668
                                        )
×
NEW
669
                                },
×
670
                                nil,
671
                        )
672
                }
673

NEW
674
                return nil
×
NEW
675
        }, func() {
×
NEW
676
                paymentIDs = nil
×
NEW
677
                attemptIndices = nil
×
NEW
678
                totalDeleted = 0
×
NEW
679
        })
×
680

NEW
681
        if err != nil {
×
NEW
682
                return 0, fmt.Errorf("unable to delete payments: %w", err)
×
NEW
683
        }
×
684

NEW
685
        return totalDeleted, nil
×
686
}
687

688
// InitPayment checks that no other payment with the same payment hash
689
// exists in the database before creating a new payment. When this
690
// method returns successfully, the payment is guaranteed to be in the
691
// InFlight state.
692
//
693
// This is part of the DB interface.
694
func (s *SQLStore) InitPayment(paymentHash lntypes.Hash,
NEW
695
        creationInfo *PaymentCreationInfo) error {
×
NEW
696

×
NEW
697
        ctx := context.Background()
×
NEW
698

×
NEW
699
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
700
                // First, try to fetch the existing payment to check
×
NEW
701
                // its status.
×
NEW
702
                dbPayment, err := db.FetchPayment(
×
NEW
703
                        ctx, paymentHash[:],
×
NEW
704
                )
×
NEW
705
                if err != nil {
×
NEW
706
                        // If the payment doesn't exist, we can proceed
×
NEW
707
                        // with initialization.
×
NEW
708
                        if errors.Is(err, sql.ErrNoRows) {
×
NEW
709
                                // Payment doesn't exist, we can
×
NEW
710
                                // initialize it.
×
NEW
711
                                return s.insertNewPayment(
×
NEW
712
                                        ctx, db, paymentHash,
×
NEW
713
                                        creationInfo,
×
NEW
714
                                )
×
NEW
715
                        }
×
716

717
                        // Some other error occurred, return it.
NEW
718
                        return fmt.Errorf("unable to fetch "+
×
NEW
719
                                "existing payment: %w", err)
×
720
                }
721

722
                // We fetch the complete payment data to determine if
723
                // a new payment can be initialized.
NEW
724
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
725
                        ctx, db, dbPayment,
×
NEW
726
                )
×
NEW
727
                if err != nil {
×
NEW
728
                        return fmt.Errorf("unable to fetch "+
×
NEW
729
                                "complete payment data: %w", err)
×
NEW
730
                }
×
731

732
                // If the payment is not initializable, we return an
733
                // error.
734
                // This is only the case if the payment is already in
735
                // the failed state.
NEW
736
                if err := payment.Status.initializable(); err != nil {
×
NEW
737
                        return fmt.Errorf("payment is not "+
×
NEW
738
                                "initializable: %w", err)
×
NEW
739
                }
×
740

741
                // This should never happen, but we check it for
742
                // completeness.
NEW
743
                if err := payment.Status.removable(); err != nil {
×
NEW
744
                        return fmt.Errorf("payment is not "+
×
NEW
745
                                "removable: %w", err)
×
NEW
746
                }
×
747

748
                // We delete the payment to avoid duplicate payments, moreover
749
                // there is a unique constraint on the payment hash so we would
750
                // not be able to insert a new payment with the same hash.
NEW
751
                err = db.DeletePayment(ctx, paymentHash[:])
×
NEW
752
                if err != nil {
×
NEW
753
                        return fmt.Errorf("unable to delete "+
×
NEW
754
                                "payment: %w", err)
×
NEW
755
                }
×
756

757
                // And insert a new payment.
NEW
758
                err = s.insertNewPayment(ctx, db, paymentHash, creationInfo)
×
NEW
759
                if err != nil {
×
NEW
760
                        return fmt.Errorf("unable to insert "+
×
NEW
761
                                "new payment: %w", err)
×
NEW
762
                }
×
763

NEW
764
                return nil
×
NEW
765
        }, func() {})
×
NEW
766
        if err != nil {
×
NEW
767
                return fmt.Errorf("unable to init payment: %w", err)
×
NEW
768
        }
×
769

NEW
770
        return nil
×
771
}
772

773
// insertNewPayment inserts a new payment into the database.
774
func (s *SQLStore) insertNewPayment(ctx context.Context, db SQLQueries,
NEW
775
        paymentHash lntypes.Hash, creationInfo *PaymentCreationInfo) error {
×
NEW
776

×
NEW
777
        // Create the payment insert parameters.
×
NEW
778
        insertParams := sqlc.InsertPaymentParams{
×
NEW
779
                PaymentRequest: creationInfo.PaymentRequest,
×
NEW
780
                AmountMsat:     int64(creationInfo.Value),
×
NEW
781
                CreatedAt:      creationInfo.CreationTime.UTC(),
×
NEW
782
                PaymentHash:    paymentHash[:],
×
NEW
783
        }
×
NEW
784

×
NEW
785
        // Insert the payment and get the payment ID.
×
NEW
786
        paymentID, err := db.InsertPayment(ctx, insertParams)
×
NEW
787
        if err != nil {
×
NEW
788
                return fmt.Errorf("unable to insert payment: %w", err)
×
NEW
789
        }
×
790

791
        // If there are first hop custom records, we insert them now.
NEW
792
        if len(creationInfo.FirstHopCustomRecords) > 0 {
×
NEW
793
                err := creationInfo.FirstHopCustomRecords.Validate()
×
NEW
794
                if err != nil {
×
NEW
795
                        return fmt.Errorf("invalid first hop custom "+
×
NEW
796
                                "records: %w", err)
×
NEW
797
                }
×
798

NEW
799
                for key, value := range creationInfo.FirstHopCustomRecords {
×
NEW
800
                        err = db.InsertFirstHopCustomRecord(
×
NEW
801
                                ctx, sqlc.InsertFirstHopCustomRecordParams{
×
NEW
802
                                        PaymentID: paymentID,
×
NEW
803
                                        Key:       int64(key),
×
NEW
804
                                        Value:     value,
×
NEW
805
                                },
×
NEW
806
                        )
×
NEW
807
                        if err != nil {
×
NEW
808
                                return fmt.Errorf("unable to insert first "+
×
NEW
809
                                        "hop custom records: %w", err)
×
NEW
810
                        }
×
811
                }
812
        }
813

NEW
814
        return nil
×
815
}
816

817
// RegisterAttempt atomically records the provided HTLCAttemptInfo.
818
//
819
// This is part of the DB interface.
820
func (s *SQLStore) RegisterAttempt(paymentHash lntypes.Hash,
NEW
821
        attempt *HTLCAttemptInfo) (*MPPayment, error) {
×
NEW
822

×
NEW
823
        var (
×
NEW
824
                ctx        = context.Background()
×
NEW
825
                mppPayment *MPPayment
×
NEW
826
        )
×
NEW
827

×
NEW
828
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
829
                dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
830
                if err != nil {
×
NEW
831
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
NEW
832
                }
×
833

834
                // We need to fetch the complete payment to make sure we can
835
                // register a new attempt.
NEW
836
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
837
                        ctx, db, dbPayment,
×
NEW
838
                )
×
NEW
839
                if err != nil {
×
NEW
840
                        return fmt.Errorf("unable to fetch "+
×
NEW
841
                                "complete payment data: %w", err)
×
NEW
842
                }
×
843

844
                // Check if registering a new attempt is allowed.
NEW
845
                if err := payment.Registrable(); err != nil {
×
NEW
846
                        return err
×
NEW
847
                }
×
848

849
                // Verify the attempt is compatible with the existing payment.
NEW
850
                if err := verifyAttempt(payment, attempt); err != nil {
×
NEW
851
                        return err
×
NEW
852
                }
×
853

854
                // The attempt is valid, we can insert it now.
NEW
855
                err = s.insertHtlcAttemptWithHops(ctx, db, payment, attempt)
×
NEW
856
                if err != nil {
×
NEW
857
                        return fmt.Errorf("unable to insert htlc attempt: %w",
×
NEW
858
                                err)
×
NEW
859
                }
×
860

861
                // Add the new attempts to the payment so we don't have to fetch
862
                // the payment again because we have all the data we need.
NEW
863
                payment.HTLCs = append(payment.HTLCs, HTLCAttempt{
×
NEW
864
                        HTLCAttemptInfo: *attempt,
×
NEW
865
                })
×
NEW
866

×
NEW
867
                // We update the payment because the state is now different and
×
NEW
868
                // we want to return the updated payment.
×
NEW
869
                err = payment.setState()
×
NEW
870
                if err != nil {
×
NEW
871
                        return fmt.Errorf("unable to set state: %w", err)
×
NEW
872
                }
×
873

NEW
874
                mppPayment = payment
×
NEW
875

×
NEW
876
                return nil
×
NEW
877
        }, func() {
×
NEW
878
                mppPayment = nil
×
NEW
879
        })
×
NEW
880
        if err != nil {
×
NEW
881
                return nil, fmt.Errorf("unable to register attempt: %w", err)
×
NEW
882
        }
×
883

NEW
884
        return mppPayment, nil
×
885
}
886

887
// insertHtlcAttemptWithHops inserts a new HTLC attempt and all its associated
888
// hop data into the database. This function handles the complete insertion
889
// of an HTLC attempt including:
890
//   - The HTLC attempt record itself
891
//   - All route hops for the attempt
892
//   - Custom records for each hop
893
func (s *SQLStore) insertHtlcAttemptWithHops(ctx context.Context, db SQLQueries,
NEW
894
        mpPayment *MPPayment, attempt *HTLCAttemptInfo) error {
×
NEW
895

×
NEW
896
        // Insert the main HTLC attempt record.
×
NEW
897
        err := s.insertHtlcAttempt(ctx, db, mpPayment, attempt)
×
NEW
898
        if err != nil {
×
NEW
899
                return fmt.Errorf("unable to insert htlc attempt record: %w",
×
NEW
900
                        err)
×
NEW
901
        }
×
902

903
        // Insert all hops for this attempt
NEW
904
        if len(attempt.Route.Hops) > 0 {
×
NEW
905
                err := s.insertHtlcAttemptHops(ctx, db, attempt)
×
NEW
906
                if err != nil {
×
NEW
907
                        return fmt.Errorf("unable to insert htlc attempt "+
×
NEW
908
                                "hops: %w", err)
×
NEW
909
                }
×
910
        }
911

NEW
912
        return nil
×
913
}
914

915
// insertHtlcAttempt inserts the main HTLC attempt record into the database.
916
func (s *SQLStore) insertHtlcAttempt(ctx context.Context, db SQLQueries,
NEW
917
        mpPayment *MPPayment, attempt *HTLCAttemptInfo) error {
×
NEW
918

×
NEW
919
        var sessionKey [btcec.PrivKeyBytesLen]byte
×
NEW
920
        copy(sessionKey[:], attempt.SessionKey().Serialize())
×
NEW
921

×
NEW
922
        var routeSourceKey [btcec.PubKeyBytesLenCompressed]byte
×
NEW
923
        copy(routeSourceKey[:], attempt.Route.SourcePubKey[:])
×
NEW
924

×
NEW
925
        // Insert the HTLC attempt using named parameters
×
NEW
926
        _, err := db.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{
×
NEW
927
                AttemptIndex:       int64(attempt.AttemptID),
×
NEW
928
                PaymentID:          int64(mpPayment.SequenceNum),
×
NEW
929
                PaymentHash:        mpPayment.Info.PaymentIdentifier[:],
×
NEW
930
                AttemptTime:        attempt.AttemptTime.UTC(),
×
NEW
931
                SessionKey:         sessionKey[:],
×
NEW
932
                RouteTotalTimeLock: int32(attempt.Route.TotalTimeLock),
×
NEW
933
                RouteTotalAmount:   int64(attempt.Route.TotalAmount),
×
NEW
934
                RouteSourceKey:     routeSourceKey[:],
×
NEW
935
                FirstHopAmountMsat: int64(
×
NEW
936
                        attempt.Route.FirstHopAmount.Val.Int(),
×
NEW
937
                ),
×
NEW
938
        })
×
NEW
939
        if err != nil {
×
NEW
940
                return fmt.Errorf("unable to insert htlc attempt: %w", err)
×
NEW
941
        }
×
942

943
        // Insert the custom records for the route.
NEW
944
        for key, value := range attempt.Route.FirstHopWireCustomRecords {
×
NEW
945
                err = db.InsertHtlAttemptFirstHopCustomRecord(
×
NEW
946
                        ctx, sqlc.InsertHtlAttemptFirstHopCustomRecordParams{
×
NEW
947
                                HtlcAttemptIndex: int64(attempt.AttemptID),
×
NEW
948
                                Key:              int64(key),
×
NEW
949
                                Value:            value,
×
NEW
950
                        },
×
NEW
951
                )
×
NEW
952
                if err != nil {
×
NEW
953
                        return fmt.Errorf("unable to insert htlc attempt "+
×
NEW
954
                                "first hop custom record: %w", err)
×
NEW
955
                }
×
956
        }
957

NEW
958
        return nil
×
959
}
960

961
// insertHtlcAttemptHops inserts all hops for a given HTLC attempt.
962
func (s *SQLStore) insertHtlcAttemptHops(ctx context.Context, db SQLQueries,
NEW
963
        attempt *HTLCAttemptInfo) error {
×
NEW
964

×
NEW
965
        for index, hop := range attempt.Route.Hops {
×
NEW
966
                // Insert the hop record
×
NEW
967
                hopID, err := s.insertHop(
×
NEW
968
                        ctx, db, attempt.AttemptID, index, hop,
×
NEW
969
                )
×
NEW
970
                if err != nil {
×
NEW
971
                        return fmt.Errorf("unable to insert hop record: %w",
×
NEW
972
                                err)
×
NEW
973
                }
×
974

975
                // Insert custom records for this hop if any exist
NEW
976
                if len(hop.CustomRecords) > 0 {
×
NEW
977
                        err := hop.CustomRecords.Validate()
×
NEW
978
                        if err != nil {
×
NEW
979
                                return fmt.Errorf("invalid hop custom "+
×
NEW
980
                                        "records: %w", err)
×
NEW
981
                        }
×
982

NEW
983
                        for key, value := range hop.CustomRecords {
×
NEW
984
                                err = db.InsertHopCustomRecord(
×
NEW
985
                                        ctx, sqlc.InsertHopCustomRecordParams{
×
NEW
986
                                                HopID: hopID,
×
NEW
987
                                                Key:   int64(key),
×
NEW
988
                                                Value: value,
×
NEW
989
                                        },
×
NEW
990
                                )
×
NEW
991
                                if err != nil {
×
NEW
992
                                        return fmt.Errorf("unable to insert "+
×
NEW
993
                                                "hop custom records: %w", err)
×
NEW
994
                                }
×
995
                        }
996
                }
997
        }
998

NEW
999
        return nil
×
1000
}
1001

1002
// insertHop inserts a single hop record into the database.
1003
func (s *SQLStore) insertHop(ctx context.Context, db SQLQueries,
NEW
1004
        attemptID uint64, hopIndex int, hop *route.Hop) (int64, error) {
×
NEW
1005

×
NEW
1006
        // Insert the hop using named parameters
×
NEW
1007
        hopID, err := db.InsertHop(ctx, sqlc.InsertHopParams{
×
NEW
1008
                HtlcAttemptIndex: int64(attemptID),
×
NEW
1009
                HopIndex:         int32(hopIndex),
×
NEW
1010
                PubKey:           hop.PubKeyBytes[:],
×
NEW
1011
                Scid:             strconv.FormatUint(hop.ChannelID, 10),
×
NEW
1012
                OutgoingTimeLock: int32(hop.OutgoingTimeLock),
×
NEW
1013
                AmtToForward:     int64(hop.AmtToForward),
×
NEW
1014
                MetaData:         hop.Metadata,
×
NEW
1015
                LegacyPayload:    hop.LegacyPayload,
×
NEW
1016
                // Handle MPP (Multi-Path Payment) data if present.
×
NEW
1017
                MppPaymentAddr: s.extractMppPaymentAddr(hop),
×
NEW
1018
                MppTotalMsat:   s.extractMppTotalMsat(hop),
×
NEW
1019
                // Handle AMP (Atomic Multi-Path) data if present.
×
NEW
1020
                AmpRootShare:  s.extractAmpRootShare(hop),
×
NEW
1021
                AmpSetID:      s.extractAmpSetID(hop),
×
NEW
1022
                AmpChildIndex: s.extractAmpChildIndex(hop),
×
NEW
1023
                // Handle blinding point data if present.
×
NEW
1024
                BlindingPoint: s.extractBlindingPoint(hop),
×
NEW
1025
                // Handle encrypted data for blinded paths if present.
×
NEW
1026
                EncryptedData: hop.EncryptedData,
×
NEW
1027
                // Handle blinded path total amount if present.
×
NEW
1028
                BlindedPathTotalAmt: s.extractBlindedPathTotalAmt(hop),
×
NEW
1029
        })
×
NEW
1030
        if err != nil {
×
NEW
1031
                return 0, fmt.Errorf("unable to insert hop: %w", err)
×
NEW
1032
        }
×
1033

NEW
1034
        return hopID, nil
×
1035
}
1036

1037
// extractMppPaymentAddr extracts the MPP payment address from a hop.
NEW
1038
func (s *SQLStore) extractMppPaymentAddr(hop *route.Hop) []byte {
×
NEW
1039
        if hop.MPP != nil {
×
NEW
1040
                paymentAddr := hop.MPP.PaymentAddr()
×
NEW
1041
                return paymentAddr[:]
×
NEW
1042
        }
×
1043

NEW
1044
        return nil
×
1045
}
1046

1047
// extractMppTotalMsat extracts the MPP total amount from a hop.
NEW
1048
func (s *SQLStore) extractMppTotalMsat(hop *route.Hop) sql.NullInt64 {
×
NEW
1049
        if hop.MPP != nil {
×
NEW
1050
                return sql.NullInt64{
×
NEW
1051
                        Int64: int64(hop.MPP.TotalMsat()),
×
NEW
1052
                        Valid: true,
×
NEW
1053
                }
×
NEW
1054
        }
×
1055

NEW
1056
        return sql.NullInt64{Valid: false}
×
1057
}
1058

1059
// extractAmpRootShare extracts the AMP root share from a hop.
NEW
1060
func (s *SQLStore) extractAmpRootShare(hop *route.Hop) []byte {
×
NEW
1061
        if hop.AMP != nil {
×
NEW
1062
                rootShare := hop.AMP.RootShare()
×
NEW
1063
                return rootShare[:]
×
NEW
1064
        }
×
1065

NEW
1066
        return nil
×
1067
}
1068

1069
// extractAmpSetID extracts the AMP set ID from a hop.
NEW
1070
func (s *SQLStore) extractAmpSetID(hop *route.Hop) []byte {
×
NEW
1071
        if hop.AMP != nil {
×
NEW
1072
                ampSetID := hop.AMP.SetID()
×
NEW
1073
                return ampSetID[:]
×
NEW
1074
        }
×
1075

NEW
1076
        return nil
×
1077
}
1078

1079
// extractAmpChildIndex extracts the AMP child index from a hop.
NEW
1080
func (s *SQLStore) extractAmpChildIndex(hop *route.Hop) sql.NullInt32 {
×
NEW
1081
        if hop.AMP != nil {
×
NEW
1082
                return sql.NullInt32{
×
NEW
1083
                        Int32: int32(hop.AMP.ChildIndex()),
×
NEW
1084
                        Valid: true,
×
NEW
1085
                }
×
NEW
1086
        }
×
1087

NEW
1088
        return sql.NullInt32{Valid: false}
×
1089
}
1090

1091
// extractBlindingPoint extracts the blinding point from a hop.
NEW
1092
func (s *SQLStore) extractBlindingPoint(hop *route.Hop) []byte {
×
NEW
1093
        if hop.BlindingPoint != nil {
×
NEW
1094
                blindingPoint := hop.BlindingPoint.SerializeCompressed()
×
NEW
1095
                return blindingPoint
×
NEW
1096
        }
×
1097

NEW
1098
        return nil
×
1099
}
1100

1101
// extractBlindedPathTotalAmt extracts the blinded path total amount from a hop.
NEW
1102
func (s *SQLStore) extractBlindedPathTotalAmt(hop *route.Hop) sql.NullInt64 {
×
NEW
1103
        if hop.EncryptedData != nil {
×
NEW
1104
                return sql.NullInt64{
×
NEW
1105
                        Int64: int64(hop.TotalAmtMsat),
×
NEW
1106
                        Valid: true,
×
NEW
1107
                }
×
NEW
1108
        }
×
1109

NEW
1110
        return sql.NullInt64{Valid: false}
×
1111
}
1112

1113
// SettleAttempt marks the given attempt settled with the preimage. If
1114
// this is a multi shard payment, this might implicitly mean the
1115
// full payment succeeded.
1116
//
1117
// This is part of the DB interface.
1118
func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash,
NEW
1119
        attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) {
×
NEW
1120

×
NEW
1121
        var (
×
NEW
1122
                ctx        = context.Background()
×
NEW
1123
                mppPayment *MPPayment
×
NEW
1124
        )
×
NEW
1125

×
NEW
1126
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
1127
                dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
1128
                if err != nil {
×
NEW
1129
                        if errors.Is(err, sql.ErrNoRows) {
×
NEW
1130
                                return ErrPaymentNotInitiated
×
NEW
1131
                        }
×
1132

NEW
1133
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
1134
                }
1135

NEW
1136
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
1137
                        ctx, db, dbPayment,
×
NEW
1138
                )
×
NEW
1139
                if err != nil {
×
NEW
1140
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
1141
                                "data: %w", err)
×
NEW
1142
                }
×
1143

NEW
1144
                if err := payment.Status.updatable(); err != nil {
×
NEW
1145
                        return fmt.Errorf("payment is not updatable: %w", err)
×
NEW
1146
                }
×
1147

1148
                // Update the HTLC attempt with settlement information.
NEW
1149
                _, err = db.UpdateHtlcAttemptSettleInfo(ctx,
×
NEW
1150
                        sqlc.UpdateHtlcAttemptSettleInfoParams{
×
NEW
1151
                                AttemptIndex:   int64(attemptID),
×
NEW
1152
                                SettlePreimage: settleInfo.Preimage[:],
×
NEW
1153
                                SettleTime: sql.NullTime{
×
NEW
1154
                                        Time:  settleInfo.SettleTime.UTC(),
×
NEW
1155
                                        Valid: true,
×
NEW
1156
                                },
×
NEW
1157
                        })
×
NEW
1158
                if err != nil {
×
NEW
1159
                        return fmt.Errorf("unable to update htlc attempt "+
×
NEW
1160
                                "settle info: %w", err)
×
NEW
1161
                }
×
1162

1163
                // After updating the HTLC attempt, we need to fetch the
1164
                // updated payment again.
1165
                //
1166
                // NOTE: No need to fetch the main payment data again because
1167
                // nothing changed there.
NEW
1168
                payment, err = s.fetchPaymentWithCompleteData(
×
NEW
1169
                        ctx, db, dbPayment,
×
NEW
1170
                )
×
NEW
1171
                if err != nil {
×
NEW
1172
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
1173
                                "data: %w", err)
×
NEW
1174
                }
×
1175

NEW
1176
                mppPayment = payment
×
NEW
1177

×
NEW
1178
                return nil
×
NEW
1179
        }, func() {
×
NEW
1180
                mppPayment = nil
×
NEW
1181
        })
×
NEW
1182
        if err != nil {
×
NEW
1183
                return nil, fmt.Errorf("unable to settle attempt: %w", err)
×
NEW
1184
        }
×
1185

NEW
1186
        return mppPayment, nil
×
1187
}
1188

1189
// FailAttempt marks the given payment attempt failed.
1190
//
1191
// This is part of the DB interface.
1192
func (s *SQLStore) FailAttempt(paymentHash lntypes.Hash,
NEW
1193
        attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) {
×
NEW
1194

×
NEW
1195
        var (
×
NEW
1196
                ctx        = context.Background()
×
NEW
1197
                mppPayment *MPPayment
×
NEW
1198
        )
×
NEW
1199

×
NEW
1200
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
1201
                dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
1202
                if err != nil {
×
NEW
1203
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
NEW
1204
                }
×
1205

NEW
1206
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
1207
                        ctx, db, dbPayment,
×
NEW
1208
                )
×
NEW
1209
                if err != nil {
×
NEW
1210
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
1211
                                "data: %w", err)
×
NEW
1212
                }
×
1213

NEW
1214
                if err := payment.Status.updatable(); err != nil {
×
NEW
1215
                        return fmt.Errorf("payment is not updatable: %w", err)
×
NEW
1216
                }
×
1217

NEW
1218
                updateParams := sqlc.UpdateHtlcAttemptFailInfoParams{
×
NEW
1219
                        AttemptIndex: int64(attemptID),
×
NEW
1220
                        FailureSourceIndex: sql.NullInt32{
×
NEW
1221
                                Int32: int32(failInfo.FailureSourceIndex),
×
NEW
1222
                                Valid: true,
×
NEW
1223
                        },
×
NEW
1224
                        HtlcFailReason: sql.NullInt32{
×
NEW
1225
                                Int32: int32(failInfo.Reason),
×
NEW
1226
                                Valid: true,
×
NEW
1227
                        },
×
NEW
1228
                        FailTime: sql.NullTime{
×
NEW
1229
                                Time:  failInfo.FailTime.UTC(),
×
NEW
1230
                                Valid: true,
×
NEW
1231
                        },
×
NEW
1232
                }
×
NEW
1233

×
NEW
1234
                // If the failure message is not nil, we need to encode it.
×
NEW
1235
                if failInfo.Message != nil {
×
NEW
1236
                        buf := bytes.NewBuffer(nil)
×
NEW
1237
                        err := lnwire.EncodeFailureMessage(
×
NEW
1238
                                buf, failInfo.Message, 0,
×
NEW
1239
                        )
×
NEW
1240
                        if err != nil {
×
NEW
1241
                                return fmt.Errorf("unable to encode failure "+
×
NEW
1242
                                        "message: %w", err)
×
NEW
1243
                        }
×
NEW
1244
                        updateParams.FailureMsg = buf.Bytes()
×
1245
                }
1246

1247
                // Update the HTLC attempt with failure information
NEW
1248
                _, err = db.UpdateHtlcAttemptFailInfo(ctx, updateParams)
×
NEW
1249
                if err != nil {
×
NEW
1250
                        return fmt.Errorf("unable to update htlc attempt"+
×
NEW
1251
                                "fail info: %w", err)
×
NEW
1252
                }
×
1253

1254
                // After updating the HTLC attempt, we need to fetch the
1255
                // updated payment again.
1256
                //
1257
                // NOTE: No need to fetch the main payment data again because
1258
                // nothing changed there. The failure reason is updated in
1259
                // `Fail` function only.
NEW
1260
                payment, err = s.fetchPaymentWithCompleteData(
×
NEW
1261
                        ctx, db, dbPayment,
×
NEW
1262
                )
×
NEW
1263
                if err != nil {
×
NEW
1264
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
1265
                                "data: %w", err)
×
NEW
1266
                }
×
1267

NEW
1268
                mppPayment = payment
×
NEW
1269

×
NEW
1270
                return nil
×
NEW
1271
        }, func() {
×
NEW
1272
                mppPayment = nil
×
NEW
1273
        })
×
NEW
1274
        if err != nil {
×
NEW
1275
                return nil, fmt.Errorf("unable to fail attempt: %w", err)
×
NEW
1276
        }
×
1277

NEW
1278
        return mppPayment, nil
×
1279
}
1280

1281
// Fail transitions a payment into the Failed state, and records
1282
// the ultimate reason the payment failed. Note that this should only
1283
// be called when all active attempts are already failed. After
1284
// invoking this method, InitPayment should return nil on its next call
1285
// for this payment hash, allowing the user to make a subsequent
1286
// payment.
1287
//
1288
// This is part of the DB interface.
1289
func (s *SQLStore) Fail(paymentHash lntypes.Hash,
NEW
1290
        failureReason FailureReason) (*MPPayment, error) {
×
NEW
1291

×
NEW
1292
        var (
×
NEW
1293
                ctx        = context.Background()
×
NEW
1294
                mppPayment *MPPayment
×
NEW
1295
        )
×
NEW
1296

×
NEW
1297
        err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
×
NEW
1298
                dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
×
NEW
1299
                if err != nil {
×
NEW
1300
                        if errors.Is(err, sql.ErrNoRows) {
×
NEW
1301
                                return ErrPaymentNotInitiated
×
NEW
1302
                        }
×
1303

NEW
1304
                        return fmt.Errorf("unable to fetch payment: %w", err)
×
1305
                }
1306

NEW
1307
                payment, err := s.fetchPaymentWithCompleteData(
×
NEW
1308
                        ctx, db, dbPayment,
×
NEW
1309
                )
×
NEW
1310
                if err != nil {
×
NEW
1311
                        return fmt.Errorf("unable to fetch complete payment "+
×
NEW
1312
                                "data: %w", err)
×
NEW
1313
                }
×
1314

NEW
1315
                _, err = db.UpdatePaymentFailReason(ctx,
×
NEW
1316
                        sqlc.UpdatePaymentFailReasonParams{
×
NEW
1317
                                PaymentHash: paymentHash[:],
×
NEW
1318
                                FailReason: sql.NullInt32{
×
NEW
1319
                                        Int32: int32(failureReason),
×
NEW
1320
                                        Valid: true,
×
NEW
1321
                                },
×
NEW
1322
                        })
×
NEW
1323
                if err != nil {
×
NEW
1324
                        return fmt.Errorf("unable to update payment fail "+
×
NEW
1325
                                "reason: %w", err)
×
NEW
1326
                }
×
1327

1328
                // Instead of another round trip to fetch the payment, we just
1329
                // update the payment here as well.
NEW
1330
                payment.FailureReason = &failureReason
×
NEW
1331

×
NEW
1332
                // We update also the payment state and status.
×
NEW
1333
                err = payment.setState()
×
NEW
1334
                if err != nil {
×
NEW
1335
                        return fmt.Errorf("unable to set payment state: %w",
×
NEW
1336
                                err)
×
NEW
1337
                }
×
1338

NEW
1339
                mppPayment = payment
×
NEW
1340

×
NEW
1341
                return nil
×
NEW
1342
        }, func() {
×
NEW
1343
                mppPayment = nil
×
NEW
1344
        })
×
1345

NEW
1346
        if err != nil {
×
NEW
1347
                return nil, fmt.Errorf("unable to fail payment: %w", err)
×
NEW
1348
        }
×
1349

NEW
1350
        return mppPayment, nil
×
1351
}
1352

1353
// DeleteFailedAttempts removes all failed HTLCs from the db. It should
1354
// be called for a given payment whenever all inflight htlcs are
1355
// completed, and the payment has reached a final terminal state.
1356
//
1357
// This is part of the DB interface.
NEW
1358
func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error {
×
NEW
1359
        if !s.keepFailedPaymentAttempts {
×
NEW
1360
                const failedAttemptsOnly = true
×
NEW
1361
                return s.DeletePayment(paymentHash, failedAttemptsOnly)
×
NEW
1362
        }
×
1363

1364
        // If we are configured to keep failed payment attempts, we
1365
        // don't delete any data.
NEW
1366
        return nil
×
1367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc