paymentsdb: implement FailAttempt for sql backend

This commit is contained in:
ziggie committed 2026-02-25 08:02:15 +01:00
1 parent fd6796e561
commit 53a7877af0
1 file changed
+93
+93
View File
@@ -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)
@@ -1363,6 +1365,97 @@ func (s *SQLStore) SettleAttempt(paymentHash lntypes.Hash,
return mpPayment, nil
}
// FailAttempt marks the specified HTLC attempt as failed, recording the
// failure reason, failure time, optional failure message, and the index of the
// node in the route that generated the failure. This information is atomically
// saved to the database for debugging and route optimization purposes.
//
// For single-path payments, failing the only attempt may lead to the payment
// being retried or ultimately failed via the Fail method. For multi-shard
// (MPP/AMP) payments, individual shard failures don't necessarily fail the
// entire payment; additional attempts can be registered until sufficient shards
// succeed or the payment is permanently failed.
//
// Returns the updated MPPayment with the attempt marked as failed and the
// payment state recalculated. The payment status remains StatusInFlight if
// other attempts are still in flight, or may transition based on the overall
// payment state.
//
// This method is part of the PaymentControl interface, which is embedded in
// the PaymentWriter interface and ultimately the DB interface. It represents
// step 3b in the payment lifecycle control flow (step 3a is SettleAttempt),
// called after RegisterAttempt when an HTLC fails.
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 {
dbPayment, err := db.FetchPayment(ctx, paymentHash[:])
if err != nil {
return fmt.Errorf("failed to fetch payment: %w", err)
}
paymentStatus, err := computePaymentStatusFromDB(
ctx, db, dbPayment,
)
if err != nil {
return fmt.Errorf("failed to compute payment "+
"status: %w", err)
}
// We check if the payment is updatable before failing the
// attempt.
if err := paymentStatus.updatable(); err != nil {
return fmt.Errorf("payment is not updatable: %w", err)
}
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 = fetchPaymentWithCompleteData(
ctx, s.cfg.QueryCfg, db, dbPayment,
)
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 records the ultimate reason why a payment failed. This method stores
// the failure reason for record keeping but does not enforce that all HTLC
// attempts are resolved - HTLCs may still be in flight when this is called.