multi: move payment related code into own package

This commit moves most of the code into its own package. It is
the smallest code move possible without moving import cycles and
keeping the changes to the code base as small as possible during
refactor.
This commit is contained in:
ziggie committed 2025-08-14 19:53:15 +02:00
1 parent 77a6b577eb
commit 03af9858d2
26 files changed
+1075 -948

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
package paymentsdb
import (
"encoding/binary"
"io"
"time"
"github.com/lightningnetwork/lnd/channeldb"
)
// Big endian is the preferred byte order, due to cursor scans over
// integer keys iterating in order.
var byteOrder = binary.BigEndian
// UnknownElementType is an alias for channeldb.UnknownElementType.
type UnknownElementType = channeldb.UnknownElementType
// ReadElement deserializes a single element from the provided io.Reader.
func ReadElement(r io.Reader, element interface{}) error {
err := channeldb.ReadElement(r, element)
switch {
// Known to channeldb codec.
case err == nil:
return nil
// Fail if error is not UnknownElementType.
default:
if _, ok := err.(UnknownElementType); !ok {
return err
}
}
// Process any paymentsdb-specific extensions to the codec.
switch e := element.(type) {
case *paymentIndexType:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}
// Type is still unknown to paymentsdb extensions, fail.
default:
return channeldb.NewUnknownElementType(
"ReadElement", element,
)
}
return nil
}
// WriteElement serializes a single element into the provided io.Writer.
func WriteElement(w io.Writer, element interface{}) error {
err := channeldb.WriteElement(w, element)
switch {
// Known to channeldb codec.
case err == nil:
return nil
// Fail if error is not UnknownElementType.
default:
if _, ok := err.(UnknownElementType); !ok {
return err
}
}
// Process any paymentsdb-specific extensions to the codec.
switch e := element.(type) {
case paymentIndexType:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
// Type is still unknown to paymentsdb extensions, fail.
default:
return channeldb.NewUnknownElementType(
"WriteElement", element,
)
}
return nil
}
// WriteElements serializes a variadic list of elements into the given
// io.Writer.
func WriteElements(w io.Writer, elements ...interface{}) error {
for _, element := range elements {
if err := WriteElement(w, element); err != nil {
return err
}
}
return nil
}
// ReadElements deserializes the provided io.Reader into a variadic list of
// target elements.
func ReadElements(r io.Reader, elements ...interface{}) error {
for _, element := range elements {
if err := ReadElement(r, element); err != nil {
return err
}
}
return nil
}
// deserializeTime deserializes time as unix nanoseconds.
func deserializeTime(r io.Reader) (time.Time, error) {
var scratch [8]byte
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return time.Time{}, err
}
// Convert to time.Time. Interpret unix nano time zero as a zero
// time.Time value.
unixNano := byteOrder.Uint64(scratch[:])
if unixNano == 0 {
return time.Time{}, nil
}
return time.Unix(0, int64(unixNano)), nil
}
// serializeTime serializes time as unix nanoseconds.
func serializeTime(w io.Writer, t time.Time) error {
var scratch [8]byte
// Convert to unix nano seconds, but only if time is non-zero. Calling
// UnixNano() on a zero time yields an undefined result.
var unixNano int64
if !t.IsZero() {
unixNano = t.UnixNano()
}
byteOrder.PutUint64(scratch[:], uint64(unixNano))
_, err := w.Write(scratch[:])
return err
}
+249
View File
@@ -0,0 +1,249 @@
package paymentsdb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
var (
// duplicatePaymentsBucket is the name of a optional sub-bucket within
// the payment hash bucket, that is used to hold duplicate payments to a
// payment hash. This is needed to support information from earlier
// versions of lnd, where it was possible to pay to a payment hash more
// than once.
duplicatePaymentsBucket = []byte("payment-duplicate-bucket")
// duplicatePaymentSettleInfoKey is a key used in the payment's
// sub-bucket to store the settle info of the payment.
duplicatePaymentSettleInfoKey = []byte("payment-settle-info")
// duplicatePaymentAttemptInfoKey is a key used in the payment's
// sub-bucket to store the info about the latest attempt that was done
// for the payment in question.
duplicatePaymentAttemptInfoKey = []byte("payment-attempt-info")
// duplicatePaymentCreationInfoKey is a key used in the payment's
// sub-bucket to store the creation info of the payment.
duplicatePaymentCreationInfoKey = []byte("payment-creation-info")
// duplicatePaymentFailInfoKey is a key used in the payment's sub-bucket
// to store information about the reason a payment failed.
duplicatePaymentFailInfoKey = []byte("payment-fail-info")
// duplicatePaymentSequenceKey is a key used in the payment's sub-bucket
// to store the sequence number of the payment.
duplicatePaymentSequenceKey = []byte("payment-sequence-key")
)
// duplicateHTLCAttemptInfo contains static information about a specific HTLC
// attempt for a payment. This information is used by the router to handle any
// errors coming back after an attempt is made, and to query the switch about
// the status of the attempt.
type duplicateHTLCAttemptInfo struct {
// attemptID is the unique ID used for this attempt.
attemptID uint64
// sessionKey is the ephemeral key used for this attempt.
sessionKey [btcec.PrivKeyBytesLen]byte
// route is the route attempted to send the HTLC.
route route.Route
}
// fetchDuplicatePaymentStatus fetches the payment status of the payment. If
// the payment isn't found, it will return error `ErrPaymentNotInitiated`.
func fetchDuplicatePaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) {
if bucket.Get(duplicatePaymentSettleInfoKey) != nil {
return StatusSucceeded, nil
}
if bucket.Get(duplicatePaymentFailInfoKey) != nil {
return StatusFailed, nil
}
if bucket.Get(duplicatePaymentCreationInfoKey) != nil {
return StatusInFlight, nil
}
return 0, ErrPaymentNotInitiated
}
func deserializeDuplicateHTLCAttemptInfo(r io.Reader) (
*duplicateHTLCAttemptInfo, error) {
a := &duplicateHTLCAttemptInfo{}
err := ReadElements(r, &a.attemptID, &a.sessionKey)
if err != nil {
return nil, err
}
a.route, err = DeserializeRoute(r)
if err != nil {
return nil, err
}
return a, nil
}
func deserializeDuplicatePaymentCreationInfo(r io.Reader) (
*channeldb.PaymentCreationInfo, error) {
var scratch [8]byte
c := &channeldb.PaymentCreationInfo{}
if _, err := io.ReadFull(r, c.PaymentIdentifier[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return nil, err
}
c.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:]))
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return nil, err
}
c.CreationTime = time.Unix(int64(byteOrder.Uint64(scratch[:])), 0)
if _, err := io.ReadFull(r, scratch[:4]); err != nil {
return nil, err
}
reqLen := byteOrder.Uint32(scratch[:4])
payReq := make([]byte, reqLen)
if reqLen > 0 {
if _, err := io.ReadFull(r, payReq); err != nil {
return nil, err
}
}
c.PaymentRequest = payReq
return c, nil
}
func fetchDuplicatePayment(bucket kvdb.RBucket) (*MPPayment, error) {
seqBytes := bucket.Get(duplicatePaymentSequenceKey)
if seqBytes == nil {
return nil, fmt.Errorf("sequence number not found")
}
sequenceNum := binary.BigEndian.Uint64(seqBytes)
// Get the payment status.
paymentStatus, err := fetchDuplicatePaymentStatus(bucket)
if err != nil {
return nil, err
}
// Get the PaymentCreationInfo.
b := bucket.Get(duplicatePaymentCreationInfoKey)
if b == nil {
return nil, fmt.Errorf("creation info not found")
}
r := bytes.NewReader(b)
creationInfo, err := deserializeDuplicatePaymentCreationInfo(r)
if err != nil {
return nil, err
}
// Get failure reason if available.
var failureReason *channeldb.FailureReason
b = bucket.Get(duplicatePaymentFailInfoKey)
if b != nil {
reason := channeldb.FailureReason(b[0])
failureReason = &reason
}
payment := &MPPayment{
SequenceNum: sequenceNum,
Info: creationInfo,
FailureReason: failureReason,
Status: paymentStatus,
}
// Get the HTLCAttemptInfo. It can be absent.
b = bucket.Get(duplicatePaymentAttemptInfoKey)
if b != nil {
r = bytes.NewReader(b)
attempt, err := deserializeDuplicateHTLCAttemptInfo(r)
if err != nil {
return nil, err
}
htlc := HTLCAttempt{
HTLCAttemptInfo: HTLCAttemptInfo{
AttemptID: attempt.attemptID,
Route: attempt.route,
sessionKey: attempt.sessionKey,
},
}
// Get the payment preimage. This is only found for
// successful payments.
b = bucket.Get(duplicatePaymentSettleInfoKey)
if b != nil {
var preimg lntypes.Preimage
copy(preimg[:], b)
htlc.Settle = &HTLCSettleInfo{
Preimage: preimg,
SettleTime: time.Time{},
}
} else {
// Otherwise the payment must have failed.
htlc.Failure = &HTLCFailInfo{
FailTime: time.Time{},
}
}
payment.HTLCs = []HTLCAttempt{htlc}
}
return payment, nil
}
func fetchDuplicatePayments(paymentHashBucket kvdb.RBucket) ([]*MPPayment,
error) {
var payments []*MPPayment
// For older versions of lnd, duplicate payments to a payment has was
// possible. These will be found in a sub-bucket indexed by their
// sequence number if available.
dup := paymentHashBucket.NestedReadBucket(duplicatePaymentsBucket)
if dup == nil {
return nil, nil
}
err := dup.ForEach(func(k, v []byte) error {
subBucket := dup.NestedReadBucket(k)
if subBucket == nil {
// We one bucket for each duplicate to be found.
return fmt.Errorf("non bucket element" +
"in duplicate bucket")
}
p, err := fetchDuplicatePayment(subBucket)
if err != nil {
return err
}
payments = append(payments, p)
return nil
})
if err != nil {
return nil, err
}
return payments, nil
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+789
View File
@@ -0,0 +1,789 @@
package paymentsdb
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
// PaymentsQuery represents a query to the payments database starting or ending
// at a certain offset index. The number of retrieved records can be limited.
type PaymentsQuery struct {
// IndexOffset determines the starting point of the payments query and
// is always exclusive. In normal order, the query starts at the next
// higher (available) index compared to IndexOffset. In reversed order,
// the query ends at the next lower (available) index compared to the
// IndexOffset. In the case of a zero index_offset, the query will start
// with the oldest payment when paginating forwards, or will end with
// the most recent payment when paginating backwards.
IndexOffset uint64
// MaxPayments is the maximal number of payments returned in the
// payments query.
MaxPayments uint64
// Reversed gives a meaning to the IndexOffset. If reversed is set to
// true, the query will fetch payments with indices lower than the
// IndexOffset, otherwise, it will return payments with indices greater
// than the IndexOffset.
Reversed bool
// If IncludeIncomplete is true, then return payments that have not yet
// fully completed. This means that pending payments, as well as failed
// payments will show up if this field is set to true.
IncludeIncomplete bool
// CountTotal indicates that all payments currently present in the
// payment index (complete and incomplete) should be counted.
CountTotal bool
// CreationDateStart, expressed in Unix seconds, if set, filters out
// all payments with a creation date greater than or equal to it.
CreationDateStart int64
// CreationDateEnd, expressed in Unix seconds, if set, filters out all
// payments with a creation date less than or equal to it.
CreationDateEnd int64
}
// PaymentsResponse contains the result of a query to the payments database.
// It includes the set of payments that match the query and integers which
// represent the index of the first and last item returned in the series of
// payments. These integers allow callers to resume their query in the event
// that the query's response exceeds the max number of returnable events.
type PaymentsResponse struct {
// Payments is the set of payments returned from the database for the
// PaymentsQuery.
Payments []*MPPayment
// FirstIndexOffset is the index of the first element in the set of
// returned MPPayments. Callers can use this to resume their query
// in the event that the slice has too many events to fit into a single
// response. The offset can be used to continue reverse pagination.
FirstIndexOffset uint64
// LastIndexOffset is the index of the last element in the set of
// returned MPPayments. Callers can use this to resume their query
// in the event that the slice has too many events to fit into a single
// response. The offset can be used to continue forward pagination.
LastIndexOffset uint64
// TotalCount represents the total number of payments that are currently
// stored in the payment database. This will only be set if the
// CountTotal field in the query was set to true.
TotalCount uint64
}
// HTLCAttemptInfo contains static information about a specific HTLC attempt
// for a payment. This information is used by the router to handle any errors
// coming back after an attempt is made, and to query the switch about the
// status of the attempt.
type HTLCAttemptInfo struct {
// AttemptID is the unique ID used for this attempt.
AttemptID uint64
// sessionKey is the raw bytes ephemeral key used for this attempt.
// These bytes are lazily read off disk to save ourselves the expensive
// EC operations used by btcec.PrivKeyFromBytes.
sessionKey [btcec.PrivKeyBytesLen]byte
// cachedSessionKey is our fully deserialized sesionKey. This value
// may be nil if the attempt has just been read from disk and its
// session key has not been used yet.
cachedSessionKey *btcec.PrivateKey
// Route is the route attempted to send the HTLC.
Route route.Route
// AttemptTime is the time at which this HTLC was attempted.
AttemptTime time.Time
// Hash is the hash used for this single HTLC attempt. For AMP payments
// this will differ across attempts, for non-AMP payments each attempt
// will use the same hash. This can be nil for older payment attempts,
// in which the payment's PaymentHash in the PaymentCreationInfo should
// be used.
Hash *lntypes.Hash
// onionBlob is the cached value for onion blob created from the sphinx
// construction.
onionBlob [lnwire.OnionPacketSize]byte
// circuit is the cached value for sphinx circuit.
circuit *sphinx.Circuit
}
// NewHtlcAttempt creates a htlc attempt.
func NewHtlcAttempt(attemptID uint64, sessionKey *btcec.PrivateKey,
route route.Route, attemptTime time.Time,
hash *lntypes.Hash) (*HTLCAttempt, error) {
var scratch [btcec.PrivKeyBytesLen]byte
copy(scratch[:], sessionKey.Serialize())
info := HTLCAttemptInfo{
AttemptID: attemptID,
sessionKey: scratch,
cachedSessionKey: sessionKey,
Route: route,
AttemptTime: attemptTime,
Hash: hash,
}
if err := info.attachOnionBlobAndCircuit(); err != nil {
return nil, err
}
return &HTLCAttempt{HTLCAttemptInfo: info}, nil
}
// SessionKey returns the ephemeral key used for a htlc attempt. This function
// performs expensive ec-ops to obtain the session key if it is not cached.
func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey {
if h.cachedSessionKey == nil {
h.cachedSessionKey, _ = btcec.PrivKeyFromBytes(
h.sessionKey[:],
)
}
return h.cachedSessionKey
}
// OnionBlob returns the onion blob created from the sphinx construction.
func (h *HTLCAttemptInfo) OnionBlob() ([lnwire.OnionPacketSize]byte, error) {
var zeroBytes [lnwire.OnionPacketSize]byte
if h.onionBlob == zeroBytes {
if err := h.attachOnionBlobAndCircuit(); err != nil {
return zeroBytes, err
}
}
return h.onionBlob, nil
}
// Circuit returns the sphinx circuit for this attempt.
func (h *HTLCAttemptInfo) Circuit() (*sphinx.Circuit, error) {
if h.circuit == nil {
if err := h.attachOnionBlobAndCircuit(); err != nil {
return nil, err
}
}
return h.circuit, nil
}
// attachOnionBlobAndCircuit creates a sphinx packet and caches the onion blob
// and circuit for this attempt.
func (h *HTLCAttemptInfo) attachOnionBlobAndCircuit() error {
onionBlob, circuit, err := generateSphinxPacket(
&h.Route, h.Hash[:], h.SessionKey(),
)
if err != nil {
return err
}
copy(h.onionBlob[:], onionBlob)
h.circuit = circuit
return nil
}
// HTLCAttempt contains information about a specific HTLC attempt for a given
// payment. It contains the HTLCAttemptInfo used to send the HTLC, as well
// as a timestamp and any known outcome of the attempt.
type HTLCAttempt struct {
HTLCAttemptInfo
// Settle is the preimage of a successful payment. This serves as a
// proof of payment. It will only be non-nil for settled payments.
//
// NOTE: Can be nil if payment is not settled.
Settle *HTLCSettleInfo
// Fail is a failure reason code indicating the reason the payment
// failed. It is only non-nil for failed payments.
//
// NOTE: Can be nil if payment is not failed.
Failure *HTLCFailInfo
}
// HTLCSettleInfo encapsulates the information that augments an HTLCAttempt in
// the event that the HTLC is successful.
type HTLCSettleInfo struct {
// Preimage is the preimage of a successful HTLC. This serves as a proof
// of payment.
Preimage lntypes.Preimage
// SettleTime is the time at which this HTLC was settled.
SettleTime time.Time
}
// HTLCFailReason is the reason an htlc failed.
type HTLCFailReason byte
const (
// HTLCFailUnknown is recorded for htlcs that failed with an unknown
// reason.
HTLCFailUnknown HTLCFailReason = 0
// HTLCFailUnreadable is recorded for htlcs that had a failure message that
// couldn't be decrypted.
HTLCFailUnreadable HTLCFailReason = 1
// HTLCFailInternal is recorded for htlcs that failed because of an
// internal error.
HTLCFailInternal HTLCFailReason = 2
// HTLCFailMessage is recorded for htlcs that failed with a network
// failure message.
HTLCFailMessage HTLCFailReason = 3
)
// HTLCFailInfo encapsulates the information that augments an HTLCAttempt in the
// event that the HTLC fails.
type HTLCFailInfo struct {
// FailTime is the time at which this HTLC was failed.
FailTime time.Time
// Message is the wire message that failed this HTLC. This field will be
// populated when the failure reason is HTLCFailMessage.
Message lnwire.FailureMessage
// Reason is the failure reason for this HTLC.
Reason HTLCFailReason
// The position in the path of the intermediate or final node that
// generated the failure message. Position zero is the sender node. This
// field will be populated when the failure reason is either
// HTLCFailMessage or HTLCFailUnknown.
FailureSourceIndex uint32
}
// MPPaymentState wraps a series of info needed for a given payment, which is
// used by both MPP and AMP. This is a memory representation of the payment's
// current state and is updated whenever the payment is read from disk.
type MPPaymentState struct {
// NumAttemptsInFlight specifies the number of HTLCs the payment is
// waiting results for.
NumAttemptsInFlight int
// RemainingAmt specifies how much more money to be sent.
RemainingAmt lnwire.MilliSatoshi
// FeesPaid specifies the total fees paid so far that can be used to
// calculate remaining fee budget.
FeesPaid lnwire.MilliSatoshi
// HasSettledHTLC is true if at least one of the payment's HTLCs is
// settled.
HasSettledHTLC bool
// PaymentFailed is true if the payment has been marked as failed with
// a reason.
PaymentFailed bool
}
// MPPayment is a wrapper around a payment's PaymentCreationInfo and
// HTLCAttempts. All payments will have the PaymentCreationInfo set, any
// HTLCs made in attempts to be completed will populated in the HTLCs slice.
// Each populated HTLCAttempt represents an attempted HTLC, each of which may
// have the associated Settle or Fail struct populated if the HTLC is no longer
// in-flight.
type MPPayment struct {
// SequenceNum is a unique identifier used to sort the payments in
// order of creation.
SequenceNum uint64
// Info holds all static information about this payment, and is
// populated when the payment is initiated.
Info *channeldb.PaymentCreationInfo
// HTLCs holds the information about individual HTLCs that we send in
// order to make the payment.
HTLCs []HTLCAttempt
// FailureReason is the failure reason code indicating the reason the
// payment failed.
//
// NOTE: Will only be set once the daemon has given up on the payment
// altogether.
FailureReason *channeldb.FailureReason
// Status is the current PaymentStatus of this payment.
Status PaymentStatus
// State is the current state of the payment that holds a number of key
// insights and is used to determine what to do on each payment loop
// iteration.
State *MPPaymentState
}
// Terminated returns a bool to specify whether the payment is in a terminal
// state.
func (m *MPPayment) Terminated() bool {
// If the payment is in terminal state, it cannot be updated.
return m.Status.updatable() != nil
}
// TerminalInfo returns any HTLC settle info recorded. If no settle info is
// recorded, any payment level failure will be returned. If neither a settle
// nor a failure is recorded, both return values will be nil.
func (m *MPPayment) TerminalInfo() (*HTLCAttempt, *channeldb.FailureReason) {
for _, h := range m.HTLCs {
if h.Settle != nil {
return &h, nil
}
}
return nil, m.FailureReason
}
// SentAmt returns the sum of sent amount and fees for HTLCs that are either
// settled or still in flight.
func (m *MPPayment) SentAmt() (lnwire.MilliSatoshi, lnwire.MilliSatoshi) {
var sent, fees lnwire.MilliSatoshi
for _, h := range m.HTLCs {
if h.Failure != nil {
continue
}
// The attempt was not failed, meaning the amount was
// potentially sent to the receiver.
sent += h.Route.ReceiverAmt()
fees += h.Route.TotalFees()
}
return sent, fees
}
// InFlightHTLCs returns the HTLCs that are still in-flight, meaning they have
// not been settled or failed.
func (m *MPPayment) InFlightHTLCs() []HTLCAttempt {
var inflights []HTLCAttempt
for _, h := range m.HTLCs {
if h.Settle != nil || h.Failure != nil {
continue
}
inflights = append(inflights, h)
}
return inflights
}
// GetAttempt returns the specified htlc attempt on the payment.
func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) {
// TODO(yy): iteration can be slow, make it into a tree or use BS.
for _, htlc := range m.HTLCs {
htlc := htlc
if htlc.AttemptID == id {
return &htlc, nil
}
}
return nil, errors.New("htlc attempt not found on payment")
}
// Registrable returns an error to specify whether adding more HTLCs to the
// payment with its current status is allowed. A payment can accept new HTLC
// registrations when it's newly created, or none of its HTLCs is in a terminal
// state.
func (m *MPPayment) Registrable() error {
// If updating the payment is not allowed, we can't register new HTLCs.
// Otherwise, the status must be either `StatusInitiated` or
// `StatusInFlight`.
if err := m.Status.updatable(); err != nil {
return err
}
// Exit early if this is not inflight.
if m.Status != StatusInFlight {
return nil
}
// There are still inflight HTLCs and we need to check whether there
// are settled HTLCs or the payment is failed. If we already have
// settled HTLCs, we won't allow adding more HTLCs.
if m.State.HasSettledHTLC {
return ErrPaymentPendingSettled
}
// If the payment is already failed, we won't allow adding more HTLCs.
if m.State.PaymentFailed {
return ErrPaymentPendingFailed
}
// Otherwise we can add more HTLCs.
return nil
}
// setState creates and attaches a new MPPaymentState to the payment. It also
// updates the payment's status based on its current state.
func (m *MPPayment) setState() error {
// Fetch the total amount and fees that has already been sent in
// settled and still in-flight shards.
sentAmt, fees := m.SentAmt()
// Sanity check we haven't sent a value larger than the payment amount.
totalAmt := m.Info.Value
if sentAmt > totalAmt {
return fmt.Errorf("%w: sent=%v, total=%v",
ErrSentExceedsTotal, sentAmt, totalAmt)
}
// Get any terminal info for this payment.
settle, failure := m.TerminalInfo()
// Now determine the payment's status.
status, err := decidePaymentStatus(m.HTLCs, m.FailureReason)
if err != nil {
return err
}
// Update the payment state and status.
m.State = &MPPaymentState{
NumAttemptsInFlight: len(m.InFlightHTLCs()),
RemainingAmt: totalAmt - sentAmt,
FeesPaid: fees,
HasSettledHTLC: settle != nil,
PaymentFailed: failure != nil,
}
m.Status = status
return nil
}
// SetState calls the internal method setState. This is a temporary method
// to be used by the tests in routing. Once the tests are updated to use mocks,
// this method can be removed.
//
// TODO(yy): delete.
func (m *MPPayment) SetState() error {
return m.setState()
}
// NeedWaitAttempts decides whether we need to hold creating more HTLC attempts
// and wait for the results of the payment's inflight HTLCs. Return an error if
// the payment is in an unexpected state.
func (m *MPPayment) NeedWaitAttempts() (bool, error) {
// Check when the remainingAmt is not zero, which means we have more
// money to be sent.
if m.State.RemainingAmt != 0 {
switch m.Status {
// If the payment is newly created, no need to wait for HTLC
// results.
case StatusInitiated:
return false, nil
// If we have inflight HTLCs, we'll check if we have terminal
// states to decide if we need to wait.
case StatusInFlight:
// We still have money to send, and one of the HTLCs is
// settled. We'd stop sending money and wait for all
// inflight HTLC attempts to finish.
if m.State.HasSettledHTLC {
log.Warnf("payment=%v has remaining amount "+
"%v, yet at least one of its HTLCs is "+
"settled", m.Info.PaymentIdentifier,
m.State.RemainingAmt)
return true, nil
}
// The payment has a failure reason though we still
// have money to send, we'd stop sending money and wait
// for all inflight HTLC attempts to finish.
if m.State.PaymentFailed {
return true, nil
}
// Otherwise we don't need to wait for inflight HTLCs
// since we still have money to be sent.
return false, nil
// We need to send more money, yet the payment is already
// succeeded. Return an error in this case as the receiver is
// violating the protocol.
case StatusSucceeded:
return false, fmt.Errorf("%w: parts of the payment "+
"already succeeded but still have remaining "+
"amount %v", ErrPaymentInternal,
m.State.RemainingAmt)
// The payment is failed and we have no inflight HTLCs, no need
// to wait.
case StatusFailed:
return false, nil
// Unknown payment status.
default:
return false, fmt.Errorf("%w: %s",
ErrUnknownPaymentStatus, m.Status)
}
}
// Now we determine whether we need to wait when the remainingAmt is
// already zero.
switch m.Status {
// When the payment is newly created, yet the payment has no remaining
// amount, return an error.
case StatusInitiated:
return false, fmt.Errorf("%w: %v",
ErrPaymentInternal, m.Status)
// If the payment is inflight, we must wait.
//
// NOTE: an edge case is when all HTLCs are failed while the payment is
// not failed we'd still be in this inflight state. However, since the
// remainingAmt is zero here, it means we cannot be in that state as
// otherwise the remainingAmt would not be zero.
case StatusInFlight:
return true, nil
// If the payment is already succeeded, no need to wait.
case StatusSucceeded:
return false, nil
// If the payment is already failed, yet the remaining amount is zero,
// return an error as this indicates an error state. We will only each
// this status when there are no inflight HTLCs and the payment is
// marked as failed with a reason, which means the remainingAmt must
// not be zero because our sentAmt is zero.
case StatusFailed:
return false, fmt.Errorf("%w: %v",
ErrPaymentInternal, m.Status)
// Unknown payment status.
default:
return false, fmt.Errorf("%w: %s",
ErrUnknownPaymentStatus, m.Status)
}
}
// GetState returns the internal state of the payment.
func (m *MPPayment) GetState() *MPPaymentState {
return m.State
}
// GetStatus returns the current status of the payment.
func (m *MPPayment) GetStatus() PaymentStatus {
return m.Status
}
// GetHTLCs returns all the HTLCs for this payment.
func (m *MPPayment) GetHTLCs() []HTLCAttempt {
return m.HTLCs
}
// AllowMoreAttempts is used to decide whether we can safely attempt more HTLCs
// for a given payment state. Return an error if the payment is in an
// unexpected state.
func (m *MPPayment) AllowMoreAttempts() (bool, error) {
// Now check whether the remainingAmt is zero or not. If we don't have
// any remainingAmt, no more HTLCs should be made.
if m.State.RemainingAmt == 0 {
// If the payment is newly created, yet we don't have any
// remainingAmt, return an error.
if m.Status == StatusInitiated {
return false, fmt.Errorf("%w: initiated payment has "+
"zero remainingAmt",
ErrPaymentInternal)
}
// Otherwise, exit early since all other statuses with zero
// remainingAmt indicate no more HTLCs can be made.
return false, nil
}
// Otherwise, the remaining amount is not zero, we now decide whether
// to make more attempts based on the payment's current status.
//
// If at least one of the payment's attempts is settled, yet we haven't
// sent all the amount, it indicates something is wrong with the peer
// as the preimage is received. In this case, return an error state.
if m.Status == StatusSucceeded {
return false, fmt.Errorf("%w: payment already succeeded but "+
"still have remaining amount %v",
ErrPaymentInternal, m.State.RemainingAmt)
}
// Now check if we can register a new HTLC.
err := m.Registrable()
if err != nil {
log.Warnf("Payment(%v): cannot register HTLC attempt: %v, "+
"current status: %s", m.Info.PaymentIdentifier,
err, m.Status)
return false, nil
}
// Now we know we can register new HTLCs.
return true, nil
}
// serializeHTLCSettleInfo serializes the details of a settled htlc.
func serializeHTLCSettleInfo(w io.Writer, s *HTLCSettleInfo) error {
if _, err := w.Write(s.Preimage[:]); err != nil {
return err
}
if err := serializeTime(w, s.SettleTime); err != nil {
return err
}
return nil
}
// deserializeHTLCSettleInfo deserializes the details of a settled htlc.
func deserializeHTLCSettleInfo(r io.Reader) (*HTLCSettleInfo, error) {
s := &HTLCSettleInfo{}
if _, err := io.ReadFull(r, s.Preimage[:]); err != nil {
return nil, err
}
var err error
s.SettleTime, err = deserializeTime(r)
if err != nil {
return nil, err
}
return s, nil
}
// serializeHTLCFailInfo serializes the details of a failed htlc including the
// wire failure.
func serializeHTLCFailInfo(w io.Writer, f *HTLCFailInfo) error {
if err := serializeTime(w, f.FailTime); err != nil {
return err
}
// Write failure. If there is no failure message, write an empty
// byte slice.
var messageBytes bytes.Buffer
if f.Message != nil {
err := lnwire.EncodeFailureMessage(&messageBytes, f.Message, 0)
if err != nil {
return err
}
}
if err := wire.WriteVarBytes(w, 0, messageBytes.Bytes()); err != nil {
return err
}
return WriteElements(w, byte(f.Reason), f.FailureSourceIndex)
}
// deserializeHTLCFailInfo deserializes the details of a failed htlc including
// the wire failure.
func deserializeHTLCFailInfo(r io.Reader) (*HTLCFailInfo, error) {
f := &HTLCFailInfo{}
var err error
f.FailTime, err = deserializeTime(r)
if err != nil {
return nil, err
}
// Read failure.
failureBytes, err := wire.ReadVarBytes(
r, 0, math.MaxUint16, "failure",
)
if err != nil {
return nil, err
}
if len(failureBytes) > 0 {
f.Message, err = lnwire.DecodeFailureMessage(
bytes.NewReader(failureBytes), 0,
)
if err != nil {
return nil, err
}
}
var reason byte
err = ReadElements(r, &reason, &f.FailureSourceIndex)
if err != nil {
return nil, err
}
f.Reason = HTLCFailReason(reason)
return f, nil
}
// generateSphinxPacket generates then encodes a sphinx packet which encodes
// the onion route specified by the passed layer 3 route. The blob returned
// from this function can immediately be included within an HTLC add packet to
// be sent to the first hop within the route.
func generateSphinxPacket(rt *route.Route, paymentHash []byte,
sessionKey *btcec.PrivateKey) ([]byte, *sphinx.Circuit, error) {
// Now that we know we have an actual route, we'll map the route into a
// sphinx payment path which includes per-hop payloads for each hop
// that give each node within the route the necessary information
// (fees, CLTV value, etc.) to properly forward the payment.
sphinxPath, err := rt.ToSphinxPath()
if err != nil {
return nil, nil, err
}
log.Tracef("Constructed per-hop payloads for payment_hash=%x: %v",
paymentHash, lnutils.NewLogClosure(func() string {
path := make(
[]sphinx.OnionHop, sphinxPath.TrueRouteLength(),
)
for i := range path {
hopCopy := sphinxPath[i]
path[i] = hopCopy
}
return spew.Sdump(path)
}),
)
// Next generate the onion routing packet which allows us to perform
// privacy preserving source routing across the network.
sphinxPacket, err := sphinx.NewOnionPacket(
sphinxPath, sessionKey, paymentHash,
sphinx.DeterministicPacketFiller,
)
if err != nil {
return nil, nil, err
}
// Finally, encode Sphinx packet using its wire representation to be
// included within the HTLC add packet.
var onionBlob bytes.Buffer
if err := sphinxPacket.Encode(&onionBlob); err != nil {
return nil, nil, err
}
log.Tracef("Generated sphinx packet: %v",
lnutils.NewLogClosure(func() string {
// We make a copy of the ephemeral key and unset the
// internal curve here in order to keep the logs from
// getting noisy.
key := *sphinxPacket.EphemeralKey
packetCopy := *sphinxPacket
packetCopy.EphemeralKey = &key
return spew.Sdump(packetCopy)
}),
)
return onionBlob.Bytes(), &sphinx.Circuit{
SessionKey: sessionKey,
PaymentPath: sphinxPath.NodeKeys(),
}, nil
}
+259
View File
@@ -0,0 +1,259 @@
package paymentsdb
import (
"fmt"
"github.com/lightningnetwork/lnd/channeldb"
)
// PaymentStatus represent current status of payment.
type PaymentStatus byte
const (
// NOTE: PaymentStatus = 0 was previously used for status unknown and
// is now deprecated.
// StatusInitiated is the status where a payment has just been
// initiated.
StatusInitiated PaymentStatus = 1
// StatusInFlight is the status where a payment has been initiated, but
// a response has not been received.
StatusInFlight PaymentStatus = 2
// StatusSucceeded is the status where a payment has been initiated and
// the payment was completed successfully.
StatusSucceeded PaymentStatus = 3
// StatusFailed is the status where a payment has been initiated and a
// failure result has come back.
StatusFailed PaymentStatus = 4
)
// errPaymentStatusUnknown is returned when a payment has an unknown status.
var errPaymentStatusUnknown = fmt.Errorf("unknown payment status")
// String returns readable representation of payment status.
func (ps PaymentStatus) String() string {
switch ps {
case StatusInitiated:
return "Initiated"
case StatusInFlight:
return "In Flight"
case StatusSucceeded:
return "Succeeded"
case StatusFailed:
return "Failed"
default:
return "Unknown"
}
}
// initializable returns an error to specify whether initiating the payment
// with its current status is allowed. A payment can only be initialized if it
// hasn't been created yet or already failed.
func (ps PaymentStatus) initializable() error {
switch ps {
// The payment has been created already. We will disallow creating it
// again in case other goroutines have already been creating HTLCs for
// it.
case StatusInitiated:
return ErrPaymentExists
// We already have an InFlight payment on the network. We will disallow
// any new payments.
case StatusInFlight:
return ErrPaymentInFlight
// The payment has been attempted and is succeeded so we won't allow
// creating it again.
case StatusSucceeded:
return ErrAlreadyPaid
// We allow retrying failed payments.
case StatusFailed:
return nil
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// removable returns an error to specify whether deleting the payment with its
// current status is allowed. A payment cannot be safely deleted if it has
// inflight HTLCs.
func (ps PaymentStatus) removable() error {
switch ps {
// The payment has been created but has no HTLCs and can be removed.
case StatusInitiated:
return nil
// There are still inflight HTLCs and the payment needs to wait for the
// final outcomes.
case StatusInFlight:
return ErrPaymentInFlight
// The payment has been attempted and is succeeded and is allowed to be
// removed.
case StatusSucceeded:
return nil
// Failed payments are allowed to be removed.
case StatusFailed:
return nil
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// updatable returns an error to specify whether the payment's HTLCs can be
// updated. A payment can update its HTLCs when it has inflight HTLCs.
func (ps PaymentStatus) updatable() error {
switch ps {
// Newly created payments can be updated.
case StatusInitiated:
return nil
// Inflight payments can be updated.
case StatusInFlight:
return nil
// If the payment has a terminal condition, we won't allow any updates.
case StatusSucceeded:
return ErrPaymentAlreadySucceeded
case StatusFailed:
return ErrPaymentAlreadyFailed
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// decidePaymentStatus uses the payment's DB state to determine a memory status
// that's used by the payment router to decide following actions.
// Together, we use four variables to determine the payment's status,
// - inflight: whether there are any pending HTLCs.
// - settled: whether any of the HTLCs has been settled.
// - htlc failed: whether any of the HTLCs has been failed.
// - payment failed: whether the payment has been marked as failed.
//
// Based on the above variables, we derive the status using the following
// table,
// | inflight | settled | htlc failed | payment failed | status |
// |:--------:|:-------:|:-----------:|:--------------:|:--------------------:|
// | true | true | true | true | StatusInFlight |
// | true | true | true | false | StatusInFlight |
// | true | true | false | true | StatusInFlight |
// | true | true | false | false | StatusInFlight |
// | true | false | true | true | StatusInFlight |
// | true | false | true | false | StatusInFlight |
// | true | false | false | true | StatusInFlight |
// | true | false | false | false | StatusInFlight |
// | false | true | true | true | StatusSucceeded |
// | false | true | true | false | StatusSucceeded |
// | false | true | false | true | StatusSucceeded |
// | false | true | false | false | StatusSucceeded |
// | false | false | true | true | StatusFailed |
// | false | false | true | false | StatusInFlight |
// | false | false | false | true | StatusFailed |
// | false | false | false | false | StatusInitiated |
//
// When `inflight`, `settled`, `htlc failed`, and `payment failed` are false,
// this indicates the payment is newly created and hasn't made any HTLCs yet.
// When `inflight` and `settled` are false, `htlc failed` is true yet `payment
// failed` is false, this indicates all the payment's HTLCs have occurred a
// temporarily failure and the payment is still in-flight.
func decidePaymentStatus(htlcs []HTLCAttempt,
reason *channeldb.FailureReason) (PaymentStatus, error) {
var (
inflight bool
htlcSettled bool
htlcFailed bool
paymentFailed bool
)
// If we have a failure reason, the payment is failed.
if reason != nil {
paymentFailed = true
}
// Go through all HTLCs for this payment, check whether we have any
// settled HTLC, and any still in-flight.
for _, h := range htlcs {
if h.Failure != nil {
htlcFailed = true
continue
}
if h.Settle != nil {
htlcSettled = true
continue
}
// If any of the HTLCs are not failed nor settled, we
// still have inflight HTLCs.
inflight = true
}
// Use the DB state to determine the status of the payment.
switch {
// If we have inflight HTLCs, no matter we have settled or failed
// HTLCs, or the payment failed, we still consider it inflight so we
// inform upper systems to wait for the results.
case inflight:
return StatusInFlight, nil
// If we have no in-flight HTLCs, and at least one of the HTLCs is
// settled, the payment succeeded.
//
// NOTE: when reaching this case, paymentFailed could be true, which
// means we have a conflicting state for this payment. We choose to
// mark the payment as succeeded because it's the receiver's
// responsibility to only settle the payment iff all HTLCs are
// received.
case htlcSettled:
return StatusSucceeded, nil
// If we have no in-flight HTLCs, and the payment failure is set, the
// payment is considered failed.
//
// NOTE: when reaching this case, settled must be false.
case paymentFailed:
return StatusFailed, nil
// If we have no in-flight HTLCs, yet the payment is NOT failed, it
// means all the HTLCs are failed. In this case we can attempt more
// HTLCs.
//
// NOTE: when reaching this case, both settled and paymentFailed must
// be false.
case htlcFailed:
return StatusInFlight, nil
// If none of the HTLCs is either settled or failed, and we have no
// inflight HTLCs, this means the payment has no HTLCs created yet.
//
// NOTE: when reaching this case, both settled and paymentFailed must
// be false.
case !htlcFailed:
return StatusInitiated, nil
// Otherwise an impossible state is reached.
//
// NOTE: we should never end up here.
default:
log.Error("Impossible payment state reached")
return 0, fmt.Errorf("%w: payment is corrupted",
errPaymentStatusUnknown)
}
}
+249
View File
@@ -0,0 +1,249 @@
package paymentsdb
import (
"fmt"
"testing"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// TestDecidePaymentStatus checks that given a set of HTLC and a failure
// reason, the payment's current status is returned as expected.
func TestDecidePaymentStatus(t *testing.T) {
t.Parallel()
// Create two attempts used for testing.
inflight := HTLCAttempt{}
settled := HTLCAttempt{
Settle: &HTLCSettleInfo{Preimage: lntypes.Preimage{}},
}
failed := HTLCAttempt{
Failure: &HTLCFailInfo{FailureSourceIndex: 1},
}
// Create a test failure reason and get the pointer.
reason := channeldb.FailureReasonNoRoute
failure := &reason
testCases := []struct {
name string
htlcs []HTLCAttempt
reason *channeldb.FailureReason
expectedStatus PaymentStatus
expectedErr error
}{
{
// Test when inflight=true, settled=true, failed=true,
// reason=yes.
name: "state 1111",
htlcs: []HTLCAttempt{
inflight, settled, failed,
},
reason: failure,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=true, failed=true,
// reason=no.
name: "state 1110",
htlcs: []HTLCAttempt{
inflight, settled, failed,
},
reason: nil,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=true, failed=false,
// reason=yes.
name: "state 1101",
htlcs: []HTLCAttempt{inflight, settled},
reason: failure,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=true, failed=false,
// reason=no.
name: "state 1100",
htlcs: []HTLCAttempt{inflight, settled},
reason: nil,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=false, failed=true,
// reason=yes.
name: "state 1011",
htlcs: []HTLCAttempt{inflight, failed},
reason: failure,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=false, failed=true,
// reason=no.
name: "state 1010",
htlcs: []HTLCAttempt{inflight, failed},
reason: nil,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=false, failed=false,
// reason=yes.
name: "state 1001",
htlcs: []HTLCAttempt{inflight},
reason: failure,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=true, settled=false, failed=false,
// reason=no.
name: "state 1000",
htlcs: []HTLCAttempt{inflight},
reason: nil,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=false, settled=true, failed=true,
// reason=yes.
name: "state 0111",
htlcs: []HTLCAttempt{settled, failed},
reason: failure,
expectedStatus: StatusSucceeded,
},
{
// Test when inflight=false, settled=true, failed=true,
// reason=no.
name: "state 0110",
htlcs: []HTLCAttempt{settled, failed},
reason: nil,
expectedStatus: StatusSucceeded,
},
{
// Test when inflight=false, settled=true,
// failed=false, reason=yes.
name: "state 0101",
htlcs: []HTLCAttempt{settled},
reason: failure,
expectedStatus: StatusSucceeded,
},
{
// Test when inflight=false, settled=true,
// failed=false, reason=no.
name: "state 0100",
htlcs: []HTLCAttempt{settled},
reason: nil,
expectedStatus: StatusSucceeded,
},
{
// Test when inflight=false, settled=false,
// failed=true, reason=yes.
name: "state 0011",
htlcs: []HTLCAttempt{failed},
reason: failure,
expectedStatus: StatusFailed,
},
{
// Test when inflight=false, settled=false,
// failed=true, reason=no.
name: "state 0010",
htlcs: []HTLCAttempt{failed},
reason: nil,
expectedStatus: StatusInFlight,
},
{
// Test when inflight=false, settled=false,
// failed=false, reason=yes.
name: "state 0001",
htlcs: []HTLCAttempt{},
reason: failure,
expectedStatus: StatusFailed,
},
{
// Test when inflight=false, settled=false,
// failed=false, reason=no.
name: "state 0000",
htlcs: []HTLCAttempt{},
reason: nil,
expectedStatus: StatusInitiated,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
status, err := decidePaymentStatus(tc.htlcs, tc.reason)
require.Equalf(t, tc.expectedStatus, status,
"got %s, want %s", status, tc.expectedStatus)
require.ErrorIs(t, err, tc.expectedErr)
})
}
}
// TestPaymentStatusActions checks whether a list of actions can be applied
// against ALL possible payment statuses. Unlike normal unit tests where we
// check against a single function, all the actions including `removable`,
// `initable`, and `updatable` are tested together so this test can be used as
// a reference of state transition.
func TestPaymentStatusActions(t *testing.T) {
t.Parallel()
testCases := []struct {
status PaymentStatus
initErr error
updateErr error
removeErr error
}{
{
status: StatusInitiated,
initErr: ErrPaymentExists,
updateErr: nil,
removeErr: nil,
},
{
status: StatusInFlight,
initErr: ErrPaymentInFlight,
updateErr: nil,
removeErr: ErrPaymentInFlight,
},
{
status: StatusSucceeded,
initErr: ErrAlreadyPaid,
updateErr: ErrPaymentAlreadySucceeded,
removeErr: nil,
},
{
status: StatusFailed,
initErr: nil,
updateErr: ErrPaymentAlreadyFailed,
removeErr: nil,
},
{
status: 0,
initErr: ErrUnknownPaymentStatus,
updateErr: ErrUnknownPaymentStatus,
removeErr: ErrUnknownPaymentStatus,
},
}
for i, tc := range testCases {
i, tc := i, tc
ps := tc.status
name := fmt.Sprintf("test_%d_%s", i, ps.String())
t.Run(name, func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, ps.initializable(), tc.initErr,
"initable under state %v", tc.status)
require.ErrorIs(t, ps.updatable(), tc.updateErr,
"updatable under state %v", tc.status)
require.ErrorIs(t, ps.removable(), tc.removeErr,
"removable under state %v", tc.status)
})
}
}
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
package paymentsdb
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
)
func TestMain(m *testing.M) {
kvdb.RunTests(m)
}
+23
View File
@@ -0,0 +1,23 @@
package paymentsdb
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates an BBolt database for testing.
func NewTestDB(t *testing.T, opts ...OptionModifier) *KVPaymentsDB {
backend, backendCleanup, err := kvdb.GetTestBackend(
t.TempDir(), "kvPaymentDB",
)
require.NoError(t, err)
t.Cleanup(backendCleanup)
paymentDB, err := NewKVPaymentsDB(backend, opts...)
require.NoError(t, err)
return paymentDB
}