multi: move payment related code into own package
What changed, and why it matters
This commit is a large but straightforward code refactor: it moves payment-related data structures, database logic, and tests out of the `channeldb` package into a new `payments/db` package. The commit message explicitly says it is the smallest move possible to avoid import cycles and keep the change set small. No security fixes, behavior changes, or vulnerability patches are visible in the diff.
No security action required. Treat as a normal refactoring commit. Reviewers may optionally verify that moved serialization code preserves byte-for-byte compatibility and that import cycles are resolved cleanly.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates payment storage, serialization, status logic, and associated tests from channeldb/ to payments/db/. It updates call sites in routing, lnrpc/routerrpc, rpcserver.go, server.go, and config_builder.go to import the new package. The database migration that previously created the payments index bucket is replaced with a no-op because the payment package now owns bucket creation. Codec handling for paymentIndexType is removed from channeldb/codec.go because it moved to the new package. No functional changes to payment state machine rules, serialization formats, or RPC behavior are introduced.
Changed components
channeldbpayments/dbroutinglnrpc/routerrpcrpcserver.goserver.goconfig_builder.goInspect captured patch +6849 / −6722
diff --git a/channeldb/codec.go b/channeldb/codec.go
index 95434a5..b82a258 100644
--- a/channeldb/codec.go
+++ b/channeldb/codec.go
@@ -183,11 +183,6 @@ func WriteElement(w io.Writer, element interface{}) error {
return err
}
- case paymentIndexType:
- if err := binary.Write(w, byteOrder, e); err != nil {
- return err
- }
-
case lnwire.FundingFlag:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
@@ -416,11 +411,6 @@ func ReadElement(r io.Reader, element interface{}) error {
return err
}
- case *paymentIndexType:
- if err := binary.Read(r, byteOrder, e); err != nil {
- return err
- }
-
case *lnwire.FundingFlag:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
diff --git a/channeldb/db.go b/channeldb/db.go
index 715b906..00b29f6 100644
--- a/channeldb/db.go
+++ b/channeldb/db.go
@@ -203,11 +203,13 @@ var (
migration: mig.CreateTLB(payAddrIndexBucket),
},
{
- // Initialize payment index bucket which will be used
- // to index payments by sequence number. This index will
- // be used to allow more efficient ListPayments queries.
- number: 15,
- migration: mig.CreateTLB(paymentsIndexBucket),
+ // This used to be create payment related top-level
+ // buckets, however this is now done by the payment
+ // package.
+ number: 15,
+ migration: func(tx kvdb.RwTx) error {
+ return nil
+ },
},
{
// Add our existing payments to the index bucket created
@@ -450,7 +452,6 @@ var dbTopLevelBuckets = [][]byte{
invoiceBucket,
payAddrIndexBucket,
setIDIndexBucket,
- paymentsIndexBucket,
peersBucket,
nodeInfoBucket,
metaBucket,
diff --git a/channeldb/duplicate_payments.go b/channeldb/duplicate_payments.go
deleted file mode 100644
index 004722f..0000000
--- a/channeldb/duplicate_payments.go
+++ /dev/null
@@ -1,249 +0,0 @@
-package channeldb
-
-import (
- "bytes"
- "encoding/binary"
- "fmt"
- "io"
- "time"
-
- "github.com/btcsuite/btcd/btcec/v2"
- "github.com/lightningnetwork/lnd/kvdb"
- "github.com/lightningnetwork/lnd/lntypes"
- "github.com/lightningnetwork/lnd/lnwire"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "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, paymentsdb.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) (
- *PaymentCreationInfo, error) {
-
- var scratch [8]byte
-
- c := &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 *FailureReason
- b = bucket.Get(duplicatePaymentFailInfoKey)
- if b != nil {
- reason := 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
-}
diff --git a/channeldb/mp_payment.go b/channeldb/mp_payment.go
deleted file mode 100644
index f4467b7..0000000
--- a/channeldb/mp_payment.go
+++ /dev/null
@@ -1,721 +0,0 @@
-package channeldb
-
-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/lntypes"
- "github.com/lightningnetwork/lnd/lnutils"
- "github.com/lightningnetwork/lnd/lnwire"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "github.com/lightningnetwork/lnd/routing/route"
-)
-
-// 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
-
- // HTLCFailUnknown 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 *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 *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, *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 paymentsdb.ErrPaymentPendingSettled
- }
-
- // If the payment is already failed, we won't allow adding more HTLCs.
- if m.State.PaymentFailed {
- return paymentsdb.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",
- paymentsdb.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", paymentsdb.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",
- paymentsdb.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",
- paymentsdb.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",
- paymentsdb.ErrPaymentInternal, m.Status)
-
- // Unknown payment status.
- default:
- return false, fmt.Errorf("%w: %s",
- paymentsdb.ErrUnknownPaymentStatus, m.Status)
- }
-}
-
-// GetState returns the internal state of the payment.
-func (m *MPPayment) GetState() *MPPaymentState {
- return m.State
-}
-
-// Status returns the current status of the payment.
-func (m *MPPayment) GetStatus() PaymentStatus {
- return m.Status
-}
-
-// GetPayment 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",
- paymentsdb.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",
- paymentsdb.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
-}
diff --git a/channeldb/mp_payment_test.go b/channeldb/mp_payment_test.go
deleted file mode 100644
index 455a04d..0000000
--- a/channeldb/mp_payment_test.go
+++ /dev/null
@@ -1,603 +0,0 @@
-package channeldb
-
-import (
- "bytes"
- "fmt"
- "testing"
-
- "github.com/btcsuite/btcd/btcec/v2"
- "github.com/lightningnetwork/lnd/lntypes"
- "github.com/lightningnetwork/lnd/lnwire"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "github.com/lightningnetwork/lnd/routing/route"
- "github.com/stretchr/testify/require"
-)
-
-var (
- testHash = [32]byte{
- 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
- 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
- 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
- 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
- }
-)
-
-// TestLazySessionKeyDeserialize tests that we can read htlc attempt session
-// keys that were previously serialized as a private key as raw bytes.
-func TestLazySessionKeyDeserialize(t *testing.T) {
- var b bytes.Buffer
-
- // Serialize as a private key.
- err := WriteElements(&b, priv)
- require.NoError(t, err)
-
- // Deserialize into [btcec.PrivKeyBytesLen]byte.
- attempt := HTLCAttemptInfo{}
- err = ReadElements(&b, &attempt.sessionKey)
- require.NoError(t, err)
- require.Zero(t, b.Len())
-
- sessionKey := attempt.SessionKey()
- require.Equal(t, priv, sessionKey)
-}
-
-// TestRegistrable checks the method `Registrable` behaves as expected for ALL
-// possible payment statuses.
-func TestRegistrable(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- status PaymentStatus
- registryErr error
- hasSettledHTLC bool
- paymentFailed bool
- }{
- {
- status: StatusInitiated,
- registryErr: nil,
- },
- {
- // Test inflight status with no settled HTLC and no
- // failed payment.
- status: StatusInFlight,
- registryErr: nil,
- },
- {
- // Test inflight status with settled HTLC but no failed
- // payment.
- status: StatusInFlight,
- registryErr: paymentsdb.ErrPaymentPendingSettled,
- hasSettledHTLC: true,
- },
- {
- // Test inflight status with no settled HTLC but failed
- // payment.
- status: StatusInFlight,
- registryErr: paymentsdb.ErrPaymentPendingFailed,
- paymentFailed: true,
- },
- {
- // Test error state with settled HTLC and failed
- // payment.
- status: 0,
- registryErr: paymentsdb.ErrUnknownPaymentStatus,
- hasSettledHTLC: true,
- paymentFailed: true,
- },
- {
- status: StatusSucceeded,
- registryErr: paymentsdb.ErrPaymentAlreadySucceeded,
- },
- {
- status: StatusFailed,
- registryErr: paymentsdb.ErrPaymentAlreadyFailed,
- },
- {
- status: 0,
- registryErr: paymentsdb.ErrUnknownPaymentStatus,
- },
- }
-
- for i, tc := range testCases {
- i, tc := i, tc
-
- p := &MPPayment{
- Status: tc.status,
- State: &MPPaymentState{
- HasSettledHTLC: tc.hasSettledHTLC,
- PaymentFailed: tc.paymentFailed,
- },
- }
-
- name := fmt.Sprintf("test_%d_%s", i, p.Status.String())
- t.Run(name, func(t *testing.T) {
- t.Parallel()
-
- err := p.Registrable()
- require.ErrorIs(t, err, tc.registryErr,
- "registrable under state %v", tc.status)
- })
- }
-}
-
-// TestPaymentSetState checks that the method setState creates the
-// MPPaymentState as expected.
-func TestPaymentSetState(t *testing.T) {
- t.Parallel()
-
- // Create a test preimage and failure reason.
- preimage := lntypes.Preimage{1}
- failureReasonError := FailureReasonError
-
- testCases := []struct {
- name string
- payment *MPPayment
- totalAmt int
-
- expectedState *MPPaymentState
- errExpected error
- }{
- {
- // Test that when the sentAmt exceeds totalAmount, the
- // error is returned.
- name: "amount exceeded error",
- // SentAmt returns 90, 10
- // TerminalInfo returns non-nil, nil
- // InFlightHTLCs returns 0
- payment: &MPPayment{
- HTLCs: []HTLCAttempt{
- makeSettledAttempt(100, 10, preimage),
- },
- },
- totalAmt: 1,
- errExpected: paymentsdb.ErrSentExceedsTotal,
- },
- {
- // Test that when the htlc is failed, the fee is not
- // used.
- name: "fee excluded for failed htlc",
- payment: &MPPayment{
- // SentAmt returns 90, 10
- // TerminalInfo returns nil, nil
- // InFlightHTLCs returns 1
- HTLCs: []HTLCAttempt{
- makeActiveAttempt(100, 10),
- makeFailedAttempt(100, 10),
- },
- },
- totalAmt: 1000,
- expectedState: &MPPaymentState{
- NumAttemptsInFlight: 1,
- RemainingAmt: 1000 - 90,
- FeesPaid: 10,
- HasSettledHTLC: false,
- PaymentFailed: false,
- },
- },
- {
- // Test when the payment is settled, the state should
- // be marked as terminated.
- name: "payment settled",
- // SentAmt returns 90, 10
- // TerminalInfo returns non-nil, nil
- // InFlightHTLCs returns 0
- payment: &MPPayment{
- HTLCs: []HTLCAttempt{
- makeSettledAttempt(100, 10, preimage),
- },
- },
- totalAmt: 1000,
- expectedState: &MPPaymentState{
- NumAttemptsInFlight: 0,
- RemainingAmt: 1000 - 90,
- FeesPaid: 10,
- HasSettledHTLC: true,
- PaymentFailed: false,
- },
- },
- {
- // Test when the payment is failed, the state should be
- // marked as terminated.
- name: "payment failed",
- // SentAmt returns 0, 0
- // TerminalInfo returns nil, non-nil
- // InFlightHTLCs returns 0
- payment: &MPPayment{
- FailureReason: &failureReasonError,
- },
- totalAmt: 1000,
- expectedState: &MPPaymentState{
- NumAttemptsInFlight: 0,
- RemainingAmt: 1000,
- FeesPaid: 0,
- HasSettledHTLC: false,
- PaymentFailed: true,
- },
- },
- }
-
- for _, tc := range testCases {
- tc := tc
-
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- // Attach the payment info.
- info := &PaymentCreationInfo{
- Value: lnwire.MilliSatoshi(tc.totalAmt),
- }
- tc.payment.Info = info
-
- // Call the method that updates the payment state.
- err := tc.payment.setState()
- require.ErrorIs(t, err, tc.errExpected)
-
- require.Equal(
- t, tc.expectedState, tc.payment.State,
- "state not updated as expected",
- )
- })
- }
-}
-
-// TestNeedWaitAttempts checks whether we need to wait for the results of the
-// HTLC attempts against ALL possible payment statuses.
-func TestNeedWaitAttempts(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- status PaymentStatus
- remainingAmt lnwire.MilliSatoshi
- hasSettledHTLC bool
- hasFailureReason bool
- needWait bool
- expectedErr error
- }{
- {
- // For a newly created payment we don't need to wait
- // for results.
- status: StatusInitiated,
- remainingAmt: 1000,
- needWait: false,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we don't need to wait when the
- // remainingAmt is not zero and we have no settled
- // HTLCs.
- status: StatusInFlight,
- remainingAmt: 1000,
- needWait: false,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we need to wait when the
- // remainingAmt is not zero but we have settled HTLCs.
- status: StatusInFlight,
- remainingAmt: 1000,
- hasSettledHTLC: true,
- needWait: true,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we need to wait when the
- // remainingAmt is not zero and the payment is failed.
- status: StatusInFlight,
- remainingAmt: 1000,
- needWait: true,
- hasFailureReason: true,
- expectedErr: nil,
- },
-
- {
- // With the payment settled, but the remainingAmt is
- // not zero, we have an error state.
- status: StatusSucceeded,
- remainingAmt: 1000,
- needWait: false,
- expectedErr: paymentsdb.ErrPaymentInternal,
- },
- {
- // Payment is in terminal state, no need to wait.
- status: StatusFailed,
- remainingAmt: 1000,
- needWait: false,
- expectedErr: nil,
- },
- {
- // A newly created payment with zero remainingAmt
- // indicates an error.
- status: StatusInitiated,
- remainingAmt: 0,
- needWait: false,
- expectedErr: paymentsdb.ErrPaymentInternal,
- },
- {
- // With zero remainingAmt we must wait for the results.
- status: StatusInFlight,
- remainingAmt: 0,
- needWait: true,
- expectedErr: nil,
- },
- {
- // Payment is terminated, no need to wait for results.
- status: StatusSucceeded,
- remainingAmt: 0,
- needWait: false,
- expectedErr: nil,
- },
- {
- // Payment is terminated, no need to wait for results.
- status: StatusFailed,
- remainingAmt: 0,
- needWait: false,
- expectedErr: paymentsdb.ErrPaymentInternal,
- },
- {
- // Payment is in an unknown status, return an error.
- status: 0,
- remainingAmt: 0,
- needWait: false,
- expectedErr: paymentsdb.ErrUnknownPaymentStatus,
- },
- {
- // Payment is in an unknown status, return an error.
- status: 0,
- remainingAmt: 1000,
- needWait: false,
- expectedErr: paymentsdb.ErrUnknownPaymentStatus,
- },
- }
-
- for _, tc := range testCases {
- tc := tc
-
- p := &MPPayment{
- Info: &PaymentCreationInfo{
- PaymentIdentifier: [32]byte{1, 2, 3},
- },
- Status: tc.status,
- State: &MPPaymentState{
- RemainingAmt: tc.remainingAmt,
- HasSettledHTLC: tc.hasSettledHTLC,
- PaymentFailed: tc.hasFailureReason,
- },
- }
-
- name := fmt.Sprintf("status=%s|remainingAmt=%v|"+
- "settledHTLC=%v|failureReason=%v", tc.status,
- tc.remainingAmt, tc.hasSettledHTLC, tc.hasFailureReason)
-
- t.Run(name, func(t *testing.T) {
- t.Parallel()
-
- result, err := p.NeedWaitAttempts()
- require.ErrorIs(t, err, tc.expectedErr)
- require.Equalf(t, tc.needWait, result, "status=%v, "+
- "remainingAmt=%v", tc.status, tc.remainingAmt)
- })
- }
-}
-
-// TestAllowMoreAttempts checks whether more attempts can be created against
-// ALL possible payment statuses.
-func TestAllowMoreAttempts(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- status PaymentStatus
- remainingAmt lnwire.MilliSatoshi
- hasSettledHTLC bool
- paymentFailed bool
- allowMore bool
- expectedErr error
- }{
- {
- // A newly created payment with zero remainingAmt
- // indicates an error.
- status: StatusInitiated,
- remainingAmt: 0,
- allowMore: false,
- expectedErr: paymentsdb.ErrPaymentInternal,
- },
- {
- // With zero remainingAmt we don't allow more HTLC
- // attempts.
- status: StatusInFlight,
- remainingAmt: 0,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With zero remainingAmt we don't allow more HTLC
- // attempts.
- status: StatusSucceeded,
- remainingAmt: 0,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With zero remainingAmt we don't allow more HTLC
- // attempts.
- status: StatusFailed,
- remainingAmt: 0,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With zero remainingAmt and settled HTLCs we don't
- // allow more HTLC attempts.
- status: StatusInFlight,
- remainingAmt: 0,
- hasSettledHTLC: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With zero remainingAmt and failed payment we don't
- // allow more HTLC attempts.
- status: StatusInFlight,
- remainingAmt: 0,
- paymentFailed: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With zero remainingAmt and both settled HTLCs and
- // failed payment, we don't allow more HTLC attempts.
- status: StatusInFlight,
- remainingAmt: 0,
- hasSettledHTLC: true,
- paymentFailed: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // A newly created payment can have more attempts.
- status: StatusInitiated,
- remainingAmt: 1000,
- allowMore: true,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we can have more attempts when
- // the remainingAmt is not zero and we have neither
- // failed payment or settled HTLCs.
- status: StatusInFlight,
- remainingAmt: 1000,
- allowMore: true,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we cannot have more attempts
- // though the remainingAmt is not zero but we have
- // settled HTLCs.
- status: StatusInFlight,
- remainingAmt: 1000,
- hasSettledHTLC: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we cannot have more attempts
- // though the remainingAmt is not zero but we have
- // failed payment.
- status: StatusInFlight,
- remainingAmt: 1000,
- paymentFailed: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With HTLCs inflight we cannot have more attempts
- // though the remainingAmt is not zero but we have
- // settled HTLCs and failed payment.
- status: StatusInFlight,
- remainingAmt: 1000,
- hasSettledHTLC: true,
- paymentFailed: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With the payment settled, but the remainingAmt is
- // not zero, we have an error state.
- status: StatusSucceeded,
- remainingAmt: 1000,
- hasSettledHTLC: true,
- allowMore: false,
- expectedErr: paymentsdb.ErrPaymentInternal,
- },
- {
- // With the payment failed with no inflight HTLCs, we
- // don't allow more attempts to be made.
- status: StatusFailed,
- remainingAmt: 1000,
- paymentFailed: true,
- allowMore: false,
- expectedErr: nil,
- },
- {
- // With the payment in an unknown state, we don't allow
- // more attempts to be made.
- status: 0,
- remainingAmt: 1000,
- allowMore: false,
- expectedErr: nil,
- },
- }
-
- for i, tc := range testCases {
- tc := tc
-
- p := &MPPayment{
- Info: &PaymentCreationInfo{
- PaymentIdentifier: [32]byte{1, 2, 3},
- },
- Status: tc.status,
- State: &MPPaymentState{
- RemainingAmt: tc.remainingAmt,
- HasSettledHTLC: tc.hasSettledHTLC,
- PaymentFailed: tc.paymentFailed,
- },
- }
-
- name := fmt.Sprintf("test_%d|status=%s|remainingAmt=%v", i,
- tc.status, tc.remainingAmt)
-
- t.Run(name, func(t *testing.T) {
- t.Parallel()
-
- result, err := p.AllowMoreAttempts()
- require.ErrorIs(t, err, tc.expectedErr)
- require.Equalf(t, tc.allowMore, result, "status=%v, "+
- "remainingAmt=%v", tc.status, tc.remainingAmt)
- })
- }
-}
-
-func makeActiveAttempt(total, fee int) HTLCAttempt {
- return HTLCAttempt{
- HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
- }
-}
-
-func makeSettledAttempt(total, fee int,
- preimage lntypes.Preimage) HTLCAttempt {
-
- return HTLCAttempt{
- HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
- Settle: &HTLCSettleInfo{Preimage: preimage},
- }
-}
-
-func makeFailedAttempt(total, fee int) HTLCAttempt {
- return HTLCAttempt{
- HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
- Failure: &HTLCFailInfo{
- Reason: HTLCFailInternal,
- },
- }
-}
-
-func makeAttemptInfo(total, amtForwarded int) HTLCAttemptInfo {
- hop := &route.Hop{AmtToForward: lnwire.MilliSatoshi(amtForwarded)}
- return HTLCAttemptInfo{
- Route: route.Route{
- TotalAmount: lnwire.MilliSatoshi(total),
- Hops: []*route.Hop{hop},
- },
- }
-}
-
-// TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket
-// function is able to gracefully handle being passed a nil set of hops for the
-// route by the caller.
-func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
- t.Parallel()
-
- sessionKey, _ := btcec.NewPrivateKey()
- emptyRoute := &route.Route{}
- _, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
- require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
-}
diff --git a/channeldb/payment_status.go b/channeldb/payment_status.go
deleted file mode 100644
index 179e22f..0000000
--- a/channeldb/payment_status.go
+++ /dev/null
@@ -1,259 +0,0 @@
-package channeldb
-
-import (
- "fmt"
-
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
-)
-
-// 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 paymentsdb.ErrPaymentExists
-
- // We already have an InFlight payment on the network. We will disallow
- // any new payments.
- case StatusInFlight:
- return paymentsdb.ErrPaymentInFlight
-
- // The payment has been attempted and is succeeded so we won't allow
- // creating it again.
- case StatusSucceeded:
- return paymentsdb.ErrAlreadyPaid
-
- // We allow retrying failed payments.
- case StatusFailed:
- return nil
-
- default:
- return fmt.Errorf("%w: %v", paymentsdb.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 paymentsdb.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", paymentsdb.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 paymentsdb.ErrPaymentAlreadySucceeded
-
- case StatusFailed:
- return paymentsdb.ErrPaymentAlreadyFailed
-
- default:
- return fmt.Errorf("%w: %v", paymentsdb.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 *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)
- }
-}
diff --git a/channeldb/payment_status_test.go b/channeldb/payment_status_test.go
deleted file mode 100644
index bd61818..0000000
--- a/channeldb/payment_status_test.go
+++ /dev/null
@@ -1,249 +0,0 @@
-package channeldb
-
-import (
- "fmt"
- "testing"
-
- "github.com/lightningnetwork/lnd/lntypes"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "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 := FailureReasonNoRoute
- failure := &reason
-
- testCases := []struct {
- name string
- htlcs []HTLCAttempt
- reason *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: paymentsdb.ErrPaymentExists,
- updateErr: nil,
- removeErr: nil,
- },
- {
- status: StatusInFlight,
- initErr: paymentsdb.ErrPaymentInFlight,
- updateErr: nil,
- removeErr: paymentsdb.ErrPaymentInFlight,
- },
- {
- status: StatusSucceeded,
- initErr: paymentsdb.ErrAlreadyPaid,
- updateErr: paymentsdb.ErrPaymentAlreadySucceeded,
- removeErr: nil,
- },
- {
- status: StatusFailed,
- initErr: nil,
- updateErr: paymentsdb.ErrPaymentAlreadyFailed,
- removeErr: nil,
- },
- {
- status: 0,
- initErr: paymentsdb.ErrUnknownPaymentStatus,
- updateErr: paymentsdb.ErrUnknownPaymentStatus,
- removeErr: paymentsdb.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)
- })
- }
-}
diff --git a/channeldb/payments.go b/channeldb/payments.go
index a23f891..3430e62 100644
--- a/channeldb/payments.go
+++ b/channeldb/payments.go
@@ -92,71 +92,3 @@ func (p *PaymentCreationInfo) String() string {
return fmt.Sprintf("payment_id=%v, amount=%v, created_at=%v",
p.PaymentIdentifier, p.Value, p.CreationTime)
}
-
-// 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
-}
diff --git a/channeldb/payments_kv_store.go b/channeldb/payments_kv_store.go
deleted file mode 100644
index d18616c..0000000
--- a/channeldb/payments_kv_store.go
+++ /dev/null
@@ -1,2105 +0,0 @@
-package channeldb
-
-import (
- "bytes"
- "context"
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "sort"
- "sync"
- "time"
-
- "github.com/btcsuite/btcd/btcec/v2"
- "github.com/btcsuite/btcd/wire"
- "github.com/lightningnetwork/lnd/kvdb"
- "github.com/lightningnetwork/lnd/lntypes"
- "github.com/lightningnetwork/lnd/lnwire"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "github.com/lightningnetwork/lnd/record"
- "github.com/lightningnetwork/lnd/routing/route"
- "github.com/lightningnetwork/lnd/tlv"
-)
-
-const (
- // paymentSeqBlockSize is the block size used when we batch allocate
- // payment sequences for future payments.
- paymentSeqBlockSize = 1000
-
- // paymentProgressLogInterval is the interval we use limiting the
- // logging output of payment processing.
- paymentProgressLogInterval = 30 * time.Second
-)
-
-//nolint:ll
-var (
- // paymentsRootBucket is the name of the top-level bucket within the
- // database that stores all data related to payments. Within this
- // bucket, each payment hash its own sub-bucket keyed by its payment
- // hash.
- //
- // Bucket hierarchy:
- //
- // root-bucket
- // |
- // |-- <paymenthash>
- // | |--sequence-key: <sequence number>
- // | |--creation-info-key: <creation info>
- // | |--fail-info-key: <(optional) fail info>
- // | |
- // | |--payment-htlcs-bucket (shard-bucket)
- // | | |
- // | | |-- ai<htlc attempt ID>: <htlc attempt info>
- // | | |-- si<htlc attempt ID>: <(optional) settle info>
- // | | |-- fi<htlc attempt ID>: <(optional) fail info>
- // | | |
- // | | ...
- // | |
- // | |
- // | |--duplicate-bucket (only for old, completed payments)
- // | |
- // | |-- <seq-num>
- // | | |--sequence-key: <sequence number>
- // | | |--creation-info-key: <creation info>
- // | | |--ai: <attempt info>
- // | | |--si: <settle info>
- // | | |--fi: <fail info>
- // | |
- // | |-- <seq-num>
- // | | |
- // | ... ...
- // |
- // |-- <paymenthash>
- // | |
- // | ...
- // ...
- //
- paymentsRootBucket = []byte("payments-root-bucket")
-
- // paymentSequenceKey is a key used in the payment's sub-bucket to
- // store the sequence number of the payment.
- paymentSequenceKey = []byte("payment-sequence-key")
-
- // paymentCreationInfoKey is a key used in the payment's sub-bucket to
- // store the creation info of the payment.
- paymentCreationInfoKey = []byte("payment-creation-info")
-
- // paymentHtlcsBucket is a bucket where we'll store the information
- // about the HTLCs that were attempted for a payment.
- paymentHtlcsBucket = []byte("payment-htlcs-bucket")
-
- // htlcAttemptInfoKey is the key used as the prefix of an HTLC attempt
- // to store the info about the attempt that was done for the HTLC in
- // question. The HTLC attempt ID is concatenated at the end.
- htlcAttemptInfoKey = []byte("ai")
-
- // htlcSettleInfoKey is the key used as the prefix of an HTLC attempt
- // settle info, if any. The HTLC attempt ID is concatenated at the end.
- htlcSettleInfoKey = []byte("si")
-
- // htlcFailInfoKey is the key used as the prefix of an HTLC attempt
- // failure information, if any.The HTLC attempt ID is concatenated at
- // the end.
- htlcFailInfoKey = []byte("fi")
-
- // paymentFailInfoKey is a key used in the payment's sub-bucket to
- // store information about the reason a payment failed.
- paymentFailInfoKey = []byte("payment-fail-info")
-
- // paymentsIndexBucket is the name of the top-level bucket within the
- // database that stores an index of payment sequence numbers to its
- // payment hash.
- // payments-sequence-index-bucket
- // |--<sequence-number>: <payment hash>
- // |--...
- // |--<sequence-number>: <payment hash>
- paymentsIndexBucket = []byte("payments-index-bucket")
-)
-
-// KVPaymentsDB implements persistence for payments and payment attempts.
-type KVPaymentsDB struct {
- // Sequence management for the kv store.
- seqMu sync.Mutex
- currSeq uint64
- storedSeq uint64
-
- // db is the underlying database implementation.
- db kvdb.Backend
-
- keepFailedPaymentAttempts bool
-}
-
-// defaultKVStoreOptions returns the default options for the KV store.
-func defaultKVStoreOptions() *paymentsdb.StoreOptions {
- return &paymentsdb.StoreOptions{
- KeepFailedPaymentAttempts: false,
- }
-}
-
-// NewKVPaymentsDB creates a new KVStore for payments.
-func NewKVPaymentsDB(db kvdb.Backend,
- options ...paymentsdb.OptionModifier) (*KVPaymentsDB, error) {
-
- opts := defaultKVStoreOptions()
- for _, applyOption := range options {
- applyOption(opts)
- }
-
- if !opts.NoMigration {
- if err := initKVStore(db); err != nil {
- return nil, err
- }
- }
-
- return &KVPaymentsDB{
- db: db,
- keepFailedPaymentAttempts: opts.KeepFailedPaymentAttempts,
- }, nil
-}
-
-var paymentsTopLevelBuckets = [][]byte{
- paymentsRootBucket,
- paymentsIndexBucket,
-}
-
-// initKVStore creates and initializes the top-level buckets for the payment db.
-func initKVStore(db kvdb.Backend) error {
- err := kvdb.Update(db, func(tx kvdb.RwTx) error {
- for _, tlb := range paymentsTopLevelBuckets {
- if _, err := tx.CreateTopLevelBucket(tlb); err != nil {
- return err
- }
- }
-
- return nil
- }, func() {})
- if err != nil {
- return fmt.Errorf("unable to create new payments db: %w", err)
- }
-
- return nil
-}
-
-// InitPayment checks or records the given PaymentCreationInfo with the DB,
-// 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 *KVPaymentsDB) InitPayment(paymentHash lntypes.Hash,
- info *PaymentCreationInfo) error {
-
- // Obtain a new sequence number for this payment. This is used
- // to sort the payments in order of creation, and also acts as
- // a unique identifier for each payment.
- sequenceNum, err := p.nextPaymentSequence()
- if err != nil {
- return err
- }
-
- var b bytes.Buffer
- if err := serializePaymentCreationInfo(&b, info); err != nil {
- return err
- }
- infoBytes := b.Bytes()
-
- var updateErr error
- err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
- // Reset the update error, to avoid carrying over an error
- // from a previous execution of the batched db transaction.
- updateErr = nil
-
- prefetchPayment(tx, paymentHash)
- bucket, err := createPaymentBucket(tx, paymentHash)
- if err != nil {
- return err
- }
-
- // Get the existing status of this payment, if any.
- paymentStatus, err := fetchPaymentStatus(bucket)
-
- switch {
- // If no error is returned, it means we already have this
- // payment. We'll check the status to decide whether we allow
- // retrying the payment or return a specific error.
- case err == nil:
- if err := paymentStatus.initializable(); err != nil {
- updateErr = err
- return nil
- }
-
- // Otherwise, if the error is not `ErrPaymentNotInitiated`,
- // we'll return the error.
- case !errors.Is(err, paymentsdb.ErrPaymentNotInitiated):
- return err
- }
-
- // Before we set our new sequence number, we check whether this
- // payment has a previously set sequence number and remove its
- // index entry if it exists. This happens in the case where we
- // have a previously attempted payment which was left in a state
- // where we can retry.
- seqBytes := bucket.Get(paymentSequenceKey)
- if seqBytes != nil {
- indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
- if err := indexBucket.Delete(seqBytes); err != nil {
- return err
- }
- }
-
- // Once we have obtained a sequence number, we add an entry
- // to our index bucket which will map the sequence number to
- // our payment identifier.
- err = createPaymentIndexEntry(
- tx, sequenceNum, info.PaymentIdentifier,
- )
- if err != nil {
- return err
- }
-
- err = bucket.Put(paymentSequenceKey, sequenceNum)
- if err != nil {
- return err
- }
-
- // Add the payment info to the bucket, which contains the
- // static information for this payment
- err = bucket.Put(paymentCreationInfoKey, infoBytes)
- if err != nil {
- return err
- }
-
- // We'll delete any lingering HTLCs to start with, in case we
- // are initializing a payment that was attempted earlier, but
- // left in a state where we could retry.
- err = bucket.DeleteNestedBucket(paymentHtlcsBucket)
- if err != nil && !errors.Is(err, kvdb.ErrBucketNotFound) {
- return err
- }
-
- // Also delete any lingering failure info now that we are
- // re-attempting.
- return bucket.Delete(paymentFailInfoKey)
- })
- if err != nil {
- return fmt.Errorf("unable to init payment: %w", err)
- }
-
- return updateErr
-}
-
-// DeleteFailedAttempts deletes all failed htlcs for a payment if configured
-// by the KVPaymentsDB db.
-func (p *KVPaymentsDB) DeleteFailedAttempts(hash lntypes.Hash) error {
- if !p.keepFailedPaymentAttempts {
- const failedHtlcsOnly = true
- err := p.DeletePayment(hash, failedHtlcsOnly)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// paymentIndexTypeHash is a payment index type which indicates that we have
-// created an index of payment sequence number to payment hash.
-type paymentIndexType uint8
-
-// paymentIndexTypeHash is a payment index type which indicates that we have
-// created an index of payment sequence number to payment hash.
-const paymentIndexTypeHash paymentIndexType = 0
-
-// createPaymentIndexEntry creates a payment hash typed index for a payment. The
-// index produced contains a payment index type (which can be used in future to
-// signal different payment index types) and the payment identifier.
-func createPaymentIndexEntry(tx kvdb.RwTx, sequenceNumber []byte,
- id lntypes.Hash) error {
-
- var b bytes.Buffer
- if err := WriteElements(&b, paymentIndexTypeHash, id[:]); err != nil {
- return err
- }
-
- indexes := tx.ReadWriteBucket(paymentsIndexBucket)
-
- return indexes.Put(sequenceNumber, b.Bytes())
-}
-
-// deserializePaymentIndex deserializes a payment index entry. This function
-// currently only supports deserialization of payment hash indexes, and will
-// fail for other types.
-func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) {
- var (
- indexType paymentIndexType
- paymentHash []byte
- )
-
- if err := ReadElements(r, &indexType, &paymentHash); err != nil {
- return lntypes.Hash{}, err
- }
-
- // While we only have on payment index type, we do not need to use our
- // index type to deserialize the index. However, we sanity check that
- // this type is as expected, since we had to read it out anyway.
- if indexType != paymentIndexTypeHash {
- return lntypes.Hash{}, fmt.Errorf("unknown payment index "+
- "type: %v", indexType)
- }
-
- hash, err := lntypes.MakeHash(paymentHash)
- if err != nil {
- return lntypes.Hash{}, err
- }
-
- return hash, nil
-}
-
-// RegisterAttempt atomically records the provided HTLCAttemptInfo to the
-// DB.
-func (p *KVPaymentsDB) RegisterAttempt(paymentHash lntypes.Hash,
- attempt *HTLCAttemptInfo) (*MPPayment, error) {
-
- // Serialize the information before opening the db transaction.
- var a bytes.Buffer
- err := serializeHTLCAttemptInfo(&a, attempt)
- if err != nil {
- return nil, err
- }
- htlcInfoBytes := a.Bytes()
-
- htlcIDBytes := make([]byte, 8)
- binary.BigEndian.PutUint64(htlcIDBytes, attempt.AttemptID)
-
- var payment *MPPayment
- err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
- prefetchPayment(tx, paymentHash)
- bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
- if err != nil {
- return err
- }
-
- payment, err = fetchPayment(bucket)
- if err != nil {
- return err
- }
-
- // Check if registering a new attempt is allowed.
- if err := payment.Registrable(); err != nil {
- return err
- }
-
- // If the final hop has encrypted data, then we know this is a
- // blinded payment. In blinded payments, MPP records are not set
- // for split payments and the recipient is responsible for using
- // a consistent PathID across the various encrypted data
- // payloads that we received from them for this payment. All we
- // need to check is that the total amount field for each HTLC
- // in the split payment is correct.
- isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
-
- // Make sure any existing shards match the new one with regards
- // to MPP options.
- mpp := attempt.Route.FinalHop().MPP
-
- // MPP records should not be set for attempts to blinded paths.
- if isBlinded && mpp != nil {
- return paymentsdb.ErrMPPRecordInBlindedPayment
- }
-
- for _, h := range payment.InFlightHTLCs() {
- hMpp := h.Route.FinalHop().MPP
-
- // If this is a blinded payment, then no existing HTLCs
- // should have MPP records.
- if isBlinded && hMpp != nil {
- return paymentsdb.ErrMPPRecordInBlindedPayment
- }
-
- // If this is a blinded payment, then we just need to
- // check that the TotalAmtMsat field for this shard
- // is equal to that of any other shard in the same
- // payment.
- if isBlinded {
- if attempt.Route.FinalHop().TotalAmtMsat !=
- h.Route.FinalHop().TotalAmtMsat {
-
- //nolint:ll
- return paymentsdb.ErrBlindedPaymentTotalAmountMismatch
- }
-
- continue
- }
-
- switch {
- // We tried to register a non-MPP attempt for a MPP
- // payment.
- case mpp == nil && hMpp != nil:
- return paymentsdb.ErrMPPayment
-
- // We tried to register a MPP shard for a non-MPP
- // payment.
- case mpp != nil && hMpp == nil:
- return paymentsdb.ErrNonMPPayment
-
- // Non-MPP payment, nothing more to validate.
- case mpp == nil:
- continue
- }
-
- // Check that MPP options match.
- if mpp.PaymentAddr() != hMpp.PaymentAddr() {
- return paymentsdb.ErrMPPPaymentAddrMismatch
- }
-
- if mpp.TotalMsat() != hMpp.TotalMsat() {
- return paymentsdb.ErrMPPTotalAmountMismatch
- }
- }
-
- // If this is a non-MPP attempt, it must match the total amount
- // exactly. Note that a blinded payment is considered an MPP
- // attempt.
- amt := attempt.Route.ReceiverAmt()
- if !isBlinded && mpp == nil && amt != payment.Info.Value {
- return paymentsdb.ErrValueMismatch
- }
-
- // Ensure we aren't sending more than the total payment amount.
- sentAmt, _ := payment.SentAmt()
- if sentAmt+amt > payment.Info.Value {
- return fmt.Errorf("%w: attempted=%v, payment amount="+
- "%v", paymentsdb.ErrValueExceedsAmt,
- sentAmt+amt, payment.Info.Value)
- }
-
- htlcsBucket, err := bucket.CreateBucketIfNotExists(
- paymentHtlcsBucket,
- )
- if err != nil {
- return err
- }
-
- err = htlcsBucket.Put(
- htlcBucketKey(htlcAttemptInfoKey, htlcIDBytes),
- htlcInfoBytes,
- )
- if err != nil {
- return err
- }
-
- // Retrieve attempt info for the notification.
- payment, err = fetchPayment(bucket)
-
- return err
- })
- if err != nil {
- return nil, err
- }
-
- return payment, err
-}
-
-// SettleAttempt marks the given attempt settled with the preimage. If this is
-// a multi shard payment, this might implicitly mean that the full payment
-// succeeded.
-//
-// 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 *KVPaymentsDB) SettleAttempt(hash lntypes.Hash,
- attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) {
-
- var b bytes.Buffer
- if err := serializeHTLCSettleInfo(&b, settleInfo); err != nil {
- return nil, err
- }
- settleBytes := b.Bytes()
-
- return p.updateHtlcKey(hash, attemptID, htlcSettleInfoKey, settleBytes)
-}
-
-// FailAttempt marks the given payment attempt failed.
-func (p *KVPaymentsDB) FailAttempt(hash lntypes.Hash,
- attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) {
-
- var b bytes.Buffer
- if err := serializeHTLCFailInfo(&b, failInfo); err != nil {
- return nil, err
- }
- failBytes := b.Bytes()
-
- return p.updateHtlcKey(hash, attemptID, htlcFailInfoKey, failBytes)
-}
-
-// updateHtlcKey updates a database key for the specified htlc.
-func (p *KVPaymentsDB) updateHtlcKey(paymentHash lntypes.Hash,
- attemptID uint64, key, value []byte) (*MPPayment, error) {
-
- aid := make([]byte, 8)
- binary.BigEndian.PutUint64(aid, attemptID)
-
- var payment *MPPayment
- err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
- payment = nil
-
- prefetchPayment(tx, paymentHash)
- bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
- if err != nil {
- return err
- }
-
- p, err := fetchPayment(bucket)
- if err != nil {
- return err
- }
-
- // We can only update keys of in-flight payments. We allow
- // updating keys even if the payment has reached a terminal
- // condition, since the HTLC outcomes must still be updated.
- if err := p.Status.updatable(); err != nil {
- return err
- }
-
- htlcsBucket := bucket.NestedReadWriteBucket(paymentHtlcsBucket)
- if htlcsBucket == nil {
- return fmt.Errorf("htlcs bucket not found")
- }
-
- attemptKey := htlcBucketKey(htlcAttemptInfoKey, aid)
- if htlcsBucket.Get(attemptKey) == nil {
- return fmt.Errorf("HTLC with ID %v not registered",
- attemptID)
- }
-
- // Make sure the shard is not already failed or settled.
- failKey := htlcBucketKey(htlcFailInfoKey, aid)
- if htlcsBucket.Get(failKey) != nil {
- return paymentsdb.ErrAttemptAlreadyFailed
- }
-
- settleKey := htlcBucketKey(htlcSettleInfoKey, aid)
- if htlcsBucket.Get(settleKey) != nil {
- return paymentsdb.ErrAttemptAlreadySettled
- }
-
- // Add or update the key for this htlc.
- err = htlcsBucket.Put(htlcBucketKey(key, aid), value)
- if err != nil {
- return err
- }
-
- // Retrieve attempt info for the notification.
- payment, err = fetchPayment(bucket)
-
- return err
- })
- if err != nil {
- return nil, err
- }
-
- return payment, err
-}
-
-// Fail transitions a payment into the Failed state, and records the reason the
-// 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 *KVPaymentsDB) Fail(paymentHash lntypes.Hash,
- reason FailureReason) (*MPPayment, error) {
-
- var (
- updateErr error
- payment *MPPayment
- )
- err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
- // Reset the update error, to avoid carrying over an error
- // from a previous execution of the batched db transaction.
- updateErr = nil
- payment = nil
-
- prefetchPayment(tx, paymentHash)
- bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
- if errors.Is(err, paymentsdb.ErrPaymentNotInitiated) {
- updateErr = paymentsdb.ErrPaymentNotInitiated
- return nil
- } else if err != nil {
- return err
- }
-
- // We mark the payment as failed as long as it is known. This
- // lets the last attempt to fail with a terminal write its
- // failure to the KVPaymentsDB without synchronizing with
- // other attempts.
- _, err = fetchPaymentStatus(bucket)
- if errors.Is(err, paymentsdb.ErrPaymentNotInitiated) {
- updateErr = paymentsdb.ErrPaymentNotInitiated
- return nil
- } else if err != nil {
- return err
- }
-
- // Put the failure reason in the bucket for record keeping.
- v := []byte{byte(reason)}
- err = bucket.Put(paymentFailInfoKey, v)
- if err != nil {
- return err
- }
-
- // Retrieve attempt info for the notification, if available.
- payment, err = fetchPayment(bucket)
- if err != nil {
- return err
- }
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- return payment, updateErr
-}
-
-// FetchPayment returns information about a payment from the database.
-func (p *KVPaymentsDB) FetchPayment(paymentHash lntypes.Hash) (
- *MPPayment, error) {
-
- var payment *MPPayment
- err := kvdb.View(p.db, func(tx kvdb.RTx) error {
- prefetchPayment(tx, paymentHash)
- bucket, err := fetchPaymentBucket(tx, paymentHash)
- if err != nil {
- return err
- }
-
- payment, err = fetchPayment(bucket)
-
- return err
- }, func() {
- payment = nil
- })
- if err != nil {
- return nil, err
- }
-
- return payment, nil
-}
-
-// prefetchPayment attempts to prefetch as much of the payment as possible to
-// reduce DB roundtrips.
-func prefetchPayment(tx kvdb.RTx, paymentHash lntypes.Hash) {
- rb := kvdb.RootBucket(tx)
- kvdb.Prefetch(
- rb,
- []string{
- // Prefetch all keys in the payment's bucket.
- string(paymentsRootBucket),
- string(paymentHash[:]),
- },
- []string{
- // Prefetch all keys in the payment's htlc bucket.
- string(paymentsRootBucket),
- string(paymentHash[:]),
- string(paymentHtlcsBucket),
- },
- )
-}
-
-// createPaymentBucket creates or fetches the sub-bucket assigned to this
-// payment hash.
-func createPaymentBucket(tx kvdb.RwTx, paymentHash lntypes.Hash) (
- kvdb.RwBucket, error) {
-
- payments, err := tx.CreateTopLevelBucket(paymentsRootBucket)
- if err != nil {
- return nil, err
- }
-
- return payments.CreateBucketIfNotExists(paymentHash[:])
-}
-
-// fetchPaymentBucket fetches the sub-bucket assigned to this payment hash. If
-// the bucket does not exist, it returns ErrPaymentNotInitiated.
-func fetchPaymentBucket(tx kvdb.RTx, paymentHash lntypes.Hash) (
- kvdb.RBucket, error) {
-
- payments := tx.ReadBucket(paymentsRootBucket)
- if payments == nil {
- return nil, paymentsdb.ErrPaymentNotInitiated
- }
-
- bucket := payments.NestedReadBucket(paymentHash[:])
- if bucket == nil {
- return nil, paymentsdb.ErrPaymentNotInitiated
- }
-
- return bucket, nil
-}
-
-// fetchPaymentBucketUpdate is identical to fetchPaymentBucket, but it returns a
-// bucket that can be written to.
-func fetchPaymentBucketUpdate(tx kvdb.RwTx, paymentHash lntypes.Hash) (
- kvdb.RwBucket, error) {
-
- payments := tx.ReadWriteBucket(paymentsRootBucket)
- if payments == nil {
- return nil, paymentsdb.ErrPaymentNotInitiated
- }
-
- bucket := payments.NestedReadWriteBucket(paymentHash[:])
- if bucket == nil {
- return nil, paymentsdb.ErrPaymentNotInitiated
- }
-
- return bucket, nil
-}
-
-// nextPaymentSequence returns the next sequence number to store for a new
-// payment.
-func (p *KVPaymentsDB) nextPaymentSequence() ([]byte, error) {
- p.seqMu.Lock()
- defer p.seqMu.Unlock()
-
- // Set a new upper bound in the DB every 1000 payments to avoid
- // conflicts on the sequence when using etcd.
- if p.currSeq == p.storedSeq {
- var currPaymentSeq, newUpperBound uint64
- if err := kvdb.Update(p.db, func(tx kvdb.RwTx) error {
- paymentsBucket, err := tx.CreateTopLevelBucket(
- paymentsRootBucket,
- )
- if err != nil {
- return err
- }
-
- currPaymentSeq = paymentsBucket.Sequence()
- newUpperBound = currPaymentSeq + paymentSeqBlockSize
-
- return paymentsBucket.SetSequence(newUpperBound)
- }, func() {}); err != nil {
- return nil, err
- }
-
- // We lazy initialize the cached currPaymentSeq here using the
- // first nextPaymentSequence() call. This if statement will auto
- // initialize our stored currPaymentSeq, since by default both
- // this variable and storedPaymentSeq are zero which in turn
- // will have us fetch the current values from the DB.
- if p.currSeq == 0 {
- p.currSeq = currPaymentSeq
- }
-
- p.storedSeq = newUpperBound
- }
-
- p.currSeq++
- b := make([]byte, 8)
- binary.BigEndian.PutUint64(b, p.currSeq)
-
- return b, nil
-}
-
-// fetchPaymentStatus fetches the payment status of the payment. If the payment
-// isn't found, it will return error `ErrPaymentNotInitiated`.
-func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) {
- // Creation info should be set for all payments, regardless of state.
- // If not, it is unknown.
- if bucket.Get(paymentCreationInfoKey) == nil {
- return 0, paymentsdb.ErrPaymentNotInitiated
- }
-
- payment, err := fetchPayment(bucket)
- if err != nil {
- return 0, err
- }
-
- return payment.Status, nil
-}
-
-// FetchInFlightPayments returns all payments with status InFlight.
-func (p *KVPaymentsDB) FetchInFlightPayments() ([]*MPPayment, error) {
- var (
- inFlights []*MPPayment
- start = time.Now()
- lastLogTime = time.Now()
- processedCount int
- )
-
- err := kvdb.View(p.db, func(tx kvdb.RTx) error {
- payments := tx.ReadBucket(paymentsRootBucket)
- if payments == nil {
- return nil
- }
-
- return payments.ForEach(func(k, _ []byte) error {
- bucket := payments.NestedReadBucket(k)
- if bucket == nil {
- return fmt.Errorf("non bucket element")
- }
-
- p, err := fetchPayment(bucket)
- if err != nil {
- return err
- }
-
- processedCount++
- if time.Since(lastLogTime) >=
- paymentProgressLogInterval {
-
- log.Debugf("Scanning inflight payments "+
- "(in progress), processed %d, last "+
- "processed payment: %v", processedCount,
- p.Info)
-
- lastLogTime = time.Now()
- }
-
- // Skip the payment if it's terminated.
- if p.Terminated() {
- return nil
- }
-
- inFlights = append(inFlights, p)
-
- return nil
- })
- }, func() {
- inFlights = nil
- })
- if err != nil {
- return nil, err
- }
-
- elapsed := time.Since(start)
- log.Debugf("Completed scanning for inflight payments: "+
- "total_processed=%d, found_inflight=%d, elapsed=%v",
- processedCount, len(inFlights),
- elapsed.Round(time.Millisecond))
-
- return inFlights, nil
-}
-
-// htlcBucketKey creates a composite key from prefix and id where the result is
-// simply the two concatenated.
-func htlcBucketKey(prefix, id []byte) []byte {
- key := make([]byte, len(prefix)+len(id))
- copy(key, prefix)
- copy(key[len(prefix):], id)
-
- return key
-}
-
-// FetchPayments returns all sent payments found in the DB.
-func (p *KVPaymentsDB) FetchPayments() ([]*MPPayment, error) {
- var payments []*MPPayment
-
- err := kvdb.View(p.db, func(tx kvdb.RTx) error {
- paymentsBucket := tx.ReadBucket(paymentsRootBucket)
- if paymentsBucket == nil {
- return nil
- }
-
- return paymentsBucket.ForEach(func(k, v []byte) error {
- bucket := paymentsBucket.NestedReadBucket(k)
- if bucket == nil {
- // We only expect sub-buckets to be found in
- // this top-level bucket.
- return fmt.Errorf("non bucket element in " +
- "payments bucket")
- }
-
- p, err := fetchPayment(bucket)
- if err != nil {
- return err
- }
-
- payments = append(payments, p)
-
- // 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.
- duplicatePayments, err := fetchDuplicatePayments(bucket)
- if err != nil {
- return err
- }
-
- payments = append(payments, duplicatePayments...)
-
- return nil
- })
- }, func() {
- payments = nil
- })
- if err != nil {
- return nil, err
- }
-
- // Before returning, sort the payments by their sequence number.
- sort.Slice(payments, func(i, j int) bool {
- return payments[i].SequenceNum < payments[j].SequenceNum
- })
-
- return payments, nil
-}
-
-func fetchCreationInfo(bucket kvdb.RBucket) (*PaymentCreationInfo, error) {
- b := bucket.Get(paymentCreationInfoKey)
- if b == nil {
- return nil, fmt.Errorf("creation info not found")
- }
-
- r := bytes.NewReader(b)
-
- return deserializePaymentCreationInfo(r)
-}
-
-func fetchPayment(bucket kvdb.RBucket) (*MPPayment, error) {
- seqBytes := bucket.Get(paymentSequenceKey)
- if seqBytes == nil {
- return nil, fmt.Errorf("sequence number not found")
- }
-
- sequenceNum := binary.BigEndian.Uint64(seqBytes)
-
- // Get the PaymentCreationInfo.
- creationInfo, err := fetchCreationInfo(bucket)
- if err != nil {
- return nil, err
- }
-
- var htlcs []HTLCAttempt
- htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket)
- if htlcsBucket != nil {
- // Get the payment attempts. This can be empty.
- htlcs, err = fetchHtlcAttempts(htlcsBucket)
- if err != nil {
- return nil, err
- }
- }
-
- // Get failure reason if available.
- var failureReason *FailureReason
- b := bucket.Get(paymentFailInfoKey)
- if b != nil {
- reason := FailureReason(b[0])
- failureReason = &reason
- }
-
- // Create a new payment.
- payment := &MPPayment{
- SequenceNum: sequenceNum,
- Info: creationInfo,
- HTLCs: htlcs,
- FailureReason: failureReason,
- }
-
- // Set its state and status.
- if err := payment.setState(); err != nil {
- return nil, err
- }
-
- return payment, nil
-}
-
-// fetchHtlcAttempts retrieves all htlc attempts made for the payment found in
-// the given bucket.
-func fetchHtlcAttempts(bucket kvdb.RBucket) ([]HTLCAttempt, error) {
- htlcsMap := make(map[uint64]*HTLCAttempt)
-
- attemptInfoCount := 0
- err := bucket.ForEach(func(k, v []byte) error {
- aid := byteOrder.Uint64(k[len(k)-8:])
-
- if _, ok := htlcsMap[aid]; !ok {
- htlcsMap[aid] = &HTLCAttempt{}
- }
-
- var err error
- switch {
- case bytes.HasPrefix(k, htlcAttemptInfoKey):
- attemptInfo, err := readHtlcAttemptInfo(v)
- if err != nil {
- return err
- }
-
- attemptInfo.AttemptID = aid
- htlcsMap[aid].HTLCAttemptInfo = *attemptInfo
- attemptInfoCount++
-
- case bytes.HasPrefix(k, htlcSettleInfoKey):
- htlcsMap[aid].Settle, err = readHtlcSettleInfo(v)
- if err != nil {
- return err
- }
-
- case bytes.HasPrefix(k, htlcFailInfoKey):
- htlcsMap[aid].Failure, err = readHtlcFailInfo(v)
- if err != nil {
- return err
- }
-
- default:
- return fmt.Errorf("unknown htlc attempt key")
- }
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- // Sanity check that all htlcs have an attempt info.
- if attemptInfoCount != len(htlcsMap) {
- return nil, paymentsdb.ErrNoAttemptInfo
- }
-
- keys := make([]uint64, len(htlcsMap))
- i := 0
- for k := range htlcsMap {
- keys[i] = k
- i++
- }
-
- // Sort HTLC attempts by their attempt ID. This is needed because in the
- // DB we store the attempts with keys prefixed by their status which
- // changes order (groups them together by status).
- sort.Slice(keys, func(i, j int) bool {
- return keys[i] < keys[j]
- })
-
- htlcs := make([]HTLCAttempt, len(htlcsMap))
- for i, key := range keys {
- htlcs[i] = *htlcsMap[key]
- }
-
- return htlcs, nil
-}
-
-// readHtlcAttemptInfo reads the payment attempt info for this htlc.
-func readHtlcAttemptInfo(b []byte) (*HTLCAttemptInfo, error) {
- r := bytes.NewReader(b)
- return deserializeHTLCAttemptInfo(r)
-}
-
-// readHtlcSettleInfo reads the settle info for the htlc. If the htlc isn't
-// settled, nil is returned.
-func readHtlcSettleInfo(b []byte) (*HTLCSettleInfo, error) {
- r := bytes.NewReader(b)
- return deserializeHTLCSettleInfo(r)
-}
-
-// readHtlcFailInfo reads the failure info for the htlc. If the htlc hasn't
-// failed, nil is returned.
-func readHtlcFailInfo(b []byte) (*HTLCFailInfo, error) {
- r := bytes.NewReader(b)
- return deserializeHTLCFailInfo(r)
-}
-
-// fetchFailedHtlcKeys retrieves the bucket keys of all failed HTLCs of a
-// payment bucket.
-func fetchFailedHtlcKeys(bucket kvdb.RBucket) ([][]byte, error) {
- htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket)
-
- var htlcs []HTLCAttempt
- var err error
- if htlcsBucket != nil {
- htlcs, err = fetchHtlcAttempts(htlcsBucket)
- if err != nil {
- return nil, err
- }
- }
-
- // Now iterate though them and save the bucket keys for the failed
- // HTLCs.
- var htlcKeys [][]byte
- for _, h := range htlcs {
- if h.Failure == nil {
- continue
- }
-
- htlcKeyBytes := make([]byte, 8)
- binary.BigEndian.PutUint64(htlcKeyBytes, h.AttemptID)
-
- htlcKeys = append(htlcKeys, htlcKeyBytes)
- }
-
- return htlcKeys, nil
-}
-
-// QueryPayments is a query to the payments database which is restricted
-// to a subset of payments by the payments query, containing an offset
-// index and a maximum number of returned payments.
-func (p *KVPaymentsDB) QueryPayments(_ context.Context,
- query PaymentsQuery) (PaymentsResponse, error) {
-
- var resp PaymentsResponse
-
- if err := kvdb.View(p.db, func(tx kvdb.RTx) error {
- // Get the root payments bucket.
- paymentsBucket := tx.ReadBucket(paymentsRootBucket)
- if paymentsBucket == nil {
- return nil
- }
-
- // Get the index bucket which maps sequence number -> payment
- // hash and duplicate bool. If we have a payments bucket, we
- // should have an indexes bucket as well.
- indexes := tx.ReadBucket(paymentsIndexBucket)
- if indexes == nil {
- return fmt.Errorf("index bucket does not exist")
- }
-
- // accumulatePayments gets payments with the sequence number
- // and hash provided and adds them to our list of payments if
- // they meet the criteria of our query. It returns the number
- // of payments that were added.
- accumulatePayments := func(sequenceKey, hash []byte) (bool,
- error) {
-
- r := bytes.NewReader(hash)
- paymentHash, err := deserializePaymentIndex(r)
- if err != nil {
- return false, err
- }
-
- payment, err := fetchPaymentWithSequenceNumber(
- tx, paymentHash, sequenceKey,
- )
- if err != nil {
- return false, err
- }
-
- // To keep compatibility with the old API, we only
- // return non-succeeded payments if requested.
- if payment.Status != StatusSucceeded &&
- !query.IncludeIncomplete {
-
- return false, err
- }
-
- // Get the creation time in Unix seconds, this always
- // rounds down the nanoseconds to full seconds.
- createTime := payment.Info.CreationTime.Unix()
-
- // Skip any payments that were created before the
- // specified time.
- if createTime < query.CreationDateStart {
- return false, nil
- }
-
- // Skip any payments that were created after the
- // specified time.
- if query.CreationDateEnd != 0 &&
- createTime > query.CreationDateEnd {
-
- return false, nil
- }
-
- // At this point, we've exhausted the offset, so we'll
- // begin collecting invoices found within the range.
- resp.Payments = append(resp.Payments, payment)
-
- return true, nil
- }
-
- // Create a paginator which reads from our sequence index bucket
- // with the parameters provided by the payments query.
- paginator := NewPaginator(
- indexes.ReadCursor(), query.Reversed, query.IndexOffset,
- query.MaxPayments,
- )
-
- // Run a paginated query, adding payments to our response.
- if err := paginator.Query(accumulatePayments); err != nil {
- return err
- }
-
- // Counting the total number of payments is expensive, since we
- // literally have to traverse the cursor linearly, which can
- // take quite a while. So it's an optional query parameter.
- if query.CountTotal {
- var (
- totalPayments uint64
- err error
- )
- countFn := func(_, _ []byte) error {
- totalPayments++
-
- return nil
- }
-
- // In non-boltdb database backends, there's a faster
- // ForAll query that allows for batch fetching items.
- fastBucket, ok := indexes.(kvdb.ExtendedRBucket)
- if ok {
- err = fastBucket.ForAll(countFn)
- } else {
- err = indexes.ForEach(countFn)
- }
- if err != nil {
- return fmt.Errorf("error counting payments: %w",
- err)
- }
-
- resp.TotalCount = totalPayments
- }
-
- return nil
- }, func() {
- resp = PaymentsResponse{}
- }); err != nil {
- return resp, err
- }
-
- // Need to swap the payments slice order if reversed order.
- if query.Reversed {
- for l, r := 0, len(resp.Payments)-1; l < r; l, r = l+1, r-1 {
- resp.Payments[l], resp.Payments[r] =
- resp.Payments[r], resp.Payments[l]
- }
- }
-
- // Set the first and last index of the returned payments so that the
- // caller can resume from this point later on.
- if len(resp.Payments) > 0 {
- resp.FirstIndexOffset = resp.Payments[0].SequenceNum
- resp.LastIndexOffset =
- resp.Payments[len(resp.Payments)-1].SequenceNum
- }
-
- return resp, nil
-}
-
-// fetchPaymentWithSequenceNumber get the payment which matches the payment hash
-// *and* sequence number provided from the database. This is required because
-// we previously had more than one payment per hash, so we have multiple indexes
-// pointing to a single payment; we want to retrieve the correct one.
-func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash,
- sequenceNumber []byte) (*MPPayment, error) {
-
- // We can now lookup the payment keyed by its hash in
- // the payments root bucket.
- bucket, err := fetchPaymentBucket(tx, paymentHash)
- if err != nil {
- return nil, err
- }
-
- // A single payment hash can have multiple payments associated with it.
- // We lookup our sequence number first, to determine whether this is
- // the payment we are actually looking for.
- seqBytes := bucket.Get(paymentSequenceKey)
- if seqBytes == nil {
- return nil, paymentsdb.ErrNoSequenceNumber
- }
-
- // If this top level payment has the sequence number we are looking for,
- // return it.
- if bytes.Equal(seqBytes, sequenceNumber) {
- return fetchPayment(bucket)
- }
-
- // If we were not looking for the top level payment, we are looking for
- // one of our duplicate payments. We need to iterate through the seq
- // numbers in this bucket to find the correct payments. If we do not
- // find a duplicate payments bucket here, something is wrong.
- dup := bucket.NestedReadBucket(duplicatePaymentsBucket)
- if dup == nil {
- return nil, paymentsdb.ErrNoDuplicateBucket
- }
-
- var duplicatePayment *MPPayment
- 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 paymentsdb.ErrNoDuplicateNestedBucket
- }
-
- seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
- if seqBytes == nil {
- return err
- }
-
- // If this duplicate payment is not the sequence number we are
- // looking for, we can continue.
- if !bytes.Equal(seqBytes, sequenceNumber) {
- return nil
- }
-
- duplicatePayment, err = fetchDuplicatePayment(subBucket)
- if err != nil {
- return err
- }
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- // If none of the duplicate payments matched our sequence number, we
- // failed to find the payment with this sequence number; something is
- // wrong.
- if duplicatePayment == nil {
- return nil, paymentsdb.ErrDuplicateNotFound
- }
-
- return duplicatePayment, 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 (p *KVPaymentsDB) DeletePayment(paymentHash lntypes.Hash,
- failedHtlcsOnly bool) error {
-
- return kvdb.Update(p.db, func(tx kvdb.RwTx) error {
- payments := tx.ReadWriteBucket(paymentsRootBucket)
- if payments == nil {
- return nil
- }
-
- bucket := payments.NestedReadWriteBucket(paymentHash[:])
- if bucket == nil {
- return fmt.Errorf("non bucket element in payments " +
- "bucket")
- }
-
- // If the status is InFlight, we cannot safely delete
- // the payment information, so we return early.
- paymentStatus, err := fetchPaymentStatus(bucket)
- if err != nil {
- return err
- }
-
- // If the payment has inflight HTLCs, we cannot safely delete
- // the payment information, so we return an error.
- if err := paymentStatus.removable(); err != nil {
- return fmt.Errorf("payment '%v' has inflight HTLCs"+
- "and therefore cannot be deleted: %w",
- paymentHash.String(), err)
- }
-
- // Delete the failed HTLC attempts we found.
- if failedHtlcsOnly {
- toDelete, err := fetchFailedHtlcKeys(bucket)
- if err != nil {
- return err
- }
-
- htlcsBucket := bucket.NestedReadWriteBucket(
- paymentHtlcsBucket,
- )
-
- for _, htlcID := range toDelete {
- err = htlcsBucket.Delete(
- htlcBucketKey(
- htlcAttemptInfoKey, htlcID,
- ),
- )
- if err != nil {
- return err
- }
-
- err = htlcsBucket.Delete(
- htlcBucketKey(htlcFailInfoKey, htlcID),
- )
- if err != nil {
- return err
- }
-
- err = htlcsBucket.Delete(
- htlcBucketKey(
- htlcSettleInfoKey, htlcID,
- ),
- )
- if err != nil {
- return err
- }
- }
-
- return nil
- }
-
- seqNrs, err := fetchSequenceNumbers(bucket)
- if err != nil {
- return err
- }
-
- err = payments.DeleteNestedBucket(paymentHash[:])
- if err != nil {
- return err
- }
-
- indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
- for _, k := range seqNrs {
- if err := indexBucket.Delete(k); err != nil {
- return err
- }
- }
-
- return nil
- }, func() {})
-}
-
-// DeletePayments deletes all completed and failed payments from the DB. If
-// failedOnly is set, only failed payments will be considered for deletion. If
-// 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 *KVPaymentsDB) DeletePayments(failedOnly,
- failedHtlcsOnly bool) (int, error) {
-
- var numPayments int
- err := kvdb.Update(p.db, func(tx kvdb.RwTx) error {
- payments := tx.ReadWriteBucket(paymentsRootBucket)
- if payments == nil {
- return nil
- }
-
- var (
- // deleteBuckets is the set of payment buckets we need
- // to delete.
- deleteBuckets [][]byte
-
- // deleteIndexes is the set of indexes pointing to these
- // payments that need to be deleted.
- deleteIndexes [][]byte
-
- // deleteHtlcs maps a payment hash to the HTLC IDs we
- // want to delete for that payment.
- deleteHtlcs = make(map[lntypes.Hash][][]byte)
- )
- err := payments.ForEach(func(k, _ []byte) error {
- bucket := payments.NestedReadBucket(k)
- if bucket == nil {
- // We only expect sub-buckets to be found in
- // this top-level bucket.
- return fmt.Errorf("non bucket element in " +
- "payments bucket")
- }
-
- // If the status is InFlight, we cannot safely delete
- // the payment information, so we return early.
- paymentStatus, err := fetchPaymentStatus(bucket)
- if err != nil {
- return err
- }
-
- // If the payment has inflight HTLCs, we cannot safely
- // delete the payment information, so we return an nil
- // to skip it.
- if err := paymentStatus.removable(); err != nil {
- return nil
- }
-
- // If we requested to only delete failed payments, we
- // can return if this one is not.
- if failedOnly && paymentStatus != StatusFailed {
- return nil
- }
-
- // If we are only deleting failed HTLCs, fetch them.
- if failedHtlcsOnly {
- toDelete, err := fetchFailedHtlcKeys(bucket)
- if err != nil {
- return err
- }
-
- hash, err := lntypes.MakeHash(k)
- if err != nil {
- return err
- }
-
- deleteHtlcs[hash] = toDelete
-
- // We return, we are only deleting attempts.
- return nil
- }
-
- // Add the bucket to the set of buckets we can delete.
- deleteBuckets = append(deleteBuckets, k)
-
- // Get all the sequence number associated with the
- // payment, including duplicates.
- seqNrs, err := fetchSequenceNumbers(bucket)
- if err != nil {
- return err
- }
-
- deleteIndexes = append(deleteIndexes, seqNrs...)
- numPayments++
-
- return nil
- })
- if err != nil {
- return err
- }
-
- // Delete the failed HTLC attempts we found.
- for hash, htlcIDs := range deleteHtlcs {
- bucket := payments.NestedReadWriteBucket(hash[:])
- htlcsBucket := bucket.NestedReadWriteBucket(
- paymentHtlcsBucket,
- )
-
- for _, aid := range htlcIDs {
- if err := htlcsBucket.Delete(
- htlcBucketKey(htlcAttemptInfoKey, aid),
- ); err != nil {
- return err
- }
-
- if err := htlcsBucket.Delete(
- htlcBucketKey(htlcFailInfoKey, aid),
- ); err != nil {
- return err
- }
-
- if err := htlcsBucket.Delete(
- htlcBucketKey(htlcSettleInfoKey, aid),
- ); err != nil {
- return err
- }
- }
- }
-
- for _, k := range deleteBuckets {
- if err := payments.DeleteNestedBucket(k); err != nil {
- return err
- }
- }
-
- // Get our index bucket and delete all indexes pointing to the
- // payments we are deleting.
- indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
- for _, k := range deleteIndexes {
- if err := indexBucket.Delete(k); err != nil {
- return err
- }
- }
-
- return nil
- }, func() {
- numPayments = 0
- })
- if err != nil {
- return 0, err
- }
-
- return numPayments, nil
-}
-
-// fetchSequenceNumbers fetches all the sequence numbers associated with a
-// payment, including those belonging to any duplicate payments.
-func fetchSequenceNumbers(paymentBucket kvdb.RBucket) ([][]byte, error) {
- seqNum := paymentBucket.Get(paymentSequenceKey)
- if seqNum == nil {
- return nil, errors.New("expected sequence number")
- }
-
- sequenceNumbers := [][]byte{seqNum}
-
- // Get the duplicate payments bucket, if it has no duplicates, just
- // return early with the payment sequence number.
- duplicates := paymentBucket.NestedReadBucket(duplicatePaymentsBucket)
- if duplicates == nil {
- return sequenceNumbers, nil
- }
-
- // If we do have duplicated, they are keyed by sequence number, so we
- // iterate through the duplicates bucket and add them to our set of
- // sequence numbers.
- if err := duplicates.ForEach(func(k, v []byte) error {
- sequenceNumbers = append(sequenceNumbers, k)
- return nil
- }); err != nil {
- return nil, err
- }
-
- return sequenceNumbers, nil
-}
-
-func serializePaymentCreationInfo(w io.Writer, c *PaymentCreationInfo) error {
- var scratch [8]byte
-
- if _, err := w.Write(c.PaymentIdentifier[:]); err != nil {
- return err
- }
-
- byteOrder.PutUint64(scratch[:], uint64(c.Value))
- if _, err := w.Write(scratch[:]); err != nil {
- return err
- }
-
- if err := serializeTime(w, c.CreationTime); err != nil {
- return err
- }
-
- byteOrder.PutUint32(scratch[:4], uint32(len(c.PaymentRequest)))
- if _, err := w.Write(scratch[:4]); err != nil {
- return err
- }
-
- if _, err := w.Write(c.PaymentRequest); err != nil {
- return err
- }
-
- // Any remaining bytes are TLV encoded records. Currently, these are
- // only the custom records provided by the user to be sent to the first
- // hop. But this can easily be extended with further records by merging
- // the records into a single TLV stream.
- err := c.FirstHopCustomRecords.SerializeTo(w)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo,
- error) {
-
- var scratch [8]byte
-
- c := &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[:]))
-
- creationTime, err := deserializeTime(r)
- if err != nil {
- return nil, err
- }
- c.CreationTime = creationTime
-
- 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
-
- // Any remaining bytes are TLV encoded records. Currently, these are
- // only the custom records provided by the user to be sent to the first
- // hop. But this can easily be extended with further records by merging
- // the records into a single TLV stream.
- c.FirstHopCustomRecords, err = lnwire.ParseCustomRecordsFrom(r)
- if err != nil {
- return nil, err
- }
-
- return c, nil
-}
-
-func serializeHTLCAttemptInfo(w io.Writer, a *HTLCAttemptInfo) error {
- if err := WriteElements(w, a.sessionKey); err != nil {
- return err
- }
-
- if err := SerializeRoute(w, a.Route); err != nil {
- return err
- }
-
- if err := serializeTime(w, a.AttemptTime); err != nil {
- return err
- }
-
- // If the hash is nil we can just return.
- if a.Hash == nil {
- return nil
- }
-
- if _, err := w.Write(a.Hash[:]); err != nil {
- return err
- }
-
- // Merge the fixed/known records together with the custom records to
- // serialize them as a single blob. We can't do this in SerializeRoute
- // because we're in the middle of the byte stream there. We can only do
- // TLV serialization at the end of the stream, since EOF is allowed for
- // a stream if no more data is expected.
- producers := []tlv.RecordProducer{
- &a.Route.FirstHopAmount,
- }
- tlvData, err := lnwire.MergeAndEncode(
- producers, nil, a.Route.FirstHopWireCustomRecords,
- )
- if err != nil {
- return err
- }
-
- if _, err := w.Write(tlvData); err != nil {
- return err
- }
-
- return nil
-}
-
-func deserializeHTLCAttemptInfo(r io.Reader) (*HTLCAttemptInfo, error) {
- a := &HTLCAttemptInfo{}
- err := ReadElements(r, &a.sessionKey)
- if err != nil {
- return nil, err
- }
-
- a.Route, err = DeserializeRoute(r)
- if err != nil {
- return nil, err
- }
-
- a.AttemptTime, err = deserializeTime(r)
- if err != nil {
- return nil, err
- }
-
- hash := lntypes.Hash{}
- _, err = io.ReadFull(r, hash[:])
-
- switch {
- // Older payment attempts wouldn't have the hash set, in which case we
- // can just return.
- case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
- return a, nil
-
- case err != nil:
- return nil, err
-
- default:
- }
-
- a.Hash = &hash
-
- // Read any remaining data (if any) and parse it into the known records
- // and custom records.
- extraData, err := io.ReadAll(r)
- if err != nil {
- return nil, err
- }
-
- customRecords, _, _, err := lnwire.ParseAndExtractCustomRecords(
- extraData, &a.Route.FirstHopAmount,
- )
- if err != nil {
- return nil, err
- }
-
- a.Route.FirstHopWireCustomRecords = customRecords
-
- return a, nil
-}
-
-func serializeHop(w io.Writer, h *route.Hop) error {
- if err := WriteElements(w,
- h.PubKeyBytes[:],
- h.ChannelID,
- h.OutgoingTimeLock,
- h.AmtToForward,
- ); err != nil {
- return err
- }
-
- if err := binary.Write(w, byteOrder, h.LegacyPayload); err != nil {
- return err
- }
-
- // For legacy payloads, we don't need to write any TLV records, so
- // we'll write a zero indicating the our serialized TLV map has no
- // records.
- if h.LegacyPayload {
- return WriteElements(w, uint32(0))
- }
-
- // Gather all non-primitive TLV records so that they can be serialized
- // as a single blob.
- //
- // TODO(conner): add migration to unify all fields in a single TLV
- // blobs. The split approach will cause headaches down the road as more
- // fields are added, which we can avoid by having a single TLV stream
- // for all payload fields.
- var records []tlv.Record
- if h.MPP != nil {
- records = append(records, h.MPP.Record())
- }
-
- // Add blinding point and encrypted data if present.
- if h.EncryptedData != nil {
- records = append(records, record.NewEncryptedDataRecord(
- &h.EncryptedData,
- ))
- }
-
- if h.BlindingPoint != nil {
- records = append(records, record.NewBlindingPointRecord(
- &h.BlindingPoint,
- ))
- }
-
- if h.AMP != nil {
- records = append(records, h.AMP.Record())
- }
-
- if h.Metadata != nil {
- records = append(records, record.NewMetadataRecord(&h.Metadata))
- }
-
- if h.TotalAmtMsat != 0 {
- totalMsatInt := uint64(h.TotalAmtMsat)
- records = append(
- records, record.NewTotalAmtMsatBlinded(&totalMsatInt),
- )
- }
-
- // Final sanity check to absolutely rule out custom records that are not
- // custom and write into the standard range.
- if err := h.CustomRecords.Validate(); err != nil {
- return err
- }
-
- // Convert custom records to tlv and add to the record list.
- // MapToRecords sorts the list, so adding it here will keep the list
- // canonical.
- tlvRecords := tlv.MapToRecords(h.CustomRecords)
- records = append(records, tlvRecords...)
-
- // Otherwise, we'll transform our slice of records into a map of the
- // raw bytes, then serialize them in-line with a length (number of
- // elements) prefix.
- mapRecords, err := tlv.RecordsToMap(records)
- if err != nil {
- return err
- }
-
- numRecords := uint32(len(mapRecords))
- if err := WriteElements(w, numRecords); err != nil {
- return err
- }
-
- for recordType, rawBytes := range mapRecords {
- if err := WriteElements(w, recordType); err != nil {
- return err
- }
-
- if err := wire.WriteVarBytes(w, 0, rawBytes); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// maxOnionPayloadSize is the largest Sphinx payload possible, so we don't need
-// to read/write a TLV stream larger than this.
-const maxOnionPayloadSize = 1300
-
-func deserializeHop(r io.Reader) (*route.Hop, error) {
- h := &route.Hop{}
-
- var pub []byte
- if err := ReadElements(r, &pub); err != nil {
- return nil, err
- }
- copy(h.PubKeyBytes[:], pub)
-
- if err := ReadElements(r,
- &h.ChannelID, &h.OutgoingTimeLock, &h.AmtToForward,
- ); err != nil {
- return nil, err
- }
-
- // TODO(roasbeef): change field to allow LegacyPayload false to be the
- // legacy default?
- err := binary.Read(r, byteOrder, &h.LegacyPayload)
- if err != nil {
- return nil, err
- }
-
- var numElements uint32
- if err := ReadElements(r, &numElements); err != nil {
- return nil, err
- }
-
- // If there're no elements, then we can return early.
- if numElements == 0 {
- return h, nil
- }
-
- tlvMap := make(map[uint64][]byte)
- for i := uint32(0); i < numElements; i++ {
- var tlvType uint64
- if err := ReadElements(r, &tlvType); err != nil {
- return nil, err
- }
-
- rawRecordBytes, err := wire.ReadVarBytes(
- r, 0, maxOnionPayloadSize, "tlv",
- )
- if err != nil {
- return nil, err
- }
-
- tlvMap[tlvType] = rawRecordBytes
- }
-
- // If the MPP type is present, remove it from the generic TLV map and
- // parse it back into a proper MPP struct.
- //
- // TODO(conner): add migration to unify all fields in a single TLV
- // blobs. The split approach will cause headaches down the road as more
- // fields are added, which we can avoid by having a single TLV stream
- // for all payload fields.
- mppType := uint64(record.MPPOnionType)
- if mppBytes, ok := tlvMap[mppType]; ok {
- delete(tlvMap, mppType)
-
- var (
- mpp = &record.MPP{}
- mppRec = mpp.Record()
- r = bytes.NewReader(mppBytes)
- )
- err := mppRec.Decode(r, uint64(len(mppBytes)))
- if err != nil {
- return nil, err
- }
- h.MPP = mpp
- }
-
- // If encrypted data or blinding key are present, remove them from
- // the TLV map and parse into proper types.
- encryptedDataType := uint64(record.EncryptedDataOnionType)
- if data, ok := tlvMap[encryptedDataType]; ok {
- delete(tlvMap, encryptedDataType)
- h.EncryptedData = data
- }
-
- blindingType := uint64(record.BlindingPointOnionType)
- if blindingPoint, ok := tlvMap[blindingType]; ok {
- delete(tlvMap, blindingType)
-
- h.BlindingPoint, err = btcec.ParsePubKey(blindingPoint)
- if err != nil {
- return nil, fmt.Errorf("invalid blinding point: %w",
- err)
- }
- }
-
- ampType := uint64(record.AMPOnionType)
- if ampBytes, ok := tlvMap[ampType]; ok {
- delete(tlvMap, ampType)
-
- var (
- amp = &record.AMP{}
- ampRec = amp.Record()
- r = bytes.NewReader(ampBytes)
- )
- err := ampRec.Decode(r, uint64(len(ampBytes)))
- if err != nil {
- return nil, err
- }
- h.AMP = amp
- }
-
- // If the metadata type is present, remove it from the tlv map and
- // populate directly on the hop.
- metadataType := uint64(record.MetadataOnionType)
- if metadata, ok := tlvMap[metadataType]; ok {
- delete(tlvMap, metadataType)
-
- h.Metadata = metadata
- }
-
- totalAmtMsatType := uint64(record.TotalAmtMsatBlindedType)
- if totalAmtMsat, ok := tlvMap[totalAmtMsatType]; ok {
- delete(tlvMap, totalAmtMsatType)
-
- var (
- totalAmtMsatInt uint64
- buf [8]byte
- )
- if err := tlv.DTUint64(
- bytes.NewReader(totalAmtMsat),
- &totalAmtMsatInt,
- &buf,
- uint64(len(totalAmtMsat)),
- ); err != nil {
- return nil, err
- }
-
- h.TotalAmtMsat = lnwire.MilliSatoshi(totalAmtMsatInt)
- }
-
- h.CustomRecords = tlvMap
-
- return h, nil
-}
-
-// SerializeRoute serializes a route.
-func SerializeRoute(w io.Writer, r route.Route) error {
- if err := WriteElements(w,
- r.TotalTimeLock, r.TotalAmount, r.SourcePubKey[:],
- ); err != nil {
- return err
- }
-
- if err := WriteElements(w, uint32(len(r.Hops))); err != nil {
- return err
- }
-
- for _, h := range r.Hops {
- if err := serializeHop(w, h); err != nil {
- return err
- }
- }
-
- // Any new/extra TLV data is encoded in serializeHTLCAttemptInfo!
-
- return nil
-}
-
-// DeserializeRoute deserializes a route.
-func DeserializeRoute(r io.Reader) (route.Route, error) {
- rt := route.Route{}
- if err := ReadElements(r,
- &rt.TotalTimeLock, &rt.TotalAmount,
- ); err != nil {
- return rt, err
- }
-
- var pub []byte
- if err := ReadElements(r, &pub); err != nil {
- return rt, err
- }
- copy(rt.SourcePubKey[:], pub)
-
- var numHops uint32
- if err := ReadElements(r, &numHops); err != nil {
- return rt, err
- }
-
- var hops []*route.Hop
- for i := uint32(0); i < numHops; i++ {
- hop, err := deserializeHop(r)
- if err != nil {
- return rt, err
- }
- hops = append(hops, hop)
- }
- rt.Hops = hops
-
- // Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo!
-
- return rt, nil
-}
diff --git a/channeldb/payments_kv_store_test.go b/channeldb/payments_kv_store_test.go
deleted file mode 100644
index d1953fa..0000000
--- a/channeldb/payments_kv_store_test.go
+++ /dev/null
@@ -1,1802 +0,0 @@
-package channeldb
-
-import (
- "bytes"
- "crypto/rand"
- "crypto/sha256"
- "errors"
- "fmt"
- "io"
- "reflect"
- "testing"
- "time"
-
- "github.com/btcsuite/btcwallet/walletdb"
- "github.com/davecgh/go-spew/spew"
- "github.com/lightningnetwork/lnd/kvdb"
- "github.com/lightningnetwork/lnd/lntypes"
- "github.com/lightningnetwork/lnd/lnwire"
- paymentsdb "github.com/lightningnetwork/lnd/payments/db"
- "github.com/lightningnetwork/lnd/record"
- "github.com/lightningnetwork/lnd/routing/route"
- "github.com/lightningnetwork/lnd/tlv"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func genPreimage() ([32]byte, error) {
- var preimage [32]byte
- if _, err := io.ReadFull(rand.Reader, preimage[:]); err != nil {
- return preimage, err
- }
- return preimage, nil
-}
-
-func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo,
- lntypes.Preimage, error) {
-
- preimage, err := genPreimage()
- if err != nil {
- return nil, nil, preimage, fmt.Errorf("unable to "+
- "generate preimage: %v", err)
- }
-
- rhash := sha256.Sum256(preimage[:])
- var hash lntypes.Hash
- copy(hash[:], rhash[:])
-
- attempt, err := NewHtlcAttempt(
- 0, priv, *testRoute.Copy(), time.Time{}, &hash,
- )
- require.NoError(t, err)
-
- return &PaymentCreationInfo{
- PaymentIdentifier: rhash,
- Value: testRoute.ReceiverAmt(),
- CreationTime: time.Unix(time.Now().Unix(), 0),
- PaymentRequest: []byte("hola"),
- }, &attempt.HTLCAttemptInfo, preimage, nil
-}
-
-// TestKVPaymentsDBSwitchFail checks that payment status returns to Failed
-// status after failing, and that InitPayment allows another HTLC for the
-// same payment hash.
-func TestKVPaymentsDBSwitchFail(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, attempt, preimg, err := genInfo(t)
- require.NoError(t, err, "unable to generate htlc message")
-
- // Sends base htlc message which initiate StatusInFlight.
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- require.NoError(t, err, "unable to send htlc message")
-
- assertPaymentIndex(t, paymentDB, info.PaymentIdentifier)
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInitiated,
- )
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, nil,
- )
-
- // Fail the payment, which should moved it to Failed.
- failReason := FailureReasonNoRoute
- _, err = paymentDB.Fail(info.PaymentIdentifier, failReason)
- require.NoError(t, err, "unable to fail payment hash")
-
- // Verify the status is indeed Failed.
- assertPaymentStatus(t, paymentDB, info.PaymentIdentifier, StatusFailed)
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, &failReason, nil,
- )
-
- // Lookup the payment so we can get its old sequence number before it is
- // overwritten.
- payment, err := paymentDB.FetchPayment(info.PaymentIdentifier)
- require.NoError(t, err)
-
- // Sends the htlc again, which should succeed since the prior payment
- // failed.
- err = paymentDB.InitPayment(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
- // removed.
- assertPaymentIndex(t, paymentDB, info.PaymentIdentifier)
- assertNoIndex(t, paymentDB, payment.SequenceNum)
-
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInitiated,
- )
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, nil,
- )
-
- // 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)
- require.NoError(t, err, "unable to register attempt")
-
- htlcReason := HTLCFailUnreadable
- _, err = paymentDB.FailAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCFailInfo{
- Reason: htlcReason,
- },
- )
- if err != nil {
- t.Fatal(err)
- }
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInFlight,
- )
-
- htlc := &htlcStatus{
- HTLCAttemptInfo: attempt,
- failure: &htlcReason,
- }
-
- assertPaymentInfo(t, paymentDB, info.PaymentIdentifier, info, nil, htlc)
-
- // Record another attempt.
- attempt.AttemptID = 1
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
- require.NoError(t, err, "unable to send htlc message")
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInFlight,
- )
-
- htlc = &htlcStatus{
- HTLCAttemptInfo: attempt,
- }
-
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, htlc,
- )
-
- // Settle the attempt and verify that status was changed to
- // StatusSucceeded.
- payment, err = paymentDB.SettleAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- require.NoError(t, err, "error shouldn't have been received, got")
-
- if len(payment.HTLCs) != 2 {
- t.Fatalf("payment should have two htlcs, got: %d",
- len(payment.HTLCs))
- }
-
- err = assertRouteEqual(&payment.HTLCs[0].Route, &attempt.Route)
- if err != nil {
- t.Fatalf("unexpected route returned: %v vs %v: %v",
- spew.Sdump(attempt.Route),
- spew.Sdump(payment.HTLCs[0].Route), err)
- }
-
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusSucceeded,
- )
-
- htlc.settle = &preimg
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, htlc,
- )
-
- // Attempt a final payment, which should now fail since the prior
- // payment succeed.
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- if !errors.Is(err, paymentsdb.ErrAlreadyPaid) {
- t.Fatalf("unable to send htlc message: %v", err)
- }
-}
-
-// TestKVPaymentsDBSwitchDoubleSend checks the ability of payment control to
-// prevent double sending of htlc message, when message is in StatusInFlight.
-func TestKVPaymentsDBSwitchDoubleSend(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, attempt, preimg, err := genInfo(t)
- require.NoError(t, err, "unable to generate htlc message")
-
- // 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)
- require.NoError(t, err, "unable to send htlc message")
-
- assertPaymentIndex(t, paymentDB, info.PaymentIdentifier)
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInitiated,
- )
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, nil,
- )
-
- // 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)
- require.ErrorIs(t, err, paymentsdb.ErrPaymentExists)
-
- // Record an attempt.
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
- require.NoError(t, err, "unable to send htlc message")
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInFlight,
- )
-
- htlc := &htlcStatus{
- HTLCAttemptInfo: attempt,
- }
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, htlc,
- )
-
- // Sends base htlc message which initiate StatusInFlight.
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- if !errors.Is(err, paymentsdb.ErrPaymentInFlight) {
- t.Fatalf("payment control wrong behaviour: " +
- "double sending must trigger ErrPaymentInFlight error")
- }
-
- // After settling, the error should be ErrAlreadyPaid.
- _, err = paymentDB.SettleAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- require.NoError(t, err, "error shouldn't have been received, got")
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusSucceeded,
- )
-
- htlc.settle = &preimg
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, htlc,
- )
-
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- if !errors.Is(err, paymentsdb.ErrAlreadyPaid) {
- t.Fatalf("unable to send htlc message: %v", err)
- }
-}
-
-// TestKVPaymentsDBSuccessesWithoutInFlight checks that the payment
-// control will disallow calls to Success when no payment is in flight.
-func TestKVPaymentsDBSuccessesWithoutInFlight(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, _, preimg, err := genInfo(t)
- require.NoError(t, err, "unable to generate htlc message")
-
- // Attempt to complete the payment should fail.
- _, err = paymentDB.SettleAttempt(
- info.PaymentIdentifier, 0,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- require.ErrorIs(t, err, paymentsdb.ErrPaymentNotInitiated)
-}
-
-// TestKVPaymentsDBFailsWithoutInFlight checks that a strict payment
-// control will disallow calls to Fail when no payment is in flight.
-func TestKVPaymentsDBFailsWithoutInFlight(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, _, _, err := genInfo(t)
- require.NoError(t, err, "unable to generate htlc message")
-
- // Calling Fail should return an error.
- _, err = paymentDB.Fail(info.PaymentIdentifier, FailureReasonNoRoute)
- require.ErrorIs(t, err, paymentsdb.ErrPaymentNotInitiated)
-}
-
-// TestKVPaymentsDBDeleteNonInFlight checks that calling DeletePayments only
-// deletes payments from the database that are not in-flight.
-func TestKVPaymentsDBDeleteNonInFlight(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- // Create a sequence number for duplicate payments that will not collide
- // with the sequence numbers for the payments we create. These values
- // start at 1, so 9999 is a safe bet for this test.
- var duplicateSeqNr = 9999
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- payments := []struct {
- failed bool
- success bool
- hasDuplicate bool
- }{
- {
- failed: true,
- success: false,
- hasDuplicate: false,
- },
- {
- failed: false,
- success: true,
- hasDuplicate: false,
- },
- {
- failed: false,
- success: false,
- hasDuplicate: false,
- },
- {
- failed: false,
- success: true,
- hasDuplicate: true,
- },
- }
-
- 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)
- }
-
- // Sends base htlc message which initiate StatusInFlight.
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- if err != nil {
- t.Fatalf("unable to send htlc message: %v", err)
- }
- _, err = paymentDB.RegisterAttempt(
- info.PaymentIdentifier, attempt,
- )
- if err != nil {
- t.Fatalf("unable to send htlc message: %v", err)
- }
-
- htlc := &htlcStatus{
- HTLCAttemptInfo: attempt,
- }
-
- if p.failed {
- // Fail the payment attempt.
- htlcFailure := HTLCFailUnreadable
- _, err := paymentDB.FailAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFailure,
- },
- )
- if err != nil {
- t.Fatalf("unable to fail htlc: %v", err)
- }
-
- // Fail the payment, which should moved it to Failed.
- failReason := FailureReasonNoRoute
- _, err = paymentDB.Fail(
- info.PaymentIdentifier, failReason,
- )
- if err != nil {
- t.Fatalf("unable to fail payment hash: %v", err)
- }
-
- // Verify the status is indeed Failed.
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier,
- StatusFailed,
- )
-
- htlc.failure = &htlcFailure
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info,
- &failReason, htlc,
- )
- } else if p.success {
- // Verifies that status was changed to StatusSucceeded.
- _, err := paymentDB.SettleAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- if err != nil {
- t.Fatalf("error shouldn't have been received,"+
- " got: %v", err)
- }
-
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier,
- StatusSucceeded,
- )
-
- htlc.settle = &preimg
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil,
- htlc,
- )
-
- numSuccess++
- } else {
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier,
- StatusInFlight,
- )
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil,
- htlc,
- )
-
- numInflight++
- }
-
- // If the payment is intended to have a duplicate payment, we
- // add one.
- if p.hasDuplicate {
- appendDuplicatePayment(
- t, paymentDB.db, info.PaymentIdentifier,
- uint64(duplicateSeqNr), preimg,
- )
- duplicateSeqNr++
- numSuccess++
- }
- }
-
- // Delete all failed payments.
- numPayments, err := paymentDB.DeletePayments(true, false)
- require.NoError(t, err)
- require.EqualValues(t, 1, numPayments)
-
- // This should leave the succeeded and in-flight payments.
- dbPayments, err := paymentDB.FetchPayments()
- if err != nil {
- t.Fatal(err)
- }
-
- if len(dbPayments) != numSuccess+numInflight {
- t.Fatalf("expected %d payments, got %d",
- numSuccess+numInflight, len(dbPayments))
- }
-
- var s, i int
- for _, p := range dbPayments {
- t.Log("fetch payment has status", p.Status)
- switch p.Status {
- case StatusSucceeded:
- s++
- case StatusInFlight:
- i++
- }
- }
-
- if s != numSuccess {
- t.Fatalf("expected %d succeeded payments , got %d",
- numSuccess, s)
- }
- if i != numInflight {
- t.Fatalf("expected %d in-flight payments, got %d",
- numInflight, i)
- }
-
- // Now delete all payments except in-flight.
- numPayments, err = paymentDB.DeletePayments(false, false)
- require.NoError(t, err)
- require.EqualValues(t, 2, numPayments)
-
- // This should leave the in-flight payment.
- dbPayments, err = paymentDB.FetchPayments()
- if err != nil {
- t.Fatal(err)
- }
-
- if len(dbPayments) != numInflight {
- t.Fatalf("expected %d payments, got %d", numInflight,
- len(dbPayments))
- }
-
- for _, p := range dbPayments {
- if p.Status != StatusInFlight {
- t.Fatalf("expected in-fligth status, got %v", p.Status)
- }
- }
-
- // Finally, check that we only have a single index left in the payment
- // index bucket.
- var indexCount int
- err = kvdb.View(db, func(tx walletdb.ReadTx) error {
- index := tx.ReadBucket(paymentsIndexBucket)
-
- return index.ForEach(func(k, v []byte) error {
- indexCount++
- return nil
- })
- }, func() { indexCount = 0 })
- require.NoError(t, err)
-
- require.Equal(t, 1, indexCount)
-}
-
-// TestKVPaymentsDBDeletePayments tests that DeletePayments correctly deletes
-// information about completed payments from the database.
-func TestKVPaymentsDBDeletePayments(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- // Register three payments:
- // 1. A payment with two failed attempts.
- // 2. A payment with one failed and one settled attempt.
- // 3. A payment with one failed and one in-flight attempt.
- payments := []*payment{
- {status: StatusFailed},
- {status: StatusSucceeded},
- {status: StatusInFlight},
- }
-
- // Use helper function to register the test payments in the data and
- // populate the data to the payments slice.
- createTestPayments(t, paymentDB, payments)
-
- // Check that all payments are there as we added them.
- assertPayments(t, paymentDB, payments)
-
- // Delete HTLC attempts for failed payments only.
- numPayments, err := paymentDB.DeletePayments(true, true)
- require.NoError(t, err)
- require.EqualValues(t, 0, numPayments)
-
- // The failed payment is the only altered one.
- payments[0].htlcs = 0
- assertPayments(t, paymentDB, payments)
-
- // Delete failed attempts for all payments.
- numPayments, err = paymentDB.DeletePayments(false, true)
- require.NoError(t, err)
- require.EqualValues(t, 0, numPayments)
-
- // The failed attempts should be deleted, except for the in-flight
- // payment, that shouldn't be altered until it has completed.
- payments[1].htlcs = 1
- assertPayments(t, paymentDB, payments)
-
- // Now delete all failed payments.
- numPayments, err = paymentDB.DeletePayments(true, false)
- require.NoError(t, err)
- require.EqualValues(t, 1, numPayments)
-
- assertPayments(t, paymentDB, payments[1:])
-
- // Finally delete all completed payments.
- numPayments, err = paymentDB.DeletePayments(false, false)
- require.NoError(t, err)
- require.EqualValues(t, 1, numPayments)
-
- assertPayments(t, paymentDB, payments[2:])
-}
-
-// TestKVPaymentsDBDeleteSinglePayment tests that DeletePayment correctly
-// deletes information about a completed payment from the database.
-func TestKVPaymentsDBDeleteSinglePayment(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- // Register four payments:
- // All payments will have one failed HTLC attempt and one HTLC attempt
- // according to its final status.
- // 1. A payment with two failed attempts.
- // 2. Another payment with two failed attempts.
- // 3. A payment with one failed and one settled attempt.
- // 4. A payment with one failed and one in-flight attempt.
-
- // Initiate payments, which is a slice of payment that is used as
- // template to create the corresponding test payments in the database.
- //
- // Note: The payment id and number of htlc attempts of each payment will
- // be added to this slice when creating the payments below.
- // This allows the slice to be used directly for testing purposes.
- payments := []*payment{
- {status: StatusFailed},
- {status: StatusFailed},
- {status: StatusSucceeded},
- {status: StatusInFlight},
- }
-
- // Use helper function to register the test payments in the data and
- // populate the data to the payments slice.
- createTestPayments(t, paymentDB, payments)
-
- // Check that all payments are there as we added them.
- assertPayments(t, paymentDB, payments)
-
- // Delete HTLC attempts for first payment only.
- require.NoError(t, paymentDB.DeletePayment(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.
- payments[0].htlcs = 0
- assertPayments(t, paymentDB, payments)
-
- // Delete the first payment completely.
- require.NoError(t, paymentDB.DeletePayment(payments[0].id, false))
-
- // The first payment should have been deleted.
- assertPayments(t, paymentDB, payments[1:])
-
- // Now delete the second payment completely.
- require.NoError(t, paymentDB.DeletePayment(payments[1].id, false))
-
- // The Second payment should have been deleted.
- assertPayments(t, paymentDB, payments[2:])
-
- // Delete failed HTLC attempts for the third payment.
- require.NoError(t, paymentDB.DeletePayment(payments[2].id, true))
-
- // Only the successful HTLC attempt should be left for the third
- // payment.
- payments[2].htlcs = 1
- assertPayments(t, paymentDB, payments[2:])
-
- // Now delete the third payment completely.
- require.NoError(t, paymentDB.DeletePayment(payments[2].id, false))
-
- // Only the last payment should be left.
- assertPayments(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))
-
- // The payment is InFlight and therefore should not have been altered.
- assertPayments(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))
-
- // The payment is InFlight and therefore should not have been altered.
- assertPayments(t, paymentDB, payments[3:])
-}
-
-// TestKVPaymentsDBMultiShard checks the ability of payment control to
-// have multiple in-flight HTLCs for a single payment.
-func TestKVPaymentsDBMultiShard(t *testing.T) {
- t.Parallel()
-
- // 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
- // expect.
- type testCase struct {
- settleFirst bool
- settleLast bool
- }
-
- var tests []testCase
- for _, f := range []bool{true, false} {
- for _, l := range []bool{true, false} {
- tests = append(tests, testCase{f, l})
- }
- }
-
- runSubTest := func(t *testing.T, test testCase) {
- db, err := MakeTestDB(t)
- if err != nil {
- t.Fatalf("unable to init db: %v", err)
- }
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, attempt, preimg, err := genInfo(t)
- if err != nil {
- t.Fatalf("unable to generate htlc message: %v", err)
- }
-
- // 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)
- }
-
- assertPaymentIndex(t, paymentDB, info.PaymentIdentifier)
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInitiated,
- )
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, nil,
- )
-
- // Create three unique attempts we'll use for the test, and
- // register them with the payment control. We set each
- // 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)
-
- _, err = paymentDB.RegisterAttempt(
- info.PaymentIdentifier, &a,
- )
- if err != nil {
- t.Fatalf("unable to send htlc message: %v", err)
- }
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier,
- StatusInFlight,
- )
-
- htlc := &htlcStatus{
- HTLCAttemptInfo: &a,
- }
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil,
- htlc,
- )
- }
-
- // 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)
- require.ErrorIs(t, err, paymentsdb.ErrValueExceedsAmt)
-
- // Fail the second attempt.
- a := attempts[1]
- htlcFail := HTLCFailUnreadable
- _, err = paymentDB.FailAttempt(
- info.PaymentIdentifier, a.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFail,
- },
- )
- if err != nil {
- t.Fatal(err)
- }
-
- htlc := &htlcStatus{
- HTLCAttemptInfo: a,
- failure: &htlcFail,
- }
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil, htlc,
- )
-
- // Payment should still be in-flight.
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInFlight,
- )
-
- // Depending on the test case, settle or fail the first attempt.
- a = attempts[0]
- htlc = &htlcStatus{
- HTLCAttemptInfo: a,
- }
-
- var firstFailReason *FailureReason
- if test.settleFirst {
- _, err := paymentDB.SettleAttempt(
- info.PaymentIdentifier, a.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- if err != nil {
- t.Fatalf("error shouldn't have been "+
- "received, got: %v", err)
- }
-
- // Assert that the HTLC has had the preimage recorded.
- htlc.settle = &preimg
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil,
- htlc,
- )
- } else {
- _, err := paymentDB.FailAttempt(
- info.PaymentIdentifier, a.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFail,
- },
- )
- if err != nil {
- t.Fatalf("error shouldn't have been "+
- "received, got: %v", err)
- }
-
- // Assert the failure was recorded.
- htlc.failure = &htlcFail
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info, nil,
- htlc,
- )
-
- // We also record a payment level fail, to move it into
- // a terminal state.
- failReason := FailureReasonNoRoute
- _, err = paymentDB.Fail(
- info.PaymentIdentifier, failReason,
- )
- if err != nil {
- t.Fatalf("unable to fail payment hash: %v", err)
- }
-
- // Record the reason we failed the payment, such that
- // we can assert this later in the test.
- firstFailReason = &failReason
-
- // The payment is now considered pending fail, since
- // there is still an active HTLC.
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier,
- StatusInFlight,
- )
- }
-
- // 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)
- if test.settleFirst {
- require.ErrorIs(
- t, err, paymentsdb.ErrPaymentPendingSettled,
- )
- } else {
- require.ErrorIs(
- t, err, paymentsdb.ErrPaymentPendingFailed,
- )
- }
-
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, StatusInFlight,
- )
-
- // Settle or fail the remaining attempt based on the testcase.
- a = attempts[2]
- htlc = &htlcStatus{
- HTLCAttemptInfo: a,
- }
- if test.settleLast {
- // Settle the last outstanding attempt.
- _, err = paymentDB.SettleAttempt(
- info.PaymentIdentifier, a.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- require.NoError(t, err, "unable to settle")
-
- htlc.settle = &preimg
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier,
- info, firstFailReason, htlc,
- )
- } else {
- // Fail the attempt.
- _, err := paymentDB.FailAttempt(
- info.PaymentIdentifier, a.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFail,
- },
- )
- if err != nil {
- t.Fatalf("error shouldn't have been "+
- "received, got: %v", err)
- }
-
- // Assert the failure was recorded.
- htlc.failure = &htlcFail
- assertPaymentInfo(
- t, paymentDB, info.PaymentIdentifier, info,
- firstFailReason, htlc,
- )
-
- // Check that we can override any perevious terminal
- // failure. This is to allow multiple concurrent shard
- // write a terminal failure to the database without
- // syncing.
- failReason := FailureReasonPaymentDetails
- _, err = paymentDB.Fail(
- info.PaymentIdentifier, failReason,
- )
- require.NoError(t, err, "unable to fail")
- }
-
- var (
- finalStatus PaymentStatus
- registerErr error
- )
-
- switch {
- // If one of the attempts settled but the other failed with
- // terminal error, we would still consider the payment is
- // settled.
- case test.settleFirst && !test.settleLast:
- finalStatus = StatusSucceeded
- registerErr = paymentsdb.ErrPaymentAlreadySucceeded
-
- case !test.settleFirst && test.settleLast:
- finalStatus = StatusSucceeded
- registerErr = paymentsdb.ErrPaymentAlreadySucceeded
-
- // If both failed, we end up in a failed status.
- case !test.settleFirst && !test.settleLast:
- finalStatus = StatusFailed
- registerErr = paymentsdb.ErrPaymentAlreadyFailed
-
- // Otherwise, the payment has a succeed status.
- case test.settleFirst && test.settleLast:
- finalStatus = StatusSucceeded
- registerErr = paymentsdb.ErrPaymentAlreadySucceeded
- }
-
- assertPaymentStatus(
- t, paymentDB, info.PaymentIdentifier, finalStatus,
- )
-
- // Finally assert we cannot register more attempts.
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
- require.Equal(t, registerErr, err)
- }
-
- for _, test := range tests {
- test := test
- subTest := fmt.Sprintf("first=%v, second=%v",
- test.settleFirst, test.settleLast)
-
- t.Run(subTest, func(t *testing.T) {
- runSubTest(t, test)
- })
- }
-}
-
-func TestKVPaymentsDBMPPRecordValidation(t *testing.T) {
- t.Parallel()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- info, attempt, _, err := genInfo(t)
- require.NoError(t, err, "unable to generate htlc message")
-
- // Init the payment.
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- require.NoError(t, err, "unable to send htlc message")
-
- // Create three unique attempts we'll use for the test, and
- // register them with the payment control. We set each
- // 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},
- )
-
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
- 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)
- require.ErrorIs(t, err, paymentsdb.ErrMPPayment)
-
- // Try to register attempt one with a different payment address.
- b.Route.FinalHop().MPP = record.NewMPP(
- info.Value, [32]byte{2},
- )
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
- require.ErrorIs(t, err, paymentsdb.ErrMPPPaymentAddrMismatch)
-
- // Try registering one with a different total amount.
- b.Route.FinalHop().MPP = record.NewMPP(
- info.Value/2, [32]byte{1},
- )
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
- require.ErrorIs(t, err, paymentsdb.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)
- require.NoError(t, err, "unable to generate htlc message")
-
- err = paymentDB.InitPayment(info.PaymentIdentifier, info)
- require.NoError(t, err, "unable to send htlc message")
-
- attempt.Route.FinalHop().MPP = nil
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
- 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(
- info.Value, [32]byte{1},
- )
-
- _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
- require.ErrorIs(t, err, paymentsdb.ErrNonMPPayment)
-}
-
-// TestDeleteFailedAttempts checks that DeleteFailedAttempts properly removes
-// failed HTLCs from finished payments.
-func TestDeleteFailedAttempts(t *testing.T) {
- t.Parallel()
-
- t.Run("keep failed payment attempts", func(t *testing.T) {
- testDeleteFailedAttempts(t, true)
- })
- t.Run("remove failed payment attempts", func(t *testing.T) {
- testDeleteFailedAttempts(t, false)
- })
-}
-
-func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) {
- db, err := MakeTestDB(t)
- require.NoError(t, err, "unable to init db")
-
- paymentDB, err := NewKVPaymentsDB(
- db,
- paymentsdb.WithKeepFailedPaymentAttempts(
- keepFailedPaymentAttempts,
- ),
- )
- require.NoError(t, err)
-
- // Register three payments:
- // All payments will have one failed HTLC attempt and one HTLC attempt
- // according to its final status.
- // 1. A payment with two failed attempts.
- // 2. A payment with one failed and one in-flight attempt.
- // 3. A payment with one failed and one settled attempt.
-
- // Initiate payments, which is a slice of payment that is used as
- // template to create the corresponding test payments in the database.
- //
- // Note: The payment id and number of htlc attempts of each payment will
- // be added to this slice when creating the payments below.
- // This allows the slice to be used directly for testing purposes.
- payments := []*payment{
- {status: StatusFailed},
- {status: StatusInFlight},
- {status: StatusSucceeded},
- }
-
- // Use helper function to register the test payments in the data and
- // populate the data to the payments slice.
- createTestPayments(t, paymentDB, payments)
-
- // Check that all payments are there as we added them.
- assertPayments(t, paymentDB, payments)
-
- // Calling DeleteFailedAttempts on a failed payment should delete all
- // HTLCs.
- require.NoError(t, paymentDB.DeleteFailedAttempts(payments[0].id))
-
- // Expect all HTLCs to be deleted if the config is set to delete them.
- if !keepFailedPaymentAttempts {
- payments[0].htlcs = 0
- }
- assertPayments(t, paymentDB, payments)
-
- // Calling DeleteFailedAttempts on an in-flight payment should return
- // an error.
- if keepFailedPaymentAttempts {
- require.NoError(
- t, paymentDB.DeleteFailedAttempts(payments[1].id),
- )
- } else {
- require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id))
- }
-
- // Since DeleteFailedAttempts returned an error, we should expect the
- // payment to be unchanged.
- assertPayments(t, paymentDB, payments)
-
- // Cleaning up a successful payment should remove failed htlcs.
- require.NoError(t, paymentDB.DeleteFailedAttempts(payments[2].id))
- // Expect all HTLCs except for the settled one to be deleted if the
- // config is set to delete them.
- if !keepFailedPaymentAttempts {
- payments[2].htlcs = 1
- }
- assertPayments(t, paymentDB, payments)
-
- if keepFailedPaymentAttempts {
- // DeleteFailedAttempts is ignored, even for non-existent
- // payments, if the control tower is configured to keep failed
- // HTLCs.
- require.NoError(
- t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
- )
- } else {
- // Attempting to cleanup a non-existent payment returns an error.
- require.Error(
- t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
- )
- }
-}
-
-// assertPaymentStatus retrieves the status of the payment referred to by hash
-// and compares it with the expected state.
-func assertPaymentStatus(t *testing.T, p *KVPaymentsDB,
- hash lntypes.Hash, expStatus PaymentStatus) {
-
- t.Helper()
-
- payment, err := p.FetchPayment(hash)
- if errors.Is(err, paymentsdb.ErrPaymentNotInitiated) {
- return
- }
- if err != nil {
- t.Fatal(err)
- }
-
- if payment.Status != expStatus {
- t.Fatalf("payment status mismatch: expected %v, got %v",
- expStatus, payment.Status)
- }
-}
-
-type htlcStatus struct {
- *HTLCAttemptInfo
- settle *lntypes.Preimage
- failure *HTLCFailReason
-}
-
-// assertPaymentInfo retrieves the payment referred to by hash and verifies the
-// expected values.
-func assertPaymentInfo(t *testing.T, p *KVPaymentsDB, hash lntypes.Hash,
- c *PaymentCreationInfo, f *FailureReason, a *htlcStatus) {
-
- t.Helper()
-
- payment, err := p.FetchPayment(hash)
- if err != nil {
- t.Fatal(err)
- }
-
- if !reflect.DeepEqual(payment.Info, c) {
- t.Fatalf("PaymentCreationInfos don't match: %v vs %v",
- spew.Sdump(payment.Info), spew.Sdump(c))
- }
-
- if f != nil {
- if *payment.FailureReason != *f {
- t.Fatal("unexpected failure reason")
- }
- } else {
- if payment.FailureReason != nil {
- t.Fatal("unexpected failure reason")
- }
- }
-
- if a == nil {
- if len(payment.HTLCs) > 0 {
- t.Fatal("expected no htlcs")
- }
- return
- }
-
- htlc := payment.HTLCs[a.AttemptID]
- if err := assertRouteEqual(&htlc.Route, &a.Route); err != nil {
- t.Fatal("routes do not match")
- }
-
- if htlc.AttemptID != a.AttemptID {
- t.Fatalf("unnexpected attempt ID %v, expected %v",
- htlc.AttemptID, a.AttemptID)
- }
-
- if a.failure != nil {
- if htlc.Failure == nil {
- t.Fatalf("expected HTLC to be failed")
- }
-
- if htlc.Failure.Reason != *a.failure {
- t.Fatalf("expected HTLC failure %v, had %v",
- *a.failure, htlc.Failure.Reason)
- }
- } else if htlc.Failure != nil {
- t.Fatalf("expected no HTLC failure")
- }
-
- if a.settle != nil {
- if htlc.Settle.Preimage != *a.settle {
- t.Fatalf("Preimages don't match: %x vs %x",
- htlc.Settle.Preimage, a.settle)
- }
- } else if htlc.Settle != nil {
- t.Fatal("expected no settle info")
- }
-}
-
-// fetchPaymentIndexEntry gets the payment hash for the sequence number provided
-// from our payment indexes bucket.
-func fetchPaymentIndexEntry(_ *testing.T, p *KVPaymentsDB,
- sequenceNumber uint64) (*lntypes.Hash, error) {
-
- var hash lntypes.Hash
-
- if err := kvdb.View(p.db, func(tx walletdb.ReadTx) error {
- indexBucket := tx.ReadBucket(paymentsIndexBucket)
- key := make([]byte, 8)
- byteOrder.PutUint64(key, sequenceNumber)
-
- indexValue := indexBucket.Get(key)
- if indexValue == nil {
- return paymentsdb.ErrNoSequenceNrIndex
- }
-
- r := bytes.NewReader(indexValue)
-
- var err error
- hash, err = deserializePaymentIndex(r)
- return err
- }, func() {
- hash = lntypes.Hash{}
- }); err != nil {
- return nil, err
- }
-
- return &hash, nil
-}
-
-// assertPaymentIndex looks up the index for a payment in the db and checks
-// that its payment hash matches the expected hash passed in.
-func assertPaymentIndex(t *testing.T, p *KVPaymentsDB,
- 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 := p.FetchPayment(expectedHash)
- require.NoError(t, err)
-
- hash, err := fetchPaymentIndexEntry(t, p, pmt.SequenceNum)
- require.NoError(t, err)
- assert.Equal(t, expectedHash, *hash)
-}
-
-// assertNoIndex checks that an index for the sequence number provided does not
-// exist.
-func assertNoIndex(t *testing.T, p *KVPaymentsDB, seqNr uint64) {
- _, err := fetchPaymentIndexEntry(t, p, seqNr)
- require.Equal(t, paymentsdb.ErrNoSequenceNrIndex, err)
-}
-
-// payment is a helper structure that holds basic information on a test payment,
-// such as the payment id, the status and the total number of HTLCs attempted.
-type payment struct {
- id lntypes.Hash
- status PaymentStatus
- htlcs int
-}
-
-// createTestPayments registers payments depending on the provided statuses in
-// the payments slice. Each payment will receive one failed HTLC and another
-// HTLC depending on the final status of the payment provided.
-func createTestPayments(t *testing.T, p *KVPaymentsDB, 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")
-
- // Set the payment id accordingly in the payments slice.
- payments[i].id = info.PaymentIdentifier
-
- attempt.AttemptID = attemptID
- attemptID++
-
- // Init the payment.
- err = p.InitPayment(info.PaymentIdentifier, info)
- require.NoError(t, err, "unable to send htlc message")
-
- // Register and fail the first attempt for all payments.
- _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
- require.NoError(t, err, "unable to send htlc message")
-
- htlcFailure := HTLCFailUnreadable
- _, err = p.FailAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFailure,
- },
- )
- require.NoError(t, err, "unable to fail htlc")
-
- // Increase the HTLC counter in the payments slice for the
- // failed attempt.
- payments[i].htlcs++
-
- // Depending on the test case, fail or succeed the next
- // attempt.
- attempt.AttemptID = attemptID
- attemptID++
-
- _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
- require.NoError(t, err, "unable to send htlc message")
-
- switch payments[i].status {
- // Fail the attempt and the payment overall.
- case StatusFailed:
- htlcFailure := HTLCFailUnreadable
- _, err = p.FailAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCFailInfo{
- Reason: htlcFailure,
- },
- )
- require.NoError(t, err, "unable to fail htlc")
-
- failReason := FailureReasonNoRoute
- _, err = p.Fail(info.PaymentIdentifier,
- failReason)
- require.NoError(t, err, "unable to fail payment hash")
-
- // Settle the attempt
- case StatusSucceeded:
- _, err := p.SettleAttempt(
- info.PaymentIdentifier, attempt.AttemptID,
- &HTLCSettleInfo{
- Preimage: preimg,
- },
- )
- require.NoError(t, err, "no error should have been "+
- "received from settling a htlc attempt")
-
- // We leave the attempt in-flight by doing nothing.
- case StatusInFlight:
- }
-
- // Increase the HTLC counter in the payments slice for any
- // attempt above.
- payments[i].htlcs++
- }
-}
-
-// assertPayments is a helper function that given a slice of payment and
-// indices for the slice asserts that exactly the same payments in the
-// slice for the provided indices exist when fetching payments from the
-// database.
-func assertPayments(t *testing.T, paymentDB *KVPaymentsDB,
- payments []*payment) {
-
- t.Helper()
-
- dbPayments, err := paymentDB.FetchPayments()
- require.NoError(t, err, "could not fetch payments from db")
-
- // Make sure that the number of fetched payments is the same
- // as expected.
- require.Len(
- t, dbPayments, len(payments), "unexpected number of payments",
- )
-
- // Convert fetched payments of type MPPayment to our helper structure.
- p := make([]*payment, len(dbPayments))
- for i, dbPayment := range dbPayments {
- p[i] = &payment{
- id: dbPayment.Info.PaymentIdentifier,
- status: dbPayment.Status,
- htlcs: len(dbPayment.HTLCs),
- }
- }
-
- // Check that each payment we want to assert exists in the database.
- require.Equal(t, payments, p)
-}
-
-func makeFakeInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo) {
- var preimg lntypes.Preimage
- copy(preimg[:], rev[:])
-
- hash := preimg.Hash()
-
- c := &PaymentCreationInfo{
- PaymentIdentifier: hash,
- Value: 1000,
- // Use single second precision to avoid false positive test
- // failures due to the monotonic time component.
- CreationTime: time.Unix(time.Now().Unix(), 0),
- PaymentRequest: []byte("test"),
- }
-
- a, err := NewHtlcAttempt(
- 44, priv, testRoute, time.Unix(100, 0), &hash,
- )
- require.NoError(t, err)
-
- return c, &a.HTLCAttemptInfo
-}
-
-func TestSentPaymentSerialization(t *testing.T) {
- t.Parallel()
-
- c, s := makeFakeInfo(t)
-
- var b bytes.Buffer
- require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
-
- // Assert the length of the serialized creation info is as expected,
- // without any custom records.
- baseLength := 32 + 8 + 8 + 4 + len(c.PaymentRequest)
- require.Len(t, b.Bytes(), baseLength)
-
- newCreationInfo, err := deserializePaymentCreationInfo(&b)
- require.NoError(t, err, "deserialize")
- require.Equal(t, c, newCreationInfo)
-
- b.Reset()
-
- // Now we add some custom records to the creation info and serialize it
- // again.
- c.FirstHopCustomRecords = lnwire.CustomRecords{
- lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
- }
- require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
-
- newCreationInfo, err = deserializePaymentCreationInfo(&b)
- require.NoError(t, err, "deserialize")
- require.Equal(t, c, newCreationInfo)
-
- b.Reset()
- require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
-
- newWireInfo, err := deserializeHTLCAttemptInfo(&b)
- require.NoError(t, err, "deserialize")
-
- // First we verify all the records match up properly.
- require.Equal(t, s.Route, newWireInfo.Route)
-
- // We now add the new fields and custom records to the route and
- // serialize it again.
- b.Reset()
- s.Route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
- tlv.NewBigSizeT(lnwire.MilliSatoshi(1234)),
- )
- s.Route.FirstHopWireCustomRecords = lnwire.CustomRecords{
- lnwire.MinCustomRecordsTlvType + 3: []byte{4, 5, 6},
- }
- require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
-
- newWireInfo, err = deserializeHTLCAttemptInfo(&b)
- require.NoError(t, err, "deserialize")
- require.Equal(t, s.Route, newWireInfo.Route)
-
- err = newWireInfo.attachOnionBlobAndCircuit()
- require.NoError(t, err)
-
- // Clear routes to allow DeepEqual to compare the remaining fields.
- newWireInfo.Route = route.Route{}
- s.Route = route.Route{}
- newWireInfo.AttemptID = s.AttemptID
-
- // Call session key method to set our cached session key so we can use
- // DeepEqual, and assert that our key equals the original key.
- require.Equal(t, s.cachedSessionKey, newWireInfo.SessionKey())
-
- require.Equal(t, s, newWireInfo)
-}
-
-// TestRouteSerialization tests serialization of a regular and blinded route.
-func TestRouteSerialization(t *testing.T) {
- t.Parallel()
-
- testSerializeRoute(t, testRoute)
- testSerializeRoute(t, testBlindedRoute)
-}
-
-func testSerializeRoute(t *testing.T, route route.Route) {
- var b bytes.Buffer
- err := SerializeRoute(&b, route)
- require.NoError(t, err)
-
- r := bytes.NewReader(b.Bytes())
- route2, err := DeserializeRoute(r)
- require.NoError(t, err)
-
- reflect.DeepEqual(route, route2)
-}
-
-// deletePayment removes a payment with paymentHash from the payments database.
-func deletePayment(t *testing.T, db *DB, paymentHash lntypes.Hash,
- seqNr uint64) {
-
- t.Helper()
-
- err := kvdb.Update(db, func(tx kvdb.RwTx) error {
- payments := tx.ReadWriteBucket(paymentsRootBucket)
-
- // Delete the payment bucket.
- err := payments.DeleteNestedBucket(paymentHash[:])
- if err != nil {
- return err
- }
-
- key := make([]byte, 8)
- byteOrder.PutUint64(key, seqNr)
-
- // Delete the index that references this payment.
- indexes := tx.ReadWriteBucket(paymentsIndexBucket)
-
- return indexes.Delete(key)
- }, func() {})
-
- if err != nil {
- t.Fatalf("could not delete "+
- "payment: %v", err)
- }
-}
-
-// TestFetchPaymentWithSequenceNumber tests lookup of payments with their
-// sequence number. It sets up one payment with no duplicates, and another with
-// two duplicates in its duplicates bucket then uses these payments to test the
-// case where a specific duplicate is not found and the duplicates bucket is not
-// present when we expect it to be.
-func TestFetchPaymentWithSequenceNumber(t *testing.T) {
- db, err := MakeTestDB(t)
- require.NoError(t, err)
-
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- // Generate a test payment which does not have duplicates.
- noDuplicates, _, _, err := genInfo(t)
- require.NoError(t, err)
-
- // Create a new payment entry in the database.
- err = paymentDB.InitPayment(
- noDuplicates.PaymentIdentifier, noDuplicates,
- )
- require.NoError(t, err)
-
- // Fetch the payment so we can get its sequence nr.
- noDuplicatesPayment, err := paymentDB.FetchPayment(
- noDuplicates.PaymentIdentifier,
- )
- require.NoError(t, err)
-
- // Generate a test payment which we will add duplicates to.
- hasDuplicates, _, preimg, err := genInfo(t)
- require.NoError(t, err)
-
- // Create a new payment entry in the database.
- err = paymentDB.InitPayment(
- hasDuplicates.PaymentIdentifier, hasDuplicates,
- )
- require.NoError(t, err)
-
- // Fetch the payment so we can get its sequence nr.
- hasDuplicatesPayment, err := paymentDB.FetchPayment(
- hasDuplicates.PaymentIdentifier,
- )
- require.NoError(t, err)
-
- // We declare the sequence numbers used here so that we can reference
- // them in tests.
- var (
- duplicateOneSeqNr = hasDuplicatesPayment.SequenceNum + 1
- duplicateTwoSeqNr = hasDuplicatesPayment.SequenceNum + 2
- )
-
- // Add two duplicates to our second payment.
- appendDuplicatePayment(
- t, db, hasDuplicates.PaymentIdentifier, duplicateOneSeqNr,
- preimg,
- )
- appendDuplicatePayment(
- t, db, hasDuplicates.PaymentIdentifier, duplicateTwoSeqNr,
- preimg,
- )
-
- tests := []struct {
- name string
- paymentHash lntypes.Hash
- sequenceNumber uint64
- expectedErr error
- }{
- {
- name: "lookup payment without duplicates",
- paymentHash: noDuplicates.PaymentIdentifier,
- sequenceNumber: noDuplicatesPayment.SequenceNum,
- expectedErr: nil,
- },
- {
- name: "lookup payment with duplicates",
- paymentHash: hasDuplicates.PaymentIdentifier,
- sequenceNumber: hasDuplicatesPayment.SequenceNum,
- expectedErr: nil,
- },
- {
- name: "lookup first duplicate",
- paymentHash: hasDuplicates.PaymentIdentifier,
- sequenceNumber: duplicateOneSeqNr,
- expectedErr: nil,
- },
- {
- name: "lookup second duplicate",
- paymentHash: hasDuplicates.PaymentIdentifier,
- sequenceNumber: duplicateTwoSeqNr,
- expectedErr: nil,
- },
- {
- name: "lookup non-existent duplicate",
- paymentHash: hasDuplicates.PaymentIdentifier,
- sequenceNumber: 999999,
- expectedErr: paymentsdb.ErrDuplicateNotFound,
- },
- {
- name: "lookup duplicate, no duplicates " +
- "bucket",
- paymentHash: noDuplicates.PaymentIdentifier,
- sequenceNumber: duplicateTwoSeqNr,
- expectedErr: paymentsdb.ErrNoDuplicateBucket,
- },
- }
-
- for _, test := range tests {
- test := test
-
- t.Run(test.name, func(t *testing.T) {
- err := kvdb.Update(
- db, func(tx walletdb.ReadWriteTx) error {
- var seqNrBytes [8]byte
- byteOrder.PutUint64(
- seqNrBytes[:],
- test.sequenceNumber,
- )
-
- //nolint:ll
- _, err := fetchPaymentWithSequenceNumber(
- tx, test.paymentHash, seqNrBytes[:],
- )
-
- return err
- }, func() {},
- )
- require.Equal(t, test.expectedErr, err)
- })
- }
-}
-
-// appendDuplicatePayment adds a duplicate payment to an existing payment. Note
-// that this function requires a unique sequence number.
-//
-// This code is *only* intended to replicate legacy duplicate payments in lnd,
-// our current schema does not allow duplicates.
-func appendDuplicatePayment(t *testing.T, db kvdb.Backend,
- paymentHash lntypes.Hash, seqNr uint64, preImg lntypes.Preimage) {
-
- err := kvdb.Update(db, func(tx walletdb.ReadWriteTx) error {
- bucket, err := fetchPaymentBucketUpdate(
- tx, paymentHash,
- )
- if err != nil {
- return err
- }
-
- // Create the duplicates bucket if it is not
- // present.
- dup, err := bucket.CreateBucketIfNotExists(
- duplicatePaymentsBucket,
- )
- if err != nil {
- return err
- }
-
- var sequenceKey [8]byte
- byteOrder.PutUint64(sequenceKey[:], seqNr)
-
- // Create duplicate payments for the two dup
- // sequence numbers we've setup.
- putDuplicatePayment(t, dup, sequenceKey[:], paymentHash, preImg)
-
- // Finally, once we have created our entry we add an index for
- // it.
- err = createPaymentIndexEntry(tx, sequenceKey[:], paymentHash)
- require.NoError(t, err)
-
- return nil
- }, func() {})
- require.NoError(t, err, "could not create payment")
-}
-
-// putDuplicatePayment creates a duplicate payment in the duplicates bucket
-// provided with the minimal information required for successful reading.
-func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket,
- sequenceKey []byte, paymentHash lntypes.Hash,
- preImg lntypes.Preimage) {
-
- paymentBucket, err := duplicateBucket.CreateBucketIfNotExists(
- sequenceKey,
- )
- require.NoError(t, err)
-
- err = paymentBucket.Put(duplicatePaymentSequenceKey, sequenceKey)
- require.NoError(t, err)
-
- // Generate fake information for the duplicate payment.
- info, _, _, err := genInfo(t)
- require.NoError(t, err)
-
- // Write the payment info to disk under the creation info key. This code
- // is copied rather than using serializePaymentCreationInfo to ensure
- // we always write in the legacy format used by duplicate payments.
- var b bytes.Buffer
- var scratch [8]byte
- _, err = b.Write(paymentHash[:])
- require.NoError(t, err)
-
- byteOrder.PutUint64(scratch[:], uint64(info.Value))
- _, err = b.Write(scratch[:])
- require.NoError(t, err)
-
- err = serializeTime(&b, info.CreationTime)
- require.NoError(t, err)
-
- byteOrder.PutUint32(scratch[:4], 0)
- _, err = b.Write(scratch[:4])
- require.NoError(t, err)
-
- // Get the PaymentCreationInfo.
- err = paymentBucket.Put(duplicatePaymentCreationInfoKey, b.Bytes())
- require.NoError(t, err)
-
- // Duolicate payments are only stored for successes, so add the
- // preimage.
- err = paymentBucket.Put(duplicatePaymentSettleInfoKey, preImg[:])
- require.NoError(t, err)
-}
diff --git a/channeldb/payments_test.go b/channeldb/payments_test.go
deleted file mode 100644
index dce993f..0000000
--- a/channeldb/payments_test.go
+++ /dev/null
@@ -1,474 +0,0 @@
-package channeldb
-
-import (
- "context"
- "fmt"
- "math"
- "reflect"
- "testing"
- "time"
-
- "github.com/btcsuite/btcd/btcec/v2"
- "github.com/davecgh/go-spew/spew"
- "github.com/lightningnetwork/lnd/record"
- "github.com/lightningnetwork/lnd/routing/route"
- "github.com/stretchr/testify/require"
-)
-
-var (
- priv, _ = btcec.NewPrivateKey()
- pub = priv.PubKey()
- vertex = route.NewVertex(pub)
-
- testHop1 = &route.Hop{
- PubKeyBytes: vertex,
- ChannelID: 12345,
- OutgoingTimeLock: 111,
- AmtToForward: 555,
- CustomRecords: record.CustomSet{
- 65536: []byte{},
- 80001: []byte{},
- },
- MPP: record.NewMPP(32, [32]byte{0x42}),
- Metadata: []byte{1, 2, 3},
- }
-
- testHop2 = &route.Hop{
- PubKeyBytes: vertex,
- ChannelID: 12345,
- OutgoingTimeLock: 111,
- AmtToForward: 555,
- LegacyPayload: true,
- }
-
- testHop3 = &route.Hop{
- PubKeyBytes: route.NewVertex(pub),
- ChannelID: 12345,
- OutgoingTimeLock: 111,
- AmtToForward: 555,
- CustomRecords: record.CustomSet{
- 65536: []byte{},
- 80001: []byte{},
- },
- AMP: record.NewAMP([32]byte{0x69}, [32]byte{0x42}, 1),
- Metadata: []byte{1, 2, 3},
- }
-
- testRoute = route.Route{
- TotalTimeLock: 123,
- TotalAmount: 1234567,
- SourcePubKey: vertex,
- Hops: []*route.Hop{
- testHop2,
- testHop1,
- },
- }
-
- testBlindedRoute = route.Route{
- TotalTimeLock: 150,
- TotalAmount: 1000,
- SourcePubKey: vertex,
- Hops: []*route.Hop{
- {
- PubKeyBytes: vertex,
- ChannelID: 9876,
- OutgoingTimeLock: 120,
- AmtToForward: 900,
- EncryptedData: []byte{1, 3, 3},
- BlindingPoint: pub,
- },
- {
- PubKeyBytes: vertex,
- EncryptedData: []byte{3, 2, 1},
- },
- {
- PubKeyBytes: vertex,
- Metadata: []byte{4, 5, 6},
- AmtToForward: 500,
- OutgoingTimeLock: 100,
- TotalAmtMsat: 500,
- },
- },
- }
-)
-
-// assertRouteEquals compares to routes for equality and returns an error if
-// they are not equal.
-func assertRouteEqual(a, b *route.Route) error {
- if !reflect.DeepEqual(a, b) {
- return fmt.Errorf("HTLCAttemptInfos don't match: %v vs %v",
- spew.Sdump(a), spew.Sdump(b))
- }
-
- return nil
-}
-
-// 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, 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.
- tests := []struct {
- name string
- query PaymentsQuery
- 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: PaymentsQuery{
- IndexOffset: 7,
- MaxPayments: 7,
- Reversed: false,
- IncludeIncomplete: true,
- },
- firstIndex: 0,
- lastIndex: 0,
- expectedSeqNrs: nil,
- },
- {
- name: "query in forwards order, start at beginning",
- query: PaymentsQuery{
- 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: PaymentsQuery{
- IndexOffset: 6,
- MaxPayments: 2,
- Reversed: false,
- IncludeIncomplete: true,
- },
- firstIndex: 7,
- lastIndex: 7,
- expectedSeqNrs: []uint64{7},
- },
- {
- name: "start at offset index outside of payments",
- query: PaymentsQuery{
- IndexOffset: 20,
- MaxPayments: 2,
- Reversed: false,
- IncludeIncomplete: true,
- },
- firstIndex: 0,
- lastIndex: 0,
- expectedSeqNrs: nil,
- },
- {
- name: "overflow in forwards order",
- query: PaymentsQuery{
- 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: PaymentsQuery{
- IndexOffset: 9,
- MaxPayments: 2,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 6,
- lastIndex: 7,
- expectedSeqNrs: []uint64{6, 7},
- },
- {
- name: "query in reverse order, start at end",
- query: PaymentsQuery{
- IndexOffset: 0,
- MaxPayments: 2,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 6,
- lastIndex: 7,
- expectedSeqNrs: []uint64{6, 7},
- },
- {
- name: "query in reverse order, starting in middle",
- query: PaymentsQuery{
- 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: PaymentsQuery{
- IndexOffset: 4,
- MaxPayments: 5,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 1,
- lastIndex: 3,
- expectedSeqNrs: []uint64{1, 3},
- },
- {
- name: "all payments in reverse, order maintained",
- query: PaymentsQuery{
- IndexOffset: 0,
- MaxPayments: 7,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 1,
- lastIndex: 7,
- expectedSeqNrs: []uint64{1, 3, 4, 5, 6, 7},
- },
- {
- name: "exclude incomplete payments",
- query: PaymentsQuery{
- IndexOffset: 0,
- MaxPayments: 7,
- Reversed: false,
- IncludeIncomplete: false,
- },
- firstIndex: 7,
- lastIndex: 7,
- expectedSeqNrs: []uint64{7},
- },
- {
- name: "query payments at index gap",
- query: PaymentsQuery{
- 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: PaymentsQuery{
- IndexOffset: 3,
- MaxPayments: 7,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 1,
- lastIndex: 1,
- expectedSeqNrs: []uint64{1},
- },
- {
- name: "query payments reverse on index gap",
- query: PaymentsQuery{
- IndexOffset: 2,
- MaxPayments: 7,
- Reversed: true,
- IncludeIncomplete: true,
- },
- firstIndex: 1,
- lastIndex: 1,
- expectedSeqNrs: []uint64{1},
- },
- {
- name: "query payments forward on index gap",
- query: PaymentsQuery{
- 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: PaymentsQuery{
- 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: PaymentsQuery{
- 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: PaymentsQuery{
- 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 {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
-
- db, err := MakeTestDB(t)
- require.NoError(t, err)
-
- // Initialize the payment database.
- paymentDB, err := NewKVPaymentsDB(db)
- require.NoError(t, err)
-
- // 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 original payments, deleting the payment
- // at index 2 so that we cover the case where sequence
- // numbers are missing. We also add a duplicate payment
- // to the last payment added to test the legacy case
- // where we have duplicates in the nested duplicates
- // bucket.
- nonDuplicatePayments := 6
-
- for i := 0; i < nonDuplicatePayments; i++ {
- // Generate a test payment.
- info, _, preimg, err := genInfo(t)
- if err != nil {
- t.Fatalf("unable to create test "+
- "payment: %v", err)
- }
- // Override creation time to allow for testing
- // of CreationDateStart and CreationDateEnd.
- info.CreationTime = time.Unix(int64(i+1), 0)
-
- // Create a new payment entry in the database.
- err = paymentDB.InitPayment(
- info.PaymentIdentifier, info,
- )
- require.NoError(t, err)
-
- // Immediately delete the payment with index 2.
- if i == 1 {
- pmt, err := paymentDB.FetchPayment(
- info.PaymentIdentifier,
- )
- require.NoError(t, err)
-
- deletePayment(
- t, db, info.PaymentIdentifier,
- pmt.SequenceNum,
- )
- }
-
- // If we are on the last payment entry, add a
- // duplicate payment with sequence number equal
- // to the parent payment + 1. Note that
- // duplicate payments will always be succeeded.
- if i == (nonDuplicatePayments - 1) {
- pmt, err := paymentDB.FetchPayment(
- info.PaymentIdentifier,
- )
- require.NoError(t, err)
-
- appendDuplicatePayment(
- t, paymentDB.db,
- info.PaymentIdentifier,
- pmt.SequenceNum+1,
- preimg,
- )
- }
- }
-
- // Fetch all payments in the database.
- allPayments, err := paymentDB.FetchPayments()
- if err != nil {
- t.Fatalf("payments could not be fetched from "+
- "database: %v", err)
- }
-
- if len(allPayments) != 6 {
- t.Fatalf("Number of payments received does "+
- "not match expected one. Got %v, "+
- "want %v.", len(allPayments), 6)
- }
-
- querySlice, err := paymentDB.QueryPayments(
- ctx, tt.query,
- )
- if err != nil {
- t.Fatalf("unexpected error: %v", 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)
- }
- }
- })
- }
-}
diff --git a/config_builder.go b/config_builder.go
index 5a24060..be56f96 100644
--- a/config_builder.go
+++ b/config_builder.go
@@ -927,7 +927,7 @@ type DatabaseInstances struct {
// KVPaymentsDB is the database that stores all payment related
// information.
- KVPaymentsDB *channeldb.KVPaymentsDB
+ KVPaymentsDB *paymentsdb.KVPaymentsDB
// MacaroonDB is the database that stores macaroon root keys.
MacaroonDB kvdb.Backend
@@ -1225,7 +1225,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
cfg.KeepFailedPaymentAttempts,
),
}
- kvPaymentsDB, err := channeldb.NewKVPaymentsDB(
+ kvPaymentsDB, err := paymentsdb.NewKVPaymentsDB(
dbs.ChanStateDB,
paymentsDBOptions...,
)
diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go
index 34cd704..b31d988 100644
--- a/lnrpc/routerrpc/router_backend.go
+++ b/lnrpc/routerrpc/router_backend.go
@@ -22,6 +22,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
+ paymentsdb "github.com/lightningnetwork/lnd/payments/db"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
@@ -1488,7 +1489,7 @@ func UnmarshalAMP(reqAMP *lnrpc.AMPRecord) (*record.AMP, error) {
// MarshalHTLCAttempt constructs an RPC HTLCAttempt from the db representation.
func (r *RouterBackend) MarshalHTLCAttempt(
- htlc channeldb.HTLCAttempt) (*lnrpc.HTLCAttempt, error) {
+ htlc paymentsdb.HTLCAttempt) (*lnrpc.HTLCAttempt, error) {
route, err := r.MarshallRoute(&htlc.Route)
if err != nil {
@@ -1529,7 +1530,7 @@ func (r *RouterBackend) MarshalHTLCAttempt(
// marshallHtlcFailure marshalls htlc fail info from the database to its rpc
// representation.
-func marshallHtlcFailure(failure *channeldb.HTLCFailInfo) (*lnrpc.Failure,
+func marshallHtlcFailure(failure *paymentsdb.HTLCFailInfo) (*lnrpc.Failure,
error) {
rpcFailure := &lnrpc.Failure{
@@ -1537,16 +1538,16 @@ func marshallHtlcFailure(failure *channeldb.HTLCFailInfo) (*lnrpc.Failure,
}
switch failure.Reason {
- case channeldb.HTLCFailUnknown:
+ case paymentsdb.HTLCFailUnknown:
rpcFailure.Code = lnrpc.Failure_UNKNOWN_FAILURE
- case channeldb.HTLCFailUnreadable:
+ case paymentsdb.HTLCFailUnreadable:
rpcFailure.Code = lnrpc.Failure_UNREADABLE_FAILURE
- case channeldb.HTLCFailInternal:
+ case paymentsdb.HTLCFailInternal:
rpcFailure.Code = lnrpc.Failure_INTERNAL_FAILURE
- case channeldb.HTLCFailMessage:
+ case paymentsdb.HTLCFailMessage:
err := marshallWireError(failure.Message, rpcFailure)
if err != nil {
return nil, err
@@ -1743,7 +1744,7 @@ func marshallChannelUpdate(update *lnwire.ChannelUpdate1) *lnrpc.ChannelUpdate {
}
// MarshallPayment marshall a payment to its rpc representation.
-func (r *RouterBackend) MarshallPayment(payment *channeldb.MPPayment) (
+func (r *RouterBackend) MarshallPayment(payment *paymentsdb.MPPayment) (
*lnrpc.Payment, error) {
// Fetch the payment's preimage and the total paid in fees.
@@ -1813,11 +1814,11 @@ func (r *RouterBackend) MarshallPayment(payment *channeldb.MPPayment) (
// convertPaymentStatus converts a channeldb.PaymentStatus to the type expected
// by the RPC.
-func convertPaymentStatus(dbStatus channeldb.PaymentStatus, useInit bool) (
+func convertPaymentStatus(dbStatus paymentsdb.PaymentStatus, useInit bool) (
lnrpc.Payment_PaymentStatus, error) {
switch dbStatus {
- case channeldb.StatusInitiated:
+ case paymentsdb.StatusInitiated:
// If the client understands the new status, return it.
if useInit {
return lnrpc.Payment_INITIATED, nil
@@ -1826,13 +1827,13 @@ func convertPaymentStatus(dbStatus channeldb.PaymentStatus, useInit bool) (
// Otherwise remain the old behavior.
return lnrpc.Payment_IN_FLIGHT, nil
- case channeldb.StatusInFlight:
+ case paymentsdb.StatusInFlight:
return lnrpc.Payment_IN_FLIGHT, nil
- case channeldb.StatusSucceeded:
+ case paymentsdb.StatusSucceeded:
return lnrpc.Payment_SUCCEEDED, nil
- case channeldb.StatusFailed:
+ case paymentsdb.StatusFailed:
return lnrpc.Payment_FAILED, nil
default:
diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go
index 843222e..10967d9 100644
--- a/lnrpc/routerrpc/router_server.go
+++ b/lnrpc/routerrpc/router_server.go
@@ -15,7 +15,6 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightningnetwork/lnd/aliasmgr"
- "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
@@ -927,7 +926,7 @@ func (s *Server) SendToRouteV2(ctx context.Context,
return nil, err
}
- var attempt *channeldb.HTLCAttempt
+ var attempt *paymentsdb.HTLCAttempt
// Pass route to the router. This call returns the full htlc attempt
// information as it is stored in the database. It is possible that both
@@ -1449,17 +1448,17 @@ func (s *Server) trackPaymentStream(context context.Context,
// No more payment updates.
return nil
}
- result := item.(*channeldb.MPPayment)
+ result := item.(*paymentsdb.MPPayment)
log.Tracef("Payment %v updated to state %v",
result.Info.PaymentIdentifier, result.Status)
// Skip in-flight updates unless requested.
if noInflightUpdates {
- if result.Status == channeldb.StatusInitiated {
+ if result.Status == paymentsdb.StatusInitiated {
continue
}
- if result.Status == channeldb.StatusInFlight {
+ if result.Status == paymentsdb.StatusInFlight {
continue
}
}
diff --git a/lnrpc/routerrpc/router_server_test.go b/lnrpc/routerrpc/router_server_test.go
index bc5a7f1..ce513d5 100644
--- a/lnrpc/routerrpc/router_server_test.go
+++ b/lnrpc/routerrpc/router_server_test.go
@@ -10,6 +10,7 @@ import (
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
+ paymentsdb "github.com/lightningnetwork/lnd/payments/db"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
@@ -129,13 +130,13 @@ func TestTrackPaymentsInflightUpdates(t *testing.T) {
}()
// Enqueue some payment updates on the mock.
- towerMock.queue.ChanIn() <- &channeldb.MPPayment{
+ towerMock.queue.ChanIn() <- &paymentsdb.MPPayment{
Info: &channeldb.PaymentCreationInfo{},
- Status: channeldb.StatusInFlight,
+ Status: paymentsdb.StatusInFlight,
}
- towerMock.queue.ChanIn() <- &channeldb.MPPayment{
+ towerMock.queue.ChanIn() <- &paymentsdb.MPPayment{
Info: &channeldb.PaymentCreationInfo{},
- Status: channeldb.StatusSucceeded,
+ Status: paymentsdb.StatusSucceeded,
}
// Wait until there's 2 updates or the deadline is exceeded.
@@ -191,13 +192,13 @@ func TestTrackPaymentsNoInflightUpdates(t *testing.T) {
}()
// Enqueue some payment updates on the mock.
- towerMock.queue.ChanIn() <- &channeldb.MPPayment{
+ towerMock.queue.ChanIn() <- &paymentsdb.MPPayment{
Info: &channeldb.PaymentCreationInfo{},
- Status: channeldb.StatusInFlight,
+ Status: paymentsdb.StatusInFlight,
}
- towerMock.queue.ChanIn() <- &channeldb.MPPayment{
+ towerMock.queue.ChanIn() <- &paymentsdb.MPPayment{
Info: &channeldb.PaymentCreationInfo{},
- Status: channeldb.StatusSucceeded,
+ Status: paymentsdb.StatusSucceeded,
}
// Wait until there's 1 update or the deadline is exceeded.
diff --git a/payments/db/codec.go b/payments/db/codec.go
new file mode 100644
index 0000000..997dab0
--- /dev/null
+++ b/payments/db/codec.go
@@ -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
+}
diff --git a/payments/db/kv_duplicate_payments.go b/payments/db/kv_duplicate_payments.go
new file mode 100644
index 0000000..9a41c92
--- /dev/null
+++ b/payments/db/kv_duplicate_payments.go
@@ -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
+}
diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go
new file mode 100644
index 0000000..0bd6576
--- /dev/null
+++ b/payments/db/kv_store.go
@@ -0,0 +1,2105 @@
+package paymentsdb
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/btcsuite/btcd/btcec/v2"
+ "github.com/btcsuite/btcd/wire"
+ "github.com/lightningnetwork/lnd/channeldb"
+ "github.com/lightningnetwork/lnd/kvdb"
+ "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/tlv"
+)
+
+const (
+ // paymentSeqBlockSize is the block size used when we batch allocate
+ // payment sequences for future payments.
+ paymentSeqBlockSize = 1000
+
+ // paymentProgressLogInterval is the interval we use limiting the
+ // logging output of payment processing.
+ paymentProgressLogInterval = 30 * time.Second
+)
+
+//nolint:ll
+var (
+ // paymentsRootBucket is the name of the top-level bucket within the
+ // database that stores all data related to payments. Within this
+ // bucket, each payment hash its own sub-bucket keyed by its payment
+ // hash.
+ //
+ // Bucket hierarchy:
+ //
+ // root-bucket
+ // |
+ // |-- <paymenthash>
+ // | |--sequence-key: <sequence number>
+ // | |--creation-info-key: <creation info>
+ // | |--fail-info-key: <(optional) fail info>
+ // | |
+ // | |--payment-htlcs-bucket (shard-bucket)
+ // | | |
+ // | | |-- ai<htlc attempt ID>: <htlc attempt info>
+ // | | |-- si<htlc attempt ID>: <(optional) settle info>
+ // | | |-- fi<htlc attempt ID>: <(optional) fail info>
+ // | | |
+ // | | ...
+ // | |
+ // | |
+ // | |--duplicate-bucket (only for old, completed payments)
+ // | |
+ // | |-- <seq-num>
+ // | | |--sequence-key: <sequence number>
+ // | | |--creation-info-key: <creation info>
+ // | | |--ai: <attempt info>
+ // | | |--si: <settle info>
+ // | | |--fi: <fail info>
+ // | |
+ // | |-- <seq-num>
+ // | | |
+ // | ... ...
+ // |
+ // |-- <paymenthash>
+ // | |
+ // | ...
+ // ...
+ //
+ paymentsRootBucket = []byte("payments-root-bucket")
+
+ // paymentSequenceKey is a key used in the payment's sub-bucket to
+ // store the sequence number of the payment.
+ paymentSequenceKey = []byte("payment-sequence-key")
+
+ // paymentCreationInfoKey is a key used in the payment's sub-bucket to
+ // store the creation info of the payment.
+ paymentCreationInfoKey = []byte("payment-creation-info")
+
+ // paymentHtlcsBucket is a bucket where we'll store the information
+ // about the HTLCs that were attempted for a payment.
+ paymentHtlcsBucket = []byte("payment-htlcs-bucket")
+
+ // htlcAttemptInfoKey is the key used as the prefix of an HTLC attempt
+ // to store the info about the attempt that was done for the HTLC in
+ // question. The HTLC attempt ID is concatenated at the end.
+ htlcAttemptInfoKey = []byte("ai")
+
+ // htlcSettleInfoKey is the key used as the prefix of an HTLC attempt
+ // settle info, if any. The HTLC attempt ID is concatenated at the end.
+ htlcSettleInfoKey = []byte("si")
+
+ // htlcFailInfoKey is the key used as the prefix of an HTLC attempt
+ // failure information, if any.The HTLC attempt ID is concatenated at
+ // the end.
+ htlcFailInfoKey = []byte("fi")
+
+ // paymentFailInfoKey is a key used in the payment's sub-bucket to
+ // store information about the reason a payment failed.
+ paymentFailInfoKey = []byte("payment-fail-info")
+
+ // paymentsIndexBucket is the name of the top-level bucket within the
+ // database that stores an index of payment sequence numbers to its
+ // payment hash.
+ // payments-sequence-index-bucket
+ // |--<sequence-number>: <payment hash>
+ // |--...
+ // |--<sequence-number>: <payment hash>
+ paymentsIndexBucket = []byte("payments-index-bucket")
+)
+
+// KVPaymentsDB implements persistence for payments and payment attempts.
+type KVPaymentsDB struct {
+ // Sequence management for the kv store.
+ seqMu sync.Mutex
+ currSeq uint64
+ storedSeq uint64
+
+ // db is the underlying database implementation.
+ db kvdb.Backend
+
+ keepFailedPaymentAttempts bool
+}
+
+// defaultKVStoreOptions returns the default options for the KV store.
+func defaultKVStoreOptions() *StoreOptions {
+ return &StoreOptions{
+ KeepFailedPaymentAttempts: false,
+ }
+}
+
+// NewKVPaymentsDB creates a new KVStore for payments.
+func NewKVPaymentsDB(db kvdb.Backend,
+ options ...OptionModifier) (*KVPaymentsDB, error) {
+
+ opts := defaultKVStoreOptions()
+ for _, applyOption := range options {
+ applyOption(opts)
+ }
+
+ if !opts.NoMigration {
+ if err := initKVStore(db); err != nil {
+ return nil, err
+ }
+ }
+
+ return &KVPaymentsDB{
+ db: db,
+ keepFailedPaymentAttempts: opts.KeepFailedPaymentAttempts,
+ }, nil
+}
+
+var paymentsTopLevelBuckets = [][]byte{
+ paymentsRootBucket,
+ paymentsIndexBucket,
+}
+
+// initKVStore creates and initializes the top-level buckets for the payment db.
+func initKVStore(db kvdb.Backend) error {
+ err := kvdb.Update(db, func(tx kvdb.RwTx) error {
+ for _, tlb := range paymentsTopLevelBuckets {
+ if _, err := tx.CreateTopLevelBucket(tlb); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }, func() {})
+ if err != nil {
+ return fmt.Errorf("unable to create new payments db: %w", err)
+ }
+
+ return nil
+}
+
+// InitPayment checks or records the given PaymentCreationInfo with the DB,
+// 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 *KVPaymentsDB) InitPayment(paymentHash lntypes.Hash,
+ info *channeldb.PaymentCreationInfo) error {
+
+ // Obtain a new sequence number for this payment. This is used
+ // to sort the payments in order of creation, and also acts as
+ // a unique identifier for each payment.
+ sequenceNum, err := p.nextPaymentSequence()
+ if err != nil {
+ return err
+ }
+
+ var b bytes.Buffer
+ if err := serializePaymentCreationInfo(&b, info); err != nil {
+ return err
+ }
+ infoBytes := b.Bytes()
+
+ var updateErr error
+ err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
+ // Reset the update error, to avoid carrying over an error
+ // from a previous execution of the batched db transaction.
+ updateErr = nil
+
+ prefetchPayment(tx, paymentHash)
+ bucket, err := createPaymentBucket(tx, paymentHash)
+ if err != nil {
+ return err
+ }
+
+ // Get the existing status of this payment, if any.
+ paymentStatus, err := fetchPaymentStatus(bucket)
+
+ switch {
+ // If no error is returned, it means we already have this
+ // payment. We'll check the status to decide whether we allow
+ // retrying the payment or return a specific error.
+ case err == nil:
+ if err := paymentStatus.initializable(); err != nil {
+ updateErr = err
+ return nil
+ }
+
+ // Otherwise, if the error is not `ErrPaymentNotInitiated`,
+ // we'll return the error.
+ case !errors.Is(err, ErrPaymentNotInitiated):
+ return err
+ }
+
+ // Before we set our new sequence number, we check whether this
+ // payment has a previously set sequence number and remove its
+ // index entry if it exists. This happens in the case where we
+ // have a previously attempted payment which was left in a state
+ // where we can retry.
+ seqBytes := bucket.Get(paymentSequenceKey)
+ if seqBytes != nil {
+ indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
+ if err := indexBucket.Delete(seqBytes); err != nil {
+ return err
+ }
+ }
+
+ // Once we have obtained a sequence number, we add an entry
+ // to our index bucket which will map the sequence number to
+ // our payment identifier.
+ err = createPaymentIndexEntry(
+ tx, sequenceNum, info.PaymentIdentifier,
+ )
+ if err != nil {
+ return err
+ }
+
+ err = bucket.Put(paymentSequenceKey, sequenceNum)
+ if err != nil {
+ return err
+ }
+
+ // Add the payment info to the bucket, which contains the
+ // static information for this payment
+ err = bucket.Put(paymentCreationInfoKey, infoBytes)
+ if err != nil {
+ return err
+ }
+
+ // We'll delete any lingering HTLCs to start with, in case we
+ // are initializing a payment that was attempted earlier, but
+ // left in a state where we could retry.
+ err = bucket.DeleteNestedBucket(paymentHtlcsBucket)
+ if err != nil && !errors.Is(err, kvdb.ErrBucketNotFound) {
+ return err
+ }
+
+ // Also delete any lingering failure info now that we are
+ // re-attempting.
+ return bucket.Delete(paymentFailInfoKey)
+ })
+ if err != nil {
+ return fmt.Errorf("unable to init payment: %w", err)
+ }
+
+ return updateErr
+}
+
+// DeleteFailedAttempts deletes all failed htlcs for a payment if configured
+// by the KVPaymentsDB db.
+func (p *KVPaymentsDB) DeleteFailedAttempts(hash lntypes.Hash) error {
+ if !p.keepFailedPaymentAttempts {
+ const failedHtlcsOnly = true
+ err := p.DeletePayment(hash, failedHtlcsOnly)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// paymentIndexTypeHash is a payment index type which indicates that we have
+// created an index of payment sequence number to payment hash.
+type paymentIndexType uint8
+
+// paymentIndexTypeHash is a payment index type which indicates that we have
+// created an index of payment sequence number to payment hash.
+const paymentIndexTypeHash paymentIndexType = 0
+
+// createPaymentIndexEntry creates a payment hash typed index for a payment. The
+// index produced contains a payment index type (which can be used in future to
+// signal different payment index types) and the payment identifier.
+func createPaymentIndexEntry(tx kvdb.RwTx, sequenceNumber []byte,
+ id lntypes.Hash) error {
+
+ var b bytes.Buffer
+ if err := WriteElements(&b, paymentIndexTypeHash, id[:]); err != nil {
+ return err
+ }
+
+ indexes := tx.ReadWriteBucket(paymentsIndexBucket)
+
+ return indexes.Put(sequenceNumber, b.Bytes())
+}
+
+// deserializePaymentIndex deserializes a payment index entry. This function
+// currently only supports deserialization of payment hash indexes, and will
+// fail for other types.
+func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) {
+ var (
+ indexType paymentIndexType
+ paymentHash []byte
+ )
+
+ if err := ReadElements(r, &indexType, &paymentHash); err != nil {
+ return lntypes.Hash{}, err
+ }
+
+ // While we only have on payment index type, we do not need to use our
+ // index type to deserialize the index. However, we sanity check that
+ // this type is as expected, since we had to read it out anyway.
+ if indexType != paymentIndexTypeHash {
+ return lntypes.Hash{}, fmt.Errorf("unknown payment index "+
+ "type: %v", indexType)
+ }
+
+ hash, err := lntypes.MakeHash(paymentHash)
+ if err != nil {
+ return lntypes.Hash{}, err
+ }
+
+ return hash, nil
+}
+
+// RegisterAttempt atomically records the provided HTLCAttemptInfo to the
+// DB.
+func (p *KVPaymentsDB) RegisterAttempt(paymentHash lntypes.Hash,
+ attempt *HTLCAttemptInfo) (*MPPayment, error) {
+
+ // Serialize the information before opening the db transaction.
+ var a bytes.Buffer
+ err := serializeHTLCAttemptInfo(&a, attempt)
+ if err != nil {
+ return nil, err
+ }
+ htlcInfoBytes := a.Bytes()
+
+ htlcIDBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(htlcIDBytes, attempt.AttemptID)
+
+ var payment *MPPayment
+ err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
+ prefetchPayment(tx, paymentHash)
+ bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
+ if err != nil {
+ return err
+ }
+
+ payment, err = fetchPayment(bucket)
+ if err != nil {
+ return err
+ }
+
+ // Check if registering a new attempt is allowed.
+ if err := payment.Registrable(); err != nil {
+ return err
+ }
+
+ // If the final hop has encrypted data, then we know this is a
+ // blinded payment. In blinded payments, MPP records are not set
+ // for split payments and the recipient is responsible for using
+ // a consistent PathID across the various encrypted data
+ // payloads that we received from them for this payment. All we
+ // need to check is that the total amount field for each HTLC
+ // in the split payment is correct.
+ isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
+
+ // Make sure any existing shards match the new one with regards
+ // to MPP options.
+ mpp := attempt.Route.FinalHop().MPP
+
+ // MPP records should not be set for attempts to blinded paths.
+ if isBlinded && mpp != nil {
+ return ErrMPPRecordInBlindedPayment
+ }
+
+ for _, h := range payment.InFlightHTLCs() {
+ hMpp := h.Route.FinalHop().MPP
+
+ // If this is a blinded payment, then no existing HTLCs
+ // should have MPP records.
+ if isBlinded && hMpp != nil {
+ return ErrMPPRecordInBlindedPayment
+ }
+
+ // If this is a blinded payment, then we just need to
+ // check that the TotalAmtMsat field for this shard
+ // is equal to that of any other shard in the same
+ // payment.
+ if isBlinded {
+ if attempt.Route.FinalHop().TotalAmtMsat !=
+ h.Route.FinalHop().TotalAmtMsat {
+
+ //nolint:ll
+ return ErrBlindedPaymentTotalAmountMismatch
+ }
+
+ continue
+ }
+
+ switch {
+ // We tried to register a non-MPP attempt for a MPP
+ // payment.
+ case mpp == nil && hMpp != nil:
+ return ErrMPPayment
+
+ // We tried to register a MPP shard for a non-MPP
+ // payment.
+ case mpp != nil && hMpp == nil:
+ return ErrNonMPPayment
+
+ // Non-MPP payment, nothing more to validate.
+ case mpp == nil:
+ continue
+ }
+
+ // Check that MPP options match.
+ if mpp.PaymentAddr() != hMpp.PaymentAddr() {
+ return ErrMPPPaymentAddrMismatch
+ }
+
+ if mpp.TotalMsat() != hMpp.TotalMsat() {
+ return ErrMPPTotalAmountMismatch
+ }
+ }
+
+ // If this is a non-MPP attempt, it must match the total amount
+ // exactly. Note that a blinded payment is considered an MPP
+ // attempt.
+ amt := attempt.Route.ReceiverAmt()
+ if !isBlinded && mpp == nil && amt != payment.Info.Value {
+ return ErrValueMismatch
+ }
+
+ // Ensure we aren't sending more than the total payment amount.
+ sentAmt, _ := payment.SentAmt()
+ if sentAmt+amt > payment.Info.Value {
+ return fmt.Errorf("%w: attempted=%v, payment amount="+
+ "%v", ErrValueExceedsAmt,
+ sentAmt+amt, payment.Info.Value)
+ }
+
+ htlcsBucket, err := bucket.CreateBucketIfNotExists(
+ paymentHtlcsBucket,
+ )
+ if err != nil {
+ return err
+ }
+
+ err = htlcsBucket.Put(
+ htlcBucketKey(htlcAttemptInfoKey, htlcIDBytes),
+ htlcInfoBytes,
+ )
+ if err != nil {
+ return err
+ }
+
+ // Retrieve attempt info for the notification.
+ payment, err = fetchPayment(bucket)
+
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return payment, err
+}
+
+// SettleAttempt marks the given attempt settled with the preimage. If this is
+// a multi shard payment, this might implicitly mean that the full payment
+// succeeded.
+//
+// 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 *KVPaymentsDB) SettleAttempt(hash lntypes.Hash,
+ attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) {
+
+ var b bytes.Buffer
+ if err := serializeHTLCSettleInfo(&b, settleInfo); err != nil {
+ return nil, err
+ }
+ settleBytes := b.Bytes()
+
+ return p.updateHtlcKey(hash, attemptID, htlcSettleInfoKey, settleBytes)
+}
+
+// FailAttempt marks the given payment attempt failed.
+func (p *KVPaymentsDB) FailAttempt(hash lntypes.Hash,
+ attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) {
+
+ var b bytes.Buffer
+ if err := serializeHTLCFailInfo(&b, failInfo); err != nil {
+ return nil, err
+ }
+ failBytes := b.Bytes()
+
+ return p.updateHtlcKey(hash, attemptID, htlcFailInfoKey, failBytes)
+}
+
+// updateHtlcKey updates a database key for the specified htlc.
+func (p *KVPaymentsDB) updateHtlcKey(paymentHash lntypes.Hash,
+ attemptID uint64, key, value []byte) (*MPPayment, error) {
+
+ aid := make([]byte, 8)
+ binary.BigEndian.PutUint64(aid, attemptID)
+
+ var payment *MPPayment
+ err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
+ payment = nil
+
+ prefetchPayment(tx, paymentHash)
+ bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
+ if err != nil {
+ return err
+ }
+
+ p, err := fetchPayment(bucket)
+ if err != nil {
+ return err
+ }
+
+ // We can only update keys of in-flight payments. We allow
+ // updating keys even if the payment has reached a terminal
+ // condition, since the HTLC outcomes must still be updated.
+ if err := p.Status.updatable(); err != nil {
+ return err
+ }
+
+ htlcsBucket := bucket.NestedReadWriteBucket(paymentHtlcsBucket)
+ if htlcsBucket == nil {
+ return fmt.Errorf("htlcs bucket not found")
+ }
+
+ attemptKey := htlcBucketKey(htlcAttemptInfoKey, aid)
+ if htlcsBucket.Get(attemptKey) == nil {
+ return fmt.Errorf("HTLC with ID %v not registered",
+ attemptID)
+ }
+
+ // Make sure the shard is not already failed or settled.
+ failKey := htlcBucketKey(htlcFailInfoKey, aid)
+ if htlcsBucket.Get(failKey) != nil {
+ return ErrAttemptAlreadyFailed
+ }
+
+ settleKey := htlcBucketKey(htlcSettleInfoKey, aid)
+ if htlcsBucket.Get(settleKey) != nil {
+ return ErrAttemptAlreadySettled
+ }
+
+ // Add or update the key for this htlc.
+ err = htlcsBucket.Put(htlcBucketKey(key, aid), value)
+ if err != nil {
+ return err
+ }
+
+ // Retrieve attempt info for the notification.
+ payment, err = fetchPayment(bucket)
+
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return payment, err
+}
+
+// Fail transitions a payment into the Failed state, and records the reason the
+// 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 *KVPaymentsDB) Fail(paymentHash lntypes.Hash,
+ reason channeldb.FailureReason) (*MPPayment, error) {
+
+ var (
+ updateErr error
+ payment *MPPayment
+ )
+ err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error {
+ // Reset the update error, to avoid carrying over an error
+ // from a previous execution of the batched db transaction.
+ updateErr = nil
+ payment = nil
+
+ prefetchPayment(tx, paymentHash)
+ bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
+ if errors.Is(err, ErrPaymentNotInitiated) {
+ updateErr = ErrPaymentNotInitiated
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ // We mark the payment as failed as long as it is known. This
+ // lets the last attempt to fail with a terminal write its
+ // failure to the KVPaymentsDB without synchronizing with
+ // other attempts.
+ _, err = fetchPaymentStatus(bucket)
+ if errors.Is(err, ErrPaymentNotInitiated) {
+ updateErr = ErrPaymentNotInitiated
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ // Put the failure reason in the bucket for record keeping.
+ v := []byte{byte(reason)}
+ err = bucket.Put(paymentFailInfoKey, v)
+ if err != nil {
+ return err
+ }
+
+ // Retrieve attempt info for the notification, if available.
+ payment, err = fetchPayment(bucket)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return payment, updateErr
+}
+
+// FetchPayment returns information about a payment from the database.
+func (p *KVPaymentsDB) FetchPayment(paymentHash lntypes.Hash) (
+ *MPPayment, error) {
+
+ var payment *MPPayment
+ err := kvdb.View(p.db, func(tx kvdb.RTx) error {
+ prefetchPayment(tx, paymentHash)
+ bucket, err := fetchPaymentBucket(tx, paymentHash)
+ if err != nil {
+ return err
+ }
+
+ payment, err = fetchPayment(bucket)
+
+ return err
+ }, func() {
+ payment = nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return payment, nil
+}
+
+// prefetchPayment attempts to prefetch as much of the payment as possible to
+// reduce DB roundtrips.
+func prefetchPayment(tx kvdb.RTx, paymentHash lntypes.Hash) {
+ rb := kvdb.RootBucket(tx)
+ kvdb.Prefetch(
+ rb,
+ []string{
+ // Prefetch all keys in the payment's bucket.
+ string(paymentsRootBucket),
+ string(paymentHash[:]),
+ },
+ []string{
+ // Prefetch all keys in the payment's htlc bucket.
+ string(paymentsRootBucket),
+ string(paymentHash[:]),
+ string(paymentHtlcsBucket),
+ },
+ )
+}
+
+// createPaymentBucket creates or fetches the sub-bucket assigned to this
+// payment hash.
+func createPaymentBucket(tx kvdb.RwTx, paymentHash lntypes.Hash) (
+ kvdb.RwBucket, error) {
+
+ payments, err := tx.CreateTopLevelBucket(paymentsRootBucket)
+ if err != nil {
+ return nil, err
+ }
+
+ return payments.CreateBucketIfNotExists(paymentHash[:])
+}
+
+// fetchPaymentBucket fetches the sub-bucket assigned to this payment hash. If
+// the bucket does not exist, it returns ErrPaymentNotInitiated.
+func fetchPaymentBucket(tx kvdb.RTx, paymentHash lntypes.Hash) (
+ kvdb.RBucket, error) {
+
+ payments := tx.ReadBucket(paymentsRootBucket)
+ if payments == nil {
+ return nil, ErrPaymentNotInitiated
+ }
+
+ bucket := payments.NestedReadBucket(paymentHash[:])
+ if bucket == nil {
+ return nil, ErrPaymentNotInitiated
+ }
+
+ return bucket, nil
+}
+
+// fetchPaymentBucketUpdate is identical to fetchPaymentBucket, but it returns a
+// bucket that can be written to.
+func fetchPaymentBucketUpdate(tx kvdb.RwTx, paymentHash lntypes.Hash) (
+ kvdb.RwBucket, error) {
+
+ payments := tx.ReadWriteBucket(paymentsRootBucket)
+ if payments == nil {
+ return nil, ErrPaymentNotInitiated
+ }
+
+ bucket := payments.NestedReadWriteBucket(paymentHash[:])
+ if bucket == nil {
+ return nil, ErrPaymentNotInitiated
+ }
+
+ return bucket, nil
+}
+
+// nextPaymentSequence returns the next sequence number to store for a new
+// payment.
+func (p *KVPaymentsDB) nextPaymentSequence() ([]byte, error) {
+ p.seqMu.Lock()
+ defer p.seqMu.Unlock()
+
+ // Set a new upper bound in the DB every 1000 payments to avoid
+ // conflicts on the sequence when using etcd.
+ if p.currSeq == p.storedSeq {
+ var currPaymentSeq, newUpperBound uint64
+ if err := kvdb.Update(p.db, func(tx kvdb.RwTx) error {
+ paymentsBucket, err := tx.CreateTopLevelBucket(
+ paymentsRootBucket,
+ )
+ if err != nil {
+ return err
+ }
+
+ currPaymentSeq = paymentsBucket.Sequence()
+ newUpperBound = currPaymentSeq + paymentSeqBlockSize
+
+ return paymentsBucket.SetSequence(newUpperBound)
+ }, func() {}); err != nil {
+ return nil, err
+ }
+
+ // We lazy initialize the cached currPaymentSeq here using the
+ // first nextPaymentSequence() call. This if statement will auto
+ // initialize our stored currPaymentSeq, since by default both
+ // this variable and storedPaymentSeq are zero which in turn
+ // will have us fetch the current values from the DB.
+ if p.currSeq == 0 {
+ p.currSeq = currPaymentSeq
+ }
+
+ p.storedSeq = newUpperBound
+ }
+
+ p.currSeq++
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, p.currSeq)
+
+ return b, nil
+}
+
+// fetchPaymentStatus fetches the payment status of the payment. If the payment
+// isn't found, it will return error `ErrPaymentNotInitiated`.
+func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) {
+ // Creation info should be set for all payments, regardless of state.
+ // If not, it is unknown.
+ if bucket.Get(paymentCreationInfoKey) == nil {
+ return 0, ErrPaymentNotInitiated
+ }
+
+ payment, err := fetchPayment(bucket)
+ if err != nil {
+ return 0, err
+ }
+
+ return payment.Status, nil
+}
+
+// FetchInFlightPayments returns all payments with status InFlight.
+func (p *KVPaymentsDB) FetchInFlightPayments() ([]*MPPayment, error) {
+ var (
+ inFlights []*MPPayment
+ start = time.Now()
+ lastLogTime = time.Now()
+ processedCount int
+ )
+
+ err := kvdb.View(p.db, func(tx kvdb.RTx) error {
+ payments := tx.ReadBucket(paymentsRootBucket)
+ if payments == nil {
+ return nil
+ }
+
+ return payments.ForEach(func(k, _ []byte) error {
+ bucket := payments.NestedReadBucket(k)
+ if bucket == nil {
+ return fmt.Errorf("non bucket element")
+ }
+
+ p, err := fetchPayment(bucket)
+ if err != nil {
+ return err
+ }
+
+ processedCount++
+ if time.Since(lastLogTime) >=
+ paymentProgressLogInterval {
+
+ log.Debugf("Scanning inflight payments "+
+ "(in progress), processed %d, last "+
+ "processed payment: %v", processedCount,
+ p.Info)
+
+ lastLogTime = time.Now()
+ }
+
+ // Skip the payment if it's terminated.
+ if p.Terminated() {
+ return nil
+ }
+
+ inFlights = append(inFlights, p)
+
+ return nil
+ })
+ }, func() {
+ inFlights = nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ elapsed := time.Since(start)
+ log.Debugf("Completed scanning for inflight payments: "+
+ "total_processed=%d, found_inflight=%d, elapsed=%v",
+ processedCount, len(inFlights),
+ elapsed.Round(time.Millisecond))
+
+ return inFlights, nil
+}
+
+// htlcBucketKey creates a composite key from prefix and id where the result is
+// simply the two concatenated.
+func htlcBucketKey(prefix, id []byte) []byte {
+ key := make([]byte, len(prefix)+len(id))
+ copy(key, prefix)
+ copy(key[len(prefix):], id)
+
+ return key
+}
+
+// FetchPayments returns all sent payments found in the DB.
+func (p *KVPaymentsDB) FetchPayments() ([]*MPPayment, error) {
+ var payments []*MPPayment
+
+ err := kvdb.View(p.db, func(tx kvdb.RTx) error {
+ paymentsBucket := tx.ReadBucket(paymentsRootBucket)
+ if paymentsBucket == nil {
+ return nil
+ }
+
+ return paymentsBucket.ForEach(func(k, v []byte) error {
+ bucket := paymentsBucket.NestedReadBucket(k)
+ if bucket == nil {
+ // We only expect sub-buckets to be found in
+ // this top-level bucket.
+ return fmt.Errorf("non bucket element in " +
+ "payments bucket")
+ }
+
+ p, err := fetchPayment(bucket)
+ if err != nil {
+ return err
+ }
+
+ payments = append(payments, p)
+
+ // 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.
+ duplicatePayments, err := fetchDuplicatePayments(bucket)
+ if err != nil {
+ return err
+ }
+
+ payments = append(payments, duplicatePayments...)
+
+ return nil
+ })
+ }, func() {
+ payments = nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Before returning, sort the payments by their sequence number.
+ sort.Slice(payments, func(i, j int) bool {
+ return payments[i].SequenceNum < payments[j].SequenceNum
+ })
+
+ return payments, nil
+}
+
+func fetchCreationInfo(bucket kvdb.RBucket) (*channeldb.PaymentCreationInfo, error) {
+ b := bucket.Get(paymentCreationInfoKey)
+ if b == nil {
+ return nil, fmt.Errorf("creation info not found")
+ }
+
+ r := bytes.NewReader(b)
+
+ return deserializePaymentCreationInfo(r)
+}
+
+func fetchPayment(bucket kvdb.RBucket) (*MPPayment, error) {
+ seqBytes := bucket.Get(paymentSequenceKey)
+ if seqBytes == nil {
+ return nil, fmt.Errorf("sequence number not found")
+ }
+
+ sequenceNum := binary.BigEndian.Uint64(seqBytes)
+
+ // Get the PaymentCreationInfo.
+ creationInfo, err := fetchCreationInfo(bucket)
+ if err != nil {
+ return nil, err
+ }
+
+ var htlcs []HTLCAttempt
+ htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket)
+ if htlcsBucket != nil {
+ // Get the payment attempts. This can be empty.
+ htlcs, err = fetchHtlcAttempts(htlcsBucket)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Get failure reason if available.
+ var failureReason *channeldb.FailureReason
+ b := bucket.Get(paymentFailInfoKey)
+ if b != nil {
+ reason := channeldb.FailureReason(b[0])
+ failureReason = &reason
+ }
+
+ // Create a new payment.
+ payment := &MPPayment{
+ SequenceNum: sequenceNum,
+ Info: creationInfo,
+ HTLCs: htlcs,
+ FailureReason: failureReason,
+ }
+
+ // Set its state and status.
+ if err := payment.setState(); err != nil {
+ return nil, err
+ }
+
+ return payment, nil
+}
+
+// fetchHtlcAttempts retrieves all htlc attempts made for the payment found in
+// the given bucket.
+func fetchHtlcAttempts(bucket kvdb.RBucket) ([]HTLCAttempt, error) {
+ htlcsMap := make(map[uint64]*HTLCAttempt)
+
+ attemptInfoCount := 0
+ err := bucket.ForEach(func(k, v []byte) error {
+ aid := byteOrder.Uint64(k[len(k)-8:])
+
+ if _, ok := htlcsMap[aid]; !ok {
+ htlcsMap[aid] = &HTLCAttempt{}
+ }
+
+ var err error
+ switch {
+ case bytes.HasPrefix(k, htlcAttemptInfoKey):
+ attemptInfo, err := readHtlcAttemptInfo(v)
+ if err != nil {
+ return err
+ }
+
+ attemptInfo.AttemptID = aid
+ htlcsMap[aid].HTLCAttemptInfo = *attemptInfo
+ attemptInfoCount++
+
+ case bytes.HasPrefix(k, htlcSettleInfoKey):
+ htlcsMap[aid].Settle, err = readHtlcSettleInfo(v)
+ if err != nil {
+ return err
+ }
+
+ case bytes.HasPrefix(k, htlcFailInfoKey):
+ htlcsMap[aid].Failure, err = readHtlcFailInfo(v)
+ if err != nil {
+ return err
+ }
+
+ default:
+ return fmt.Errorf("unknown htlc attempt key")
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Sanity check that all htlcs have an attempt info.
+ if attemptInfoCount != len(htlcsMap) {
+ return nil, ErrNoAttemptInfo
+ }
+
+ keys := make([]uint64, len(htlcsMap))
+ i := 0
+ for k := range htlcsMap {
+ keys[i] = k
+ i++
+ }
+
+ // Sort HTLC attempts by their attempt ID. This is needed because in the
+ // DB we store the attempts with keys prefixed by their status which
+ // changes order (groups them together by status).
+ sort.Slice(keys, func(i, j int) bool {
+ return keys[i] < keys[j]
+ })
+
+ htlcs := make([]HTLCAttempt, len(htlcsMap))
+ for i, key := range keys {
+ htlcs[i] = *htlcsMap[key]
+ }
+
+ return htlcs, nil
+}
+
+// readHtlcAttemptInfo reads the payment attempt info for this htlc.
+func readHtlcAttemptInfo(b []byte) (*HTLCAttemptInfo, error) {
+ r := bytes.NewReader(b)
+ return deserializeHTLCAttemptInfo(r)
+}
+
+// readHtlcSettleInfo reads the settle info for the htlc. If the htlc isn't
+// settled, nil is returned.
+func readHtlcSettleInfo(b []byte) (*HTLCSettleInfo, error) {
+ r := bytes.NewReader(b)
+ return deserializeHTLCSettleInfo(r)
+}
+
+// readHtlcFailInfo reads the failure info for the htlc. If the htlc hasn't
+// failed, nil is returned.
+func readHtlcFailInfo(b []byte) (*HTLCFailInfo, error) {
+ r := bytes.NewReader(b)
+ return deserializeHTLCFailInfo(r)
+}
+
+// fetchFailedHtlcKeys retrieves the bucket keys of all failed HTLCs of a
+// payment bucket.
+func fetchFailedHtlcKeys(bucket kvdb.RBucket) ([][]byte, error) {
+ htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket)
+
+ var htlcs []HTLCAttempt
+ var err error
+ if htlcsBucket != nil {
+ htlcs, err = fetchHtlcAttempts(htlcsBucket)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Now iterate though them and save the bucket keys for the failed
+ // HTLCs.
+ var htlcKeys [][]byte
+ for _, h := range htlcs {
+ if h.Failure == nil {
+ continue
+ }
+
+ htlcKeyBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(htlcKeyBytes, h.AttemptID)
+
+ htlcKeys = append(htlcKeys, htlcKeyBytes)
+ }
+
+ return htlcKeys, nil
+}
+
+// QueryPayments is a query to the payments database which is restricted
+// to a subset of payments by the payments query, containing an offset
+// index and a maximum number of returned payments.
+func (p *KVPaymentsDB) QueryPayments(_ context.Context,
+ query PaymentsQuery) (PaymentsResponse, error) {
+
+ var resp PaymentsResponse
+
+ if err := kvdb.View(p.db, func(tx kvdb.RTx) error {
+ // Get the root payments bucket.
+ paymentsBucket := tx.ReadBucket(paymentsRootBucket)
+ if paymentsBucket == nil {
+ return nil
+ }
+
+ // Get the index bucket which maps sequence number -> payment
+ // hash and duplicate bool. If we have a payments bucket, we
+ // should have an indexes bucket as well.
+ indexes := tx.ReadBucket(paymentsIndexBucket)
+ if indexes == nil {
+ return fmt.Errorf("index bucket does not exist")
+ }
+
+ // accumulatePayments gets payments with the sequence number
+ // and hash provided and adds them to our list of payments if
+ // they meet the criteria of our query. It returns the number
+ // of payments that were added.
+ accumulatePayments := func(sequenceKey, hash []byte) (bool,
+ error) {
+
+ r := bytes.NewReader(hash)
+ paymentHash, err := deserializePaymentIndex(r)
+ if err != nil {
+ return false, err
+ }
+
+ payment, err := fetchPaymentWithSequenceNumber(
+ tx, paymentHash, sequenceKey,
+ )
+ if err != nil {
+ return false, err
+ }
+
+ // To keep compatibility with the old API, we only
+ // return non-succeeded payments if requested.
+ if payment.Status != StatusSucceeded &&
+ !query.IncludeIncomplete {
+
+ return false, err
+ }
+
+ // Get the creation time in Unix seconds, this always
+ // rounds down the nanoseconds to full seconds.
+ createTime := payment.Info.CreationTime.Unix()
+
+ // Skip any payments that were created before the
+ // specified time.
+ if createTime < query.CreationDateStart {
+ return false, nil
+ }
+
+ // Skip any payments that were created after the
+ // specified time.
+ if query.CreationDateEnd != 0 &&
+ createTime > query.CreationDateEnd {
+
+ return false, nil
+ }
+
+ // At this point, we've exhausted the offset, so we'll
+ // begin collecting invoices found within the range.
+ resp.Payments = append(resp.Payments, payment)
+
+ return true, nil
+ }
+
+ // Create a paginator which reads from our sequence index bucket
+ // with the parameters provided by the payments query.
+ paginator := channeldb.NewPaginator(
+ indexes.ReadCursor(), query.Reversed, query.IndexOffset,
+ query.MaxPayments,
+ )
+
+ // Run a paginated query, adding payments to our response.
+ if err := paginator.Query(accumulatePayments); err != nil {
+ return err
+ }
+
+ // Counting the total number of payments is expensive, since we
+ // literally have to traverse the cursor linearly, which can
+ // take quite a while. So it's an optional query parameter.
+ if query.CountTotal {
+ var (
+ totalPayments uint64
+ err error
+ )
+ countFn := func(_, _ []byte) error {
+ totalPayments++
+
+ return nil
+ }
+
+ // In non-boltdb database backends, there's a faster
+ // ForAll query that allows for batch fetching items.
+ fastBucket, ok := indexes.(kvdb.ExtendedRBucket)
+ if ok {
+ err = fastBucket.ForAll(countFn)
+ } else {
+ err = indexes.ForEach(countFn)
+ }
+ if err != nil {
+ return fmt.Errorf("error counting payments: %w",
+ err)
+ }
+
+ resp.TotalCount = totalPayments
+ }
+
+ return nil
+ }, func() {
+ resp = PaymentsResponse{}
+ }); err != nil {
+ return resp, err
+ }
+
+ // Need to swap the payments slice order if reversed order.
+ if query.Reversed {
+ for l, r := 0, len(resp.Payments)-1; l < r; l, r = l+1, r-1 {
+ resp.Payments[l], resp.Payments[r] =
+ resp.Payments[r], resp.Payments[l]
+ }
+ }
+
+ // Set the first and last index of the returned payments so that the
+ // caller can resume from this point later on.
+ if len(resp.Payments) > 0 {
+ resp.FirstIndexOffset = resp.Payments[0].SequenceNum
+ resp.LastIndexOffset =
+ resp.Payments[len(resp.Payments)-1].SequenceNum
+ }
+
+ return resp, nil
+}
+
+// fetchPaymentWithSequenceNumber get the payment which matches the payment hash
+// *and* sequence number provided from the database. This is required because
+// we previously had more than one payment per hash, so we have multiple indexes
+// pointing to a single payment; we want to retrieve the correct one.
+func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash,
+ sequenceNumber []byte) (*MPPayment, error) {
+
+ // We can now lookup the payment keyed by its hash in
+ // the payments root bucket.
+ bucket, err := fetchPaymentBucket(tx, paymentHash)
+ if err != nil {
+ return nil, err
+ }
+
+ // A single payment hash can have multiple payments associated with it.
+ // We lookup our sequence number first, to determine whether this is
+ // the payment we are actually looking for.
+ seqBytes := bucket.Get(paymentSequenceKey)
+ if seqBytes == nil {
+ return nil, ErrNoSequenceNumber
+ }
+
+ // If this top level payment has the sequence number we are looking for,
+ // return it.
+ if bytes.Equal(seqBytes, sequenceNumber) {
+ return fetchPayment(bucket)
+ }
+
+ // If we were not looking for the top level payment, we are looking for
+ // one of our duplicate payments. We need to iterate through the seq
+ // numbers in this bucket to find the correct payments. If we do not
+ // find a duplicate payments bucket here, something is wrong.
+ dup := bucket.NestedReadBucket(duplicatePaymentsBucket)
+ if dup == nil {
+ return nil, ErrNoDuplicateBucket
+ }
+
+ var duplicatePayment *MPPayment
+ 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 ErrNoDuplicateNestedBucket
+ }
+
+ seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
+ if seqBytes == nil {
+ return err
+ }
+
+ // If this duplicate payment is not the sequence number we are
+ // looking for, we can continue.
+ if !bytes.Equal(seqBytes, sequenceNumber) {
+ return nil
+ }
+
+ duplicatePayment, err = fetchDuplicatePayment(subBucket)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // If none of the duplicate payments matched our sequence number, we
+ // failed to find the payment with this sequence number; something is
+ // wrong.
+ if duplicatePayment == nil {
+ return nil, ErrDuplicateNotFound
+ }
+
+ return duplicatePayment, 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 (p *KVPaymentsDB) DeletePayment(paymentHash lntypes.Hash,
+ failedHtlcsOnly bool) error {
+
+ return kvdb.Update(p.db, func(tx kvdb.RwTx) error {
+ payments := tx.ReadWriteBucket(paymentsRootBucket)
+ if payments == nil {
+ return nil
+ }
+
+ bucket := payments.NestedReadWriteBucket(paymentHash[:])
+ if bucket == nil {
+ return fmt.Errorf("non bucket element in payments " +
+ "bucket")
+ }
+
+ // If the status is InFlight, we cannot safely delete
+ // the payment information, so we return early.
+ paymentStatus, err := fetchPaymentStatus(bucket)
+ if err != nil {
+ return err
+ }
+
+ // If the payment has inflight HTLCs, we cannot safely delete
+ // the payment information, so we return an error.
+ if err := paymentStatus.removable(); err != nil {
+ return fmt.Errorf("payment '%v' has inflight HTLCs"+
+ "and therefore cannot be deleted: %w",
+ paymentHash.String(), err)
+ }
+
+ // Delete the failed HTLC attempts we found.
+ if failedHtlcsOnly {
+ toDelete, err := fetchFailedHtlcKeys(bucket)
+ if err != nil {
+ return err
+ }
+
+ htlcsBucket := bucket.NestedReadWriteBucket(
+ paymentHtlcsBucket,
+ )
+
+ for _, htlcID := range toDelete {
+ err = htlcsBucket.Delete(
+ htlcBucketKey(
+ htlcAttemptInfoKey, htlcID,
+ ),
+ )
+ if err != nil {
+ return err
+ }
+
+ err = htlcsBucket.Delete(
+ htlcBucketKey(htlcFailInfoKey, htlcID),
+ )
+ if err != nil {
+ return err
+ }
+
+ err = htlcsBucket.Delete(
+ htlcBucketKey(
+ htlcSettleInfoKey, htlcID,
+ ),
+ )
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ seqNrs, err := fetchSequenceNumbers(bucket)
+ if err != nil {
+ return err
+ }
+
+ err = payments.DeleteNestedBucket(paymentHash[:])
+ if err != nil {
+ return err
+ }
+
+ indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
+ for _, k := range seqNrs {
+ if err := indexBucket.Delete(k); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }, func() {})
+}
+
+// DeletePayments deletes all completed and failed payments from the DB. If
+// failedOnly is set, only failed payments will be considered for deletion. If
+// 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 *KVPaymentsDB) DeletePayments(failedOnly,
+ failedHtlcsOnly bool) (int, error) {
+
+ var numPayments int
+ err := kvdb.Update(p.db, func(tx kvdb.RwTx) error {
+ payments := tx.ReadWriteBucket(paymentsRootBucket)
+ if payments == nil {
+ return nil
+ }
+
+ var (
+ // deleteBuckets is the set of payment buckets we need
+ // to delete.
+ deleteBuckets [][]byte
+
+ // deleteIndexes is the set of indexes pointing to these
+ // payments that need to be deleted.
+ deleteIndexes [][]byte
+
+ // deleteHtlcs maps a payment hash to the HTLC IDs we
+ // want to delete for that payment.
+ deleteHtlcs = make(map[lntypes.Hash][][]byte)
+ )
+ err := payments.ForEach(func(k, _ []byte) error {
+ bucket := payments.NestedReadBucket(k)
+ if bucket == nil {
+ // We only expect sub-buckets to Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.