From 18f03ecef680130d09116fd290609b0149c12fa7 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 15:00:40 +0200 Subject: [PATCH 01/40] sqldb: add index and comment to payment tables --- sqldb/sqlc/migrations/000009_payments.up.sql | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sqldb/sqlc/migrations/000009_payments.up.sql b/sqldb/sqlc/migrations/000009_payments.up.sql index c856db8f44..0d85b497b0 100644 --- a/sqldb/sqlc/migrations/000009_payments.up.sql +++ b/sqldb/sqlc/migrations/000009_payments.up.sql @@ -32,9 +32,13 @@ CREATE TABLE IF NOT EXISTS payment_intents ( ); -- Index for efficient querying by intent type -CREATE INDEX IF NOT EXISTS idx_payment_intents_type +CREATE INDEX IF NOT EXISTS idx_payment_intents_type ON payment_intents(intent_type); +-- Unique constraint for deduplication of payment intents +CREATE UNIQUE INDEX IF NOT EXISTS idx_payment_intents_unique +ON payment_intents(intent_type, intent_payload); + -- ───────────────────────────────────────────── -- Payments Table -- ───────────────────────────────────────────── @@ -187,7 +191,8 @@ CREATE TABLE IF NOT EXISTS payment_htlc_attempt_resolutions ( -- HTLC failure reason code htlc_fail_reason INTEGER, - -- Failure message from the failing node + -- Failure message from the failing node, this message is binary encoded + -- using the lightning wire protocol, see also lnwire/onion_error.go failure_msg BLOB, -- Ensure data integrity: settled attempts must have preimage, From c908b52758334d2ff3bc1964e518613f749975fc Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 12 Oct 2025 15:18:05 +0200 Subject: [PATCH 02/40] multi: add relevant queries for QueryPayments implemenation --- payments/db/sql_store.go | 19 + sqldb/sqlc/payments.sql.go | 655 ++++++++++++++++++++++++++++++++ sqldb/sqlc/querier.go | 13 + sqldb/sqlc/queries/payments.sql | 174 +++++++++ 4 files changed, 861 insertions(+) create mode 100644 sqldb/sqlc/payments.sql.go create mode 100644 sqldb/sqlc/queries/payments.sql diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 12585caf64..22c003361d 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1,14 +1,33 @@ package paymentsdb import ( + "context" "fmt" "github.com/lightningnetwork/lnd/sqldb" + "github.com/lightningnetwork/lnd/sqldb/sqlc" ) // SQLQueries is a subset of the sqlc.Querier interface that can be used to // execute queries against the SQL payments tables. type SQLQueries interface { + /* + Payment DB read operations. + */ + FilterPayments(ctx context.Context, query sqlc.FilterPaymentsParams) ([]sqlc.FilterPaymentsRow, error) + FetchPayment(ctx context.Context, paymentIdentifier []byte) (sqlc.FetchPaymentRow, error) + FetchPaymentsByIDs(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsRow, error) + + CountPayments(ctx context.Context) (int64, error) + + FetchHtlcAttemptsForPayment(ctx context.Context, paymentID int64) ([]sqlc.FetchHtlcAttemptsForPaymentRow, error) + FetchAllInflightAttempts(ctx context.Context) ([]sqlc.PaymentHtlcAttempt, error) + FetchHopsForAttempt(ctx context.Context, htlcAttemptIndex int64) ([]sqlc.FetchHopsForAttemptRow, error) + FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error) + + FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentID int64) ([]sqlc.PaymentFirstHopCustomRecord, error) + FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error) + FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error) } // BatchedSQLQueries is a version of the SQLQueries that's capable diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go new file mode 100644 index 0000000000..b925c78358 --- /dev/null +++ b/sqldb/sqlc/payments.sql.go @@ -0,0 +1,655 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: payments.sql + +package sqlc + +import ( + "context" + "database/sql" + "strings" + "time" +) + +const countPayments = `-- name: CountPayments :one +SELECT COUNT(*) FROM payments +` + +func (q *Queries) CountPayments(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countPayments) + var count int64 + err := row.Scan(&count) + return count, err +} + +const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key +FROM payment_htlc_attempts ha +WHERE NOT EXISTS ( + SELECT 1 FROM payment_htlc_attempt_resolutions hr + WHERE hr.attempt_index = ha.attempt_index +) +` + +// Fetch all inflight attempts across all payments +func (q *Queries) FetchAllInflightAttempts(ctx context.Context) ([]PaymentHtlcAttempt, error) { + rows, err := q.db.QueryContext(ctx, fetchAllInflightAttempts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHtlcAttempt + for rows.Next() { + var i PaymentHtlcAttempt + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopLevelCustomRecords = `-- name: FetchHopLevelCustomRecords :many +SELECT + l.id, + l.hop_id, + l.key, + l.value +FROM payment_hop_custom_records l +WHERE l.hop_id IN (/*SLICE:hop_ids*/?) +` + +func (q *Queries) FetchHopLevelCustomRecords(ctx context.Context, hopIds []int64) ([]PaymentHopCustomRecord, error) { + query := fetchHopLevelCustomRecords + var queryParams []interface{} + if len(hopIds) > 0 { + for _, v := range hopIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:hop_ids*/?", makeQueryParams(len(queryParams), len(hopIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:hop_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHopCustomRecord + for rows.Next() { + var i PaymentHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HopID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopsForAttempt = `-- name: FetchHopsForAttempt :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index = $1 +ORDER BY h.hop_index ASC +` + +type FetchHopsForAttemptRow struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte + MppPaymentAddr []byte + MppTotalMsat sql.NullInt64 + AmpRootShare []byte + AmpSetID []byte + AmpChildIndex sql.NullInt32 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) FetchHopsForAttempt(ctx context.Context, htlcAttemptIndex int64) ([]FetchHopsForAttemptRow, error) { + rows, err := q.db.QueryContext(ctx, fetchHopsForAttempt, htlcAttemptIndex) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHopsForAttemptRow + for rows.Next() { + var i FetchHopsForAttemptRow + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.HopIndex, + &i.PubKey, + &i.Scid, + &i.OutgoingTimeLock, + &i.AmtToForward, + &i.MetaData, + &i.MppPaymentAddr, + &i.MppTotalMsat, + &i.AmpRootShare, + &i.AmpSetID, + &i.AmpChildIndex, + &i.EncryptedData, + &i.BlindingPoint, + &i.BlindedPathTotalAmt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopsForAttempts = `-- name: FetchHopsForAttempts :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +` + +type FetchHopsForAttemptsRow struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte + MppPaymentAddr []byte + MppTotalMsat sql.NullInt64 + AmpRootShare []byte + AmpSetID []byte + AmpChildIndex sql.NullInt32 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]FetchHopsForAttemptsRow, error) { + query := fetchHopsForAttempts + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHopsForAttemptsRow + for rows.Next() { + var i FetchHopsForAttemptsRow + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.HopIndex, + &i.PubKey, + &i.Scid, + &i.OutgoingTimeLock, + &i.AmtToForward, + &i.MetaData, + &i.MppPaymentAddr, + &i.MppTotalMsat, + &i.AmpRootShare, + &i.AmpSetID, + &i.AmpChildIndex, + &i.EncryptedData, + &i.BlindingPoint, + &i.BlindedPathTotalAmt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHtlcAttemptsForPayment = `-- name: FetchHtlcAttemptsForPayment :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key, + hr.resolution_type, + hr.resolution_time, + hr.failure_source_index, + hr.htlc_fail_reason, + hr.failure_msg, + hr.settle_preimage +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id = $1 +ORDER BY ha.attempt_time ASC +` + +type FetchHtlcAttemptsForPaymentRow struct { + ID int64 + AttemptIndex int64 + PaymentID int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte + ResolutionType sql.NullInt32 + ResolutionTime sql.NullTime + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte + SettlePreimage []byte +} + +// This fetches all htlc attempts for a payment. +func (q *Queries) FetchHtlcAttemptsForPayment(ctx context.Context, paymentID int64) ([]FetchHtlcAttemptsForPaymentRow, error) { + rows, err := q.db.QueryContext(ctx, fetchHtlcAttemptsForPayment, paymentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHtlcAttemptsForPaymentRow + for rows.Next() { + var i FetchHtlcAttemptsForPaymentRow + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + &i.ResolutionType, + &i.ResolutionTime, + &i.FailureSourceIndex, + &i.HtlcFailReason, + &i.FailureMsg, + &i.SettlePreimage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPayment = `-- name: FetchPayment :one +SELECT + p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE p.payment_identifier = $1 +` + +type FetchPaymentRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) { + row := q.db.QueryRowContext(ctx, fetchPayment, paymentIdentifier) + var i FetchPaymentRow + err := row.Scan( + &i.Payment.ID, + &i.Payment.IntentID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ) + return i, err +} + +const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many +SELECT + l.id, + l.payment_id, + l.key, + l.value +FROM payment_first_hop_custom_records l +WHERE l.payment_id = $1 +` + +func (q *Queries) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentID int64) ([]PaymentFirstHopCustomRecord, error) { + rows, err := q.db.QueryContext(ctx, fetchPaymentLevelFirstHopCustomRecords, paymentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentFirstHopCustomRecord + for rows.Next() { + var i PaymentFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPaymentsByIDs = `-- name: FetchPaymentsByIDs :many +SELECT + p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE p.id IN (/*SLICE:payment_ids*/?) +` + +type FetchPaymentsByIDsRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) { + query := fetchPaymentsByIDs + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchPaymentsByIDsRow + for rows.Next() { + var i FetchPaymentsByIDsRow + if err := rows.Scan( + &i.Payment.ID, + &i.Payment.IntentID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many +SELECT + l.id, + l.htlc_attempt_index, + l.key, + l.value +FROM payment_attempt_first_hop_custom_records l +WHERE l.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +` + +func (q *Queries) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) { + query := fetchRouteLevelFirstHopCustomRecords + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentAttemptFirstHopCustomRecord + for rows.Next() { + var i PaymentAttemptFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const filterPayments = `-- name: FilterPayments :many +/* ───────────────────────────────────────────── + fetch queries + ───────────────────────────────────────────── +*/ + +SELECT + p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE ( + p.id > $1 OR + $1 IS NULL +) AND ( + p.id < $2 OR + $2 IS NULL +) AND ( + p.created_at >= $3 OR + $3 IS NULL +) AND ( + p.created_at <= $4 OR + $4 IS NULL +) AND ( + i.intent_type = $5 OR + $5 IS NULL OR i.intent_type IS NULL +) +ORDER BY + CASE WHEN $6 = false OR $6 IS NULL THEN p.id END ASC, + CASE WHEN $6 = true THEN p.id END DESC +LIMIT $7 +` + +type FilterPaymentsParams struct { + IndexOffsetGet sql.NullInt64 + IndexOffsetLet sql.NullInt64 + CreatedAfter sql.NullTime + CreatedBefore sql.NullTime + IntentType sql.NullInt16 + Reverse interface{} + NumLimit int32 +} + +type FilterPaymentsRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) { + rows, err := q.db.QueryContext(ctx, filterPayments, + arg.IndexOffsetGet, + arg.IndexOffsetLet, + arg.CreatedAfter, + arg.CreatedBefore, + arg.IntentType, + arg.Reverse, + arg.NumLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FilterPaymentsRow + for rows.Next() { + var i FilterPaymentsRow + if err := rows.Scan( + &i.Payment.ID, + &i.Payment.IntentID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 0087559be8..5024e32b63 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -14,6 +14,7 @@ type Querier interface { AddSourceNode(ctx context.Context, nodeID int64) error AddV1ChannelProof(ctx context.Context, arg AddV1ChannelProofParams) (sql.Result, error) ClearKVInvoiceHashIndex(ctx context.Context) error + CountPayments(ctx context.Context) (int64, error) CountZombieChannels(ctx context.Context, version int16) (int64, error) CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error) DeleteCanceledInvoices(ctx context.Context) (sql.Result, error) @@ -30,8 +31,20 @@ type Querier interface { DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, error) + // Fetch all inflight attempts across all payments + FetchAllInflightAttempts(ctx context.Context) ([]PaymentHtlcAttempt, error) + FetchHopLevelCustomRecords(ctx context.Context, hopIds []int64) ([]PaymentHopCustomRecord, error) + FetchHopsForAttempt(ctx context.Context, htlcAttemptIndex int64) ([]FetchHopsForAttemptRow, error) + FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]FetchHopsForAttemptsRow, error) + // This fetches all htlc attempts for a payment. + FetchHtlcAttemptsForPayment(ctx context.Context, paymentID int64) ([]FetchHtlcAttemptsForPaymentRow, error) + FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) + FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentID int64) ([]PaymentFirstHopCustomRecord, error) + FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) + FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) FetchSettledAMPSubInvoices(ctx context.Context, arg FetchSettledAMPSubInvoicesParams) ([]FetchSettledAMPSubInvoicesRow, error) FilterInvoices(ctx context.Context, arg FilterInvoicesParams) ([]Invoice, error) + FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) GetAMPInvoiceID(ctx context.Context, setID []byte) (int64, error) GetChannelAndNodesBySCID(ctx context.Context, arg GetChannelAndNodesBySCIDParams) (GetChannelAndNodesBySCIDRow, error) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetChannelByOutpointWithPoliciesParams) (GetChannelByOutpointWithPoliciesRow, error) diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql new file mode 100644 index 0000000000..034864e09a --- /dev/null +++ b/sqldb/sqlc/queries/payments.sql @@ -0,0 +1,174 @@ +/* ───────────────────────────────────────────── + fetch queries + ───────────────────────────────────────────── +*/ + +-- name: FilterPayments :many +SELECT + sqlc.embed(p), + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE ( + p.id > sqlc.narg('index_offset_get') OR + sqlc.narg('index_offset_get') IS NULL +) AND ( + p.id < sqlc.narg('index_offset_let') OR + sqlc.narg('index_offset_let') IS NULL +) AND ( + p.created_at >= sqlc.narg('created_after') OR + sqlc.narg('created_after') IS NULL +) AND ( + p.created_at <= sqlc.narg('created_before') OR + sqlc.narg('created_before') IS NULL +) AND ( + i.intent_type = sqlc.narg('intent_type') OR + sqlc.narg('intent_type') IS NULL OR i.intent_type IS NULL +) +ORDER BY + CASE WHEN sqlc.narg('reverse') = false OR sqlc.narg('reverse') IS NULL THEN p.id END ASC, + CASE WHEN sqlc.narg('reverse') = true THEN p.id END DESC +LIMIT @num_limit; + +-- name: FetchPayment :one +SELECT + sqlc.embed(p), + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE p.payment_identifier = $1; + +-- name: FetchPaymentsByIDs :many +SELECT + sqlc.embed(p), + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.id = p.intent_id +WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/); + +-- name: CountPayments :one +SELECT COUNT(*) FROM payments; + + +-- This fetches all htlc attempts for a payment. +-- name: FetchHtlcAttemptsForPayment :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key, + hr.resolution_type, + hr.resolution_time, + hr.failure_source_index, + hr.htlc_fail_reason, + hr.failure_msg, + hr.settle_preimage +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id = $1 +ORDER BY ha.attempt_time ASC; + +-- name: FetchAllInflightAttempts :many +-- Fetch all inflight attempts across all payments +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key +FROM payment_htlc_attempts ha +WHERE NOT EXISTS ( + SELECT 1 FROM payment_htlc_attempt_resolutions hr + WHERE hr.attempt_index = ha.attempt_index +); + +-- name: FetchHopsForAttempt :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index = $1 +ORDER BY h.hop_index ASC; + +-- name: FetchHopsForAttempts :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index IN (sqlc.slice('htlc_attempt_indices')/*SLICE:htlc_attempt_indices*/); + +-- name: FetchPaymentLevelFirstHopCustomRecords :many +SELECT + l.id, + l.payment_id, + l.key, + l.value +FROM payment_first_hop_custom_records l +WHERE l.payment_id = $1; + +-- name: FetchRouteLevelFirstHopCustomRecords :many +SELECT + l.id, + l.htlc_attempt_index, + l.key, + l.value +FROM payment_attempt_first_hop_custom_records l +WHERE l.htlc_attempt_index IN (sqlc.slice('htlc_attempt_indices')/*SLICE:htlc_attempt_indices*/); + +-- name: FetchHopLevelCustomRecords :many +SELECT + l.id, + l.hop_id, + l.key, + l.value +FROM payment_hop_custom_records l +WHERE l.hop_id IN (sqlc.slice('hop_ids')/*SLICE:hop_ids*/); + From 52d466f841d32af5ac5d34b87668805ddf3649b1 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 12 Oct 2025 19:15:26 +0200 Subject: [PATCH 03/40] paymentsdb: add new internal error --- payments/db/errors.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/payments/db/errors.go b/payments/db/errors.go index 40e37d95c6..424b63c7bd 100644 --- a/payments/db/errors.go +++ b/payments/db/errors.go @@ -130,4 +130,8 @@ var ( // NOTE: Only used for the kv backend. ErrNoSequenceNrIndex = errors.New("payment sequence number index " + "does not exist") + + // errMaxPaymentsReached is used internally to signal that the maximum + // number of payments has been reached during a paginated query. + errMaxPaymentsReached = errors.New("max payments reached") ) From 353160cda1049850c6953b12095436d55e36edd5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 12 Oct 2025 19:15:49 +0200 Subject: [PATCH 04/40] paymentsdb: implement QueryPayments for sql backend --- payments/db/sql_converters.go | 274 ++++++++++++++++++++++++ payments/db/sql_store.go | 390 ++++++++++++++++++++++++++++++++++ sqldb/sqlc/db_custom.go | 76 +++++++ 3 files changed, 740 insertions(+) create mode 100644 payments/db/sql_converters.go diff --git a/payments/db/sql_converters.go b/payments/db/sql_converters.go new file mode 100644 index 0000000000..4343dfd268 --- /dev/null +++ b/payments/db/sql_converters.go @@ -0,0 +1,274 @@ +package paymentsdb + +import ( + "bytes" + "fmt" + "sort" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/sqldb/sqlc" + "github.com/lightningnetwork/lnd/tlv" +) + +// dbPaymentToCreationInfo converts database payment data to +// PaymentCreationInfo. +func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64, + createdAt time.Time, intentPayload []byte, + firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo { + + // This is the payment hash for non-AMP payments and the SetID for AMP + // payments. + var identifier lntypes.Hash + copy(identifier[:], paymentIdentifier) + + return &PaymentCreationInfo{ + PaymentIdentifier: identifier, + Value: lnwire.MilliSatoshi(amountMsat), + CreationTime: createdAt.Local(), + PaymentRequest: intentPayload, + FirstHopCustomRecords: firstHopCustomRecords, + } +} + +// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt. +func dbAttemptToHTLCAttempt( + dbAttempt sqlc.FetchHtlcAttemptsForPaymentRow, + hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) ( + *HTLCAttempt, error) { + + // Convert route-level first hop custom records to CustomRecords map. + var firstHopWireCustomRecords lnwire.CustomRecords + if len(routeCustomRecords) > 0 { + firstHopWireCustomRecords = make(lnwire.CustomRecords) + for _, record := range routeCustomRecords { + firstHopWireCustomRecords[uint64(record.Key)] = + record.Value + } + } + + // Build the route from the database data. + route, err := dbDataToRoute( + hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat, + dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount, + dbAttempt.RouteSourceKey, firstHopWireCustomRecords, + ) + if err != nil { + return nil, fmt.Errorf("failed to convert to route: %w", + err) + } + + hash, err := lntypes.MakeHash(dbAttempt.PaymentHash) + if err != nil { + return nil, fmt.Errorf("failed to parse payment "+ + "hash: %w", err) + } + + // Create the attempt info. + var sessionKey [32]byte + copy(sessionKey[:], dbAttempt.SessionKey) + + info := HTLCAttemptInfo{ + AttemptID: uint64(dbAttempt.AttemptIndex), + sessionKey: sessionKey, + Route: *route, + AttemptTime: dbAttempt.AttemptTime, + Hash: &hash, + } + + attempt := &HTLCAttempt{ + HTLCAttemptInfo: info, + } + + // Add settlement info if present. + if dbAttempt.ResolutionType.Valid && + HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionSettled { + + var preimage lntypes.Preimage + copy(preimage[:], dbAttempt.SettlePreimage) + + attempt.Settle = &HTLCSettleInfo{ + Preimage: preimage, + SettleTime: dbAttempt.ResolutionTime.Time, + } + } + + // Add failure info if present. + if dbAttempt.ResolutionType.Valid && + HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionFailed { + + failure := &HTLCFailInfo{ + FailTime: dbAttempt.ResolutionTime.Time, + } + + if dbAttempt.HtlcFailReason.Valid { + failure.Reason = HTLCFailReason( + dbAttempt.HtlcFailReason.Int32, + ) + } + + if dbAttempt.FailureSourceIndex.Valid { + failure.FailureSourceIndex = uint32( + dbAttempt.FailureSourceIndex.Int32, + ) + } + + // Decode the failure message if present. + if len(dbAttempt.FailureMsg) > 0 { + msg, err := lnwire.DecodeFailureMessage( + bytes.NewReader(dbAttempt.FailureMsg), 0, + ) + if err != nil { + return nil, fmt.Errorf("failed to decode "+ + "failure message: %w", err) + } + failure.Message = msg + } + + attempt.Failure = failure + } + + return attempt, nil +} + +// dbDataToRoute converts database route data to a route.Route. +func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64, + sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) ( + *route.Route, error) { + + if len(hops) == 0 { + return nil, fmt.Errorf("no hops provided") + } + + // Sort hops by hop index. + sort.Slice(hops, func(i, j int) bool { + return hops[i].HopIndex < hops[j].HopIndex + }) + + routeHops := make([]*route.Hop, len(hops)) + + for i, hop := range hops { + pubKey, err := route.NewVertexFromBytes(hop.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to parse pub key: %w", + err) + } + + var channelID uint64 + if hop.Scid != "" { + // The SCID is stored as a string representation + // of the uint64. + var err error + channelID, err = strconv.ParseUint(hop.Scid, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "scid: %w", err) + } + } + + routeHop := &route.Hop{ + PubKeyBytes: pubKey, + ChannelID: channelID, + OutgoingTimeLock: uint32(hop.OutgoingTimeLock), + AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward), + } + + // Add MPP record if present. + if len(hop.MppPaymentAddr) > 0 { + var paymentAddr [32]byte + copy(paymentAddr[:], hop.MppPaymentAddr) + routeHop.MPP = record.NewMPP( + lnwire.MilliSatoshi(hop.MppTotalMsat.Int64), + paymentAddr, + ) + } + + // Add AMP record if present. + if len(hop.AmpRootShare) > 0 { + var rootShare [32]byte + copy(rootShare[:], hop.AmpRootShare) + var setID [32]byte + copy(setID[:], hop.AmpSetID) + + routeHop.AMP = record.NewAMP( + rootShare, setID, + uint32(hop.AmpChildIndex.Int32), + ) + } + + // Add blinding point if present (only for introduction node). + if len(hop.BlindingPoint) > 0 { + pubKey, err := btcec.ParsePubKey(hop.BlindingPoint) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "blinding point: %w", err) + } + routeHop.BlindingPoint = pubKey + } + + // Add encrypted data if present (for all blinded hops). + if len(hop.EncryptedData) > 0 { + routeHop.EncryptedData = hop.EncryptedData + } + + // Add total amount if present (only for final hop in blinded + // route). + if hop.BlindedPathTotalAmt.Valid { + routeHop.TotalAmtMsat = lnwire.MilliSatoshi( + hop.BlindedPathTotalAmt.Int64, + ) + } + + // Add hop-level custom records. + if records, ok := hopCustomRecords[hop.ID]; ok { + routeHop.CustomRecords = make( + record.CustomSet, + ) + for _, rec := range records { + routeHop.CustomRecords[uint64(rec.Key)] = + rec.Value + } + } + + // Add metadata if present. + if len(hop.MetaData) > 0 { + routeHop.Metadata = hop.MetaData + } + + routeHops[i] = routeHop + } + + // Parse the source node public key. + var sourceNode route.Vertex + copy(sourceNode[:], sourceKey) + + route := &route.Route{ + TotalTimeLock: uint32(totalTimeLock), + TotalAmount: lnwire.MilliSatoshi(totalAmount), + SourcePubKey: sourceNode, + Hops: routeHops, + FirstHopWireCustomRecords: firstHopWireCustomRecords, + } + + // Set the first hop amount if it is set. + if firstHopAmountMsat != 0 { + route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0]( + tlv.NewBigSizeT(lnwire.MilliSatoshi( + firstHopAmountMsat, + )), + ) + } + + return route, nil +} diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 22c003361d..0ab121e4f8 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -2,14 +2,40 @@ package paymentsdb import ( "context" + "errors" "fmt" + "math" + "time" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" ) +// PaymentIntentType represents the type of payment intent. +type PaymentIntentType int16 + +const ( + // PaymentIntentTypeBolt11 indicates a BOLT11 invoice payment. + PaymentIntentTypeBolt11 PaymentIntentType = 0 +) + +// HTLCAttemptResolutionType represents the type of HTLC attempt resolution. +type HTLCAttemptResolutionType int32 + +const ( + // HTLCAttemptResolutionSettled indicates the HTLC attempt was settled + // successfully with a preimage. + HTLCAttemptResolutionSettled HTLCAttemptResolutionType = 1 + + // HTLCAttemptResolutionFailed indicates the HTLC attempt failed. + HTLCAttemptResolutionFailed HTLCAttemptResolutionType = 2 +) + // SQLQueries is a subset of the sqlc.Querier interface that can be used to // execute queries against the SQL payments tables. +// +//nolint:ll,interfacebloat type SQLQueries interface { /* Payment DB read operations. @@ -84,3 +110,367 @@ func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries, // A compile-time constraint to ensure SQLStore implements DB. var _ DB = (*SQLStore)(nil) + +// fetchPaymentWithCompleteData fetches a payment with all its related data +// including attempts, hops, and custom records from the database. +func (s *SQLStore) fetchPaymentWithCompleteData(ctx context.Context, + db SQLQueries, dbPayment sqlc.PaymentAndIntent) (*MPPayment, error) { + + // The query will only return BOLT 11 payment intents or intents with + // no intent type set. + paymentIntent := dbPayment.GetPaymentIntent() + paymentRequest := paymentIntent.IntentPayload + + // Fetch payment-level first hop custom records. + payment := dbPayment.GetPayment() + customRecords, err := db.FetchPaymentLevelFirstHopCustomRecords( + ctx, payment.ID, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment level custom "+ + "records: %w", err) + } + + // Convert to the FirstHopCustomRecords map. + var firstHopCustomRecords lnwire.CustomRecords + if len(customRecords) > 0 { + firstHopCustomRecords = make(lnwire.CustomRecords) + for _, record := range customRecords { + firstHopCustomRecords[uint64(record.Key)] = record.Value + } + } + + // Convert the basic payment info. + info := dbPaymentToCreationInfo( + payment.PaymentIdentifier, payment.AmountMsat, + payment.CreatedAt, paymentRequest, firstHopCustomRecords, + ) + + // Fetch all HTLC attempts for this payment. + attempts, err := s.fetchHTLCAttemptsForPayment( + ctx, db, payment.ID, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC attempts: %w", + err) + } + + // Set the failure reason if present. + var failureReason *FailureReason + if payment.FailReason.Valid { + reason := FailureReason(payment.FailReason.Int32) + failureReason = &reason + } + + mpPayment := &MPPayment{ + SequenceNum: uint64(payment.ID), + Info: info, + HTLCs: attempts, + FailureReason: failureReason, + } + + // The status and state will be determined by calling + // SetState after construction. + if err := mpPayment.SetState(); err != nil { + return nil, fmt.Errorf("failed to set payment state: %w", err) + } + + return mpPayment, nil +} + +// fetchHTLCAttemptsForPayment fetches all HTLC attempts for a payment and +// uses ExecuteBatchQuery to efficiently fetch hops and custom records. +func (s *SQLStore) fetchHTLCAttemptsForPayment(ctx context.Context, + db SQLQueries, paymentID int64) ([]HTLCAttempt, error) { + + // Fetch all HTLC attempts for this payment. + dbAttempts, err := db.FetchHtlcAttemptsForPayment( + ctx, paymentID, + ) + + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC attempts: %w", + err) + } + + if len(dbAttempts) == 0 { + return nil, nil + } + + // Collect all attempt indices for batch fetching. + attemptIndices := make([]int64, len(dbAttempts)) + for i, attempt := range dbAttempts { + attemptIndices[i] = attempt.AttemptIndex + } + + // Fetch all hops for all attempts using ExecuteBatchQuery. + hopsByAttempt := make(map[int64][]sqlc.FetchHopsForAttemptsRow) + err = sqldb.ExecuteBatchQuery( + ctx, s.cfg.QueryCfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.FetchHopsForAttemptsRow, error) { + + return db.FetchHopsForAttempts(ctx, indices) + }, + func(ctx context.Context, + hop sqlc.FetchHopsForAttemptsRow) error { + + hopsByAttempt[hop.HtlcAttemptIndex] = append( + hopsByAttempt[hop.HtlcAttemptIndex], hop, + ) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch hops for attempts: %w", + err) + } + + // Collect all hop IDs for fetching hop-level custom records. + var hopIDs []int64 + for _, hops := range hopsByAttempt { + for _, hop := range hops { + hopIDs = append(hopIDs, hop.ID) + } + } + + // Fetch all hop-level custom records using ExecuteBatchQuery. + hopCustomRecords := make(map[int64][]sqlc.PaymentHopCustomRecord) + if len(hopIDs) > 0 { + err = sqldb.ExecuteBatchQuery( + ctx, s.cfg.QueryCfg, hopIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.PaymentHopCustomRecord, error) { + + return db.FetchHopLevelCustomRecords(ctx, ids) + }, + func(ctx context.Context, + record sqlc.PaymentHopCustomRecord) error { + + // TODO(ziggie): Can we get rid of this? + // This has to be in place otherwise the + // comparison will not match. + if record.Value == nil { + record.Value = []byte{} + } + + hopCustomRecords[record.HopID] = append( + hopCustomRecords[record.HopID], record, + ) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch hop custom "+ + "records: %w", err) + } + } + + // Fetch route-level first hop custom records using ExecuteBatchQuery. + routeCustomRecords := make( + map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord, + ) + err = sqldb.ExecuteBatchQuery( + ctx, s.cfg.QueryCfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.PaymentAttemptFirstHopCustomRecord, error) { + + return db.FetchRouteLevelFirstHopCustomRecords( + ctx, indices, + ) + }, + func(ctx context.Context, + record sqlc.PaymentAttemptFirstHopCustomRecord) error { + + routeCustomRecords[record.HtlcAttemptIndex] = append( + routeCustomRecords[record.HtlcAttemptIndex], + record, + ) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch route custom "+ + "records: %w", err) + } + + // Now convert all attempts to HTLCAttempt structs. + attempts := make([]HTLCAttempt, 0, len(dbAttempts)) + for _, dbAttempt := range dbAttempts { + attemptIndex := dbAttempt.AttemptIndex + attempt, err := dbAttemptToHTLCAttempt( + dbAttempt, hopsByAttempt[attemptIndex], + hopCustomRecords, + routeCustomRecords[attemptIndex], + ) + if err != nil { + return nil, fmt.Errorf("failed to convert attempt "+ + "%d: %w", attemptIndex, err) + } + attempts = append(attempts, *attempt) + } + + return attempts, nil +} + +// QueryPayments queries the payments from the database. +// +// This is part of the DB interface. +func (s *SQLStore) QueryPayments(ctx context.Context, + query Query) (Response, error) { + + if query.MaxPayments == 0 { + return Response{}, fmt.Errorf("max payments must be non-zero") + } + + var ( + allPayments []*MPPayment + totalCount int64 + initialCursor int64 + ) + + extractCursor := func( + row sqlc.FilterPaymentsRow) int64 { + + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + // We first count all payments to determine the total count + // if requested. + if query.CountTotal { + totalPayments, err := db.CountPayments(ctx) + if err != nil { + return fmt.Errorf("failed to count "+ + "payments: %w", err) + } + totalCount = totalPayments + } + + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow) error { + + // Fetch all the additional data for the payment. + mpPayment, err := s.fetchPaymentWithCompleteData( + ctx, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + + // To keep compatibility with the old API, we only + // return non-succeeded payments if requested. + if mpPayment.Status != StatusSucceeded && + !query.IncludeIncomplete { + + return nil + } + + if uint64(len(allPayments)) >= query.MaxPayments { + return errMaxPaymentsReached + } + + allPayments = append(allPayments, mpPayment) + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + Reverse: query.Reversed, + // For now there only BOLT 11 payment intents + // exist. + IntentType: sqldb.SQLInt16( + PaymentIntentTypeBolt11, + ), + } + + if query.Reversed { + filterParams.IndexOffsetLet = sqldb.SQLInt64( + lastID, + ) + } else { + filterParams.IndexOffsetGet = sqldb.SQLInt64( + lastID, + ) + } + + // Add potential date filters if specified. + if query.CreationDateStart != 0 { + filterParams.CreatedAfter = sqldb.SQLTime( + time.Unix(query.CreationDateStart, 0). + UTC(), + ) + } + if query.CreationDateEnd != 0 { + filterParams.CreatedBefore = sqldb.SQLTime( + time.Unix(query.CreationDateEnd, 0). + UTC(), + ) + } + + return db.FilterPayments(ctx, filterParams) + } + + if query.Reversed { + if query.IndexOffset == 0 { + initialCursor = int64(math.MaxInt64) + } else { + initialCursor = int64(query.IndexOffset) + } + } else { + initialCursor = int64(query.IndexOffset) + } + + return sqldb.ExecutePaginatedQuery( + ctx, s.cfg.QueryCfg, initialCursor, queryFunc, + extractCursor, processPayment, + ) + }, func() { + allPayments = nil + }) + + // We make sure we don't return an error if we reached the maximum + // number of payments. Which is the pagination limit for the query + // itself. + if err != nil && !errors.Is(err, errMaxPaymentsReached) { + return Response{}, fmt.Errorf("failed to query payments: %w", + err) + } + + // Handle case where no payments were found + if len(allPayments) == 0 { + return Response{ + Payments: allPayments, + FirstIndexOffset: 0, + LastIndexOffset: 0, + TotalCount: uint64(totalCount), + }, nil + } + + // If the query was reversed, we need to reverse the payment list + // to match the kvstore behavior and return payments in forward order. + if query.Reversed { + for i, j := 0, len(allPayments)-1; i < j; i, j = i+1, j-1 { + allPayments[i], allPayments[j] = allPayments[j], + allPayments[i] + } + } + + return Response{ + Payments: allPayments, + FirstIndexOffset: allPayments[0].SequenceNum, + LastIndexOffset: allPayments[len(allPayments)-1].SequenceNum, + TotalCount: uint64(totalCount), + }, nil +} diff --git a/sqldb/sqlc/db_custom.go b/sqldb/sqlc/db_custom.go index f7bc499185..db0a03a421 100644 --- a/sqldb/sqlc/db_custom.go +++ b/sqldb/sqlc/db_custom.go @@ -161,3 +161,79 @@ func (r GetChannelsBySCIDRangeRow) Node1Pub() []byte { func (r GetChannelsBySCIDRangeRow) Node2Pub() []byte { return r.Node2PubKey } + +// PaymentAndIntent is an interface that provides access to a payment and its +// associated payment intent. +type PaymentAndIntent interface { + // GetPayment returns the Payment associated with this interface. + GetPayment() Payment + + // GetPaymentIntent returns the PaymentIntent associated with this payment. + GetPaymentIntent() PaymentIntent +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentsByIDsRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentsByIDsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} From fc4ce67a2a38273aaf71706fcf09d13ad664a364 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 14:53:07 +0200 Subject: [PATCH 05/40] paymentsdb: implement FetchPayment for sql backend --- payments/db/sql_store.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 0ab121e4f8..0b406d2031 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -2,11 +2,13 @@ package paymentsdb import ( "context" + "database/sql" "errors" "fmt" "math" "time" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" @@ -474,3 +476,40 @@ func (s *SQLStore) QueryPayments(ctx context.Context, TotalCount: uint64(totalCount), }, nil } + +// FetchPayment fetches the payment corresponding to the given payment +// hash. +// +// This is part of the DB interface. +func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) { + ctx := context.TODO() + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + dbPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("failed to fetch payment: %w", err) + } + + if errors.Is(err, sql.ErrNoRows) { + return ErrPaymentNotInitiated + } + + mpPayment, err = s.fetchPaymentWithCompleteData( + ctx, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment: %w", err) + } + + return mpPayment, nil +} From 0849296aa4b1c7f49ac629a673e3b2092642f1f0 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 12 Oct 2025 19:35:55 +0200 Subject: [PATCH 06/40] docs: add release-notes --- docs/release-notes/release-notes-0.21.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index bb02b9c276..8f5e4cb69f 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -54,6 +54,8 @@ refacotor the payment related LND code to make it more modular. * Implement the SQL backend for the [payments database](https://github.com/lightningnetwork/lnd/pull/9147) + * Implement QueryPayments for the [payments db + SQL Backend](https://github.com/lightningnetwork/lnd/pull/10287) ## Code Health From ed5622ca3b1639f1dc1cb176aa4c586d39417db0 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 16:46:55 +0200 Subject: [PATCH 07/40] sqldb: add queries for deleting a payment and attempts --- payments/db/sql_store.go | 10 ++++++++++ sqldb/sqlc/payments.sql.go | 20 ++++++++++++++++++++ sqldb/sqlc/querier.go | 2 ++ sqldb/sqlc/queries/payments.sql | 8 ++++++++ 4 files changed, 40 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 0b406d2031..b2ca646e3b 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -56,6 +56,16 @@ type SQLQueries interface { FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentID int64) ([]sqlc.PaymentFirstHopCustomRecord, error) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error) FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error) + + /* + Payment DB write operations. + */ + + DeletePayment(ctx context.Context, paymentID int64) error + + // DeleteFailedAttempts removes all failed HTLCs from the db for a + // given payment. + DeleteFailedAttempts(ctx context.Context, paymentID int64) error } // BatchedSQLQueries is a version of the SQLQueries that's capable diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index b925c78358..635360d3f7 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -23,6 +23,26 @@ func (q *Queries) CountPayments(ctx context.Context) (int64, error) { return count, err } +const deleteFailedAttempts = `-- name: DeleteFailedAttempts :exec +DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( + SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 +) +` + +func (q *Queries) DeleteFailedAttempts(ctx context.Context, paymentID int64) error { + _, err := q.db.ExecContext(ctx, deleteFailedAttempts, paymentID) + return err +} + +const deletePayment = `-- name: DeletePayment :exec +DELETE FROM payments WHERE id = $1 +` + +func (q *Queries) DeletePayment(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deletePayment, id) + return err +} + const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many SELECT ha.id, diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 5024e32b63..b1490f09af 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -21,11 +21,13 @@ type Querier interface { DeleteChannelPolicyExtraTypes(ctx context.Context, channelPolicyID int64) error DeleteChannels(ctx context.Context, ids []int64) error DeleteExtraNodeType(ctx context.Context, arg DeleteExtraNodeTypeParams) error + DeleteFailedAttempts(ctx context.Context, paymentID int64) error DeleteInvoice(ctx context.Context, arg DeleteInvoiceParams) (sql.Result, error) DeleteNode(ctx context.Context, id int64) error DeleteNodeAddresses(ctx context.Context, nodeID int64) error DeleteNodeByPubKey(ctx context.Context, arg DeleteNodeByPubKeyParams) (sql.Result, error) DeleteNodeFeature(ctx context.Context, arg DeleteNodeFeatureParams) error + DeletePayment(ctx context.Context, id int64) error DeletePruneLogEntriesInRange(ctx context.Context, arg DeletePruneLogEntriesInRangeParams) error DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index 034864e09a..ee4834f9da 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -172,3 +172,11 @@ SELECT FROM payment_hop_custom_records l WHERE l.hop_id IN (sqlc.slice('hop_ids')/*SLICE:hop_ids*/); + +-- name: DeletePayment :exec +DELETE FROM payments WHERE id = $1; + +-- name: DeleteFailedAttempts :exec +DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( + SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 +); From 215490cc222f4aabfe684a003cf6053ce0f05997 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 16:48:34 +0200 Subject: [PATCH 08/40] paymentsdb: implement DeletePayment for sql backend --- payments/db/sql_store.go | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index b2ca646e3b..ca792c9f91 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -523,3 +523,49 @@ func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) { return mpPayment, nil } + +// DeletePayment deletes a payment from the DB given its payment hash. If +// failedHtlcsOnly is set, only failed HTLC attempts of the payment will be +// deleted. +func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash, + failedHtlcsOnly bool) error { + + ctx := context.TODO() + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + fetchPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + completePayment, err := s.fetchPaymentWithCompleteData( + ctx, db, fetchPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + if err := completePayment.Status.removable(); err != nil { + return fmt.Errorf("payment %v cannot be deleted: %w", + paymentHash, err) + } + + // If we are only deleting failed HTLCs, we delete them. + if failedHtlcsOnly { + return s.db.DeleteFailedAttempts( + ctx, fetchPayment.Payment.ID, + ) + } + + return db.DeletePayment(ctx, fetchPayment.Payment.ID) + + }, func() { + }) + if err != nil { + return fmt.Errorf("failed to delete payment "+ + "(failedHtlcsOnly: %v, paymentHash: %v): %w", + failedHtlcsOnly, paymentHash, err) + } + + return nil +} From 3a39bdcea995328545fb4634a5599177fe35e532 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 16:49:15 +0200 Subject: [PATCH 09/40] paymentsdb: implement DeleteFailedAttempts for sql backend --- payments/db/sql_store.go | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index ca792c9f91..5a51350bfa 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -552,11 +552,13 @@ func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash, // If we are only deleting failed HTLCs, we delete them. if failedHtlcsOnly { - return s.db.DeleteFailedAttempts( + return db.DeleteFailedAttempts( ctx, fetchPayment.Payment.ID, ) } + // Be careful to not use s.db here, because we are in a + // transaction, is there a way to make this more secure? return db.DeletePayment(ctx, fetchPayment.Payment.ID) }, func() { @@ -569,3 +571,47 @@ func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash, return nil } + +// DeleteFailedAttempts removes all failed HTLCs from the db. It should +// be called for a given payment whenever all inflight htlcs are +// completed, and the payment has reached a final terminal state. +func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error { + // In case we are configured to keep failed payment attempts, we exit + // early. + if s.keepFailedPaymentAttempts { + return nil + } + + ctx := context.TODO() + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // We first fetch the payment to get the payment ID. + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + + completePayment, err := s.fetchPaymentWithCompleteData( + ctx, db, payment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + if err := completePayment.Status.removable(); err != nil { + return fmt.Errorf("payment %v cannot be deleted: %w", + paymentHash, err) + } + + // Then we delete the failed attempts for this payment. + return db.DeleteFailedAttempts(ctx, payment.Payment.ID) + }, func() { + }) + if err != nil { + return fmt.Errorf("failed to delete failed attempts for "+ + "payment %v: %w", paymentHash, err) + } + + return nil +} From 1aae195fdf71240f5058f19d36d70487d35509af Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:02:21 +0200 Subject: [PATCH 10/40] sqldb+paymentsdb: add queries to insert all relavant data In this commit we add all queries which we will need to insert payment related data into the db. --- payments/db/sql_store.go | 14 ++ sqldb/sqlc/payments.sql.go | 393 ++++++++++++++++++++++++++++++++ sqldb/sqlc/querier.go | 14 ++ sqldb/sqlc/queries/payments.sql | 182 +++++++++++++++ 4 files changed, 603 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 5a51350bfa..48fbd8534d 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -60,6 +60,20 @@ type SQLQueries interface { /* Payment DB write operations. */ + InsertPaymentIntent(ctx context.Context, arg sqlc.InsertPaymentIntentParams) (int64, error) + InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) error + InsertPaymentFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentFirstHopCustomRecordParams) error + + InsertHtlcAttempt(ctx context.Context, arg sqlc.InsertHtlcAttemptParams) (int64, error) + InsertRouteHop(ctx context.Context, arg sqlc.InsertRouteHopParams) (int64, error) + InsertRouteHopMpp(ctx context.Context, arg sqlc.InsertRouteHopMppParams) error + InsertRouteHopAmp(ctx context.Context, arg sqlc.InsertRouteHopAmpParams) error + InsertRouteHopBlinded(ctx context.Context, arg sqlc.InsertRouteHopBlindedParams) error + + InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentAttemptFirstHopCustomRecordParams) error + InsertPaymentHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentHopCustomRecordParams) error + + SettleAttempt(ctx context.Context, arg sqlc.SettleAttemptParams) error DeletePayment(ctx context.Context, paymentID int64) error diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index 635360d3f7..4c8b225ab3 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -43,6 +43,46 @@ func (q *Queries) DeletePayment(ctx context.Context, id int64) error { return err } +const failAttempt = `-- name: FailAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + failure_source_index, + htlc_fail_reason, + failure_msg +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) +` + +type FailAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte +} + +func (q *Queries) FailAttempt(ctx context.Context, arg FailAttemptParams) error { + _, err := q.db.ExecContext(ctx, failAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.FailureSourceIndex, + arg.HtlcFailReason, + arg.FailureMsg, + ) + return err +} + const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many SELECT ha.id, @@ -673,3 +713,356 @@ func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams) } return items, nil } + +const insertHtlcAttempt = `-- name: InsertHtlcAttempt :one +INSERT INTO payment_htlc_attempts ( + payment_id, + attempt_index, + session_key, + attempt_time, + payment_hash, + first_hop_amount_msat, + route_total_time_lock, + route_total_amount, + route_source_key) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9) +RETURNING id +` + +type InsertHtlcAttemptParams struct { + PaymentID int64 + AttemptIndex int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte +} + +func (q *Queries) InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertHtlcAttempt, + arg.PaymentID, + arg.AttemptIndex, + arg.SessionKey, + arg.AttemptTime, + arg.PaymentHash, + arg.FirstHopAmountMsat, + arg.RouteTotalTimeLock, + arg.RouteTotalAmount, + arg.RouteSourceKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPayment = `-- name: InsertPayment :exec +INSERT INTO payments ( + intent_id, + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + $4, + NULL +) +` + +type InsertPaymentParams struct { + IntentID sql.NullInt64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte +} + +// Insert a new payment with the given intent ID and return its ID. +func (q *Queries) InsertPayment(ctx context.Context, arg InsertPaymentParams) error { + _, err := q.db.ExecContext(ctx, insertPayment, + arg.IntentID, + arg.AmountMsat, + arg.CreatedAt, + arg.PaymentIdentifier, + ) + return err +} + +const insertPaymentAttemptFirstHopCustomRecord = `-- name: InsertPaymentAttemptFirstHopCustomRecord :exec +INSERT INTO payment_attempt_first_hop_custom_records ( + htlc_attempt_index, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentAttemptFirstHopCustomRecordParams struct { + HtlcAttemptIndex int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentAttemptFirstHopCustomRecord, arg.HtlcAttemptIndex, arg.Key, arg.Value) + return err +} + +const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec +INSERT INTO payment_first_hop_custom_records ( + payment_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentFirstHopCustomRecordParams struct { + PaymentID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentFirstHopCustomRecord, arg.PaymentID, arg.Key, arg.Value) + return err +} + +const insertPaymentHopCustomRecord = `-- name: InsertPaymentHopCustomRecord :exec +INSERT INTO payment_hop_custom_records ( + hop_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentHopCustomRecordParams struct { + HopID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentHopCustomRecord, arg.HopID, arg.Key, arg.Value) + return err +} + +const insertPaymentIntent = `-- name: InsertPaymentIntent :one +INSERT INTO payment_intents ( + intent_type, + intent_payload) +VALUES ( + $1, + $2 +) +ON CONFLICT (intent_type, intent_payload) DO UPDATE SET + intent_type = EXCLUDED.intent_type, + intent_payload = EXCLUDED.intent_payload +RETURNING id +` + +type InsertPaymentIntentParams struct { + IntentType int16 + IntentPayload []byte +} + +// Insert a new payment intent and return its ID. +func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentIntent, arg.IntentType, arg.IntentPayload) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHop = `-- name: InsertRouteHop :one +INSERT INTO payment_route_hops ( + htlc_attempt_index, + hop_index, + pub_key, + scid, + outgoing_time_lock, + amt_to_forward, + meta_data +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id +` + +type InsertRouteHopParams struct { + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte +} + +func (q *Queries) InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertRouteHop, + arg.HtlcAttemptIndex, + arg.HopIndex, + arg.PubKey, + arg.Scid, + arg.OutgoingTimeLock, + arg.AmtToForward, + arg.MetaData, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHopAmp = `-- name: InsertRouteHopAmp :exec +INSERT INTO payment_route_hop_amp ( + hop_id, + root_share, + set_id, + child_index +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopAmpParams struct { + HopID int64 + RootShare []byte + SetID []byte + ChildIndex int32 +} + +func (q *Queries) InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopAmp, + arg.HopID, + arg.RootShare, + arg.SetID, + arg.ChildIndex, + ) + return err +} + +const insertRouteHopBlinded = `-- name: InsertRouteHopBlinded :exec +INSERT INTO payment_route_hop_blinded ( + hop_id, + encrypted_data, + blinding_point, + blinded_path_total_amt +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopBlindedParams struct { + HopID int64 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopBlinded, + arg.HopID, + arg.EncryptedData, + arg.BlindingPoint, + arg.BlindedPathTotalAmt, + ) + return err +} + +const insertRouteHopMpp = `-- name: InsertRouteHopMpp :exec +INSERT INTO payment_route_hop_mpp ( + hop_id, + payment_addr, + total_msat +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertRouteHopMppParams struct { + HopID int64 + PaymentAddr []byte + TotalMsat int64 +} + +func (q *Queries) InsertRouteHopMpp(ctx context.Context, arg InsertRouteHopMppParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopMpp, arg.HopID, arg.PaymentAddr, arg.TotalMsat) + return err +} + +const settleAttempt = `-- name: SettleAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + settle_preimage +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type SettleAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + SettlePreimage []byte +} + +func (q *Queries) SettleAttempt(ctx context.Context, arg SettleAttemptParams) error { + _, err := q.db.ExecContext(ctx, settleAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.SettlePreimage, + ) + return err +} diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index b1490f09af..6e5b4a4fd1 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -31,6 +31,7 @@ type Querier interface { DeletePruneLogEntriesInRange(ctx context.Context, arg DeletePruneLogEntriesInRangeParams) error DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) + FailAttempt(ctx context.Context, arg FailAttemptParams) error FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, error) // Fetch all inflight attempts across all payments @@ -116,6 +117,7 @@ type Querier interface { // UpsertEdgePolicy query is used because of the constraint in that query that // requires a policy update to have a newer last_update than the existing one). InsertEdgePolicyMig(ctx context.Context, arg InsertEdgePolicyMigParams) (int64, error) + InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptParams) (int64, error) InsertInvoice(ctx context.Context, arg InsertInvoiceParams) (int64, error) InsertInvoiceFeature(ctx context.Context, arg InsertInvoiceFeatureParams) error InsertInvoiceHTLC(ctx context.Context, arg InsertInvoiceHTLCParams) (int64, error) @@ -129,6 +131,17 @@ type Querier interface { // is used because of the constraint in that query that requires a node update // to have a newer last_update than the existing node). InsertNodeMig(ctx context.Context, arg InsertNodeMigParams) (int64, error) + // Insert a new payment with the given intent ID and return its ID. + InsertPayment(ctx context.Context, arg InsertPaymentParams) error + InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error + InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error + InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error + // Insert a new payment intent and return its ID. + InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) + InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) + InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error + InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error + InsertRouteHopMpp(ctx context.Context, arg InsertRouteHopMppParams) error IsClosedChannel(ctx context.Context, scid []byte) (bool, error) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error) @@ -148,6 +161,7 @@ type Querier interface { OnInvoiceSettled(ctx context.Context, arg OnInvoiceSettledParams) error SetKVInvoicePaymentHash(ctx context.Context, arg SetKVInvoicePaymentHashParams) error SetMigration(ctx context.Context, arg SetMigrationParams) error + SettleAttempt(ctx context.Context, arg SettleAttemptParams) error UpdateAMPSubInvoiceHTLCPreimage(ctx context.Context, arg UpdateAMPSubInvoiceHTLCPreimageParams) (sql.Result, error) UpdateAMPSubInvoiceState(ctx context.Context, arg UpdateAMPSubInvoiceStateParams) error UpdateInvoiceAmountPaid(ctx context.Context, arg UpdateInvoiceAmountPaidParams) (sql.Result, error) diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index ee4834f9da..c9036acf14 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -180,3 +180,185 @@ DELETE FROM payments WHERE id = $1; DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 ); + +-- name: InsertPaymentIntent :one +-- Insert a new payment intent and return its ID. +INSERT INTO payment_intents ( + intent_type, + intent_payload) +VALUES ( + @intent_type, + @intent_payload +) +ON CONFLICT (intent_type, intent_payload) DO UPDATE SET + intent_type = EXCLUDED.intent_type, + intent_payload = EXCLUDED.intent_payload +RETURNING id; + +-- name: InsertPayment :exec +-- Insert a new payment with the given intent ID and return its ID. +INSERT INTO payments ( + intent_id, + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + @intent_id, + @amount_msat, + @created_at, + @payment_identifier, + NULL +); + +-- name: InsertPaymentFirstHopCustomRecord :exec +INSERT INTO payment_first_hop_custom_records ( + payment_id, + key, + value +) +VALUES ( + @payment_id, + @key, + @value +); + +-- name: InsertHtlcAttempt :one +INSERT INTO payment_htlc_attempts ( + payment_id, + attempt_index, + session_key, + attempt_time, + payment_hash, + first_hop_amount_msat, + route_total_time_lock, + route_total_amount, + route_source_key) +VALUES ( + @payment_id, + @attempt_index, + @session_key, + @attempt_time, + @payment_hash, + @first_hop_amount_msat, + @route_total_time_lock, + @route_total_amount, + @route_source_key) +RETURNING id; + +-- name: InsertPaymentAttemptFirstHopCustomRecord :exec +INSERT INTO payment_attempt_first_hop_custom_records ( + htlc_attempt_index, + key, + value +) +VALUES ( + @htlc_attempt_index, + @key, + @value +); + +-- name: InsertRouteHop :one +INSERT INTO payment_route_hops ( + htlc_attempt_index, + hop_index, + pub_key, + scid, + outgoing_time_lock, + amt_to_forward, + meta_data +) +VALUES ( + @htlc_attempt_index, + @hop_index, + @pub_key, + @scid, + @outgoing_time_lock, + @amt_to_forward, + @meta_data +) +RETURNING id; + +-- name: InsertRouteHopMpp :exec +INSERT INTO payment_route_hop_mpp ( + hop_id, + payment_addr, + total_msat +) +VALUES ( + @hop_id, + @payment_addr, + @total_msat +); + +-- name: InsertRouteHopAmp :exec +INSERT INTO payment_route_hop_amp ( + hop_id, + root_share, + set_id, + child_index +) +VALUES ( + @hop_id, + @root_share, + @set_id, + @child_index +); + +-- name: InsertRouteHopBlinded :exec +INSERT INTO payment_route_hop_blinded ( + hop_id, + encrypted_data, + blinding_point, + blinded_path_total_amt +) +VALUES ( + @hop_id, + @encrypted_data, + @blinding_point, + @blinded_path_total_amt +); + +-- name: InsertPaymentHopCustomRecord :exec +INSERT INTO payment_hop_custom_records ( + hop_id, + key, + value +) +VALUES ( + @hop_id, + @key, + @value +); + +-- name: SettleAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + settle_preimage +) +VALUES ( + @attempt_index, + @resolution_time, + @resolution_type, + @settle_preimage +); + +-- name: FailAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + failure_source_index, + htlc_fail_reason, + failure_msg +) +VALUES ( + @attempt_index, + @resolution_time, + @resolution_type, + @failure_source_index, + @htlc_fail_reason, + @failure_msg +); From f6f2ee32162036bd1fcec81b4fe4b8491fc520be Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:06:05 +0200 Subject: [PATCH 11/40] paymentsdb: implement InitPayment for sql backend --- payments/db/sql_store.go | 107 +++++++++++++++++++++++++++++++- sqldb/sqlc/payments.sql.go | 11 ++-- sqldb/sqlc/querier.go | 2 +- sqldb/sqlc/queries/payments.sql | 5 +- 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 48fbd8534d..1a7ce127a5 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -61,7 +61,7 @@ type SQLQueries interface { Payment DB write operations. */ InsertPaymentIntent(ctx context.Context, arg sqlc.InsertPaymentIntentParams) (int64, error) - InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) error + InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) (int64, error) InsertPaymentFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentFirstHopCustomRecordParams) error InsertHtlcAttempt(ctx context.Context, arg sqlc.InsertHtlcAttemptParams) (int64, error) @@ -629,3 +629,108 @@ func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error { return nil } + +// InitPayment initializes a payment. +// +// This is part of the DB interface. +func (s *SQLStore) InitPayment(paymentHash lntypes.Hash, + paymentCreationInfo *PaymentCreationInfo) error { + + ctx := context.TODO() + + // Create the payment in the database. + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + existingPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err == nil { + completePayment, err := s.fetchPaymentWithCompleteData( + ctx, db, existingPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + + // Check if the payment is initializable otherwise + // we'll return early. + err = completePayment.Status.initializable() + if err != nil { + return err + } + } else if !errors.Is(err, sql.ErrNoRows) { + // Some other error occurred + return fmt.Errorf("failed to check existing "+ + "payment: %w", err) + } + + // If payment exists and is failed, delete it first. + if existingPayment.Payment.ID != 0 { + err := db.DeletePayment(ctx, existingPayment.Payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + } + + var intentID *int64 + if len(paymentCreationInfo.PaymentRequest) > 0 { + intentIDValue, err := db.InsertPaymentIntent(ctx, + sqlc.InsertPaymentIntentParams{ + IntentType: int16( + PaymentIntentTypeBolt11, + ), + IntentPayload: paymentCreationInfo. + PaymentRequest, + }) + if err != nil { + return fmt.Errorf("failed to initialize "+ + "payment intent: %w", err) + } + intentID = &intentIDValue + } + + // Only set the intent ID if it's not nil. + var intentIDParam sql.NullInt64 + if intentID != nil { + intentIDParam = sqldb.SQLInt64(*intentID) + } + + paymentID, err := db.InsertPayment(ctx, + sqlc.InsertPaymentParams{ + IntentID: intentIDParam, + AmountMsat: int64( + paymentCreationInfo.Value, + ), + CreatedAt: paymentCreationInfo. + CreationTime.UTC(), + PaymentIdentifier: paymentHash[:], + }) + if err != nil { + return fmt.Errorf("failed to insert payment: %w", err) + } + + firstHopCustomRecords := paymentCreationInfo. + FirstHopCustomRecords + + for key, value := range firstHopCustomRecords { + err = db.InsertPaymentFirstHopCustomRecord(ctx, + sqlc.InsertPaymentFirstHopCustomRecordParams{ + PaymentID: paymentID, + Key: int64(key), + Value: value, + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment first hop custom "+ + "record: %w", err) + } + } + + return nil + }, func() { + }) + if err != nil { + return fmt.Errorf("failed to initialize payment: %w", err) + } + + return nil +} diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index 4c8b225ab3..cc287febb2 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -767,7 +767,7 @@ func (q *Queries) InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptPa return id, err } -const insertPayment = `-- name: InsertPayment :exec +const insertPayment = `-- name: InsertPayment :one INSERT INTO payments ( intent_id, amount_msat, @@ -781,6 +781,7 @@ VALUES ( $4, NULL ) +RETURNING id ` type InsertPaymentParams struct { @@ -791,14 +792,16 @@ type InsertPaymentParams struct { } // Insert a new payment with the given intent ID and return its ID. -func (q *Queries) InsertPayment(ctx context.Context, arg InsertPaymentParams) error { - _, err := q.db.ExecContext(ctx, insertPayment, +func (q *Queries) InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPayment, arg.IntentID, arg.AmountMsat, arg.CreatedAt, arg.PaymentIdentifier, ) - return err + var id int64 + err := row.Scan(&id) + return id, err } const insertPaymentAttemptFirstHopCustomRecord = `-- name: InsertPaymentAttemptFirstHopCustomRecord :exec diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 6e5b4a4fd1..4d3707bb06 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -132,7 +132,7 @@ type Querier interface { // to have a newer last_update than the existing node). InsertNodeMig(ctx context.Context, arg InsertNodeMigParams) (int64, error) // Insert a new payment with the given intent ID and return its ID. - InsertPayment(ctx context.Context, arg InsertPaymentParams) error + InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index c9036acf14..2b5316a92a 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -195,7 +195,7 @@ ON CONFLICT (intent_type, intent_payload) DO UPDATE SET intent_payload = EXCLUDED.intent_payload RETURNING id; --- name: InsertPayment :exec +-- name: InsertPayment :one -- Insert a new payment with the given intent ID and return its ID. INSERT INTO payments ( intent_id, @@ -209,7 +209,8 @@ VALUES ( @created_at, @payment_identifier, NULL -); +) +RETURNING id; -- name: InsertPaymentFirstHopCustomRecord :exec INSERT INTO payment_first_hop_custom_records ( From 53b47eaccd668799e335dcec8cf68411a5126520 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:08:03 +0200 Subject: [PATCH 12/40] paymentsdb: implement RegisterAttempt for sql backend --- payments/db/sql_store.go | 202 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 1a7ce127a5..7ec4554051 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -6,10 +6,12 @@ import ( "errors" "fmt" "math" + "strconv" "time" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" ) @@ -734,3 +736,203 @@ func (s *SQLStore) InitPayment(paymentHash lntypes.Hash, return nil } + +// insertRouteHops inserts all route hop data for a given set of hops. +func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, + hops []*route.Hop, attemptID uint64) error { + + for i, hop := range hops { + // Insert the basic route hop data and get the generated ID + hopID, err := db.InsertRouteHop(ctx, sqlc.InsertRouteHopParams{ + HtlcAttemptIndex: int64(attemptID), + HopIndex: int32(i), + PubKey: hop.PubKeyBytes[:], + Scid: strconv.FormatUint( + hop.ChannelID, 10, + ), + OutgoingTimeLock: int32(hop.OutgoingTimeLock), + AmtToForward: int64(hop.AmtToForward), + MetaData: hop.Metadata, + }) + if err != nil { + return fmt.Errorf("failed to insert route hop: %w", err) + } + + // Insert the per-hop custom records + if len(hop.CustomRecords) > 0 { + for key, value := range hop.CustomRecords { + err = db.InsertPaymentHopCustomRecord(ctx, sqlc.InsertPaymentHopCustomRecordParams{ + HopID: hopID, + Key: int64(key), + Value: value, + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment hop custom record: %w", err) + } + } + } + + // Insert MPP data if present + if hop.MPP != nil { + paymentAddr := hop.MPP.PaymentAddr() + err = db.InsertRouteHopMpp(ctx, sqlc.InsertRouteHopMppParams{ + HopID: hopID, + PaymentAddr: paymentAddr[:], + TotalMsat: int64(hop.MPP.TotalMsat()), + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop MPP: %w", err) + } + } + + // Insert AMP data if present + if hop.AMP != nil { + rootShare := hop.AMP.RootShare() + setID := hop.AMP.SetID() + err = db.InsertRouteHopAmp(ctx, sqlc.InsertRouteHopAmpParams{ + HopID: hopID, + RootShare: rootShare[:], + SetID: setID[:], + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop AMP: %w", err) + } + } + + // Insert blinded route data if present + if hop.EncryptedData != nil || hop.BlindingPoint != nil { + var blindingPointBytes []byte + if hop.BlindingPoint != nil { + blindingPointBytes = hop.BlindingPoint. + SerializeCompressed() + } + + err = db.InsertRouteHopBlinded(ctx, + sqlc.InsertRouteHopBlindedParams{ + HopID: hopID, + EncryptedData: hop.EncryptedData, + BlindingPoint: blindingPointBytes, + BlindedPathTotalAmt: sqldb.SQLInt64( + hop.TotalAmtMsat, + ), + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop blinded: %w", err) + } + } + } + + return nil +} + +// RegisterAttempt registers an attempt for a payment. +// +// This is part of the DB interface. +func (s *SQLStore) RegisterAttempt(paymentHash lntypes.Hash, + attempt *HTLCAttemptInfo) (*MPPayment, error) { + + ctx := context.TODO() + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // 1. First Fetch the payment and check if it is registrable. + existingPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + + mpPayment, err = s.fetchPaymentWithCompleteData( + ctx, db, existingPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + if err := mpPayment.Registrable(); err != nil { + return fmt.Errorf("htlc attempt not registrable: %w", + err) + } + + // Verify the attempt is compatible with the existing payment. + if err := verifyAttempt(mpPayment, attempt); err != nil { + return fmt.Errorf("failed to verify attempt: %w", err) + } + + // Fist register the plain HTLC attempt. + // Prepare the session key. + sessionKey := attempt.SessionKey() + sessionKeyBytes := sessionKey.Serialize() + + _, err = db.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{ + PaymentID: existingPayment.Payment.ID, + AttemptIndex: int64(attempt.AttemptID), + SessionKey: sessionKeyBytes, + AttemptTime: attempt.AttemptTime, + PaymentHash: paymentHash[:], + FirstHopAmountMsat: int64( + attempt.Route.FirstHopAmount.Val.Int(), + ), + RouteTotalTimeLock: int32(attempt.Route.TotalTimeLock), + RouteTotalAmount: int64(attempt.Route.TotalAmount), + RouteSourceKey: attempt.Route.SourcePubKey[:], + }) + if err != nil { + return fmt.Errorf("failed to insert HTLC "+ + "attempt: %w", err) + } + + // Insert the route level first hop custom records. + attemptFirstHopCustomRecords := attempt.Route. + FirstHopWireCustomRecords + + for key, value := range attemptFirstHopCustomRecords { + err = db.InsertPaymentAttemptFirstHopCustomRecord(ctx, + sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{ + HtlcAttemptIndex: int64(attempt.AttemptID), + Key: int64(key), + Value: value, + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment attempt first hop custom "+ + "record: %w", err) + } + } + + // Insert the route hops. + err = s.insertRouteHops( + ctx, db, attempt.Route.Hops, attempt.AttemptID, + ) + if err != nil { + return fmt.Errorf("failed to insert route hops: %w", + err) + } + + // Add the attempt to the payment without fetching it from the + // DB again. + mpPayment.HTLCs = append(mpPayment.HTLCs, HTLCAttempt{ + HTLCAttemptInfo: *attempt, + }) + + if err := mpPayment.SetState(); err != nil { + return fmt.Errorf("failed to set payment state: %w", + err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to register attempt: %w", err) + } + + return mpPayment, nil +} From b3f36273881514f50f3a83df60d703c1c7a9ff42 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:09:05 +0200 Subject: [PATCH 13/40] paymentsdb: implement SettleAttempt for sql backend --- payments/db/sql_store.go | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 7ec4554051..1342b0e3c4 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -936,3 +936,51 @@ func (s *SQLStore) RegisterAttempt(paymentHash lntypes.Hash, return mpPayment, nil } + +// SettleAttempt marks the given attempt settled with the preimage. +func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash, + attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { + + ctx := context.TODO() + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Before updating the attempt, we fetch the payment to get the + // payment ID. + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("failed to fetch payment: %w", err) + } + if errors.Is(err, sql.ErrNoRows) { + return ErrPaymentNotInitiated + } + + err = db.SettleAttempt(ctx, sqlc.SettleAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionSettled), + SettlePreimage: settleInfo.Preimage[:], + }) + if err != nil { + return fmt.Errorf("failed to settle attempt: %w", err) + } + + mpPayment, err = s.fetchPaymentWithCompleteData( + ctx, db, payment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to settle attempt: %w", err) + } + + return mpPayment, nil +} From 6145682a1cfbe20445674670d2e8a9fea0c7c5ea Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:11:11 +0200 Subject: [PATCH 14/40] docs: add release-notes --- docs/release-notes/release-notes-0.21.0.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 8f5e4cb69f..a468e30156 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -54,8 +54,10 @@ refacotor the payment related LND code to make it more modular. * Implement the SQL backend for the [payments database](https://github.com/lightningnetwork/lnd/pull/9147) - * Implement QueryPayments for the [payments db + * Implement query methods for the [payments db SQL Backend](https://github.com/lightningnetwork/lnd/pull/10287) + * Implement insert methods for the [payments db + SQL Backend](https://github.com/lightningnetwork/lnd/pull/10291) ## Code Health From 1005816919c54f13dfe7922ba402624edffc3eb7 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 17:26:45 +0200 Subject: [PATCH 15/40] paymentsdb: add harness to run payment db agnostic tests In commit add the harness which will be used to run db agnostic tests against the kv and sql backend. However it does not use it in this commit but it will over the next commits enable these db agnostic tests for the relevant test functions. --- payments/db/payment_test.go | 18 +++---- payments/db/test_kvdb.go | 2 + payments/db/test_postgres.go | 95 ++++++++++++++++++++++++++++++++++++ payments/db/test_sqlite.go | 74 ++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 payments/db/test_postgres.go create mode 100644 payments/db/test_sqlite.go diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 534a1b1e5e..472c91af3f 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -388,7 +388,7 @@ func TestDeleteFailedAttempts(t *testing.T) { // testDeleteFailedAttempts tests the DeleteFailedAttempts method with the // given keepFailedPaymentAttempts flag as argument. func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { - paymentDB := NewTestDB( + paymentDB := NewKVTestDB( t, WithKeepFailedPaymentAttempts(keepFailedPaymentAttempts), ) @@ -479,7 +479,7 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { func TestMPPRecordValidation(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, attempt, _, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") @@ -550,7 +550,7 @@ func TestMPPRecordValidation(t *testing.T) { func TestDeleteSinglePayment(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) // Register four payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -1195,7 +1195,7 @@ func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) { func TestSuccessesWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, _, preimg, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") @@ -1215,7 +1215,7 @@ func TestSuccessesWithoutInFlight(t *testing.T) { func TestFailsWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, _, _, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") @@ -1232,7 +1232,7 @@ func TestFailsWithoutInFlight(t *testing.T) { func TestDeletePayments(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) // Register three payments: // 1. A payment with two failed attempts. @@ -1290,7 +1290,7 @@ func TestDeletePayments(t *testing.T) { func TestSwitchDoubleSend(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, attempt, preimg, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") @@ -1363,7 +1363,7 @@ func TestSwitchDoubleSend(t *testing.T) { func TestSwitchFail(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, attempt, preimg, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") @@ -1520,7 +1520,7 @@ func TestMultiShard(t *testing.T) { } runSubTest := func(t *testing.T, test testCase) { - paymentDB := NewTestDB(t) + paymentDB := NewKVTestDB(t) info, attempt, preimg, err := genInfo(t) if err != nil { diff --git a/payments/db/test_kvdb.go b/payments/db/test_kvdb.go index e0ee1738d7..a4bbfccbd9 100644 --- a/payments/db/test_kvdb.go +++ b/payments/db/test_kvdb.go @@ -1,3 +1,5 @@ +//go:build !test_db_sqlite && !test_db_postgres + package paymentsdb import ( diff --git a/payments/db/test_postgres.go b/payments/db/test_postgres.go new file mode 100644 index 0000000000..1a20c7c0df --- /dev/null +++ b/payments/db/test_postgres.go @@ -0,0 +1,95 @@ +//go:build test_db_postgres && !test_db_sqlite + +package paymentsdb + +import ( + "database/sql" + "testing" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) DB { + return NewTestDBWithFixture(t, nil, opts...) +} + +// NewTestDBFixture creates a new sqldb.TestPgFixture for testing purposes. +func NewTestDBFixture(t *testing.T) *sqldb.TestPgFixture { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + return pgFixture +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture, opts ...OptionModifier) DB { + + var querier BatchedSQLQueries + if pgFixture == nil { + querier = newBatchQuerier(t) + } else { + querier = newBatchQuerierWithFixture(t, pgFixture) + } + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultPostgresConfig(), + }, querier, opts..., + ) + require.NoError(t, err) + + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a PostgreSQL database fixture. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + + return newBatchQuerierWithFixture(t, pgFixture) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a PostgreSQL database fixture. +func newBatchQuerierWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture) BatchedSQLQueries { + + db := sqldb.NewTestPostgresDB(t, pgFixture).BaseDB + + return sqldb.NewTransactionExecutor( + db, func(tx *sql.Tx) SQLQueries { + return db.WithTx(tx) + }, + ) +} + +// NewKVTestDB is a helper function that creates an BBolt database for testing +// and there is no need to convert the interface to the KVStore because for +// some unit tests we still need access to the kvdb interface. +func NewKVTestDB(t *testing.T, opts ...OptionModifier) *KVStore { + backend, backendCleanup, err := kvdb.GetTestBackend( + t.TempDir(), "kvPaymentDB", + ) + require.NoError(t, err) + + t.Cleanup(backendCleanup) + + paymentDB, err := NewKVStore(backend, opts...) + require.NoError(t, err) + + return paymentDB +} diff --git a/payments/db/test_sqlite.go b/payments/db/test_sqlite.go new file mode 100644 index 0000000000..d751bb9977 --- /dev/null +++ b/payments/db/test_sqlite.go @@ -0,0 +1,74 @@ +//go:build !test_db_postgres && test_db_sqlite + +package paymentsdb + +import ( + "database/sql" + "testing" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) DB { + return NewTestDBWithFixture(t, nil, opts...) +} + +// NewTestDBFixture is a no-op for the sqlite build. +func NewTestDBFixture(_ *testing.T) *sqldb.TestPgFixture { + return nil +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture, + opts ...OptionModifier) DB { + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultSQLiteConfig(), + }, newBatchQuerier(t), opts..., + ) + require.NoError(t, err) + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a SQLite database. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + return newBatchQuerierWithFixture(t, nil) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a SQLite database. +func newBatchQuerierWithFixture(t testing.TB, + _ *sqldb.TestPgFixture) BatchedSQLQueries { + + db := sqldb.NewTestSqliteDB(t).BaseDB + + return sqldb.NewTransactionExecutor( + db, func(tx *sql.Tx) SQLQueries { + return db.WithTx(tx) + }, + ) +} + +// NewKVTestDB is a helper function that creates an BBolt database for testing +// and there is no need to convert the interface to the KVStore because for +// some unit tests we still need access to the kvdb interface. +func NewKVTestDB(t *testing.T, opts ...OptionModifier) *KVStore { + backend, backendCleanup, err := kvdb.GetTestBackend( + t.TempDir(), "kvPaymentDB", + ) + require.NoError(t, err) + + t.Cleanup(backendCleanup) + + paymentDB, err := NewKVStore(backend, opts...) + require.NoError(t, err) + + return paymentDB +} From 3b1dd2afe1ce33465dca23f359d16bac08ac733e Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:08:59 +0200 Subject: [PATCH 16/40] paymentsdb: refactor test helpers Since now the sql backend is more strict in using the same session key we refactor the helper so that we can easily change the session key for every new attempt. --- payments/db/kv_store_test.go | 22 +-- payments/db/payment_test.go | 255 ++++++++++++++++++++++++++--------- 2 files changed, 203 insertions(+), 74 deletions(-) diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 73df8ead4e..80ae00db2f 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -2,6 +2,7 @@ package paymentsdb import ( "bytes" + "crypto/sha256" "math" "reflect" "testing" @@ -61,10 +62,15 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { var numSuccess, numInflight int for _, p := range payments { - info, attempt, preimg, err := genInfo(t) - if err != nil { - t.Fatalf("unable to generate htlc message: %v", err) - } + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt, err := genAttemptWithHash( + t, 0, genSessionKey(t), rhash, + ) + require.NoError(t, err) // Sends base htlc message which initiate StatusInFlight. err = paymentDB.InitPayment(info.PaymentIdentifier, info) @@ -474,7 +480,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { paymentDB := NewKVTestDB(t) // Generate a test payment which does not have duplicates. - noDuplicates, _, _, err := genInfo(t) + noDuplicates, _, err := genInfo(t) require.NoError(t, err) // Create a new payment entry in the database. @@ -490,7 +496,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { require.NoError(t, err) // Generate a test payment which we will add duplicates to. - hasDuplicates, _, preimg, err := genInfo(t) + hasDuplicates, preimg, err := genInfo(t) require.NoError(t, err) // Create a new payment entry in the database. @@ -648,7 +654,7 @@ func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket, require.NoError(t, err) // Generate fake information for the duplicate payment. - info, _, _, err := genInfo(t) + info, _, err := genInfo(t) require.NoError(t, err) // Write the payment info to disk under the creation info key. This code @@ -956,7 +962,7 @@ func TestQueryPayments(t *testing.T) { for i := 0; i < nonDuplicatePayments; i++ { // Generate a test payment. - info, _, preimg, err := genInfo(t) + info, preimg, err := genInfo(t) if err != nil { t.Fatalf("unable to create test "+ "payment: %v", err) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 472c91af3f..f01224c05e 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -116,13 +116,20 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { attemptID := uint64(0) for i := 0; i < len(payments); i++ { - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Set the payment id accordingly in the payments slice. payments[i].id = info.PaymentIdentifier - attempt.AttemptID = attemptID + attempt, err := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + require.NoError(t, err) + attemptID++ // Init the payment. @@ -148,7 +155,10 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { // Depending on the test case, fail or succeed the next // attempt. - attempt.AttemptID = attemptID + attempt, err = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + require.NoError(t, err) attemptID++ _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt) @@ -334,7 +344,7 @@ func assertDBPayments(t *testing.T, paymentDB DB, payments []*payment) { } // genPreimage generates a random preimage. -func genPreimage(t *testing.T) ([32]byte, error) { +func genPreimage(t *testing.T) (lntypes.Preimage, error) { t.Helper() var preimage [32]byte @@ -345,31 +355,75 @@ func genPreimage(t *testing.T) ([32]byte, error) { return preimage, nil } -// genInfo generates a payment creation info, an attempt info and a preimage. -func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo, - lntypes.Preimage, error) { +// genSessionKey generates a new random private key for use as a session key. +func genSessionKey(t *testing.T) *btcec.PrivateKey { + t.Helper() - preimage, err := genPreimage(t) - if err != nil { - return nil, nil, preimage, fmt.Errorf("unable to "+ - "generate preimage: %v", err) + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return key +} + +// genPaymentCreationInfo generates a payment creation info. +func genPaymentCreationInfo(t *testing.T, + paymentHash lntypes.Hash) *PaymentCreationInfo { + + t.Helper() + + return &PaymentCreationInfo{ + PaymentIdentifier: paymentHash, + Value: testRoute.ReceiverAmt(), + CreationTime: time.Unix(time.Now().Unix(), 0), + PaymentRequest: []byte("hola"), } +} + +// genPreimageAndHash generates a random preimage and its corresponding hash. +func genPreimageAndHash(t *testing.T) (lntypes.Preimage, lntypes.Hash, error) { + t.Helper() + + preimage, err := genPreimage(t) + require.NoError(t, err) rhash := sha256.Sum256(preimage[:]) var hash lntypes.Hash copy(hash[:], rhash[:]) + return preimage, hash, nil +} + +// genAttemptWithPreimage generates an HTLC attempt and returns both the +// attempt and preimage. +func genAttemptWithHash(t *testing.T, attemptID uint64, + sessionKey *btcec.PrivateKey, hash lntypes.Hash) (*HTLCAttemptInfo, + error) { + + t.Helper() + attempt, err := NewHtlcAttempt( - 0, priv, *testRoute.Copy(), time.Time{}, &hash, + attemptID, sessionKey, *testRoute.Copy(), time.Time{}, + &hash, ) - require.NoError(t, err) + if err != nil { + return nil, err + } - return &PaymentCreationInfo{ - PaymentIdentifier: rhash, - Value: testRoute.ReceiverAmt(), - CreationTime: time.Unix(time.Now().Unix(), 0), - PaymentRequest: []byte("hola"), - }, &attempt.HTLCAttemptInfo, preimage, nil + return &attempt.HTLCAttemptInfo, nil +} + +// genInfo generates a payment creation info and the corresponding preimage. +func genInfo(t *testing.T) (*PaymentCreationInfo, lntypes.Preimage, error) { + + preimage, _, err := genPreimageAndHash(t) + if err != nil { + return nil, preimage, err + } + + rhash := sha256.Sum256(preimage[:]) + creationInfo := genPaymentCreationInfo(t, rhash) + + return creationInfo, preimage, nil } // TestDeleteFailedAttempts checks that DeleteFailedAttempts properly removes @@ -481,7 +535,17 @@ func TestMPPRecordValidation(t *testing.T) { paymentDB := NewKVTestDB(t) - info, attempt, _, err := genInfo(t) + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + attemptID := uint64(0) + + attempt, err := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) require.NoError(t, err, "unable to generate htlc message") // Init the payment. @@ -502,29 +566,45 @@ func TestMPPRecordValidation(t *testing.T) { require.NoError(t, err, "unable to send htlc message") // Now try to register a non-MPP attempt, which should fail. - b := *attempt - b.AttemptID = 1 - b.Route.FinalHop().MPP = nil - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + attemptID++ + attempt2, err := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + attempt2.Route.FinalHop().MPP = nil + + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) require.ErrorIs(t, err, ErrMPPayment) // Try to register attempt one with a different payment address. - b.Route.FinalHop().MPP = record.NewMPP( + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{2}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) require.ErrorIs(t, err, ErrMPPPaymentAddrMismatch) // Try registering one with a different total amount. - b.Route.FinalHop().MPP = record.NewMPP( + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value/2, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) require.ErrorIs(t, err, ErrMPPTotalAmountMismatch) // Create and init a new payment. This time we'll check that we cannot // register an MPP attempt if we already registered a non-MPP one. - info, attempt, _, err = genInfo(t) + preimg, err = genPreimage(t) + require.NoError(t, err) + + rhash = sha256.Sum256(preimg[:]) + info = genPaymentCreationInfo(t, rhash) + + attemptID++ + attempt, err = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + require.NoError(t, err) + require.NoError(t, err, "unable to generate htlc message") err = paymentDB.InitPayment(info.PaymentIdentifier, info) @@ -535,13 +615,17 @@ func TestMPPRecordValidation(t *testing.T) { require.NoError(t, err, "unable to send htlc message") // Attempt to register an MPP attempt, which should fail. - b = *attempt - b.AttemptID = 1 - b.Route.FinalHop().MPP = record.NewMPP( + attemptID++ + attempt2, err = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) require.ErrorIs(t, err, ErrNonMPPayment) } @@ -1197,8 +1281,11 @@ func TestSuccessesWithoutInFlight(t *testing.T) { paymentDB := NewKVTestDB(t) - info, _, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Attempt to complete the payment should fail. _, err = paymentDB.SettleAttempt( @@ -1217,8 +1304,11 @@ func TestFailsWithoutInFlight(t *testing.T) { paymentDB := NewKVTestDB(t) - info, _, _, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Calling Fail should return an error. _, err = paymentDB.Fail( @@ -1292,8 +1382,13 @@ func TestSwitchDoubleSend(t *testing.T) { paymentDB := NewKVTestDB(t) - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt, err := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + require.NoError(t, err) // Sends base htlc message which initiate base status and move it to // StatusInFlight and verifies that it was changed. @@ -1365,8 +1460,13 @@ func TestSwitchFail(t *testing.T) { paymentDB := NewKVTestDB(t) - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt, err := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + require.NoError(t, err) // Sends base htlc message which initiate StatusInFlight. err = paymentDB.InitPayment(info.PaymentIdentifier, info) @@ -1444,7 +1544,11 @@ func TestSwitchFail(t *testing.T) { assertPaymentInfo(t, paymentDB, info.PaymentIdentifier, info, nil, htlc) // Record another attempt. - attempt.AttemptID = 1 + attempt, err = genAttemptWithHash( + t, 1, genSessionKey(t), rhash, + ) + require.NoError(t, err) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") assertDBPaymentstatus( @@ -1522,16 +1626,15 @@ func TestMultiShard(t *testing.T) { runSubTest := func(t *testing.T, test testCase) { paymentDB := NewKVTestDB(t) - info, attempt, preimg, err := genInfo(t) - if err != nil { - t.Fatalf("unable to generate htlc message: %v", err) - } + preimg, err := genPreimage(t) + require.NoError(t, err) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Init the payment, moving it to the StatusInFlight state. err = paymentDB.InitPayment(info.PaymentIdentifier, info) - if err != nil { - t.Fatalf("unable to send htlc message: %v", err) - } + require.NoError(t, err) assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) assertDBPaymentstatus( @@ -1546,19 +1649,23 @@ func TestMultiShard(t *testing.T) { // attempts's value to one third of the payment amount, and // populate the MPP options. shardAmt := info.Value / 3 - attempt.Route.FinalHop().AmtToForward = shardAmt - attempt.Route.FinalHop().MPP = record.NewMPP( - info.Value, [32]byte{1}, - ) var attempts []*HTLCAttemptInfo for i := uint64(0); i < 3; i++ { - a := *attempt - a.AttemptID = i - attempts = append(attempts, &a) + a, err := genAttemptWithHash( + t, i, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + a.Route.FinalHop().AmtToForward = shardAmt + a.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + attempts = append(attempts, a) _, err = paymentDB.RegisterAttempt( - info.PaymentIdentifier, &a, + info.PaymentIdentifier, a, ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) @@ -1569,7 +1676,7 @@ func TestMultiShard(t *testing.T) { ) htlc := &htlcStatus{ - HTLCAttemptInfo: &a, + HTLCAttemptInfo: a, } assertPaymentInfo( t, paymentDB, info.PaymentIdentifier, info, nil, @@ -1580,9 +1687,17 @@ func TestMultiShard(t *testing.T) { // For a fourth attempt, check that attempting to // register it will fail since the total sent amount // will be too large. - b := *attempt - b.AttemptID = 3 - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + b, err := genAttemptWithHash( + t, 3, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + b.Route.FinalHop().AmtToForward = shardAmt + b.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) require.ErrorIs(t, err, ErrValueExceedsAmt) // Fail the second attempt. @@ -1679,9 +1794,17 @@ func TestMultiShard(t *testing.T) { // Try to register yet another attempt. This should fail now // that the payment has reached a terminal condition. - b = *attempt - b.AttemptID = 3 - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + b, err = genAttemptWithHash( + t, 3, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + b.Route.FinalHop().AmtToForward = shardAmt + b.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) if test.settleFirst { require.ErrorIs( t, err, ErrPaymentPendingSettled, @@ -1780,7 +1903,7 @@ func TestMultiShard(t *testing.T) { ) // Finally assert we cannot register more attempts. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) require.Equal(t, registerErr, err) } From 3362dcea3817506c3635f3a67be34c47f3bc0d69 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:09:54 +0200 Subject: [PATCH 17/40] paymentsdb: make QueryPayments test db agnostic We make the QueryPayments test db agnostic and also keep a small test for querying the duplicate payments case in the kv world. --- payments/db/kv_store_test.go | 207 ++------------- payments/db/payment_test.go | 479 +++++++++++++++++++++++++++++++++++ 2 files changed, 502 insertions(+), 184 deletions(-) diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 80ae00db2f..1d3652409f 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -686,17 +686,19 @@ func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket, require.NoError(t, err) } -// TestQueryPayments tests retrieval of payments with forwards and reversed -// queries. -// -// TODO(ziggie): Make this test db agnostic. -func TestQueryPayments(t *testing.T) { - // Define table driven test for QueryPayments. +// TestKVStoreQueryPaymentsDuplicates tests the KV store's legacy duplicate +// payment handling. This tests the specific case where duplicate payments +// are stored in a nested bucket within the parent payment bucket. +func TestKVStoreQueryPaymentsDuplicates(t *testing.T) { + t.Parallel() + // Test payments have sequence indices [1, 3, 4, 5, 6, 7]. // Note that the payment with index 7 has the same payment hash as 6, // and is stored in a nested bucket within payment 6 rather than being - // its own entry in the payments bucket. We do this to test retrieval - // of legacy payments. + // its own entry in the payments bucket. This tests retrieval of legacy + // duplicate payments which is KV-store specific. + // These test cases focus on validating that duplicate payments (seq 7, + // nested under payment 6) are correctly returned in queries. tests := []struct { name string query Query @@ -708,31 +710,20 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs []uint64 }{ { - name: "IndexOffset at the end of the payments range", + name: "query includes duplicate payment in forward " + + "order", query: Query{ - IndexOffset: 7, - MaxPayments: 7, + IndexOffset: 5, + MaxPayments: 3, Reversed: false, IncludeIncomplete: true, }, - firstIndex: 0, - lastIndex: 0, - expectedSeqNrs: nil, - }, - { - name: "query in forwards order, start at beginning", - query: Query{ - IndexOffset: 0, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, + firstIndex: 6, + lastIndex: 7, + expectedSeqNrs: []uint64{6, 7}, }, { - name: "query in forwards order, start at end, overflow", + name: "query duplicate payment at end", query: Query{ IndexOffset: 6, MaxPayments: 2, @@ -744,44 +735,7 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{7}, }, { - name: "start at offset index outside of payments", - query: Query{ - IndexOffset: 20, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 0, - lastIndex: 0, - expectedSeqNrs: nil, - }, - { - name: "overflow in forwards order", - query: Query{ - IndexOffset: 4, - MaxPayments: math.MaxUint64, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 5, - lastIndex: 7, - expectedSeqNrs: []uint64{5, 6, 7}, - }, - { - name: "start at offset index outside of payments, " + - "reversed order", - query: Query{ - IndexOffset: 9, - MaxPayments: 2, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 6, - lastIndex: 7, - expectedSeqNrs: []uint64{6, 7}, - }, - { - name: "query in reverse order, start at end", + name: "query includes duplicate in reverse order", query: Query{ IndexOffset: 0, MaxPayments: 2, @@ -793,36 +747,11 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{6, 7}, }, { - name: "query in reverse order, starting in middle", - query: Query{ - IndexOffset: 4, - MaxPayments: 2, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, - }, - { - name: "query in reverse order, starting in middle, " + - "with underflow", - query: Query{ - IndexOffset: 4, - MaxPayments: 5, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, - }, - { - name: "all payments in reverse, order maintained", + name: "query all payments includes duplicate", query: Query{ IndexOffset: 0, - MaxPayments: 7, - Reversed: true, + MaxPayments: math.MaxUint64, + Reversed: false, IncludeIncomplete: true, }, firstIndex: 1, @@ -830,7 +759,7 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{1, 3, 4, 5, 6, 7}, }, { - name: "exclude incomplete payments", + name: "exclude incomplete includes duplicate", query: Query{ IndexOffset: 0, MaxPayments: 7, @@ -841,96 +770,6 @@ func TestQueryPayments(t *testing.T) { lastIndex: 7, expectedSeqNrs: []uint64{7}, }, - { - name: "query payments at index gap", - query: Query{ - IndexOffset: 1, - MaxPayments: 7, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 3, - lastIndex: 7, - expectedSeqNrs: []uint64{3, 4, 5, 6, 7}, - }, - { - name: "query payments reverse before index gap", - query: Query{ - IndexOffset: 3, - MaxPayments: 7, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 1, - expectedSeqNrs: []uint64{1}, - }, - { - name: "query payments reverse on index gap", - query: Query{ - IndexOffset: 2, - MaxPayments: 7, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 1, - expectedSeqNrs: []uint64{1}, - }, - { - name: "query payments forward on index gap", - query: Query{ - IndexOffset: 2, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 3, - lastIndex: 4, - expectedSeqNrs: []uint64{3, 4}, - }, - { - name: "query in forwards order, with start creation " + - "time", - query: Query{ - IndexOffset: 0, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - CreationDateStart: 5, - }, - firstIndex: 5, - lastIndex: 6, - expectedSeqNrs: []uint64{5, 6}, - }, - { - name: "query in forwards order, with start creation " + - "time at end, overflow", - query: Query{ - IndexOffset: 0, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - CreationDateStart: 7, - }, - firstIndex: 7, - lastIndex: 7, - expectedSeqNrs: []uint64{7}, - }, - { - name: "query with start and end creation time", - query: Query{ - IndexOffset: 9, - MaxPayments: math.MaxUint64, - Reversed: true, - IncludeIncomplete: true, - CreationDateStart: 3, - CreationDateEnd: 5, - }, - firstIndex: 3, - lastIndex: 5, - expectedSeqNrs: []uint64{3, 4, 5}, - }, } for _, tt := range tests { diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index f01224c05e..304d20567f 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math" "reflect" "testing" "time" @@ -1916,3 +1917,481 @@ func TestMultiShard(t *testing.T) { }) } } + +// TestQueryPayments tests retrieval of payments with forwards and reversed +// queries. +func TestQueryPayments(t *testing.T) { + // Define table driven test for QueryPayments. + // Test payments have sequence indices [1, 3, 4, 5, 6]. + // Note that payment with index 2 is deleted to create a gap in the + // sequence numbers. + tests := []struct { + name string + query Query + firstIndex uint64 + lastIndex uint64 + + // expectedSeqNrs contains the set of sequence numbers we expect + // our query to return. + expectedSeqNrs []uint64 + }{ + { + name: "IndexOffset at the end of the payments range", + query: Query{ + IndexOffset: 6, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 0, + lastIndex: 0, + expectedSeqNrs: nil, + }, + { + name: "query in forwards order, start at beginning", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "query in forwards order, start at end, overflow", + query: Query{ + IndexOffset: 5, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "start at offset index outside of payments", + query: Query{ + IndexOffset: 20, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 0, + lastIndex: 0, + expectedSeqNrs: nil, + }, + { + name: "overflow in forwards order", + query: Query{ + IndexOffset: 4, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "start at offset index outside of payments, " + + "reversed order", + query: Query{ + IndexOffset: 9, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in reverse order, start at end", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in reverse order, starting in middle", + query: Query{ + IndexOffset: 4, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "query in reverse order, starting in middle, " + + "with underflow", + query: Query{ + IndexOffset: 4, + MaxPayments: 5, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "all payments in reverse, order maintained", + query: Query{ + IndexOffset: 0, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 6, + expectedSeqNrs: []uint64{1, 3, 4, 5, 6}, + }, + { + name: "exclude incomplete payments", + query: Query{ + IndexOffset: 0, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: false, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "query payments at index gap", + query: Query{ + IndexOffset: 1, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 3, + lastIndex: 6, + expectedSeqNrs: []uint64{3, 4, 5, 6}, + }, + { + name: "query payments reverse before index gap", + query: Query{ + IndexOffset: 3, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 1, + expectedSeqNrs: []uint64{1}, + }, + { + name: "query payments reverse on index gap", + query: Query{ + IndexOffset: 2, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 1, + expectedSeqNrs: []uint64{1}, + }, + { + name: "query payments forward on index gap", + query: Query{ + IndexOffset: 2, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 3, + lastIndex: 4, + expectedSeqNrs: []uint64{3, 4}, + }, + { + name: "query in forwards order, with start creation " + + "time", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CreationDateStart: 5, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in forwards order, with start creation " + + "time at end, overflow", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CreationDateStart: 6, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "query with start and end creation time", + query: Query{ + IndexOffset: 9, + MaxPayments: math.MaxUint64, + Reversed: true, + IncludeIncomplete: true, + CreationDateStart: 3, + CreationDateEnd: 5, + }, + firstIndex: 3, + lastIndex: 5, + expectedSeqNrs: []uint64{3, 4, 5}, + }, + { + name: "query with only end creation time", + query: Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: true, + CreationDateEnd: 4, + }, + firstIndex: 1, + lastIndex: 4, + expectedSeqNrs: []uint64{1, 3, 4}, + }, + { + name: "query reversed with creation date start", + query: Query{ + IndexOffset: 0, + MaxPayments: 3, + Reversed: true, + IncludeIncomplete: true, + CreationDateStart: 3, + }, + firstIndex: 4, + lastIndex: 6, + expectedSeqNrs: []uint64{4, 5, 6}, + }, + { + name: "count total with forward pagination", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CountTotal: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "count total with reverse pagination", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + CountTotal: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "count total with filters", + query: Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: false, + CountTotal: true, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB := NewTestDB(t) + + // Make a preliminary query to make sure it's ok to + // query when we have no payments. + resp, err := paymentDB.QueryPayments(ctx, tt.query) + require.NoError(t, err) + require.Len(t, resp.Payments, 0) + + // Populate the database with a set of test payments. + // We create 6 payments, deleting the payment at index + // 2 so that we cover the case where sequence numbers + // are missing. + numberOfPayments := 6 + + // Store payment info for all payments so we can delete + // one after all are created. + var paymentInfos []*PaymentCreationInfo + + // First, create all payments. + for i := range numberOfPayments { + // Generate a test payment. + info, _, err := genInfo(t) + require.NoError(t, err) + + // Override creation time to allow for testing + // of CreationDateStart and CreationDateEnd. + info.CreationTime = time.Unix(int64(i+1), 0) + + paymentInfos = append(paymentInfos, info) + + // Create a new payment entry in the database. + err = paymentDB.InitPayment( + info.PaymentIdentifier, info, + ) + require.NoError(t, err) + } + + // Now delete the payment at index 1 (the second + // payment). + pmt, err := paymentDB.FetchPayment( + paymentInfos[1].PaymentIdentifier, + ) + require.NoError(t, err) + + // We delete the whole payment. + err = paymentDB.DeletePayment( + paymentInfos[1].PaymentIdentifier, false, + ) + require.NoError(t, err) + + // Verify the payment is deleted. + _, err = paymentDB.FetchPayment( + paymentInfos[1].PaymentIdentifier, + ) + require.ErrorIs( + t, err, ErrPaymentNotInitiated, + ) + + // Verify the index is removed (KV store only). + assertNoIndex( + t, paymentDB, pmt.SequenceNum, + ) + + // For the last payment, settle it so we have at least + // one completed payment for the "exclude incomplete" + // test case. + lastPaymentInfo := paymentInfos[numberOfPayments-1] + attempt, err := NewHtlcAttempt( + 1, priv, testRoute, + time.Unix(100, 0), + &lastPaymentInfo.PaymentIdentifier, + ) + require.NoError(t, err) + + _, err = paymentDB.RegisterAttempt( + lastPaymentInfo.PaymentIdentifier, + &attempt.HTLCAttemptInfo, + ) + require.NoError(t, err) + + var preimg lntypes.Preimage + copy(preimg[:], rev[:]) + + _, err = paymentDB.SettleAttempt( + lastPaymentInfo.PaymentIdentifier, + attempt.AttemptID, + &HTLCSettleInfo{ + Preimage: preimg, + }, + ) + require.NoError(t, err) + + // Fetch all payments in the database. + resp, err = paymentDB.QueryPayments( + ctx, Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + IncludeIncomplete: true, + }, + ) + require.NoError(t, err) + + allPayments := resp.Payments + + if len(allPayments) != 5 { + t.Fatalf("Number of payments received does "+ + "not match expected one. Got %v, "+ + "want %v.", len(allPayments), 5) + } + + querySlice, err := paymentDB.QueryPayments( + ctx, tt.query, + ) + require.NoError(t, err) + + if tt.firstIndex != querySlice.FirstIndexOffset || + tt.lastIndex != querySlice.LastIndexOffset { + + t.Errorf("First or last index does not match "+ + "expected index. Want (%d, %d), "+ + "got (%d, %d).", + tt.firstIndex, tt.lastIndex, + querySlice.FirstIndexOffset, + querySlice.LastIndexOffset) + } + + if len(querySlice.Payments) != len(tt.expectedSeqNrs) { + t.Errorf("expected: %v payments, got: %v", + len(tt.expectedSeqNrs), + len(querySlice.Payments)) + } + + for i, seqNr := range tt.expectedSeqNrs { + q := querySlice.Payments[i] + if seqNr != q.SequenceNum { + t.Errorf("sequence numbers do not "+ + "match, got %v, want %v", + q.SequenceNum, seqNr) + } + } + + // Verify CountTotal is set correctly when requested. + if tt.query.CountTotal { + // We should have 5 total payments + // (6 created - 1 deleted). + expectedTotal := uint64(5) + if querySlice.TotalCount != expectedTotal { + t.Errorf("expected total count %v, "+ + "got %v", expectedTotal, + querySlice.TotalCount) + } + } else { + // When CountTotal is false, TotalCount should + // be 0. + if querySlice.TotalCount != 0 { + t.Errorf("expected total count 0 when "+ + "CountTotal=false, got %v", + querySlice.TotalCount) + } + } + }) + } +} From b43f2c83edad77d81cc88f34b26274f78b47598b Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:19:11 +0200 Subject: [PATCH 18/40] multi: implement Fail method for sql backend --- payments/db/sql_store.go | 55 +++++++++++++++++++++++++++++++++ sqldb/sqlc/payments.sql.go | 13 ++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/payments.sql | 3 ++ 4 files changed, 72 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 1342b0e3c4..ec5382fa3e 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -77,6 +77,8 @@ type SQLQueries interface { SettleAttempt(ctx context.Context, arg sqlc.SettleAttemptParams) error + FailPayment(ctx context.Context, arg sqlc.FailPaymentParams) (sql.Result, error) + DeletePayment(ctx context.Context, paymentID int64) error // DeleteFailedAttempts removes all failed HTLCs from the db for a @@ -984,3 +986,56 @@ func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash, return mpPayment, nil } + +// Fail transitions a payment into the Failed state, and records the ultimate +// reason the payment failed. Note that this should only be called when all +// active attempts are already failed. After invoking this method, InitPayment +// should return nil on its next call for this payment hash, allowing the user +// to make a subsequent payments for the same payment hash. +func (s *SQLStore) Fail(paymentHash lntypes.Hash, + reason FailureReason) (*MPPayment, error) { + + ctx := context.TODO() + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + result, err := db.FailPayment(ctx, sqlc.FailPaymentParams{ + PaymentIdentifier: paymentHash[:], + FailReason: sqldb.SQLInt32(reason), + }) + if err != nil { + return fmt.Errorf("failed to fail payment: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", + err) + } + if rowsAffected == 0 { + return ErrPaymentNotInitiated + } + + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + mpPayment, err = s.fetchPaymentWithCompleteData( + ctx, db, payment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail payment: %w", err) + } + + return mpPayment, nil +} diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index cc287febb2..babab68247 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -83,6 +83,19 @@ func (q *Queries) FailAttempt(ctx context.Context, arg FailAttemptParams) error return err } +const failPayment = `-- name: FailPayment :execresult +UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2 +` + +type FailPaymentParams struct { + FailReason sql.NullInt32 + PaymentIdentifier []byte +} + +func (q *Queries) FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) { + return q.db.ExecContext(ctx, failPayment, arg.FailReason, arg.PaymentIdentifier) +} + const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many SELECT ha.id, diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 4d3707bb06..3319e1e302 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -32,6 +32,7 @@ type Querier interface { DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) FailAttempt(ctx context.Context, arg FailAttemptParams) error + FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, error) // Fetch all inflight attempts across all payments diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index 2b5316a92a..db21a9be4b 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -363,3 +363,6 @@ VALUES ( @htlc_fail_reason, @failure_msg ); + +-- name: FailPayment :execresult +UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2; From 39a9aaa301775890f25042d4be34e707db5b18f9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:20:14 +0200 Subject: [PATCH 19/40] paymentsdb: implement DeletePayments for sql backend --- payments/db/sql_store.go | 95 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index ec5382fa3e..07c34aa118 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1039,3 +1039,98 @@ func (s *SQLStore) Fail(paymentHash lntypes.Hash, return mpPayment, nil } + +// DeletePayments deletes all payments from the DB given the specified flags. +// +// TODO(ziggie): batch and use iterator instead. +func (s *SQLStore) DeletePayments(failedOnly, failedHtlcsOnly bool) (int, + error) { + + var numPayments int + ctx := context.TODO() + + extractCursor := func( + row sqlc.FilterPaymentsRow) int64 { + + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow) error { + + // Fetch all the additional data for the payment. + mpPayment, err := s.fetchPaymentWithCompleteData( + ctx, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + + // Payments which are not final yet cannot be deleted. + // we skip them. + if err := mpPayment.Status.removable(); err != nil { + return nil + } + + // If we are only deleting failed payments, we skip + // if the payment is not failed. + if failedOnly && mpPayment.Status != StatusFailed { + return nil + } + + // If we are only deleting failed HTLCs, we delete them + // and return early. + if failedHtlcsOnly { + return db.DeleteFailedAttempts( + ctx, dbPayment.Payment.ID, + ) + } + + // Otherwise we delete the payment. + err = db.DeletePayment(ctx, dbPayment.Payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + + numPayments++ + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + // For now there are only BOLT 11 payment + // intents. + IntentType: sqldb.SQLInt16( + PaymentIntentTypeBolt11, + ), + IndexOffsetGet: sqldb.SQLInt64( + lastID, + ), + } + + return db.FilterPayments(ctx, filterParams) + } + + return sqldb.ExecutePaginatedQuery( + ctx, s.cfg.QueryCfg, int64(-1), queryFunc, + extractCursor, processPayment, + ) + + }, func() { + numPayments = 0 + }) + if err != nil { + return 0, fmt.Errorf("failed to delete payments "+ + "(failedOnly: %v, failedHtlcsOnly: %v): %w", + failedOnly, failedHtlcsOnly, err) + } + + return numPayments, nil +} From 4e0c0fe8c6b6804331f818cd0762d76be79fb412 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:22:24 +0200 Subject: [PATCH 20/40] paymentsdb: implement FailAttempt for sql backend --- payments/db/sql_store.go | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 07c34aa118..80bf17eb21 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1,6 +1,7 @@ package paymentsdb import ( + "bytes" "context" "database/sql" "errors" @@ -76,6 +77,7 @@ type SQLQueries interface { InsertPaymentHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentHopCustomRecordParams) error SettleAttempt(ctx context.Context, arg sqlc.SettleAttemptParams) error + FailAttempt(ctx context.Context, arg sqlc.FailAttemptParams) error FailPayment(ctx context.Context, arg sqlc.FailPaymentParams) (sql.Result, error) @@ -987,6 +989,66 @@ func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash, return mpPayment, nil } +// FailAttempt marks the given attempt failed. +func (s *SQLStore) FailAttempt(paymentHash lntypes.Hash, + attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { + + ctx := context.TODO() + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Before updating the attempt, we fetch the payment to get the + // payment ID. + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("failed to fetch payment: %w", err) + } + if errors.Is(err, sql.ErrNoRows) { + return ErrPaymentNotInitiated + } + + var failureMsg bytes.Buffer + if failInfo.Message != nil { + err := lnwire.EncodeFailureMessage( + &failureMsg, failInfo.Message, 0, + ) + if err != nil { + return fmt.Errorf("failed to encode "+ + "failure message: %w", err) + } + } + + err = db.FailAttempt(ctx, sqlc.FailAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionFailed), + FailureSourceIndex: sqldb.SQLInt32( + failInfo.FailureSourceIndex, + ), + HtlcFailReason: sqldb.SQLInt32(failInfo.Reason), + FailureMsg: failureMsg.Bytes(), + }) + if err != nil { + return fmt.Errorf("failed to fail attempt: %w", err) + } + + mpPayment, err = s.fetchPaymentWithCompleteData(ctx, db, payment) + if err != nil { + return fmt.Errorf("failed to fetch payment with complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail attempt: %w", err) + } + + return mpPayment, nil +} + // Fail transitions a payment into the Failed state, and records the ultimate // reason the payment failed. Note that this should only be called when all // active attempts are already failed. After invoking this method, InitPayment From 2c806ea14a87463f15a950836a0ee0d914e2d5e2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:23:55 +0200 Subject: [PATCH 21/40] paymentsdb: implement FetchInFlightPayments for sql backend --- payments/db/sql_store.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 80bf17eb21..7dfe98f1fe 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -544,6 +544,58 @@ func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) { return mpPayment, nil } +// FetchInFlightPayments fetches all payments with status InFlight. +// +// TODO(ziggie): Add pagination (LIMIT)) to this function? +// +// This is part of the DB interface. +func (s *SQLStore) FetchInFlightPayments() ([]*MPPayment, error) { + ctx := context.TODO() + + var mpPayments []*MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + inflightDBAttempts, err := db.FetchAllInflightAttempts(ctx) + if err != nil { + return fmt.Errorf("failed to fetch inflight "+ + "attempts: %w", err) + } + + paymentIDs := make([]int64, len(inflightDBAttempts)) + for i, attempt := range inflightDBAttempts { + paymentIDs[i] = attempt.PaymentID + } + + dbPayments, err := db.FetchPaymentsByIDs(ctx, paymentIDs) + if err != nil { + return fmt.Errorf("failed to fetch payments by IDs: %w", + err) + } + + mpPayments = make([]*MPPayment, len(dbPayments)) + for i, dbPayment := range dbPayments { + mpPayment, err := s.fetchPaymentWithCompleteData( + ctx, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + mpPayments[i] = mpPayment + } + + return nil + }, func() { + mpPayments = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch inflight "+ + "attempts: %w", err) + } + + return mpPayments, nil +} + // DeletePayment deletes a payment from the DB given its payment hash. If // failedHtlcsOnly is set, only failed HTLC attempts of the payment will be // deleted. From d245664e33537228a4c2f753f78328d4c44f86d4 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:59:26 +0200 Subject: [PATCH 22/40] paymentsdb: fix test case before testing sql backend --- payments/db/payment_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 304d20567f..ebd754a5ba 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -59,7 +59,10 @@ var ( ChannelID: 12345, OutgoingTimeLock: 111, AmtToForward: 555, - LegacyPayload: true, + + // Only tlv payloads are now supported in LND therefore we set + // LegacyPayload to false. + LegacyPayload: false, } testRoute = route.Route{ @@ -1905,7 +1908,7 @@ func TestMultiShard(t *testing.T) { // Finally assert we cannot register more attempts. _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) - require.Equal(t, registerErr, err) + require.ErrorIs(t, err, registerErr) } for _, test := range tests { From 492a34f7ba8993ebb696f50d44071ab25818b6b5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 18:25:08 +0200 Subject: [PATCH 23/40] paymentsdb: remove kvstore from sql db implementation Now that every method of the interface was implemented we can remove the embedded reference to the KVStore. --- payments/db/sql_store.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 7dfe98f1fe..d53c9f833d 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -97,10 +97,6 @@ type BatchedSQLQueries interface { // SQLStore represents a storage backend. type SQLStore struct { - // TODO(ziggie): Remove the KVStore once all the interface functions are - // implemented. - KVStore - cfg *SQLStoreConfig db BatchedSQLQueries From 4f7c9bdc2f7cfdb486d3277d915afafc2d4cb09b Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 15 Oct 2025 19:00:35 +0200 Subject: [PATCH 24/40] paymentsdb: test all db agnostic tests for all backends We now test every test in the payment_test.go file against all databases. --- payments/db/payment_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index ebd754a5ba..cd9e94c7e0 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -446,7 +446,7 @@ func TestDeleteFailedAttempts(t *testing.T) { // testDeleteFailedAttempts tests the DeleteFailedAttempts method with the // given keepFailedPaymentAttempts flag as argument. func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { - paymentDB := NewKVTestDB( + paymentDB := NewTestDB( t, WithKeepFailedPaymentAttempts(keepFailedPaymentAttempts), ) @@ -537,7 +537,7 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { func TestMPPRecordValidation(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) @@ -638,7 +638,7 @@ func TestMPPRecordValidation(t *testing.T) { func TestDeleteSinglePayment(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) // Register four payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -1283,7 +1283,7 @@ func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) { func TestSuccessesWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) @@ -1306,7 +1306,7 @@ func TestSuccessesWithoutInFlight(t *testing.T) { func TestFailsWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) @@ -1326,7 +1326,7 @@ func TestFailsWithoutInFlight(t *testing.T) { func TestDeletePayments(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) // Register three payments: // 1. A payment with two failed attempts. @@ -1384,7 +1384,7 @@ func TestDeletePayments(t *testing.T) { func TestSwitchDoubleSend(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) @@ -1462,7 +1462,7 @@ func TestSwitchDoubleSend(t *testing.T) { func TestSwitchFail(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) @@ -1628,7 +1628,7 @@ func TestMultiShard(t *testing.T) { } runSubTest := func(t *testing.T, test testCase) { - paymentDB := NewKVTestDB(t) + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) require.NoError(t, err) From 9bb4feff04da36b4ea40cc3381a10c8cafa00512 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 16 Oct 2025 01:10:28 +0200 Subject: [PATCH 25/40] itest: fix list_payments accuracy edge case --- itest/lnd_payment_test.go | 59 ++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 37aff05226..f683cd44a8 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -504,61 +504,86 @@ func testListPayments(ht *lntest.HarnessTest) { expected bool } - // Create test cases to check the timestamp filters. - createCases := func(createTimeSeconds uint64) []testCase { + // Create test cases with proper rounding for start and end dates. + createCases := func(startTimeSeconds, + endTimeSeconds uint64) []testCase { + return []testCase{ { // Use a start date same as the creation date - // should return us the item. + // (truncated) should return us the item. name: "exact start date", - startDate: createTimeSeconds, + startDate: startTimeSeconds, expected: true, }, { // Use an earlier start date should return us // the item. name: "earlier start date", - startDate: createTimeSeconds - 1, + startDate: startTimeSeconds - 1, expected: true, }, { // Use a future start date should return us // nothing. name: "future start date", - startDate: createTimeSeconds + 1, + startDate: startTimeSeconds + 1, expected: false, }, { // Use an end date same as the creation date - // should return us the item. + // (ceiling) should return us the item. name: "exact end date", - endDate: createTimeSeconds, + endDate: endTimeSeconds, expected: true, }, { // Use an end date in the future should return // us the item. name: "future end date", - endDate: createTimeSeconds + 1, + endDate: endTimeSeconds + 1, expected: true, }, { // Use an earlier end date should return us // nothing. - name: "earlier end date", - endDate: createTimeSeconds - 1, + name: "earlier end date", + // The native sql backend has a higher + // precision than the kv backend, the native sql + // backend uses microseconds, the kv backend + // when filtering uses seconds so we need to + // subtract 2 seconds to ensure the payment is + // not included. + // We could also truncate before inserting + // into the sql db but I rather relax this test + // here. + endDate: endTimeSeconds - 2, expected: false, }, } } - // Get the payment creation time in seconds. - paymentCreateSeconds := uint64( - p.CreationTimeNs / time.Second.Nanoseconds(), + // Get the payment creation time in seconds, using different approaches + // for start and end date comparisons to avoid rounding issues. + creationTime := time.Unix(0, p.CreationTimeNs) + + // For start date comparisons: use truncation (floor) to include + // payments from the beginning of that second. + paymentCreateSecondsStart := uint64( + creationTime.Truncate(time.Second).Unix(), + ) + + // For end date comparisons: use ceiling to include payments up to the + // end of that second. + paymentCreateSecondsEnd := uint64( + (p.CreationTimeNs + time.Second.Nanoseconds() - 1) / + time.Second.Nanoseconds(), ) // Create test cases from the payment creation time. - testCases := createCases(paymentCreateSeconds) + testCases := createCases( + paymentCreateSecondsStart, paymentCreateSecondsEnd, + ) // We now check the timestamp filters in `ListPayments`. for _, tc := range testCases { @@ -578,7 +603,9 @@ func testListPayments(ht *lntest.HarnessTest) { } // Create test cases from the invoice creation time. - testCases = createCases(uint64(invoice.CreationDate)) + testCases = createCases( + uint64(invoice.CreationDate), uint64(invoice.CreationDate), + ) // We now do the same check for `ListInvoices`. for _, tc := range testCases { From a8d7e85c680a645b43c2849db06952d4d0613b98 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 16 Oct 2025 21:29:31 +0200 Subject: [PATCH 26/40] docs: add release-notes --- docs/release-notes/release-notes-0.21.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index a468e30156..d7f416f5be 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -58,6 +58,8 @@ SQL Backend](https://github.com/lightningnetwork/lnd/pull/10287) * Implement insert methods for the [payments db SQL Backend](https://github.com/lightningnetwork/lnd/pull/10291) + * Finalize SQL payments implementation [enabling unit and itests + for SQL backend](https://github.com/lightningnetwork/lnd/pull/10292) ## Code Health From cfc827790285f0544a9a1fe486102e441ace8450 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 16 Oct 2025 22:38:20 +0200 Subject: [PATCH 27/40] mulit: fix linter --- lnrpc/routerrpc/router_backend.go | 2 ++ payments/db/payment_test.go | 13 +++----- payments/db/sql_store.go | 49 ++++++++++++++++++------------- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 377d0be3ea..04faa7840b 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -1745,6 +1745,8 @@ func (r *RouterBackend) MarshallPayment(payment *paymentsdb.MPPayment) ( // If any of the htlcs have settled, extract a valid // preimage. if htlc.Settle != nil { + // For AMP payments all hashes will be different so we + // will depict the last htlc preimage. preimage = htlc.Settle.Preimage fee += htlc.Route.TotalFees() } diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index cd9e94c7e0..ce470159e8 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -418,7 +418,6 @@ func genAttemptWithHash(t *testing.T, attemptID uint64, // genInfo generates a payment creation info and the corresponding preimage. func genInfo(t *testing.T) (*PaymentCreationInfo, lntypes.Preimage, error) { - preimage, _, err := genPreimageAndHash(t) if err != nil { return nil, preimage, err @@ -2386,14 +2385,10 @@ func TestQueryPayments(t *testing.T) { "got %v", expectedTotal, querySlice.TotalCount) } - } else { - // When CountTotal is false, TotalCount should - // be 0. - if querySlice.TotalCount != 0 { - t.Errorf("expected total count 0 when "+ - "CountTotal=false, got %v", - querySlice.TotalCount) - } + } else if querySlice.TotalCount != 0 { + t.Errorf("expected total count 0 when "+ + "CountTotal=false, got %v", + querySlice.TotalCount) } }) } diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index d53c9f833d..63b5c968b2 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -628,7 +628,6 @@ func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash, // Be careful to not use s.db here, because we are in a // transaction, is there a way to make this more secure? return db.DeletePayment(ctx, fetchPayment.Payment.ID) - }, func() { }) if err != nil { @@ -813,14 +812,17 @@ func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, // Insert the per-hop custom records if len(hop.CustomRecords) > 0 { for key, value := range hop.CustomRecords { - err = db.InsertPaymentHopCustomRecord(ctx, sqlc.InsertPaymentHopCustomRecordParams{ - HopID: hopID, - Key: int64(key), - Value: value, - }) + err = db.InsertPaymentHopCustomRecord(ctx, + sqlc.InsertPaymentHopCustomRecordParams{ + HopID: hopID, + Key: int64(key), + Value: value, + }, + ) if err != nil { return fmt.Errorf("failed to insert "+ - "payment hop custom record: %w", err) + "payment hop custom "+ + "records: %w", err) } } } @@ -828,11 +830,13 @@ func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, // Insert MPP data if present if hop.MPP != nil { paymentAddr := hop.MPP.PaymentAddr() - err = db.InsertRouteHopMpp(ctx, sqlc.InsertRouteHopMppParams{ - HopID: hopID, - PaymentAddr: paymentAddr[:], - TotalMsat: int64(hop.MPP.TotalMsat()), - }) + err = db.InsertRouteHopMpp(ctx, + sqlc.InsertRouteHopMppParams{ + HopID: hopID, + PaymentAddr: paymentAddr[:], + TotalMsat: int64(hop.MPP.TotalMsat()), + }, + ) if err != nil { return fmt.Errorf("failed to insert "+ "route hop MPP: %w", err) @@ -843,11 +847,13 @@ func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, if hop.AMP != nil { rootShare := hop.AMP.RootShare() setID := hop.AMP.SetID() - err = db.InsertRouteHopAmp(ctx, sqlc.InsertRouteHopAmpParams{ - HopID: hopID, - RootShare: rootShare[:], - SetID: setID[:], - }) + err = db.InsertRouteHopAmp(ctx, + sqlc.InsertRouteHopAmpParams{ + HopID: hopID, + RootShare: rootShare[:], + SetID: setID[:], + }, + ) if err != nil { return fmt.Errorf("failed to insert "+ "route hop AMP: %w", err) @@ -946,6 +952,7 @@ func (s *SQLStore) RegisterAttempt(paymentHash lntypes.Hash, for key, value := range attemptFirstHopCustomRecords { err = db.InsertPaymentAttemptFirstHopCustomRecord(ctx, + //nolint:ll sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{ HtlcAttemptIndex: int64(attempt.AttemptID), Key: int64(key), @@ -1081,9 +1088,12 @@ func (s *SQLStore) FailAttempt(paymentHash lntypes.Hash, return fmt.Errorf("failed to fail attempt: %w", err) } - mpPayment, err = s.fetchPaymentWithCompleteData(ctx, db, payment) + mpPayment, err = s.fetchPaymentWithCompleteData( + ctx, db, payment, + ) if err != nil { - return fmt.Errorf("failed to fetch payment with complete data: %w", err) + return fmt.Errorf("failed to fetch payment with"+ + "complete data: %w", err) } return nil @@ -1232,7 +1242,6 @@ func (s *SQLStore) DeletePayments(failedOnly, failedHtlcsOnly bool) (int, ctx, s.cfg.QueryCfg, int64(-1), queryFunc, extractCursor, processPayment, ) - }, func() { numPayments = 0 }) From 192302e73e104c53172bf5358c64da44115de626 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 17 Oct 2025 09:23:27 +0200 Subject: [PATCH 28/40] paymentsdb: add firstcustom records to unit tests --- payments/db/payment_test.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index ce470159e8..f2d772c800 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -375,11 +375,22 @@ func genPaymentCreationInfo(t *testing.T, t.Helper() + // Add constant first hop custom records for testing for testing + // purposes. + firstHopCustomRecords := lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType + 1: []byte("test_record_1"), + lnwire.MinCustomRecordsTlvType + 2: []byte("test_record_2"), + lnwire.MinCustomRecordsTlvType + 3: []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, + }, + } + return &PaymentCreationInfo{ - PaymentIdentifier: paymentHash, - Value: testRoute.ReceiverAmt(), - CreationTime: time.Unix(time.Now().Unix(), 0), - PaymentRequest: []byte("hola"), + PaymentIdentifier: paymentHash, + Value: testRoute.ReceiverAmt(), + CreationTime: time.Unix(time.Now().Unix(), 0), + PaymentRequest: []byte("hola"), + FirstHopCustomRecords: firstHopCustomRecords, } } From bdbcc54867656289d3a15eaa1ba02759510facb4 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 17 Oct 2025 09:23:49 +0200 Subject: [PATCH 29/40] paymentsdb: add more comments --- payments/db/sql_converters.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/payments/db/sql_converters.go b/payments/db/sql_converters.go index 4343dfd268..7f6d222def 100644 --- a/payments/db/sql_converters.go +++ b/payments/db/sql_converters.go @@ -28,8 +28,10 @@ func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64, copy(identifier[:], paymentIdentifier) return &PaymentCreationInfo{ - PaymentIdentifier: identifier, - Value: lnwire.MilliSatoshi(amountMsat), + PaymentIdentifier: identifier, + Value: lnwire.MilliSatoshi(amountMsat), + // The creation time is stored in the database as UTC but here + // we convert it to local time. CreationTime: createdAt.Local(), PaymentRequest: intentPayload, FirstHopCustomRecords: firstHopCustomRecords, @@ -207,7 +209,8 @@ func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow, ) } - // Add blinding point if present (only for introduction node). + // Add blinding point if present (only for introduction node + // in blinded route). if len(hop.BlindingPoint) > 0 { pubKey, err := btcec.ParsePubKey(hop.BlindingPoint) if err != nil { From 0b7036e4c93cff67df6c76bb536cddc16e624aae Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 18:30:58 +0200 Subject: [PATCH 30/40] multi: thread context through DeletePayment --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 6 ++++-- payments/db/payment_test.go | 36 ++++++++++++++++++++++++++---------- payments/db/sql_store.go | 5 +---- rpcserver.go | 2 +- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index c41dc371f8..70cee859b8 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -31,7 +31,8 @@ type PaymentReader interface { // database. type PaymentWriter interface { // DeletePayment deletes a payment from the DB given its payment hash. - DeletePayment(paymentHash lntypes.Hash, failedAttemptsOnly bool) error + DeletePayment(ctx context.Context, paymentHash lntypes.Hash, + failedAttemptsOnly bool) error // DeletePayments deletes all payments from the DB given the specified // flags. diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index e5915a0034..5d17313dd4 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -291,9 +291,11 @@ func (p *KVStore) InitPayment(paymentHash lntypes.Hash, // DeleteFailedAttempts deletes all failed htlcs for a payment if configured // by the KVStore db. func (p *KVStore) DeleteFailedAttempts(hash lntypes.Hash) error { + ctx := context.TODO() + if !p.keepFailedPaymentAttempts { const failedHtlcsOnly = true - err := p.DeletePayment(hash, failedHtlcsOnly) + err := p.DeletePayment(ctx, hash, failedHtlcsOnly) if err != nil { return err } @@ -1273,7 +1275,7 @@ func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash, // DeletePayment deletes a payment from the DB given its payment hash. If // failedHtlcsOnly is set, only failed HTLC attempts of the payment will be // deleted. -func (p *KVStore) DeletePayment(paymentHash lntypes.Hash, +func (p *KVStore) DeletePayment(_ context.Context, paymentHash lntypes.Hash, failedHtlcsOnly bool) error { return kvdb.Update(p.db, func(tx kvdb.RwTx) error { diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index f2d772c800..6164d440ab 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -503,8 +503,8 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // operation are performed in general therefore we do NOT expect an // error in this case. if keepFailedPaymentAttempts { - require.NoError( - t, paymentDB.DeleteFailedAttempts(payments[1].id), + require.NoError(t, paymentDB.DeleteFailedAttempts( + payments[1].id), ) } else { require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id)) @@ -648,6 +648,8 @@ func TestMPPRecordValidation(t *testing.T) { func TestDeleteSinglePayment(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewTestDB(t) // Register four payments: @@ -679,7 +681,9 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete HTLC attempts for first payment only. - require.NoError(t, paymentDB.DeletePayment(payments[0].id, true)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[0].id, true, + )) // The first payment is the only altered one as its failed HTLC should // have been removed but is still present as payment. @@ -687,19 +691,25 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete the first payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[0].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[0].id, false, + )) // The first payment should have been deleted. assertDBPayments(t, paymentDB, payments[1:]) // Now delete the second payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[1].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[1].id, false, + )) // The Second payment should have been deleted. assertDBPayments(t, paymentDB, payments[2:]) // Delete failed HTLC attempts for the third payment. - require.NoError(t, paymentDB.DeletePayment(payments[2].id, true)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[2].id, true, + )) // Only the successful HTLC attempt should be left for the third // payment. @@ -707,21 +717,27 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments[2:]) // Now delete the third payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[2].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[2].id, false, + )) // Only the last payment should be left. assertDBPayments(t, paymentDB, payments[3:]) // Deleting HTLC attempts from InFlight payments should not work and an // error returned. - require.Error(t, paymentDB.DeletePayment(payments[3].id, true)) + require.Error(t, paymentDB.DeletePayment( + ctx, payments[3].id, true, + )) // The payment is InFlight and therefore should not have been altered. assertDBPayments(t, paymentDB, payments[3:]) // Finally deleting the InFlight payment should also not work and an // error returned. - require.Error(t, paymentDB.DeletePayment(payments[3].id, false)) + require.Error(t, paymentDB.DeletePayment( + ctx, payments[3].id, false, + )) // The payment is InFlight and therefore should not have been altered. assertDBPayments(t, paymentDB, payments[3:]) @@ -2291,7 +2307,7 @@ func TestQueryPayments(t *testing.T) { // We delete the whole payment. err = paymentDB.DeletePayment( - paymentInfos[1].PaymentIdentifier, false, + ctx, paymentInfos[1].PaymentIdentifier, false, ) require.NoError(t, err) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 63b5c968b2..2dafe8dc43 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -595,11 +595,9 @@ func (s *SQLStore) FetchInFlightPayments() ([]*MPPayment, error) { // DeletePayment deletes a payment from the DB given its payment hash. If // failedHtlcsOnly is set, only failed HTLC attempts of the payment will be // deleted. -func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash, +func (s *SQLStore) DeletePayment(ctx context.Context, paymentHash lntypes.Hash, failedHtlcsOnly bool) error { - ctx := context.TODO() - err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { fetchPayment, err := db.FetchPayment(ctx, paymentHash[:]) if err != nil { @@ -648,7 +646,6 @@ func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error { if s.keepFailedPaymentAttempts { return nil } - ctx := context.TODO() err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { diff --git a/rpcserver.go b/rpcserver.go index d3d3c51801..56e104aa86 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -7666,7 +7666,7 @@ func (r *rpcServer) DeletePayment(ctx context.Context, rpcsLog.Infof("[DeletePayment] payment_identifier=%v, "+ "failed_htlcs_only=%v", hash, req.FailedHtlcsOnly) - err = r.server.paymentsDB.DeletePayment(hash, req.FailedHtlcsOnly) + err = r.server.paymentsDB.DeletePayment(ctx, hash, req.FailedHtlcsOnly) if err != nil { return nil, err } From 148d415652454cae02117a166e3d28132837f67e Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 19:04:14 +0200 Subject: [PATCH 31/40] multi: thread context through DeletePayments --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 6 ++++-- payments/db/payment_test.go | 10 ++++++---- payments/db/sql_store.go | 5 ++--- rpcserver.go | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index 70cee859b8..ea0dffa5e7 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -36,7 +36,8 @@ type PaymentWriter interface { // DeletePayments deletes all payments from the DB given the specified // flags. - DeletePayments(failedOnly, failedAttemptsOnly bool) (int, error) + DeletePayments(ctx context.Context, failedOnly, + failedAttemptsOnly bool) (int, error) PaymentControl } diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index 5d17313dd4..d0446ad0d5 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -1372,7 +1372,7 @@ func (p *KVStore) DeletePayment(_ context.Context, paymentHash lntypes.Hash, // failedHtlcsOnly is set, the payment itself won't be deleted, only failed HTLC // attempts. The method returns the number of deleted payments, which is always // 0 if failedHtlcsOnly is set. -func (p *KVStore) DeletePayments(failedOnly, +func (p *KVStore) DeletePayments(_ context.Context, failedOnly, failedHtlcsOnly bool) (int, error) { var numPayments int diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 1d3652409f..da4a8a1439 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -25,6 +25,8 @@ import ( func TestKVStoreDeleteNonInFlight(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewKVTestDB(t) // Create a sequence number for duplicate payments that will not collide @@ -175,7 +177,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { } // Delete all failed payments. - numPayments, err := paymentDB.DeletePayments(true, false) + numPayments, err := paymentDB.DeletePayments(ctx, true, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) @@ -211,7 +213,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { } // Now delete all payments except in-flight. - numPayments, err = paymentDB.DeletePayments(false, false) + numPayments, err = paymentDB.DeletePayments(ctx, false, false) require.NoError(t, err) require.EqualValues(t, 2, numPayments) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 6164d440ab..683ef6a35a 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -1352,6 +1352,8 @@ func TestFailsWithoutInFlight(t *testing.T) { func TestDeletePayments(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewTestDB(t) // Register three payments: @@ -1372,7 +1374,7 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete HTLC attempts for failed payments only. - numPayments, err := paymentDB.DeletePayments(true, true) + numPayments, err := paymentDB.DeletePayments(ctx, true, true) require.NoError(t, err) require.EqualValues(t, 0, numPayments) @@ -1381,7 +1383,7 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete failed attempts for all payments. - numPayments, err = paymentDB.DeletePayments(false, true) + numPayments, err = paymentDB.DeletePayments(ctx, false, true) require.NoError(t, err) require.EqualValues(t, 0, numPayments) @@ -1391,14 +1393,14 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Now delete all failed payments. - numPayments, err = paymentDB.DeletePayments(true, false) + numPayments, err = paymentDB.DeletePayments(ctx, true, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) assertDBPayments(t, paymentDB, payments[1:]) // Finally delete all completed payments. - numPayments, err = paymentDB.DeletePayments(false, false) + numPayments, err = paymentDB.DeletePayments(ctx, false, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 2dafe8dc43..7c6a5bdb6a 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1160,11 +1160,10 @@ func (s *SQLStore) Fail(paymentHash lntypes.Hash, // DeletePayments deletes all payments from the DB given the specified flags. // // TODO(ziggie): batch and use iterator instead. -func (s *SQLStore) DeletePayments(failedOnly, failedHtlcsOnly bool) (int, - error) { +func (s *SQLStore) DeletePayments(ctx context.Context, failedOnly, + failedHtlcsOnly bool) (int, error) { var numPayments int - ctx := context.TODO() extractCursor := func( row sqlc.FilterPaymentsRow) int64 { diff --git a/rpcserver.go b/rpcserver.go index 56e104aa86..91de4327c1 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -7707,7 +7707,7 @@ func (r *rpcServer) DeleteAllPayments(ctx context.Context, req.FailedHtlcsOnly) numDeletedPayments, err := r.server.paymentsDB.DeletePayments( - req.FailedPaymentsOnly, req.FailedHtlcsOnly, + ctx, req.FailedPaymentsOnly, req.FailedHtlcsOnly, ) if err != nil { return nil, err From 19d73866b2a7547128bc708c7dbcd5a484260b9d Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 20:16:47 +0200 Subject: [PATCH 32/40] multi: thread context through FetchPayment --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 4 ++-- payments/db/kv_store_test.go | 14 +++++++++----- payments/db/payment_test.go | 16 +++++++++++----- payments/db/sql_store.go | 4 ++-- routing/control_tower.go | 17 ++++++++++++----- routing/mock_test.go | 9 +++++---- routing/payment_lifecycle.go | 8 ++++++-- routing/router.go | 4 +++- routing/router_test.go | 4 +++- 10 files changed, 55 insertions(+), 28 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index ea0dffa5e7..7d12914299 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -21,7 +21,8 @@ type PaymentReader interface { // FetchPayment fetches the payment corresponding to the given payment // hash. - FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) + FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) // FetchInFlightPayments returns all payments with status InFlight. FetchInFlightPayments() ([]*MPPayment, error) diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index d0446ad0d5..f0b717fb75 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -585,8 +585,8 @@ func (p *KVStore) Fail(paymentHash lntypes.Hash, } // FetchPayment returns information about a payment from the database. -func (p *KVStore) FetchPayment(paymentHash lntypes.Hash) ( - *MPPayment, error) { +func (p *KVStore) FetchPayment(_ context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { var payment *MPPayment err := kvdb.View(p.db, func(tx kvdb.RTx) error { diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index da4a8a1439..6e3105435a 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -295,6 +295,8 @@ func fetchPaymentIndexEntry(t *testing.T, p *KVStore, func assertPaymentIndex(t *testing.T, p DB, expectedHash lntypes.Hash) { t.Helper() + ctx := t.Context() + // Only the kv implementation uses the index so we exit early if the // payment db is not a kv implementation. This helps us to reuse the // same test for both implementations. @@ -305,7 +307,7 @@ func assertPaymentIndex(t *testing.T, p DB, expectedHash lntypes.Hash) { // Lookup the payment so that we have its sequence number and check // that is has correctly been indexed in the payment indexes bucket. - pmt, err := kvPaymentDB.FetchPayment(expectedHash) + pmt, err := kvPaymentDB.FetchPayment(ctx, expectedHash) require.NoError(t, err) hash, err := fetchPaymentIndexEntry(t, kvPaymentDB, pmt.SequenceNum) @@ -481,6 +483,8 @@ func deletePayment(t *testing.T, db kvdb.Backend, paymentHash lntypes.Hash, func TestFetchPaymentWithSequenceNumber(t *testing.T) { paymentDB := NewKVTestDB(t) + ctx := t.Context() + // Generate a test payment which does not have duplicates. noDuplicates, _, err := genInfo(t) require.NoError(t, err) @@ -493,7 +497,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { // Fetch the payment so we can get its sequence nr. noDuplicatesPayment, err := paymentDB.FetchPayment( - noDuplicates.PaymentIdentifier, + ctx, noDuplicates.PaymentIdentifier, ) require.NoError(t, err) @@ -509,7 +513,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { // Fetch the payment so we can get its sequence nr. hasDuplicatesPayment, err := paymentDB.FetchPayment( - hasDuplicates.PaymentIdentifier, + ctx, hasDuplicates.PaymentIdentifier, ) require.NoError(t, err) @@ -821,7 +825,7 @@ func TestKVStoreQueryPaymentsDuplicates(t *testing.T) { // Immediately delete the payment with index 2. if i == 1 { pmt, err := paymentDB.FetchPayment( - info.PaymentIdentifier, + ctx, info.PaymentIdentifier, ) require.NoError(t, err) @@ -838,7 +842,7 @@ func TestKVStoreQueryPaymentsDuplicates(t *testing.T) { // duplicate payments will always be succeeded. if i == (nonDuplicatePayments - 1) { pmt, err := paymentDB.FetchPayment( - info.PaymentIdentifier, + ctx, info.PaymentIdentifier, ) require.NoError(t, err) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 683ef6a35a..609a12dfeb 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -227,7 +227,9 @@ func assertPaymentInfo(t *testing.T, p DB, hash lntypes.Hash, t.Helper() - payment, err := p.FetchPayment(hash) + ctx := t.Context() + + payment, err := p.FetchPayment(ctx, hash) if err != nil { t.Fatal(err) } @@ -295,7 +297,9 @@ func assertDBPaymentstatus(t *testing.T, p DB, hash lntypes.Hash, t.Helper() - payment, err := p.FetchPayment(hash) + ctx := t.Context() + + payment, err := p.FetchPayment(ctx, hash) if errors.Is(err, ErrPaymentNotInitiated) { return } @@ -1490,6 +1494,8 @@ func TestSwitchDoubleSend(t *testing.T) { func TestSwitchFail(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) @@ -1528,7 +1534,7 @@ func TestSwitchFail(t *testing.T) { // Lookup the payment so we can get its old sequence number before it is // overwritten. - payment, err := paymentDB.FetchPayment(info.PaymentIdentifier) + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) require.NoError(t, err) // Sends the htlc again, which should succeed since the prior payment @@ -2303,7 +2309,7 @@ func TestQueryPayments(t *testing.T) { // Now delete the payment at index 1 (the second // payment). pmt, err := paymentDB.FetchPayment( - paymentInfos[1].PaymentIdentifier, + ctx, paymentInfos[1].PaymentIdentifier, ) require.NoError(t, err) @@ -2315,7 +2321,7 @@ func TestQueryPayments(t *testing.T) { // Verify the payment is deleted. _, err = paymentDB.FetchPayment( - paymentInfos[1].PaymentIdentifier, + ctx, paymentInfos[1].PaymentIdentifier, ) require.ErrorIs( t, err, ErrPaymentNotInitiated, diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 7c6a5bdb6a..23e053c8ae 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -507,8 +507,8 @@ func (s *SQLStore) QueryPayments(ctx context.Context, // hash. // // This is part of the DB interface. -func (s *SQLStore) FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) { - ctx := context.TODO() +func (s *SQLStore) FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { var mpPayment *MPPayment diff --git a/routing/control_tower.go b/routing/control_tower.go index 2b9e7dd9d2..3102894877 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -1,6 +1,7 @@ package routing import ( + "context" "sync" "github.com/lightningnetwork/lnd/lntypes" @@ -52,7 +53,8 @@ type ControlTower interface { // FetchPayment fetches the payment corresponding to the given payment // hash. - FetchPayment(paymentHash lntypes.Hash) (paymentsdb.DBMPPayment, error) + FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (paymentsdb.DBMPPayment, error) // FailPayment transitions a payment into the Failed state, and records // the ultimate reason the payment failed. Note that this should only @@ -164,6 +166,8 @@ func NewControlTower(db paymentsdb.DB) ControlTower { func (p *controlTower) InitPayment(paymentHash lntypes.Hash, info *paymentsdb.PaymentCreationInfo) error { + ctx := context.TODO() + err := p.db.InitPayment(paymentHash, info) if err != nil { return err @@ -174,7 +178,7 @@ func (p *controlTower) InitPayment(paymentHash lntypes.Hash, p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FetchPayment(paymentHash) + payment, err := p.db.FetchPayment(ctx, paymentHash) if err != nil { return err } @@ -250,10 +254,11 @@ func (p *controlTower) FailAttempt(paymentHash lntypes.Hash, } // FetchPayment fetches the payment corresponding to the given payment hash. -func (p *controlTower) FetchPayment(paymentHash lntypes.Hash) ( +func (p *controlTower) FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) ( paymentsdb.DBMPPayment, error) { - return p.db.FetchPayment(paymentHash) + return p.db.FetchPayment(ctx, paymentHash) } // FailPayment transitions a payment into the Failed state, and records the @@ -293,12 +298,14 @@ func (p *controlTower) FetchInFlightPayments() ([]*paymentsdb.MPPayment, func (p *controlTower) SubscribePayment(paymentHash lntypes.Hash) ( ControlTowerSubscriber, error) { + ctx := context.TODO() + // Take lock before querying the db to prevent missing or duplicating an // update. p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FetchPayment(paymentHash) + payment, err := p.db.FetchPayment(ctx, paymentHash) if err != nil { return nil, err } diff --git a/routing/mock_test.go b/routing/mock_test.go index 19a76ee901..556601ecd0 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -1,6 +1,7 @@ package routing import ( + "context" "errors" "fmt" "sync" @@ -509,8 +510,8 @@ func (m *mockControlTowerOld) FailPayment(phash lntypes.Hash, return nil } -func (m *mockControlTowerOld) FetchPayment(phash lntypes.Hash) ( - paymentsdb.DBMPPayment, error) { +func (m *mockControlTowerOld) FetchPayment(_ context.Context, + phash lntypes.Hash) (paymentsdb.DBMPPayment, error) { m.Lock() defer m.Unlock() @@ -786,8 +787,8 @@ func (m *mockControlTower) FailPayment(phash lntypes.Hash, return args.Error(0) } -func (m *mockControlTower) FetchPayment(phash lntypes.Hash) ( - paymentsdb.DBMPPayment, error) { +func (m *mockControlTower) FetchPayment(_ context.Context, + phash lntypes.Hash) (paymentsdb.DBMPPayment, error) { args := m.Called(phash) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 8353cba157..4eb78c8e22 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -1114,7 +1114,9 @@ func (p *paymentLifecycle) patchLegacyPaymentHash( func (p *paymentLifecycle) reloadInflightAttempts() (paymentsdb.DBMPPayment, error) { - payment, err := p.router.cfg.Control.FetchPayment(p.identifier) + ctx := context.TODO() + + payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { return nil, err } @@ -1139,8 +1141,10 @@ func (p *paymentLifecycle) reloadInflightAttempts() (paymentsdb.DBMPPayment, func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, *paymentsdb.MPPaymentState, error) { + ctx := context.TODO() + // Read the db to get the latest state of the payment. - payment, err := p.router.cfg.Control.FetchPayment(p.identifier) + payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { return nil, nil, err } diff --git a/routing/router.go b/routing/router.go index 3c35b7c52c..cdf2013aa8 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1064,13 +1064,15 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { + ctx := context.TODO() + // Helper function to fail a payment. It makes sure the payment is only // failed once so that the failure reason is not overwritten. failPayment := func(paymentIdentifier lntypes.Hash, reason paymentsdb.FailureReason) error { payment, fetchErr := r.cfg.Control.FetchPayment( - paymentIdentifier, + ctx, paymentIdentifier, ) if fetchErr != nil { return fetchErr diff --git a/routing/router_test.go b/routing/router_test.go index b811793d25..6f384137f1 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -1094,7 +1094,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { require.Equal(t, paymentsdb.FailureReasonNoRoute, err) // Inspect the two attempts that were made before the payment failed. - p, err := ctx.router.cfg.Control.FetchPayment(*payment.paymentHash) + p, err := ctx.router.cfg.Control.FetchPayment( + t.Context(), *payment.paymentHash, + ) require.NoError(t, err) htlcs := p.GetHTLCs() From 37d2aa73e4ed91b9f27f4831fa68bd699cf8574e Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 20:24:32 +0200 Subject: [PATCH 33/40] multi: thread context through FetchInflightPayments --- payments/db/interface.go | 2 +- payments/db/kv_store.go | 4 +++- payments/db/sql_store.go | 4 ++-- routing/control_tower.go | 13 ++++++++----- routing/mock_test.go | 4 ++-- routing/router.go | 4 +++- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index 7d12914299..1de03e133d 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -25,7 +25,7 @@ type PaymentReader interface { paymentHash lntypes.Hash) (*MPPayment, error) // FetchInFlightPayments returns all payments with status InFlight. - FetchInFlightPayments() ([]*MPPayment, error) + FetchInFlightPayments(ctx context.Context) ([]*MPPayment, error) } // PaymentWriter represents the interface to write operations to the payments diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index f0b717fb75..aee7e6aa2f 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -741,7 +741,9 @@ func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) { } // FetchInFlightPayments returns all payments with status InFlight. -func (p *KVStore) FetchInFlightPayments() ([]*MPPayment, error) { +func (p *KVStore) FetchInFlightPayments(_ context.Context) ([]*MPPayment, + error) { + var ( inFlights []*MPPayment start = time.Now() diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 23e053c8ae..b3594e90db 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -545,8 +545,8 @@ func (s *SQLStore) FetchPayment(ctx context.Context, // TODO(ziggie): Add pagination (LIMIT)) to this function? // // This is part of the DB interface. -func (s *SQLStore) FetchInFlightPayments() ([]*MPPayment, error) { - ctx := context.TODO() +func (s *SQLStore) FetchInFlightPayments(ctx context.Context) ([]*MPPayment, + error) { var mpPayments []*MPPayment diff --git a/routing/control_tower.go b/routing/control_tower.go index 3102894877..718dca3ff5 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -67,7 +67,8 @@ type ControlTower interface { FailPayment(lntypes.Hash, paymentsdb.FailureReason) error // FetchInFlightPayments returns all payments with status InFlight. - FetchInFlightPayments() ([]*paymentsdb.MPPayment, error) + FetchInFlightPayments(ctx context.Context) ([]*paymentsdb.MPPayment, + error) // SubscribePayment subscribes to updates for the payment with the given // hash. A first update with the current state of the payment is always @@ -286,10 +287,10 @@ func (p *controlTower) FailPayment(paymentHash lntypes.Hash, } // FetchInFlightPayments returns all payments with status InFlight. -func (p *controlTower) FetchInFlightPayments() ([]*paymentsdb.MPPayment, - error) { +func (p *controlTower) FetchInFlightPayments( + ctx context.Context) ([]*paymentsdb.MPPayment, error) { - return p.db.FetchInFlightPayments() + return p.db.FetchInFlightPayments(ctx) } // SubscribePayment subscribes to updates for the payment with the given hash. A @@ -342,6 +343,8 @@ func (p *controlTower) SubscribePayment(paymentHash lntypes.Hash) ( func (p *controlTower) SubscribeAllPayments() (ControlTowerSubscriber, error) { subscriber := newControlTowerSubscriber() + ctx := context.TODO() + // Add the subscriber to the list before fetching in-flight payments, so // no events are missed. If a payment attempt update occurs after // appending and before fetching in-flight payments, an out-of-order @@ -353,7 +356,7 @@ func (p *controlTower) SubscribeAllPayments() (ControlTowerSubscriber, error) { p.subscribersMtx.Unlock() log.Debugf("Scanning for inflight payments") - inflightPayments, err := p.db.FetchInFlightPayments() + inflightPayments, err := p.db.FetchInFlightPayments(ctx) if err != nil { return nil, err } diff --git a/routing/mock_test.go b/routing/mock_test.go index 556601ecd0..b30627165d 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -546,7 +546,7 @@ func (m *mockControlTowerOld) fetchPayment(phash lntypes.Hash) ( return mp, nil } -func (m *mockControlTowerOld) FetchInFlightPayments() ( +func (m *mockControlTowerOld) FetchInFlightPayments(_ context.Context) ( []*paymentsdb.MPPayment, error) { if m.fetchInFlight != nil { @@ -801,7 +801,7 @@ func (m *mockControlTower) FetchPayment(_ context.Context, return payment, args.Error(1) } -func (m *mockControlTower) FetchInFlightPayments() ( +func (m *mockControlTower) FetchInFlightPayments(_ context.Context) ( []*paymentsdb.MPPayment, error) { args := m.Called() diff --git a/routing/router.go b/routing/router.go index cdf2013aa8..f713216913 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1417,9 +1417,11 @@ func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi], // resumePayments fetches inflight payments and resumes their payment // lifecycles. func (r *ChannelRouter) resumePayments() error { + ctx := context.TODO() + // Get all payments that are inflight. log.Debugf("Scanning for inflight payments") - payments, err := r.cfg.Control.FetchInFlightPayments() + payments, err := r.cfg.Control.FetchInFlightPayments(ctx) if err != nil { return err } From 602d37943fb2579dbc792508bf922c263090f242 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 22:00:16 +0200 Subject: [PATCH 34/40] multi: thread context through InitPayment --- payments/db/interface.go | 2 +- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 8 ++++---- payments/db/payment_test.go | 32 ++++++++++++++++++++------------ payments/db/sql_store.go | 4 +--- routing/control_tower.go | 11 +++++------ routing/control_tower_test.go | 12 ++++++------ routing/mock_test.go | 6 +++--- routing/router.go | 6 ++++-- 9 files changed, 45 insertions(+), 38 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index 1de03e133d..bea4aee74f 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -61,7 +61,7 @@ type PaymentControl interface { // exists in the database before creating a new payment. However, it // should allow the user making a subsequent payment if the payment is // in a Failed state. - InitPayment(lntypes.Hash, *PaymentCreationInfo) error + InitPayment(context.Context, lntypes.Hash, *PaymentCreationInfo) error // RegisterAttempt atomically records the provided HTLCAttemptInfo. RegisterAttempt(lntypes.Hash, *HTLCAttemptInfo) (*MPPayment, error) diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index aee7e6aa2f..c4cd67acb6 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -186,7 +186,7 @@ func initKVStore(db kvdb.Backend) error { // making sure it does not already exist as an in-flight payment. When this // method returns successfully, the payment is guaranteed to be in the InFlight // state. -func (p *KVStore) InitPayment(paymentHash lntypes.Hash, +func (p *KVStore) InitPayment(_ context.Context, paymentHash lntypes.Hash, info *PaymentCreationInfo) error { // Obtain a new sequence number for this payment. This is used diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 6e3105435a..5d98374201 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -75,7 +75,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { require.NoError(t, err) // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if err != nil { t.Fatalf("unable to send htlc message: %v", err) } @@ -491,7 +491,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { // Create a new payment entry in the database. err = paymentDB.InitPayment( - noDuplicates.PaymentIdentifier, noDuplicates, + ctx, noDuplicates.PaymentIdentifier, noDuplicates, ) require.NoError(t, err) @@ -507,7 +507,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) { // Create a new payment entry in the database. err = paymentDB.InitPayment( - hasDuplicates.PaymentIdentifier, hasDuplicates, + ctx, hasDuplicates.PaymentIdentifier, hasDuplicates, ) require.NoError(t, err) @@ -818,7 +818,7 @@ func TestKVStoreQueryPaymentsDuplicates(t *testing.T) { // Create a new payment entry in the database. err = paymentDB.InitPayment( - info.PaymentIdentifier, info, + ctx, info.PaymentIdentifier, info, ) require.NoError(t, err) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 609a12dfeb..e49f93e1b5 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -117,6 +117,8 @@ type payment struct { func createTestPayments(t *testing.T, p DB, payments []*payment) { t.Helper() + ctx := t.Context() + attemptID := uint64(0) for i := 0; i < len(payments); i++ { @@ -137,7 +139,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { attemptID++ // Init the payment. - err = p.InitPayment(info.PaymentIdentifier, info) + err = p.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Register and fail the first attempt for all payments. @@ -551,6 +553,8 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { func TestMPPRecordValidation(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) @@ -567,7 +571,7 @@ func TestMPPRecordValidation(t *testing.T) { require.NoError(t, err, "unable to generate htlc message") // Init the payment. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Create three unique attempts we'll use for the test, and @@ -625,7 +629,7 @@ func TestMPPRecordValidation(t *testing.T) { require.NoError(t, err, "unable to generate htlc message") - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") attempt.Route.FinalHop().MPP = nil @@ -1416,6 +1420,8 @@ func TestDeletePayments(t *testing.T) { func TestSwitchDoubleSend(t *testing.T) { t.Parallel() + ctx := t.Context() + paymentDB := NewTestDB(t) preimg, err := genPreimage(t) @@ -1428,7 +1434,7 @@ func TestSwitchDoubleSend(t *testing.T) { // Sends base htlc message which initiate base status and move it to // StatusInFlight and verifies that it was changed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) @@ -1442,7 +1448,7 @@ func TestSwitchDoubleSend(t *testing.T) { // Try to initiate double sending of htlc message with the same // payment hash, should result in error indicating that payment has // already been sent. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.ErrorIs(t, err, ErrPaymentExists) // Record an attempt. @@ -1460,7 +1466,7 @@ func TestSwitchDoubleSend(t *testing.T) { ) // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrPaymentInFlight) { t.Fatalf("payment control wrong behaviour: " + "double sending must trigger ErrPaymentInFlight error") @@ -1483,7 +1489,7 @@ func TestSwitchDoubleSend(t *testing.T) { t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } @@ -1507,7 +1513,7 @@ func TestSwitchFail(t *testing.T) { require.NoError(t, err) // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) @@ -1539,7 +1545,7 @@ func TestSwitchFail(t *testing.T) { // Sends the htlc again, which should succeed since the prior payment // failed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Check that our index has been updated, and the old index has been @@ -1634,7 +1640,7 @@ func TestSwitchFail(t *testing.T) { // Attempt a final payment, which should now fail since the prior // payment succeed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } @@ -1645,6 +1651,8 @@ func TestSwitchFail(t *testing.T) { func TestMultiShard(t *testing.T) { t.Parallel() + ctx := t.Context() + // We will register three HTLC attempts, and always fail the second // one. We'll generate all combinations of settling/failing the first // and third HTLC, and assert that the payment status end up as we @@ -1671,7 +1679,7 @@ func TestMultiShard(t *testing.T) { info := genPaymentCreationInfo(t, rhash) // Init the payment, moving it to the StatusInFlight state. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err) assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) @@ -2301,7 +2309,7 @@ func TestQueryPayments(t *testing.T) { // Create a new payment entry in the database. err = paymentDB.InitPayment( - info.PaymentIdentifier, info, + ctx, info.PaymentIdentifier, info, ) require.NoError(t, err) } diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index b3594e90db..553fbbc80b 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -683,11 +683,9 @@ func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error { // InitPayment initializes a payment. // // This is part of the DB interface. -func (s *SQLStore) InitPayment(paymentHash lntypes.Hash, +func (s *SQLStore) InitPayment(ctx context.Context, paymentHash lntypes.Hash, paymentCreationInfo *PaymentCreationInfo) error { - ctx := context.TODO() - // Create the payment in the database. err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { existingPayment, err := db.FetchPayment(ctx, paymentHash[:]) diff --git a/routing/control_tower.go b/routing/control_tower.go index 718dca3ff5..8df87b473b 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -20,7 +20,8 @@ type ControlTower interface { // also notifies subscribers of the payment creation. // // NOTE: Subscribers should be notified by the new state of the payment. - InitPayment(lntypes.Hash, *paymentsdb.PaymentCreationInfo) error + InitPayment(context.Context, lntypes.Hash, + *paymentsdb.PaymentCreationInfo) error // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are @@ -164,12 +165,10 @@ func NewControlTower(db paymentsdb.DB) ControlTower { // making sure it does not already exist as an in-flight payment. Then this // method returns successfully, the payment is guaranteed to be in the // Initiated state. -func (p *controlTower) InitPayment(paymentHash lntypes.Hash, - info *paymentsdb.PaymentCreationInfo) error { +func (p *controlTower) InitPayment(ctx context.Context, + paymentHash lntypes.Hash, info *paymentsdb.PaymentCreationInfo) error { - ctx := context.TODO() - - err := p.db.InitPayment(paymentHash, info) + err := p.db.InitPayment(ctx, paymentHash, info) if err != nil { return err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index de0aacf880..0993fb2a68 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -81,7 +81,7 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { t.Fatal(err) } - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { info1, attempt1, preimg1, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info1.PaymentIdentifier, info1) + err = pControl.InitPayment(t.Context(), info1.PaymentIdentifier, info1) require.NoError(t, err) // Subscription should succeed and immediately report the Initiated @@ -228,7 +228,7 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { info2, attempt2, preimg2, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info2.PaymentIdentifier, info2) + err = pControl.InitPayment(t.Context(), info2.PaymentIdentifier, info2) require.NoError(t, err) // Register an attempt on the second payment. @@ -337,7 +337,7 @@ func TestKVStoreSubscribeAllImmediate(t *testing.T) { info, attempt, _, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) require.NoError(t, err) // Register a payment update. @@ -392,7 +392,7 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { info, attempt, _, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) require.NoError(t, err) // Assert all subscriptions receive the update. @@ -465,7 +465,7 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, t.Fatal(err) } - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) if err != nil { t.Fatal(err) } diff --git a/routing/mock_test.go b/routing/mock_test.go index b30627165d..5b9d4854b1 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -297,8 +297,8 @@ func makeMockControlTower() *mockControlTowerOld { } } -func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash, - c *paymentsdb.PaymentCreationInfo) error { +func (m *mockControlTowerOld) InitPayment(_ context.Context, + phash lntypes.Hash, c *paymentsdb.PaymentCreationInfo) error { if m.init != nil { m.init <- initArgs{c} @@ -734,7 +734,7 @@ type mockControlTower struct { var _ ControlTower = (*mockControlTower)(nil) -func (m *mockControlTower) InitPayment(phash lntypes.Hash, +func (m *mockControlTower) InitPayment(_ context.Context, phash lntypes.Hash, c *paymentsdb.PaymentCreationInfo) error { args := m.Called(phash, c) diff --git a/routing/router.go b/routing/router.go index f713216913..55d58e78bd 100644 --- a/routing/router.go +++ b/routing/router.go @@ -967,6 +967,8 @@ func spewPayment(payment *LightningPayment) lnutils.LogClosure { func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( PaymentSession, shards.ShardTracker, error) { + ctx := context.TODO() + // Assemble any custom data we want to send to the first hop only. var firstHopData fn.Option[tlv.Blob] if len(payment.FirstHopCustomRecords) > 0 { @@ -1026,7 +1028,7 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( ) } - err = r.cfg.Control.InitPayment(payment.Identifier(), info) + err = r.cfg.Control.InitPayment(ctx, payment.Identifier(), info) if err != nil { return nil, nil, err } @@ -1131,7 +1133,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, FirstHopCustomRecords: firstHopCustomRecords, } - err := r.cfg.Control.InitPayment(paymentIdentifier, info) + err := r.cfg.Control.InitPayment(ctx, paymentIdentifier, info) switch { // If this is an MPP attempt and the hash is already registered with // the database, we can go on to launch the shard. From 82e20d76ba778ceea380993b5eec2ad0615cdcab Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 20 Oct 2025 22:18:09 +0200 Subject: [PATCH 35/40] multi: thread context through RegisterAttempt method --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 2 +- payments/db/payment_test.go | 48 +++++++++++++++++++++++------------ payments/db/sql_store.go | 7 +++-- routing/control_tower.go | 9 ++++--- routing/control_tower_test.go | 28 +++++++++++++------- routing/mock_test.go | 8 +++--- routing/payment_lifecycle.go | 4 ++- 9 files changed, 70 insertions(+), 41 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index bea4aee74f..ad54cb53c5 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -64,7 +64,8 @@ type PaymentControl interface { InitPayment(context.Context, lntypes.Hash, *PaymentCreationInfo) error // RegisterAttempt atomically records the provided HTLCAttemptInfo. - RegisterAttempt(lntypes.Hash, *HTLCAttemptInfo) (*MPPayment, error) + RegisterAttempt(context.Context, lntypes.Hash, + *HTLCAttemptInfo) (*MPPayment, error) // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index c4cd67acb6..70b81c2fca 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -359,7 +359,7 @@ func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) { // RegisterAttempt atomically records the provided HTLCAttemptInfo to the // DB. -func (p *KVStore) RegisterAttempt(paymentHash lntypes.Hash, +func (p *KVStore) RegisterAttempt(_ context.Context, paymentHash lntypes.Hash, attempt *HTLCAttemptInfo) (*MPPayment, error) { // Serialize the information before opening the db transaction. diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 5d98374201..d0ad1f432e 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -80,7 +80,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { t.Fatalf("unable to send htlc message: %v", err) } _, err = paymentDB.RegisterAttempt( - info.PaymentIdentifier, attempt, + ctx, info.PaymentIdentifier, attempt, ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index e49f93e1b5..36cb31ec40 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -143,7 +143,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { require.NoError(t, err, "unable to send htlc message") // Register and fail the first attempt for all payments. - _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = p.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") htlcFailure := HTLCFailUnreadable @@ -167,7 +167,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { require.NoError(t, err) attemptID++ - _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = p.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") switch payments[i].status { @@ -584,7 +584,7 @@ func TestMPPRecordValidation(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") // Now try to register a non-MPP attempt, which should fail. @@ -596,21 +596,27 @@ func TestMPPRecordValidation(t *testing.T) { attempt2.Route.FinalHop().MPP = nil - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPayment) // Try to register attempt one with a different payment address. attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{2}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPPaymentAddrMismatch) // Try registering one with a different total amount. attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value/2, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPTotalAmountMismatch) // Create and init a new payment. This time we'll check that we cannot @@ -633,7 +639,9 @@ func TestMPPRecordValidation(t *testing.T) { require.NoError(t, err, "unable to send htlc message") attempt.Route.FinalHop().MPP = nil - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt, + ) require.NoError(t, err, "unable to send htlc message") // Attempt to register an MPP attempt, which should fail. @@ -647,7 +655,9 @@ func TestMPPRecordValidation(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt2) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrNonMPPayment) } @@ -1452,7 +1462,7 @@ func TestSwitchDoubleSend(t *testing.T) { require.ErrorIs(t, err, ErrPaymentExists) // Record an attempt. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInFlight, @@ -1563,7 +1573,7 @@ func TestSwitchFail(t *testing.T) { // Record a new attempt. In this test scenario, the attempt fails. // However, this is not communicated to control tower in the current // implementation. It only registers the initiation of the attempt. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to register attempt") htlcReason := HTLCFailUnreadable @@ -1593,7 +1603,7 @@ func TestSwitchFail(t *testing.T) { ) require.NoError(t, err) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInFlight, @@ -1711,7 +1721,7 @@ func TestMultiShard(t *testing.T) { attempts = append(attempts, a) _, err = paymentDB.RegisterAttempt( - info.PaymentIdentifier, a, + ctx, info.PaymentIdentifier, a, ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) @@ -1743,7 +1753,9 @@ func TestMultiShard(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) require.ErrorIs(t, err, ErrValueExceedsAmt) // Fail the second attempt. @@ -1850,7 +1862,9 @@ func TestMultiShard(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) if test.settleFirst { require.ErrorIs( t, err, ErrPaymentPendingSettled, @@ -1949,7 +1963,9 @@ func TestMultiShard(t *testing.T) { ) // Finally assert we cannot register more attempts. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) require.ErrorIs(t, err, registerErr) } @@ -2352,7 +2368,7 @@ func TestQueryPayments(t *testing.T) { require.NoError(t, err) _, err = paymentDB.RegisterAttempt( - lastPaymentInfo.PaymentIdentifier, + ctx, lastPaymentInfo.PaymentIdentifier, &attempt.HTLCAttemptInfo, ) require.NoError(t, err) diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 553fbbc80b..4d3c12336b 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -886,10 +886,9 @@ func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, // RegisterAttempt registers an attempt for a payment. // // This is part of the DB interface. -func (s *SQLStore) RegisterAttempt(paymentHash lntypes.Hash, - attempt *HTLCAttemptInfo) (*MPPayment, error) { - - ctx := context.TODO() +func (s *SQLStore) RegisterAttempt(ctx context.Context, + paymentHash lntypes.Hash, attempt *HTLCAttemptInfo) (*MPPayment, + error) { var mpPayment *MPPayment diff --git a/routing/control_tower.go b/routing/control_tower.go index 8df87b473b..28432d7322 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -31,7 +31,8 @@ type ControlTower interface { // RegisterAttempt atomically records the provided HTLCAttemptInfo. // // NOTE: Subscribers should be notified by the new state of the payment. - RegisterAttempt(lntypes.Hash, *paymentsdb.HTLCAttemptInfo) error + RegisterAttempt(context.Context, lntypes.Hash, + *paymentsdb.HTLCAttemptInfo) error // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the the @@ -196,13 +197,13 @@ func (p *controlTower) DeleteFailedAttempts(paymentHash lntypes.Hash) error { // RegisterAttempt atomically records the provided HTLCAttemptInfo to the // DB. -func (p *controlTower) RegisterAttempt(paymentHash lntypes.Hash, - attempt *paymentsdb.HTLCAttemptInfo) error { +func (p *controlTower) RegisterAttempt(ctx context.Context, + paymentHash lntypes.Hash, attempt *paymentsdb.HTLCAttemptInfo) error { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.RegisterAttempt(paymentHash, attempt) + payment, err := p.db.RegisterAttempt(ctx, paymentHash, attempt) if err != nil { return err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 0993fb2a68..20bdd17564 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -92,7 +92,9 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { require.NoError(t, err, "expected subscribe to succeed, but got") // Register an attempt. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) if err != nil { t.Fatal(err) } @@ -221,7 +223,9 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { require.NoError(t, err, "expected subscribe to succeed, but got: %v") // Register an attempt. - err = pControl.RegisterAttempt(info1.PaymentIdentifier, attempt1) + err = pControl.RegisterAttempt( + t.Context(), info1.PaymentIdentifier, attempt1, + ) require.NoError(t, err) // Initiate a second payment after the subscription is already active. @@ -232,7 +236,9 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { require.NoError(t, err) // Register an attempt on the second payment. - err = pControl.RegisterAttempt(info2.PaymentIdentifier, attempt2) + err = pControl.RegisterAttempt( + t.Context(), info2.PaymentIdentifier, attempt2, + ) require.NoError(t, err) // Mark the first payment as successful. @@ -341,7 +347,9 @@ func TestKVStoreSubscribeAllImmediate(t *testing.T) { require.NoError(t, err) // Register a payment update. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) require.NoError(t, err) subscription, err := pControl.SubscribeAllPayments() @@ -414,7 +422,9 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { subscription1.Close() // Register a payment update. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) require.NoError(t, err) // Assert only subscription 2 receives the update. @@ -479,10 +489,10 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, // making any attempts at all. if registerAttempt { // Register an attempt. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) - if err != nil { - t.Fatal(err) - } + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) + require.NoError(t, err) // Fail the payment attempt. failInfo := paymentsdb.HTLCFailInfo{ diff --git a/routing/mock_test.go b/routing/mock_test.go index 5b9d4854b1..daad344fdf 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -354,8 +354,8 @@ func (m *mockControlTowerOld) DeleteFailedAttempts(phash lntypes.Hash) error { return nil } -func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash, - a *paymentsdb.HTLCAttemptInfo) error { +func (m *mockControlTowerOld) RegisterAttempt(_ context.Context, + phash lntypes.Hash, a *paymentsdb.HTLCAttemptInfo) error { if m.registerAttempt != nil { m.registerAttempt <- registerAttemptArgs{a} @@ -746,8 +746,8 @@ func (m *mockControlTower) DeleteFailedAttempts(phash lntypes.Hash) error { return args.Error(0) } -func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash, - a *paymentsdb.HTLCAttemptInfo) error { +func (m *mockControlTower) RegisterAttempt(_ context.Context, + phash lntypes.Hash, a *paymentsdb.HTLCAttemptInfo) error { args := m.Called(phash, a) return args.Error(0) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 4eb78c8e22..0499475416 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -584,6 +584,8 @@ func (p *paymentLifecycle) collectResult( func (p *paymentLifecycle) registerAttempt(rt *route.Route, remainingAmt lnwire.MilliSatoshi) (*paymentsdb.HTLCAttempt, error) { + ctx := context.TODO() + // If this route will consume the last remaining amount to send // to the receiver, this will be our last shard (for now). isLastAttempt := rt.ReceiverAmt() == remainingAmt @@ -601,7 +603,7 @@ func (p *paymentLifecycle) registerAttempt(rt *route.Route, // Switch for its whereabouts. The route is needed to handle the result // when it eventually comes back. err = p.router.cfg.Control.RegisterAttempt( - p.identifier, &attempt.HTLCAttemptInfo, + ctx, p.identifier, &attempt.HTLCAttemptInfo, ) return attempt, err From 4a27dbca0c5876d9ca6596c38983bf2d52b392e8 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 08:54:50 +0200 Subject: [PATCH 36/40] multi: thread context through SettleAttempt --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 2 +- payments/db/payment_test.go | 13 +++++++------ payments/db/sql_store.go | 4 +--- routing/control_tower.go | 15 +++++++++------ routing/control_tower_test.go | 9 ++++++--- routing/mock_test.go | 6 +++--- routing/payment_lifecycle.go | 4 +++- 9 files changed, 33 insertions(+), 25 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index ad54cb53c5..f051de1f2b 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -75,7 +75,8 @@ type PaymentControl interface { // error to prevent us from making duplicate payments to the same // payment hash. The provided preimage is atomically saved to the DB // for record keeping. - SettleAttempt(lntypes.Hash, uint64, *HTLCSettleInfo) (*MPPayment, error) + SettleAttempt(context.Context, lntypes.Hash, uint64, + *HTLCSettleInfo) (*MPPayment, error) // FailAttempt marks the given payment attempt failed. FailAttempt(lntypes.Hash, uint64, *HTLCFailInfo) (*MPPayment, error) diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index 70b81c2fca..c1c2e9c9bc 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -430,7 +430,7 @@ func (p *KVStore) RegisterAttempt(_ context.Context, paymentHash lntypes.Hash, // After invoking this method, InitPayment should always return an error to // prevent us from making duplicate payments to the same payment hash. The // provided preimage is atomically saved to the DB for record keeping. -func (p *KVStore) SettleAttempt(hash lntypes.Hash, +func (p *KVStore) SettleAttempt(_ context.Context, hash lntypes.Hash, attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { var b bytes.Buffer diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index d0ad1f432e..4495880e68 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -128,7 +128,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { case p.success: // Verifies that status was changed to StatusSucceeded. _, err := paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 36cb31ec40..4b301cce79 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -190,7 +190,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { // Settle the attempt case StatusSucceeded: _, err := p.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1337,6 +1337,7 @@ func TestSuccessesWithoutInFlight(t *testing.T) { // Attempt to complete the payment should fail. _, err = paymentDB.SettleAttempt( + t.Context(), info.PaymentIdentifier, 0, &HTLCSettleInfo{ Preimage: preimg, @@ -1484,7 +1485,7 @@ func TestSwitchDoubleSend(t *testing.T) { // After settling, the error should be ErrAlreadyPaid. _, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1620,7 +1621,7 @@ func TestSwitchFail(t *testing.T) { // Settle the attempt and verify that status was changed to // StatusSucceeded. payment, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1793,7 +1794,7 @@ func TestMultiShard(t *testing.T) { var firstFailReason *FailureReason if test.settleFirst { _, err := paymentDB.SettleAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1887,7 +1888,7 @@ func TestMultiShard(t *testing.T) { if test.settleLast { // Settle the last outstanding attempt. _, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -2377,7 +2378,7 @@ func TestQueryPayments(t *testing.T) { copy(preimg[:], rev[:]) _, err = paymentDB.SettleAttempt( - lastPaymentInfo.PaymentIdentifier, + ctx, lastPaymentInfo.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 4d3c12336b..4f9eeafdc8 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -991,11 +991,9 @@ func (s *SQLStore) RegisterAttempt(ctx context.Context, } // SettleAttempt marks the given attempt settled with the preimage. -func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash, +func (s *SQLStore) SettleAttempt(ctx context.Context, paymentHash lntypes.Hash, attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { - ctx := context.TODO() - var mpPayment *MPPayment err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { diff --git a/routing/control_tower.go b/routing/control_tower.go index 28432d7322..163aec3e75 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -44,8 +44,8 @@ type ControlTower interface { // for record keeping. // // NOTE: Subscribers should be notified by the new state of the payment. - SettleAttempt(lntypes.Hash, uint64, *paymentsdb.HTLCSettleInfo) ( - *paymentsdb.HTLCAttempt, error) + SettleAttempt(context.Context, lntypes.Hash, uint64, + *paymentsdb.HTLCSettleInfo) (*paymentsdb.HTLCAttempt, error) // FailAttempt marks the given payment attempt failed. // @@ -217,14 +217,17 @@ func (p *controlTower) RegisterAttempt(ctx context.Context, // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the the // full payment succeeded. -func (p *controlTower) SettleAttempt(paymentHash lntypes.Hash, - attemptID uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( - *paymentsdb.HTLCAttempt, error) { +func (p *controlTower) SettleAttempt(ctx context.Context, + paymentHash lntypes.Hash, attemptID uint64, + settleInfo *paymentsdb.HTLCSettleInfo) (*paymentsdb.HTLCAttempt, + error) { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.SettleAttempt(paymentHash, attemptID, settleInfo) + payment, err := p.db.SettleAttempt( + ctx, paymentHash, attemptID, settleInfo, + ) if err != nil { return nil, err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 20bdd17564..20e8e82e42 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -108,7 +108,8 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { Preimage: preimg, } htlcAttempt, err := pControl.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, &settleInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &settleInfo, ) if err != nil { t.Fatal(err) @@ -246,7 +247,8 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { Preimage: preimg1, } htlcAttempt1, err := pControl.SettleAttempt( - info1.PaymentIdentifier, attempt1.AttemptID, &settleInfo1, + t.Context(), info1.PaymentIdentifier, attempt1.AttemptID, + &settleInfo1, ) require.NoError(t, err) require.Equal( @@ -259,7 +261,8 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { Preimage: preimg2, } htlcAttempt2, err := pControl.SettleAttempt( - info2.PaymentIdentifier, attempt2.AttemptID, &settleInfo2, + t.Context(), info2.PaymentIdentifier, attempt2.AttemptID, + &settleInfo2, ) require.NoError(t, err) require.Equal( diff --git a/routing/mock_test.go b/routing/mock_test.go index daad344fdf..77f98ab017 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -408,8 +408,8 @@ func (m *mockControlTowerOld) RegisterAttempt(_ context.Context, return nil } -func (m *mockControlTowerOld) SettleAttempt(phash lntypes.Hash, - pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( +func (m *mockControlTowerOld) SettleAttempt(_ context.Context, + phash lntypes.Hash, pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( *paymentsdb.HTLCAttempt, error) { if m.settleAttempt != nil { @@ -753,7 +753,7 @@ func (m *mockControlTower) RegisterAttempt(_ context.Context, return args.Error(0) } -func (m *mockControlTower) SettleAttempt(phash lntypes.Hash, +func (m *mockControlTower) SettleAttempt(_ context.Context, phash lntypes.Hash, pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( *paymentsdb.HTLCAttempt, error) { diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 0499475416..a2e3935a0b 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -1166,6 +1166,8 @@ func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, result *htlcswitch.PaymentResult) (*attemptResult, error) { + ctx := context.TODO() + // If the result has an error, we need to further process it by failing // the attempt and maybe fail the payment. if result.Error != nil { @@ -1187,7 +1189,7 @@ func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, // In case of success we atomically store settle result to the DB and // move the shard to the settled state. htlcAttempt, err := p.router.cfg.Control.SettleAttempt( - p.identifier, attempt.AttemptID, + ctx, p.identifier, attempt.AttemptID, &paymentsdb.HTLCSettleInfo{ Preimage: result.Preimage, SettleTime: p.router.cfg.Clock.Now(), From 6b88d04af17401d2a20e15584af1bfdaaa630b06 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 09:02:22 +0200 Subject: [PATCH 37/40] multi: thread context through FailAttempt --- payments/db/interface.go | 3 ++- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 2 +- payments/db/payment_test.go | 12 ++++++------ payments/db/sql_store.go | 4 +--- routing/control_tower.go | 12 ++++++------ routing/control_tower_test.go | 6 ++++-- routing/mock_test.go | 10 ++++++---- routing/payment_lifecycle.go | 4 +++- routing/router.go | 4 +++- 10 files changed, 33 insertions(+), 26 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index f051de1f2b..dfb4fa48a9 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -79,7 +79,8 @@ type PaymentControl interface { *HTLCSettleInfo) (*MPPayment, error) // FailAttempt marks the given payment attempt failed. - FailAttempt(lntypes.Hash, uint64, *HTLCFailInfo) (*MPPayment, error) + FailAttempt(context.Context, lntypes.Hash, uint64, + *HTLCFailInfo) (*MPPayment, error) // Fail transitions a payment into the Failed state, and records // the ultimate reason the payment failed. Note that this should only diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index c1c2e9c9bc..ebaf7d7ad5 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -443,7 +443,7 @@ func (p *KVStore) SettleAttempt(_ context.Context, hash lntypes.Hash, } // FailAttempt marks the given payment attempt failed. -func (p *KVStore) FailAttempt(hash lntypes.Hash, +func (p *KVStore) FailAttempt(_ context.Context, hash lntypes.Hash, attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { var b bytes.Buffer diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 4495880e68..5c42c7b0d2 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -95,7 +95,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { // Fail the payment attempt. htlcFailure := HTLCFailUnreadable _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, }, diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index 4b301cce79..efe192cbde 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -148,7 +148,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { htlcFailure := HTLCFailUnreadable _, err = p.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, }, @@ -175,7 +175,7 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { case StatusFailed: htlcFailure := HTLCFailUnreadable _, err = p.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, }, @@ -1579,7 +1579,7 @@ func TestSwitchFail(t *testing.T) { htlcReason := HTLCFailUnreadable _, err = paymentDB.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcReason, }, @@ -1763,7 +1763,7 @@ func TestMultiShard(t *testing.T) { a := attempts[1] htlcFail := HTLCFailUnreadable _, err = paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, @@ -1812,7 +1812,7 @@ func TestMultiShard(t *testing.T) { ) } else { _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, @@ -1903,7 +1903,7 @@ func TestMultiShard(t *testing.T) { } else { // Fail the attempt. _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 4f9eeafdc8..e0ebc04cce 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1037,11 +1037,9 @@ func (s *SQLStore) SettleAttempt(ctx context.Context, paymentHash lntypes.Hash, } // FailAttempt marks the given attempt failed. -func (s *SQLStore) FailAttempt(paymentHash lntypes.Hash, +func (s *SQLStore) FailAttempt(ctx context.Context, paymentHash lntypes.Hash, attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { - ctx := context.TODO() - var mpPayment *MPPayment err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { diff --git a/routing/control_tower.go b/routing/control_tower.go index 163aec3e75..cbb79d4c79 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -50,8 +50,8 @@ type ControlTower interface { // FailAttempt marks the given payment attempt failed. // // NOTE: Subscribers should be notified by the new state of the payment. - FailAttempt(lntypes.Hash, uint64, *paymentsdb.HTLCFailInfo) ( - *paymentsdb.HTLCAttempt, error) + FailAttempt(context.Context, lntypes.Hash, uint64, + *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) // FetchPayment fetches the payment corresponding to the given payment // hash. @@ -239,14 +239,14 @@ func (p *controlTower) SettleAttempt(ctx context.Context, } // FailAttempt marks the given payment attempt failed. -func (p *controlTower) FailAttempt(paymentHash lntypes.Hash, - attemptID uint64, failInfo *paymentsdb.HTLCFailInfo) ( - *paymentsdb.HTLCAttempt, error) { +func (p *controlTower) FailAttempt(ctx context.Context, + paymentHash lntypes.Hash, attemptID uint64, + failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FailAttempt(paymentHash, attemptID, failInfo) + payment, err := p.db.FailAttempt(ctx, paymentHash, attemptID, failInfo) if err != nil { return nil, err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 20e8e82e42..5241d814df 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -448,7 +448,8 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { Reason: paymentsdb.HTLCFailInternal, } _, err = pControl.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, &failInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &failInfo, ) require.NoError(t, err, "unable to fail htlc") @@ -502,7 +503,8 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, Reason: paymentsdb.HTLCFailInternal, } htlcAttempt, err := pControl.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, &failInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &failInfo, ) if err != nil { t.Fatalf("unable to fail htlc: %v", err) diff --git a/routing/mock_test.go b/routing/mock_test.go index 77f98ab017..f10c38ad0d 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -451,8 +451,9 @@ func (m *mockControlTowerOld) SettleAttempt(_ context.Context, return nil, fmt.Errorf("pid not found") } -func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64, - failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { +func (m *mockControlTowerOld) FailAttempt(_ context.Context, phash lntypes.Hash, + pid uint64, failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, + error) { if m.failAttempt != nil { m.failAttempt <- failAttemptArgs{failInfo} @@ -767,8 +768,9 @@ func (m *mockControlTower) SettleAttempt(_ context.Context, phash lntypes.Hash, return attempt.(*paymentsdb.HTLCAttempt), args.Error(1) } -func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64, - failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { +func (m *mockControlTower) FailAttempt(_ context.Context, phash lntypes.Hash, + pid uint64, failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, + error) { args := m.Called(phash, pid, failInfo) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index a2e3935a0b..904d399cd1 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -1003,6 +1003,8 @@ func (p *paymentLifecycle) handleFailureMessage(rt *route.Route, func (p *paymentLifecycle) failAttempt(attemptID uint64, sendError error) (*attemptResult, error) { + ctx := context.TODO() + log.Warnf("Attempt %v for payment %v failed: %v", attemptID, p.identifier, sendError) @@ -1019,7 +1021,7 @@ func (p *paymentLifecycle) failAttempt(attemptID uint64, } attempt, err := p.router.cfg.Control.FailAttempt( - p.identifier, attemptID, failInfo, + ctx, p.identifier, attemptID, failInfo, ) if err != nil { return nil, err diff --git a/routing/router.go b/routing/router.go index 55d58e78bd..e48b7b43db 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1531,6 +1531,8 @@ func (r *ChannelRouter) resumePayments() error { func (r *ChannelRouter) failStaleAttempt(a paymentsdb.HTLCAttempt, payHash lntypes.Hash) { + ctx := context.TODO() + // We can only fail inflight HTLCs so we skip the settled/failed ones. if a.Failure != nil || a.Settle != nil { return @@ -1614,7 +1616,7 @@ func (r *ChannelRouter) failStaleAttempt(a paymentsdb.HTLCAttempt, Reason: paymentsdb.HTLCFailUnknown, FailTime: r.cfg.Clock.Now(), } - _, err = r.cfg.Control.FailAttempt(payHash, a.AttemptID, failInfo) + _, err = r.cfg.Control.FailAttempt(ctx, payHash, a.AttemptID, failInfo) if err != nil { log.Errorf("Fail attempt=%v got error: %v", a.AttemptID, err) } From ac70d700c199b675901d0a6d585f58fc5ef12827 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 10:11:32 +0200 Subject: [PATCH 38/40] multi: thread context through Fail payment functions --- payments/db/interface.go | 2 +- payments/db/kv_store.go | 2 +- payments/db/kv_store_test.go | 2 +- payments/db/payment_test.go | 13 +++++++------ payments/db/sql_store.go | 4 +--- routing/control_tower.go | 9 +++++---- routing/control_tower_test.go | 3 ++- routing/mock_test.go | 4 ++-- routing/payment_lifecycle.go | 16 +++++++++++++--- routing/router.go | 4 +++- 10 files changed, 36 insertions(+), 23 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index dfb4fa48a9..a9e235a905 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -88,7 +88,7 @@ type PaymentControl interface { // invoking this method, InitPayment should return nil on its next call // for this payment hash, allowing the user to make a subsequent // payment. - Fail(lntypes.Hash, FailureReason) (*MPPayment, error) + Fail(context.Context, lntypes.Hash, FailureReason) (*MPPayment, error) // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index ebaf7d7ad5..b4a1ea10ef 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -528,7 +528,7 @@ func (p *KVStore) updateHtlcKey(paymentHash lntypes.Hash, // payment failed. After invoking this method, InitPayment should return nil on // its next call for this payment hash, allowing the switch to make a // subsequent payment. -func (p *KVStore) Fail(paymentHash lntypes.Hash, +func (p *KVStore) Fail(_ context.Context, paymentHash lntypes.Hash, reason FailureReason) (*MPPayment, error) { var ( diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 5c42c7b0d2..b969d29d88 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -107,7 +107,7 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { // Fail the payment, which should moved it to Failed. failReason := FailureReasonNoRoute _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, + ctx, info.PaymentIdentifier, failReason, ) if err != nil { t.Fatalf("unable to fail payment hash: %v", err) diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index efe192cbde..d88c5739e5 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -183,8 +183,9 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { require.NoError(t, err, "unable to fail htlc") failReason := FailureReasonNoRoute - _, err = p.Fail(info.PaymentIdentifier, - failReason) + _, err = p.Fail( + ctx, info.PaymentIdentifier, failReason, + ) require.NoError(t, err, "unable to fail payment hash") // Settle the attempt @@ -1361,7 +1362,7 @@ func TestFailsWithoutInFlight(t *testing.T) { // Calling Fail should return an error. _, err = paymentDB.Fail( - info.PaymentIdentifier, FailureReasonNoRoute, + t.Context(), info.PaymentIdentifier, FailureReasonNoRoute, ) require.ErrorIs(t, err, ErrPaymentNotInitiated) } @@ -1537,7 +1538,7 @@ func TestSwitchFail(t *testing.T) { // Fail the payment, which should moved it to Failed. failReason := FailureReasonNoRoute - _, err = paymentDB.Fail(info.PaymentIdentifier, failReason) + _, err = paymentDB.Fail(ctx, info.PaymentIdentifier, failReason) require.NoError(t, err, "unable to fail payment hash") // Verify the status is indeed Failed. @@ -1833,7 +1834,7 @@ func TestMultiShard(t *testing.T) { // a terminal state. failReason := FailureReasonNoRoute _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, + ctx, info.PaymentIdentifier, failReason, ) if err != nil { t.Fatalf("unable to fail payment hash: %v", err) @@ -1926,7 +1927,7 @@ func TestMultiShard(t *testing.T) { // syncing. failReason := FailureReasonPaymentDetails _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, + ctx, info.PaymentIdentifier, failReason, ) require.NoError(t, err, "unable to fail") } diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index e0ebc04cce..fb5d731aee 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -1102,11 +1102,9 @@ func (s *SQLStore) FailAttempt(ctx context.Context, paymentHash lntypes.Hash, // active attempts are already failed. After invoking this method, InitPayment // should return nil on its next call for this payment hash, allowing the user // to make a subsequent payments for the same payment hash. -func (s *SQLStore) Fail(paymentHash lntypes.Hash, +func (s *SQLStore) Fail(ctx context.Context, paymentHash lntypes.Hash, reason FailureReason) (*MPPayment, error) { - ctx := context.TODO() - var mpPayment *MPPayment err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { diff --git a/routing/control_tower.go b/routing/control_tower.go index cbb79d4c79..b39a378bd4 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -66,7 +66,8 @@ type ControlTower interface { // payment. // // NOTE: Subscribers should be notified by the new state of the payment. - FailPayment(lntypes.Hash, paymentsdb.FailureReason) error + FailPayment(context.Context, lntypes.Hash, + paymentsdb.FailureReason) error // FetchInFlightPayments returns all payments with status InFlight. FetchInFlightPayments(ctx context.Context) ([]*paymentsdb.MPPayment, @@ -272,13 +273,13 @@ func (p *controlTower) FetchPayment(ctx context.Context, // // NOTE: This method will overwrite the failure reason if the payment is already // failed. -func (p *controlTower) FailPayment(paymentHash lntypes.Hash, - reason paymentsdb.FailureReason) error { +func (p *controlTower) FailPayment(ctx context.Context, + paymentHash lntypes.Hash, reason paymentsdb.FailureReason) error { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.Fail(paymentHash, reason) + payment, err := p.db.Fail(ctx, paymentHash, reason) if err != nil { return err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 5241d814df..c9e8f48573 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -516,7 +516,8 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, // Mark the payment as failed. err = pControl.FailPayment( - info.PaymentIdentifier, paymentsdb.FailureReasonTimeout, + t.Context(), info.PaymentIdentifier, + paymentsdb.FailureReasonTimeout, ) if err != nil { t.Fatal(err) diff --git a/routing/mock_test.go b/routing/mock_test.go index f10c38ad0d..e72b392496 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -491,7 +491,7 @@ func (m *mockControlTowerOld) FailAttempt(_ context.Context, phash lntypes.Hash, return nil, fmt.Errorf("pid not found") } -func (m *mockControlTowerOld) FailPayment(phash lntypes.Hash, +func (m *mockControlTowerOld) FailPayment(_ context.Context, phash lntypes.Hash, reason paymentsdb.FailureReason) error { m.Lock() @@ -782,7 +782,7 @@ func (m *mockControlTower) FailAttempt(_ context.Context, phash lntypes.Hash, return attempt.(*paymentsdb.HTLCAttempt), args.Error(1) } -func (m *mockControlTower) FailPayment(phash lntypes.Hash, +func (m *mockControlTower) FailPayment(_ context.Context, phash lntypes.Hash, reason paymentsdb.FailureReason) error { args := m.Called(phash, reason) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 904d399cd1..a70e0a0594 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -368,7 +368,9 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { // inflight HTLCs or not, its status will now either be // `StatusInflight` or `StatusFailed`. In either case, no more // HTLCs will be attempted. - err := p.router.cfg.Control.FailPayment(p.identifier, reason) + err := p.router.cfg.Control.FailPayment( + ctx, p.identifier, reason, + ) if err != nil { return fmt.Errorf("FailPayment got %w", err) } @@ -389,6 +391,8 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { func (p *paymentLifecycle) requestRoute( ps *paymentsdb.MPPaymentState) (*route.Route, error) { + ctx := context.TODO() + remainingFees := p.calcFeeBudget(ps.FeesPaid) // Query our payment session to construct a route. @@ -430,7 +434,9 @@ func (p *paymentLifecycle) requestRoute( log.Warnf("Marking payment %v permanently failed with no route: %v", p.identifier, failureCode) - err = p.router.cfg.Control.FailPayment(p.identifier, failureCode) + err = p.router.cfg.Control.FailPayment( + ctx, p.identifier, failureCode, + ) if err != nil { return nil, fmt.Errorf("FailPayment got: %w", err) } @@ -800,6 +806,8 @@ func (p *paymentLifecycle) failPaymentAndAttempt( attemptID uint64, reason *paymentsdb.FailureReason, sendErr error) (*attemptResult, error) { + ctx := context.TODO() + log.Errorf("Payment %v failed: final_outcome=%v, raw_err=%v", p.identifier, *reason, sendErr) @@ -808,7 +816,9 @@ func (p *paymentLifecycle) failPaymentAndAttempt( // NOTE: we must fail the payment first before failing the attempt. // Otherwise, once the attempt is marked as failed, another goroutine // might make another attempt while we are failing the payment. - err := p.router.cfg.Control.FailPayment(p.identifier, *reason) + err := p.router.cfg.Control.FailPayment( + ctx, p.identifier, *reason, + ) if err != nil { log.Errorf("Unable to fail payment: %v", err) return nil, err diff --git a/routing/router.go b/routing/router.go index e48b7b43db..acd572e638 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1088,7 +1088,9 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, return nil } - return r.cfg.Control.FailPayment(paymentIdentifier, reason) + return r.cfg.Control.FailPayment( + ctx, paymentIdentifier, reason, + ) } log.Debugf("SendToRoute for payment %v with skipTempErr=%v", From b1d607881e1c37faa7dcde3579b938fdaa291012 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 10:18:02 +0200 Subject: [PATCH 39/40] multi: thread context through DeleteFailedAttempts --- payments/db/interface.go | 2 +- payments/db/kv_store.go | 4 ++-- payments/db/payment_test.go | 26 +++++++++++++++++++------- payments/db/sql_store.go | 5 +++-- routing/control_tower.go | 8 +++++--- routing/mock_test.go | 8 ++++++-- routing/payment_lifecycle.go | 2 +- 7 files changed, 37 insertions(+), 18 deletions(-) diff --git a/payments/db/interface.go b/payments/db/interface.go index a9e235a905..d788b7a221 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -93,7 +93,7 @@ type PaymentControl interface { // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are // completed, and the payment has reached a final terminal state. - DeleteFailedAttempts(lntypes.Hash) error + DeleteFailedAttempts(context.Context, lntypes.Hash) error } // DBMPPayment is an interface that represents the payment state during a diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index b4a1ea10ef..f38f3235b6 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -290,8 +290,8 @@ func (p *KVStore) InitPayment(_ context.Context, paymentHash lntypes.Hash, // DeleteFailedAttempts deletes all failed htlcs for a payment if configured // by the KVStore db. -func (p *KVStore) DeleteFailedAttempts(hash lntypes.Hash) error { - ctx := context.TODO() +func (p *KVStore) DeleteFailedAttempts(ctx context.Context, + hash lntypes.Hash) error { if !p.keepFailedPaymentAttempts { const failedHtlcsOnly = true diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index d88c5739e5..d511bfa6fc 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -495,7 +495,9 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // Calling DeleteFailedAttempts on a failed payment should delete all // HTLCs. - require.NoError(t, paymentDB.DeleteFailedAttempts(payments[0].id)) + require.NoError(t, paymentDB.DeleteFailedAttempts( + t.Context(), payments[0].id, + )) // Expect all HTLCs to be deleted if the config is set to delete them. if !keepFailedPaymentAttempts { @@ -510,11 +512,15 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // operation are performed in general therefore we do NOT expect an // error in this case. if keepFailedPaymentAttempts { - require.NoError(t, paymentDB.DeleteFailedAttempts( - payments[1].id), + err := paymentDB.DeleteFailedAttempts( + t.Context(), payments[1].id, ) + require.NoError(t, err) } else { - require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id)) + err := paymentDB.DeleteFailedAttempts( + t.Context(), payments[1].id, + ) + require.Error(t, err) } // Since DeleteFailedAttempts returned an error, we should expect the @@ -522,7 +528,9 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { assertDBPayments(t, paymentDB, payments) // Cleaning up a successful payment should remove failed htlcs. - require.NoError(t, paymentDB.DeleteFailedAttempts(payments[2].id)) + require.NoError(t, paymentDB.DeleteFailedAttempts( + t.Context(), payments[2].id, + )) // Expect all HTLCs except for the settled one to be deleted if the // config is set to delete them. @@ -539,13 +547,17 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // payments, if the control tower is configured to keep failed // HTLCs. require.NoError( - t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash), + t, paymentDB.DeleteFailedAttempts( + t.Context(), lntypes.ZeroHash, + ), ) } else { // Attempting to cleanup a non-existent payment returns an // error. require.Error( - t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash), + t, paymentDB.DeleteFailedAttempts( + t.Context(), lntypes.ZeroHash, + ), ) } } diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index fb5d731aee..a7efba8afc 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -640,13 +640,14 @@ func (s *SQLStore) DeletePayment(ctx context.Context, paymentHash lntypes.Hash, // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are // completed, and the payment has reached a final terminal state. -func (s *SQLStore) DeleteFailedAttempts(paymentHash lntypes.Hash) error { +func (s *SQLStore) DeleteFailedAttempts(ctx context.Context, + paymentHash lntypes.Hash) error { + // In case we are configured to keep failed payment attempts, we exit // early. if s.keepFailedPaymentAttempts { return nil } - ctx := context.TODO() err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { // We first fetch the payment to get the payment ID. diff --git a/routing/control_tower.go b/routing/control_tower.go index b39a378bd4..1c246f17d9 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -26,7 +26,7 @@ type ControlTower interface { // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are // completed, and the payment has reached a final settled state. - DeleteFailedAttempts(lntypes.Hash) error + DeleteFailedAttempts(context.Context, lntypes.Hash) error // RegisterAttempt atomically records the provided HTLCAttemptInfo. // @@ -192,8 +192,10 @@ func (p *controlTower) InitPayment(ctx context.Context, // DeleteFailedAttempts deletes all failed htlcs if the payment was // successfully settled. -func (p *controlTower) DeleteFailedAttempts(paymentHash lntypes.Hash) error { - return p.db.DeleteFailedAttempts(paymentHash) +func (p *controlTower) DeleteFailedAttempts(ctx context.Context, + paymentHash lntypes.Hash) error { + + return p.db.DeleteFailedAttempts(ctx, paymentHash) } // RegisterAttempt atomically records the provided HTLCAttemptInfo to the diff --git a/routing/mock_test.go b/routing/mock_test.go index e72b392496..472f126162 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -328,7 +328,9 @@ func (m *mockControlTowerOld) InitPayment(_ context.Context, return nil } -func (m *mockControlTowerOld) DeleteFailedAttempts(phash lntypes.Hash) error { +func (m *mockControlTowerOld) DeleteFailedAttempts(_ context.Context, + phash lntypes.Hash) error { + p, ok := m.payments[phash] if !ok { return paymentsdb.ErrPaymentNotInitiated @@ -742,7 +744,9 @@ func (m *mockControlTower) InitPayment(_ context.Context, phash lntypes.Hash, return args.Error(0) } -func (m *mockControlTower) DeleteFailedAttempts(phash lntypes.Hash) error { +func (m *mockControlTower) DeleteFailedAttempts(_ context.Context, + phash lntypes.Hash) error { + args := m.Called(phash) return args.Error(0) } diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index a70e0a0594..f0342a5e19 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -328,7 +328,7 @@ lifecycle: // Optionally delete the failed attempts from the database. Depends on // the database options deleting attempts is not allowed so this will // just be a no-op. - err = p.router.cfg.Control.DeleteFailedAttempts(p.identifier) + err = p.router.cfg.Control.DeleteFailedAttempts(ctx, p.identifier) if err != nil { log.Errorf("Error deleting failed htlc attempts for payment "+ "%v: %v", p.identifier, err) From bc81c3a89c401878c2bc6f1c62e0b3cddadac66a Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 10:42:44 +0200 Subject: [PATCH 40/40] docs: add release notes --- docs/release-notes/release-notes-0.21.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index d7f416f5be..bfafbcc857 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -60,6 +60,9 @@ SQL Backend](https://github.com/lightningnetwork/lnd/pull/10291) * Finalize SQL payments implementation [enabling unit and itests for SQL backend](https://github.com/lightningnetwork/lnd/pull/10292) + * [Thread context through payment + db functions Part 1](https://github.com/lightningnetwork/lnd/pull/10307) + ## Code Health