diff --git a/channeldb/payment_control.go b/channeldb/payment_control.go deleted file mode 100644 index 61739e92d..000000000 --- a/channeldb/payment_control.go +++ /dev/null @@ -1,836 +0,0 @@ -package channeldb - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "sync" - "time" - - "github.com/lightningnetwork/lnd/kvdb" - "github.com/lightningnetwork/lnd/lntypes" -) - -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 -) - -var ( - // ErrAlreadyPaid signals we have already paid this payment hash. - ErrAlreadyPaid = errors.New("invoice is already paid") - - // ErrPaymentInFlight signals that payment for this payment hash is - // already "in flight" on the network. - ErrPaymentInFlight = errors.New("payment is in transition") - - // ErrPaymentExists is returned when we try to initialize an already - // existing payment that is not failed. - ErrPaymentExists = errors.New("payment already exists") - - // ErrPaymentInternal is returned when performing the payment has a - // conflicting state, such as, - // - payment has StatusSucceeded but remaining amount is not zero. - // - payment has StatusInitiated but remaining amount is zero. - // - payment has StatusFailed but remaining amount is zero. - ErrPaymentInternal = errors.New("internal error") - - // ErrPaymentNotInitiated is returned if the payment wasn't initiated. - ErrPaymentNotInitiated = errors.New("payment isn't initiated") - - // ErrPaymentAlreadySucceeded is returned in the event we attempt to - // change the status of a payment already succeeded. - ErrPaymentAlreadySucceeded = errors.New("payment is already succeeded") - - // ErrPaymentAlreadyFailed is returned in the event we attempt to alter - // a failed payment. - ErrPaymentAlreadyFailed = errors.New("payment has already failed") - - // ErrUnknownPaymentStatus is returned when we do not recognize the - // existing state of a payment. - ErrUnknownPaymentStatus = errors.New("unknown payment status") - - // ErrPaymentTerminal is returned if we attempt to alter a payment that - // already has reached a terminal condition. - ErrPaymentTerminal = errors.New("payment has reached terminal " + - "condition") - - // ErrAttemptAlreadySettled is returned if we try to alter an already - // settled HTLC attempt. - ErrAttemptAlreadySettled = errors.New("attempt already settled") - - // ErrAttemptAlreadyFailed is returned if we try to alter an already - // failed HTLC attempt. - ErrAttemptAlreadyFailed = errors.New("attempt already failed") - - // ErrValueMismatch is returned if we try to register a non-MPP attempt - // with an amount that doesn't match the payment amount. - ErrValueMismatch = errors.New("attempted value doesn't match payment " + - "amount") - - // ErrValueExceedsAmt is returned if we try to register an attempt that - // would take the total sent amount above the payment amount. - ErrValueExceedsAmt = errors.New("attempted value exceeds payment " + - "amount") - - // ErrNonMPPayment is returned if we try to register an MPP attempt for - // a payment that already has a non-MPP attempt registered. - ErrNonMPPayment = errors.New("payment has non-MPP attempts") - - // ErrMPPayment is returned if we try to register a non-MPP attempt for - // a payment that already has an MPP attempt registered. - ErrMPPayment = errors.New("payment has MPP attempts") - - // ErrMPPRecordInBlindedPayment is returned if we try to register an - // attempt with an MPP record for a payment to a blinded path. - ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " + - "contain MPP records") - - // ErrBlindedPaymentTotalAmountMismatch is returned if we try to - // register an HTLC shard to a blinded route where the total amount - // doesn't match existing shards. - ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " + - "total amount mismatch") - - // ErrMPPPaymentAddrMismatch is returned if we try to register an MPP - // shard where the payment address doesn't match existing shards. - ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch") - - // ErrMPPTotalAmountMismatch is returned if we try to register an MPP - // shard where the total amount doesn't match existing shards. - ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " + - "mismatch") - - // ErrPaymentPendingSettled is returned when we try to add a new - // attempt to a payment that has at least one of its HTLCs settled. - ErrPaymentPendingSettled = errors.New("payment has settled htlcs") - - // ErrPaymentPendingFailed is returned when we try to add a new attempt - // to a payment that already has a failure reason. - ErrPaymentPendingFailed = errors.New("payment has failure reason") - - // ErrSentExceedsTotal is returned if the payment's current total sent - // amount exceed the total amount. - ErrSentExceedsTotal = errors.New("total sent exceeds total amount") - - // errNoAttemptInfo is returned when no attempt info is stored yet. - errNoAttemptInfo = errors.New("unable to find attempt info for " + - "inflight payment") - - // errNoSequenceNrIndex is returned when an attempt to lookup a payment - // index is made for a sequence number that is not indexed. - errNoSequenceNrIndex = errors.New("payment sequence number index " + - "does not exist") -) - -// PaymentControl implements persistence for payments and payment attempts. -type PaymentControl struct { - paymentSeqMx sync.Mutex - currPaymentSeq uint64 - storedPaymentSeq uint64 - db *DB -} - -// NewPaymentControl creates a new instance of the PaymentControl. -func NewPaymentControl(db *DB) *PaymentControl { - return &PaymentControl{ - db: db, - } -} - -// 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 *PaymentControl) 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.Backend, 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 && 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 PaymentControl db. -func (p *PaymentControl) DeleteFailedAttempts(hash lntypes.Hash) error { - if !p.db.keepFailedPaymentAttempts { - const failedHtlcsOnly = true - err := p.db.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 *PaymentControl) 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.Backend, 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 *PaymentControl) 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 *PaymentControl) 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 *PaymentControl) 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.Backend, 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") - } - - if htlcsBucket.Get(htlcBucketKey(htlcAttemptInfoKey, aid)) == nil { - return fmt.Errorf("HTLC with ID %v not registered", - attemptID) - } - - // Make sure the shard is not already failed or settled. - if htlcsBucket.Get(htlcBucketKey(htlcFailInfoKey, aid)) != nil { - return ErrAttemptAlreadyFailed - } - - if htlcsBucket.Get(htlcBucketKey(htlcSettleInfoKey, aid)) != 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 *PaymentControl) Fail(paymentHash lntypes.Hash, - reason FailureReason) (*MPPayment, error) { - - var ( - updateErr error - payment *MPPayment - ) - err := kvdb.Batch(p.db.Backend, 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 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 PaymentControl 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 *PaymentControl) 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 *PaymentControl) nextPaymentSequence() ([]byte, error) { - p.paymentSeqMx.Lock() - defer p.paymentSeqMx.Unlock() - - // Set a new upper bound in the DB every 1000 payments to avoid - // conflicts on the sequence when using etcd. - if p.currPaymentSeq == p.storedPaymentSeq { - var currPaymentSeq, newUpperBound uint64 - if err := kvdb.Update(p.db.Backend, 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.currPaymentSeq == 0 { - p.currPaymentSeq = currPaymentSeq - } - - p.storedPaymentSeq = newUpperBound - } - - p.currPaymentSeq++ - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, p.currPaymentSeq) - - 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 *PaymentControl) 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 -} diff --git a/channeldb/payments.go b/channeldb/payments.go index 51457671a..a23f891d0 100644 --- a/channeldb/payments.go +++ b/channeldb/payments.go @@ -1,126 +1,11 @@ package channeldb import ( - "bytes" - "encoding/binary" - "errors" "fmt" - "io" - "sort" "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" - "github.com/lightningnetwork/lnd/record" - "github.com/lightningnetwork/lnd/routing/route" - "github.com/lightningnetwork/lnd/tlv" -) - -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 - // | - // |-- - // | |--sequence-key: - // | |--creation-info-key: - // | |--fail-info-key: <(optional) fail info> - // | | - // | |--payment-htlcs-bucket (shard-bucket) - // | | | - // | | |-- ai: - // | | |-- si: <(optional) settle info> - // | | |-- fi: <(optional) fail info> - // | | | - // | | ... - // | | - // | | - // | |--duplicate-bucket (only for old, completed payments) - // | | - // | |-- - // | | |--sequence-key: - // | | |--creation-info-key: - // | | |--ai: - // | | |--si: - // | | |--fi: - // | | - // | |-- - // | | | - // | ... ... - // | - // |-- - // | | - // | ... - // ... - // - 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 - // |--: - // |--... - // |--: - paymentsIndexBucket = []byte("payments-index-bucket") -) - -var ( - // ErrNoSequenceNumber is returned if we look up a payment which does - // not have a sequence number. - ErrNoSequenceNumber = errors.New("sequence number not found") - - // ErrDuplicateNotFound is returned when we lookup a payment by its - // index and cannot find a payment with a matching sequence number. - ErrDuplicateNotFound = errors.New("duplicate payment not found") - - // ErrNoDuplicateBucket is returned when we expect to find duplicates - // when looking up a payment from its index, but the payment does not - // have any. - ErrNoDuplicateBucket = errors.New("expected duplicate bucket") - - // ErrNoDuplicateNestedBucket is returned if we do not find duplicate - // payments in their own sub-bucket. - ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " + - "found") ) // FailureReason encodes the reason a payment ultimately failed. @@ -208,253 +93,6 @@ func (p *PaymentCreationInfo) String() string { p.PaymentIdentifier, p.Value, p.CreationTime) } -// 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. -// -// nolint: dupl -func (d *DB) FetchPayments() ([]*MPPayment, error) { - var payments []*MPPayment - - err := kvdb.View(d, 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, 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 -} - // 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 { @@ -522,969 +160,3 @@ type PaymentsResponse struct { // CountTotal field in the query was set to true. TotalCount uint64 } - -// 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 (d *DB) QueryPayments(query PaymentsQuery) (PaymentsResponse, error) { - var resp PaymentsResponse - - if err := kvdb.View(d, 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. - if fastBucket, ok := indexes.(kvdb.ExtendedRBucket); 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 (d *DB) DeletePayment(paymentHash lntypes.Hash, - failedHtlcsOnly bool) error { - - return kvdb.Update(d, 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 - } - - if err := payments.DeleteNestedBucket(paymentHash[:]); 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 (d *DB) DeletePayments(failedOnly, failedHtlcsOnly bool) (int, error) { - var numPayments int - err := kvdb.Update(d, 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 -} - -// nolint: dupl -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 := uint32(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 err == io.EOF, 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.go b/channeldb/payments_kv_store.go new file mode 100644 index 000000000..584dc0f73 --- /dev/null +++ b/channeldb/payments_kv_store.go @@ -0,0 +1,2181 @@ +package channeldb + +import ( + "bytes" + "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" + "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 +) + +var ( + // ErrAlreadyPaid signals we have already paid this payment hash. + ErrAlreadyPaid = errors.New("invoice is already paid") + + // ErrPaymentInFlight signals that payment for this payment hash is + // already "in flight" on the network. + ErrPaymentInFlight = errors.New("payment is in transition") + + // ErrPaymentExists is returned when we try to initialize an already + // existing payment that is not failed. + ErrPaymentExists = errors.New("payment already exists") + + // ErrPaymentInternal is returned when performing the payment has a + // conflicting state, such as, + // - payment has StatusSucceeded but remaining amount is not zero. + // - payment has StatusInitiated but remaining amount is zero. + // - payment has StatusFailed but remaining amount is zero. + ErrPaymentInternal = errors.New("internal error") + + // ErrPaymentNotInitiated is returned if the payment wasn't initiated. + ErrPaymentNotInitiated = errors.New("payment isn't initiated") + + // ErrPaymentAlreadySucceeded is returned in the event we attempt to + // change the status of a payment already succeeded. + ErrPaymentAlreadySucceeded = errors.New("payment is already succeeded") + + // ErrPaymentAlreadyFailed is returned in the event we attempt to alter + // a failed payment. + ErrPaymentAlreadyFailed = errors.New("payment has already failed") + + // ErrUnknownPaymentStatus is returned when we do not recognize the + // existing state of a payment. + ErrUnknownPaymentStatus = errors.New("unknown payment status") + + // ErrPaymentTerminal is returned if we attempt to alter a payment that + // already has reached a terminal condition. + ErrPaymentTerminal = errors.New("payment has reached terminal " + + "condition") + + // ErrAttemptAlreadySettled is returned if we try to alter an already + // settled HTLC attempt. + ErrAttemptAlreadySettled = errors.New("attempt already settled") + + // ErrAttemptAlreadyFailed is returned if we try to alter an already + // failed HTLC attempt. + ErrAttemptAlreadyFailed = errors.New("attempt already failed") + + // ErrValueMismatch is returned if we try to register a non-MPP attempt + // with an amount that doesn't match the payment amount. + ErrValueMismatch = errors.New("attempted value doesn't match payment " + + "amount") + + // ErrValueExceedsAmt is returned if we try to register an attempt that + // would take the total sent amount above the payment amount. + ErrValueExceedsAmt = errors.New("attempted value exceeds payment " + + "amount") + + // ErrNonMPPayment is returned if we try to register an MPP attempt for + // a payment that already has a non-MPP attempt registered. + ErrNonMPPayment = errors.New("payment has non-MPP attempts") + + // ErrMPPayment is returned if we try to register a non-MPP attempt for + // a payment that already has an MPP attempt registered. + ErrMPPayment = errors.New("payment has MPP attempts") + + // ErrMPPRecordInBlindedPayment is returned if we try to register an + // attempt with an MPP record for a payment to a blinded path. + ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " + + "contain MPP records") + + // ErrBlindedPaymentTotalAmountMismatch is returned if we try to + // register an HTLC shard to a blinded route where the total amount + // doesn't match existing shards. + ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " + + "total amount mismatch") + + // ErrMPPPaymentAddrMismatch is returned if we try to register an MPP + // shard where the payment address doesn't match existing shards. + ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch") + + // ErrMPPTotalAmountMismatch is returned if we try to register an MPP + // shard where the total amount doesn't match existing shards. + ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " + + "mismatch") + + // ErrPaymentPendingSettled is returned when we try to add a new + // attempt to a payment that has at least one of its HTLCs settled. + ErrPaymentPendingSettled = errors.New("payment has settled htlcs") + + // ErrPaymentPendingFailed is returned when we try to add a new attempt + // to a payment that already has a failure reason. + ErrPaymentPendingFailed = errors.New("payment has failure reason") + + // ErrSentExceedsTotal is returned if the payment's current total sent + // amount exceed the total amount. + ErrSentExceedsTotal = errors.New("total sent exceeds total amount") + + // errNoAttemptInfo is returned when no attempt info is stored yet. + errNoAttemptInfo = errors.New("unable to find attempt info for " + + "inflight payment") + + // errNoSequenceNrIndex is returned when an attempt to lookup a payment + // index is made for a sequence number that is not indexed. + errNoSequenceNrIndex = errors.New("payment sequence number index " + + "does not exist") +) + +//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 + // | + // |-- + // | |--sequence-key: + // | |--creation-info-key: + // | |--fail-info-key: <(optional) fail info> + // | | + // | |--payment-htlcs-bucket (shard-bucket) + // | | | + // | | |-- ai: + // | | |-- si: <(optional) settle info> + // | | |-- fi: <(optional) fail info> + // | | | + // | | ... + // | | + // | | + // | |--duplicate-bucket (only for old, completed payments) + // | | + // | |-- + // | | |--sequence-key: + // | | |--creation-info-key: + // | | |--ai: + // | | |--si: + // | | |--fi: + // | | + // | |-- + // | | | + // | ... ... + // | + // |-- + // | | + // | ... + // ... + // + 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 + // |--: + // |--... + // |--: + paymentsIndexBucket = []byte("payments-index-bucket") +) + +var ( + // ErrNoSequenceNumber is returned if we look up a payment which does + // not have a sequence number. + ErrNoSequenceNumber = errors.New("sequence number not found") + + // ErrDuplicateNotFound is returned when we lookup a payment by its + // index and cannot find a payment with a matching sequence number. + ErrDuplicateNotFound = errors.New("duplicate payment not found") + + // ErrNoDuplicateBucket is returned when we expect to find duplicates + // when looking up a payment from its index, but the payment does not + // have any. + ErrNoDuplicateBucket = errors.New("expected duplicate bucket") + + // ErrNoDuplicateNestedBucket is returned if we do not find duplicate + // payments in their own sub-bucket. + ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " + + "found") +) + +// KVPaymentsDB implements persistence for payments and payment attempts. +type KVPaymentsDB struct { + paymentSeqMx sync.Mutex + currPaymentSeq uint64 + storedPaymentSeq uint64 + db *DB +} + +// NewKVPaymentsDB creates a new instance of the KVPaymentsDB. +func NewKVPaymentsDB(db *DB) *KVPaymentsDB { + return &KVPaymentsDB{ + db: db, + } +} + +// 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.Backend, 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.db.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.Backend, 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.Backend, 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 FailureReason) (*MPPayment, error) { + + var ( + updateErr error + payment *MPPayment + ) + err := kvdb.Batch(p.db.Backend, 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.paymentSeqMx.Lock() + defer p.paymentSeqMx.Unlock() + + // Set a new upper bound in the DB every 1000 payments to avoid + // conflicts on the sequence when using etcd. + if p.currPaymentSeq == p.storedPaymentSeq { + var currPaymentSeq, newUpperBound uint64 + if err := kvdb.Update(p.db.Backend, 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.currPaymentSeq == 0 { + p.currPaymentSeq = currPaymentSeq + } + + p.storedPaymentSeq = newUpperBound + } + + p.currPaymentSeq++ + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, p.currPaymentSeq) + + 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) (*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, 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(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, 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 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/payment_control_test.go b/channeldb/payments_kv_store_test.go similarity index 60% rename from channeldb/payment_control_test.go rename to channeldb/payments_kv_store_test.go index 9bc486a83..3e825597d 100644 --- a/channeldb/payment_control_test.go +++ b/channeldb/payments_kv_store_test.go @@ -15,7 +15,10 @@ import ( "github.com/davecgh/go-spew/spew" "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" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -54,73 +57,73 @@ func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo, }, &attempt.HTLCAttemptInfo, preimage, nil } -// TestPaymentControlSwitchFail checks that payment status returns to Failed +// TestKVPaymentsDBSwitchFail checks that payment status returns to Failed // status after failing, and that InitPayment allows another HTLC for the // same payment hash. -func TestPaymentControlSwitchFail(t *testing.T) { +func TestKVPaymentsDBSwitchFail(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) info, attempt, preimg, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") // Sends base htlc message which initiate StatusInFlight. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") - assertPaymentIndex(t, pControl, info.PaymentIdentifier) + assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) assertPaymentStatus( - t, pControl, info.PaymentIdentifier, StatusInitiated, + t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, nil, + t, paymentDB, info.PaymentIdentifier, info, nil, nil, ) // Fail the payment, which should moved it to Failed. failReason := FailureReasonNoRoute - _, err = pControl.Fail(info.PaymentIdentifier, failReason) + _, err = paymentDB.Fail(info.PaymentIdentifier, failReason) require.NoError(t, err, "unable to fail payment hash") // Verify the status is indeed Failed. - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusFailed) + assertPaymentStatus(t, paymentDB, info.PaymentIdentifier, StatusFailed) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, &failReason, nil, + t, paymentDB, info.PaymentIdentifier, info, &failReason, nil, ) // Lookup the payment so we can get its old sequence number before it is // overwritten. - payment, err := pControl.FetchPayment(info.PaymentIdentifier) + payment, err := paymentDB.FetchPayment(info.PaymentIdentifier) require.NoError(t, err) // Sends the htlc again, which should succeed since the prior payment // failed. - err = pControl.InitPayment(info.PaymentIdentifier, info) + 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, pControl, info.PaymentIdentifier) - assertNoIndex(t, pControl, payment.SequenceNum) + assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) + assertNoIndex(t, paymentDB, payment.SequenceNum) assertPaymentStatus( - t, pControl, info.PaymentIdentifier, StatusInitiated, + t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, nil, + 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 = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to register attempt") htlcReason := HTLCFailUnreadable - _, err = pControl.FailAttempt( + _, err = paymentDB.FailAttempt( info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcReason, @@ -130,7 +133,7 @@ func TestPaymentControlSwitchFail(t *testing.T) { t.Fatal(err) } assertPaymentStatus( - t, pControl, info.PaymentIdentifier, StatusInFlight, + t, paymentDB, info.PaymentIdentifier, StatusInFlight, ) htlc := &htlcStatus{ @@ -138,25 +141,27 @@ func TestPaymentControlSwitchFail(t *testing.T) { failure: &htlcReason, } - assertPaymentInfo(t, pControl, info.PaymentIdentifier, info, nil, htlc) + assertPaymentInfo(t, paymentDB, info.PaymentIdentifier, info, nil, htlc) // Record another attempt. attempt.AttemptID = 1 - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusInFlight) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusInFlight, + ) htlc = &htlcStatus{ HTLCAttemptInfo: attempt, } assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) // Settle the attempt and verify that status was changed to // StatusSucceeded. - payment, err = pControl.SettleAttempt( + payment, err = paymentDB.SettleAttempt( info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, @@ -176,106 +181,114 @@ func TestPaymentControlSwitchFail(t *testing.T) { spew.Sdump(payment.HTLCs[0].Route), err) } - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusSucceeded) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusSucceeded, + ) htlc.settle = &preimg assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) // Attempt a final payment, which should now fail since the prior // payment succeed. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } } -// TestPaymentControlSwitchDoubleSend checks the ability of payment control to +// TestKVPaymentsDBSwitchDoubleSend checks the ability of payment control to // prevent double sending of htlc message, when message is in StatusInFlight. -func TestPaymentControlSwitchDoubleSend(t *testing.T) { +func TestKVPaymentsDBSwitchDoubleSend(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) 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 = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") - assertPaymentIndex(t, pControl, info.PaymentIdentifier) + assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) assertPaymentStatus( - t, pControl, info.PaymentIdentifier, StatusInitiated, + t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, nil, + 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 = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) require.ErrorIs(t, err, ErrPaymentExists) // Record an attempt. - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusInFlight) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusInFlight, + ) htlc := &htlcStatus{ HTLCAttemptInfo: attempt, } assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) // Sends base htlc message which initiate StatusInFlight. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) if !errors.Is(err, ErrPaymentInFlight) { t.Fatalf("payment control wrong behaviour: " + "double sending must trigger ErrPaymentInFlight error") } // After settling, the error should be ErrAlreadyPaid. - _, err = pControl.SettleAttempt( + _, err = paymentDB.SettleAttempt( info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, ) require.NoError(t, err, "error shouldn't have been received, got") - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusSucceeded) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusSucceeded, + ) htlc.settle = &preimg - assertPaymentInfo(t, pControl, info.PaymentIdentifier, info, nil, htlc) + assertPaymentInfo( + t, paymentDB, info.PaymentIdentifier, info, nil, htlc, + ) - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } } -// TestPaymentControlSuccessesWithoutInFlight checks that the payment +// TestKVPaymentsDBSuccessesWithoutInFlight checks that the payment // control will disallow calls to Success when no payment is in flight. -func TestPaymentControlSuccessesWithoutInFlight(t *testing.T) { +func TestKVPaymentsDBSuccessesWithoutInFlight(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) info, _, preimg, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") // Attempt to complete the payment should fail. - _, err = pControl.SettleAttempt( + _, err = paymentDB.SettleAttempt( info.PaymentIdentifier, 0, &HTLCSettleInfo{ Preimage: preimg, @@ -286,29 +299,29 @@ func TestPaymentControlSuccessesWithoutInFlight(t *testing.T) { } } -// TestPaymentControlFailsWithoutInFlight checks that a strict payment +// TestKVPaymentsDBFailsWithoutInFlight checks that a strict payment // control will disallow calls to Fail when no payment is in flight. -func TestPaymentControlFailsWithoutInFlight(t *testing.T) { +func TestKVPaymentsDBFailsWithoutInFlight(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) info, _, _, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") // Calling Fail should return an error. - _, err = pControl.Fail(info.PaymentIdentifier, FailureReasonNoRoute) + _, err = paymentDB.Fail(info.PaymentIdentifier, FailureReasonNoRoute) if err != ErrPaymentNotInitiated { t.Fatalf("expected ErrPaymentNotInitiated, got %v", err) } } -// TestPaymentControlDeleteNonInFlight checks that calling DeletePayments only +// TestKVPaymentsDBDeleteNonInFlight checks that calling DeletePayments only // deletes payments from the database that are not in-flight. -func TestPaymentControlDeleteNonInFlight(t *testing.T) { +func TestKVPaymentsDBDeleteNonInFlight(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) @@ -319,7 +332,7 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { // start at 1, so 9999 is a safe bet for this test. var duplicateSeqNr = 9999 - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) payments := []struct { failed bool @@ -357,11 +370,13 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { } // Sends base htlc message which initiate StatusInFlight. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) if err != nil { t.Fatalf("unable to send htlc message: %v", err) } - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt( + info.PaymentIdentifier, attempt, + ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) } @@ -373,7 +388,7 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { if p.failed { // Fail the payment attempt. htlcFailure := HTLCFailUnreadable - _, err := pControl.FailAttempt( + _, err := paymentDB.FailAttempt( info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, @@ -385,43 +400,57 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { // Fail the payment, which should moved it to Failed. failReason := FailureReasonNoRoute - _, err = pControl.Fail(info.PaymentIdentifier, failReason) + _, 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, pControl, info.PaymentIdentifier, StatusFailed) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, + StatusFailed, + ) htlc.failure = &htlcFailure assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, + t, paymentDB, info.PaymentIdentifier, info, &failReason, htlc, ) } else if p.success { // Verifies that status was changed to StatusSucceeded. - _, err := pControl.SettleAttempt( + _, err := paymentDB.SettleAttempt( info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, ) if err != nil { - t.Fatalf("error shouldn't have been received, got: %v", err) + t.Fatalf("error shouldn't have been received,"+ + " got: %v", err) } - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusSucceeded) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, + StatusSucceeded, + ) htlc.settle = &preimg assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, + htlc, ) numSuccess++ } else { - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusInFlight) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, + StatusInFlight, + ) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, + htlc, ) numInflight++ @@ -431,7 +460,7 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { // add one. if p.hasDuplicate { appendDuplicatePayment( - t, pControl.db, info.PaymentIdentifier, + t, paymentDB.db, info.PaymentIdentifier, uint64(duplicateSeqNr), preimg, ) duplicateSeqNr++ @@ -440,12 +469,12 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { } // Delete all failed payments. - numPayments, err := db.DeletePayments(true, false) + 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 := db.FetchPayments() + dbPayments, err := paymentDB.FetchPayments() if err != nil { t.Fatal(err) } @@ -476,12 +505,12 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { } // Now delete all payments except in-flight. - numPayments, err = db.DeletePayments(false, false) + numPayments, err = paymentDB.DeletePayments(false, false) require.NoError(t, err) require.EqualValues(t, 2, numPayments) // This should leave the in-flight payment. - dbPayments, err = db.FetchPayments() + dbPayments, err = paymentDB.FetchPayments() if err != nil { t.Fatal(err) } @@ -513,15 +542,15 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) { require.Equal(t, 1, indexCount) } -// TestPaymentControlDeletePayments tests that DeletePayments correctly deletes +// TestKVPaymentsDBDeletePayments tests that DeletePayments correctly deletes // information about completed payments from the database. -func TestPaymentControlDeletePayments(t *testing.T) { +func TestKVPaymentsDBDeletePayments(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) // Register three payments: // 1. A payment with two failed attempts. @@ -535,54 +564,54 @@ func TestPaymentControlDeletePayments(t *testing.T) { // Use helper function to register the test payments in the data and // populate the data to the payments slice. - createTestPayments(t, pControl, payments) + createTestPayments(t, paymentDB, payments) // Check that all payments are there as we added them. - assertPayments(t, db, payments) + assertPayments(t, paymentDB, payments) // Delete HTLC attempts for failed payments only. - numPayments, err := db.DeletePayments(true, true) + 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, db, payments) + assertPayments(t, paymentDB, payments) // Delete failed attempts for all payments. - numPayments, err = db.DeletePayments(false, true) + 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, db, payments) + assertPayments(t, paymentDB, payments) // Now delete all failed payments. - numPayments, err = db.DeletePayments(true, false) + numPayments, err = paymentDB.DeletePayments(true, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) - assertPayments(t, db, payments[1:]) + assertPayments(t, paymentDB, payments[1:]) // Finally delete all completed payments. - numPayments, err = db.DeletePayments(false, false) + numPayments, err = paymentDB.DeletePayments(false, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) - assertPayments(t, db, payments[2:]) + assertPayments(t, paymentDB, payments[2:]) } -// TestPaymentControlDeleteSinglePayment tests that DeletePayment correctly +// TestKVPaymentsDBDeleteSinglePayment tests that DeletePayment correctly // deletes information about a completed payment from the database. -func TestPaymentControlDeleteSinglePayment(t *testing.T) { +func TestKVPaymentsDBDeleteSinglePayment(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) // Register four payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -607,62 +636,63 @@ func TestPaymentControlDeleteSinglePayment(t *testing.T) { // Use helper function to register the test payments in the data and // populate the data to the payments slice. - createTestPayments(t, pControl, payments) + createTestPayments(t, paymentDB, payments) // Check that all payments are there as we added them. - assertPayments(t, db, payments) + assertPayments(t, paymentDB, payments) // Delete HTLC attempts for first payment only. - require.NoError(t, db.DeletePayment(payments[0].id, true)) + 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, db, payments) + assertPayments(t, paymentDB, payments) // Delete the first payment completely. - require.NoError(t, db.DeletePayment(payments[0].id, false)) + require.NoError(t, paymentDB.DeletePayment(payments[0].id, false)) // The first payment should have been deleted. - assertPayments(t, db, payments[1:]) + assertPayments(t, paymentDB, payments[1:]) // Now delete the second payment completely. - require.NoError(t, db.DeletePayment(payments[1].id, false)) + require.NoError(t, paymentDB.DeletePayment(payments[1].id, false)) // The Second payment should have been deleted. - assertPayments(t, db, payments[2:]) + assertPayments(t, paymentDB, payments[2:]) // Delete failed HTLC attempts for the third payment. - require.NoError(t, db.DeletePayment(payments[2].id, true)) + require.NoError(t, paymentDB.DeletePayment(payments[2].id, true)) - // Only the successful HTLC attempt should be left for the third payment. + // Only the successful HTLC attempt should be left for the third + // payment. payments[2].htlcs = 1 - assertPayments(t, db, payments[2:]) + assertPayments(t, paymentDB, payments[2:]) // Now delete the third payment completely. - require.NoError(t, db.DeletePayment(payments[2].id, false)) + require.NoError(t, paymentDB.DeletePayment(payments[2].id, false)) // Only the last payment should be left. - assertPayments(t, db, payments[3:]) + assertPayments(t, paymentDB, payments[3:]) // Deleting HTLC attempts from InFlight payments should not work and an // error returned. - require.Error(t, db.DeletePayment(payments[3].id, true)) + require.Error(t, paymentDB.DeletePayment(payments[3].id, true)) // The payment is InFlight and therefore should not have been altered. - assertPayments(t, db, payments[3:]) + assertPayments(t, paymentDB, payments[3:]) // Finally deleting the InFlight payment should also not work and an // error returned. - require.Error(t, db.DeletePayment(payments[3].id, false)) + require.Error(t, paymentDB.DeletePayment(payments[3].id, false)) // The payment is InFlight and therefore should not have been altered. - assertPayments(t, db, payments[3:]) + assertPayments(t, paymentDB, payments[3:]) } -// TestPaymentControlMultiShard checks the ability of payment control to +// TestKVPaymentsDBMultiShard checks the ability of payment control to // have multiple in-flight HTLCs for a single payment. -func TestPaymentControlMultiShard(t *testing.T) { +func TestKVPaymentsDBMultiShard(t *testing.T) { t.Parallel() // We will register three HTLC attempts, and always fail the second @@ -687,7 +717,7 @@ func TestPaymentControlMultiShard(t *testing.T) { t.Fatalf("unable to init db: %v", err) } - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) info, attempt, preimg, err := genInfo(t) if err != nil { @@ -695,17 +725,17 @@ func TestPaymentControlMultiShard(t *testing.T) { } // Init the payment, moving it to the StatusInFlight state. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) if err != nil { t.Fatalf("unable to send htlc message: %v", err) } - assertPaymentIndex(t, pControl, info.PaymentIdentifier) + assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) assertPaymentStatus( - t, pControl, info.PaymentIdentifier, StatusInitiated, + t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, nil, + t, paymentDB, info.PaymentIdentifier, info, nil, nil, ) // Create three unique attempts we'll use for the test, and @@ -724,12 +754,14 @@ func TestPaymentControlMultiShard(t *testing.T) { a.AttemptID = i attempts = append(attempts, &a) - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &a) + _, err = paymentDB.RegisterAttempt( + info.PaymentIdentifier, &a, + ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) } assertPaymentStatus( - t, pControl, info.PaymentIdentifier, + t, paymentDB, info.PaymentIdentifier, StatusInFlight, ) @@ -737,7 +769,8 @@ func TestPaymentControlMultiShard(t *testing.T) { HTLCAttemptInfo: &a, } assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, + htlc, ) } @@ -746,13 +779,13 @@ func TestPaymentControlMultiShard(t *testing.T) { // will be too large. b := *attempt b.AttemptID = 3 - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) require.ErrorIs(t, err, ErrValueExceedsAmt) // Fail the second attempt. a := attempts[1] htlcFail := HTLCFailUnreadable - _, err = pControl.FailAttempt( + _, err = paymentDB.FailAttempt( info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, @@ -767,11 +800,13 @@ func TestPaymentControlMultiShard(t *testing.T) { failure: &htlcFail, } assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) // Payment should still be in-flight. - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusInFlight) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusInFlight, + ) // Depending on the test case, settle or fail the first attempt. a = attempts[0] @@ -781,7 +816,7 @@ func TestPaymentControlMultiShard(t *testing.T) { var firstFailReason *FailureReason if test.settleFirst { - _, err := pControl.SettleAttempt( + _, err := paymentDB.SettleAttempt( info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, @@ -795,10 +830,11 @@ func TestPaymentControlMultiShard(t *testing.T) { // Assert that the HTLC has had the preimage recorded. htlc.settle = &preimg assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, + htlc, ) } else { - _, err := pControl.FailAttempt( + _, err := paymentDB.FailAttempt( info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, @@ -812,13 +848,16 @@ func TestPaymentControlMultiShard(t *testing.T) { // Assert the failure was recorded. htlc.failure = &htlcFail assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, nil, htlc, + t, paymentDB, info.PaymentIdentifier, info, nil, + htlc, ) // We also record a payment level fail, to move it into // a terminal state. failReason := FailureReasonNoRoute - _, err = pControl.Fail(info.PaymentIdentifier, failReason) + _, err = paymentDB.Fail( + info.PaymentIdentifier, failReason, + ) if err != nil { t.Fatalf("unable to fail payment hash: %v", err) } @@ -830,7 +869,7 @@ func TestPaymentControlMultiShard(t *testing.T) { // The payment is now considered pending fail, since // there is still an active HTLC. assertPaymentStatus( - t, pControl, info.PaymentIdentifier, + t, paymentDB, info.PaymentIdentifier, StatusInFlight, ) } @@ -839,14 +878,16 @@ func TestPaymentControlMultiShard(t *testing.T) { // that the payment has reached a terminal condition. b = *attempt b.AttemptID = 3 - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) if test.settleFirst { require.ErrorIs(t, err, ErrPaymentPendingSettled) } else { require.ErrorIs(t, err, ErrPaymentPendingFailed) } - assertPaymentStatus(t, pControl, info.PaymentIdentifier, StatusInFlight) + assertPaymentStatus( + t, paymentDB, info.PaymentIdentifier, StatusInFlight, + ) // Settle or fail the remaining attempt based on the testcase. a = attempts[2] @@ -855,7 +896,7 @@ func TestPaymentControlMultiShard(t *testing.T) { } if test.settleLast { // Settle the last outstanding attempt. - _, err = pControl.SettleAttempt( + _, err = paymentDB.SettleAttempt( info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, @@ -865,12 +906,12 @@ func TestPaymentControlMultiShard(t *testing.T) { htlc.settle = &preimg assertPaymentInfo( - t, pControl, info.PaymentIdentifier, + t, paymentDB, info.PaymentIdentifier, info, firstFailReason, htlc, ) } else { // Fail the attempt. - _, err := pControl.FailAttempt( + _, err := paymentDB.FailAttempt( info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, @@ -884,7 +925,7 @@ func TestPaymentControlMultiShard(t *testing.T) { // Assert the failure was recorded. htlc.failure = &htlcFail assertPaymentInfo( - t, pControl, info.PaymentIdentifier, info, + t, paymentDB, info.PaymentIdentifier, info, firstFailReason, htlc, ) @@ -893,7 +934,9 @@ func TestPaymentControlMultiShard(t *testing.T) { // write a terminal failure to the database without // syncing. failReason := FailureReasonPaymentDetails - _, err = pControl.Fail(info.PaymentIdentifier, failReason) + _, err = paymentDB.Fail( + info.PaymentIdentifier, failReason, + ) require.NoError(t, err, "unable to fail") } @@ -926,11 +969,11 @@ func TestPaymentControlMultiShard(t *testing.T) { } assertPaymentStatus( - t, pControl, info.PaymentIdentifier, finalStatus, + t, paymentDB, info.PaymentIdentifier, finalStatus, ) // Finally assert we cannot register more attempts. - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) require.Equal(t, registerErr, err) } @@ -945,19 +988,19 @@ func TestPaymentControlMultiShard(t *testing.T) { } } -func TestPaymentControlMPPRecordValidation(t *testing.T) { +func TestKVPaymentsDBMPPRecordValidation(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to init db") - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) info, attempt, _, err := genInfo(t) require.NoError(t, err, "unable to generate htlc message") // Init the payment. - err = pControl.InitPayment(info.PaymentIdentifier, info) + 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 @@ -970,14 +1013,14 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, 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 = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) if err != ErrMPPayment { t.Fatalf("expected ErrMPPayment, got: %v", err) } @@ -986,7 +1029,7 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) { b.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{2}, ) - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) if err != ErrMPPPaymentAddrMismatch { t.Fatalf("expected ErrMPPPaymentAddrMismatch, got: %v", err) } @@ -995,7 +1038,7 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) { b.Route.FinalHop().MPP = record.NewMPP( info.Value/2, [32]byte{1}, ) - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) if err != ErrMPPTotalAmountMismatch { t.Fatalf("expected ErrMPPTotalAmountMismatch, got: %v", err) } @@ -1005,11 +1048,11 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) { info, attempt, _, err = genInfo(t) require.NoError(t, err, "unable to generate htlc message") - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") attempt.Route.FinalHop().MPP = nil - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") // Attempt to register an MPP attempt, which should fail. @@ -1019,7 +1062,7 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = pControl.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) if err != ErrNonMPPayment { t.Fatalf("expected ErrNonMPPayment, got: %v", err) } @@ -1044,7 +1087,7 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { require.NoError(t, err, "unable to init db") db.keepFailedPaymentAttempts = keepFailedPaymentAttempts - pControl := NewPaymentControl(db) + paymentDB := NewKVPaymentsDB(db) // Register three payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -1067,56 +1110,62 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // Use helper function to register the test payments in the data and // populate the data to the payments slice. - createTestPayments(t, pControl, payments) + createTestPayments(t, paymentDB, payments) // Check that all payments are there as we added them. - assertPayments(t, db, payments) + assertPayments(t, paymentDB, payments) // Calling DeleteFailedAttempts on a failed payment should delete all // HTLCs. - require.NoError(t, pControl.DeleteFailedAttempts(payments[0].id)) + 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, db, payments) + assertPayments(t, paymentDB, payments) // Calling DeleteFailedAttempts on an in-flight payment should return // an error. if keepFailedPaymentAttempts { - require.NoError(t, pControl.DeleteFailedAttempts(payments[1].id)) + require.NoError( + t, paymentDB.DeleteFailedAttempts(payments[1].id), + ) } else { - require.Error(t, pControl.DeleteFailedAttempts(payments[1].id)) + require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id)) } // Since DeleteFailedAttempts returned an error, we should expect the // payment to be unchanged. - assertPayments(t, db, payments) + assertPayments(t, paymentDB, payments) // Cleaning up a successful payment should remove failed htlcs. - require.NoError(t, pControl.DeleteFailedAttempts(payments[2].id)) + 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, db, payments) + 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, pControl.DeleteFailedAttempts(lntypes.ZeroHash)) + require.NoError( + t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash), + ) } else { // Attempting to cleanup a non-existent payment returns an error. - require.Error(t, pControl.DeleteFailedAttempts(lntypes.ZeroHash)) + 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 *PaymentControl, +func assertPaymentStatus(t *testing.T, p *KVPaymentsDB, hash lntypes.Hash, expStatus PaymentStatus) { t.Helper() @@ -1143,7 +1192,7 @@ type htlcStatus struct { // assertPaymentInfo retrieves the payment referred to by hash and verifies the // expected values. -func assertPaymentInfo(t *testing.T, p *PaymentControl, hash lntypes.Hash, +func assertPaymentInfo(t *testing.T, p *KVPaymentsDB, hash lntypes.Hash, c *PaymentCreationInfo, f *FailureReason, a *htlcStatus) { t.Helper() @@ -1210,7 +1259,7 @@ func assertPaymentInfo(t *testing.T, p *PaymentControl, hash lntypes.Hash, // fetchPaymentIndexEntry gets the payment hash for the sequence number provided // from our payment indexes bucket. -func fetchPaymentIndexEntry(_ *testing.T, p *PaymentControl, +func fetchPaymentIndexEntry(_ *testing.T, p *KVPaymentsDB, sequenceNumber uint64) (*lntypes.Hash, error) { var hash lntypes.Hash @@ -1241,7 +1290,7 @@ func fetchPaymentIndexEntry(_ *testing.T, p *PaymentControl, // 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 *PaymentControl, +func assertPaymentIndex(t *testing.T, p *KVPaymentsDB, expectedHash lntypes.Hash) { // Lookup the payment so that we have its sequence number and check @@ -1256,7 +1305,7 @@ func assertPaymentIndex(t *testing.T, p *PaymentControl, // assertNoIndex checks that an index for the sequence number provided does not // exist. -func assertNoIndex(t *testing.T, p *PaymentControl, seqNr uint64) { +func assertNoIndex(t *testing.T, p *KVPaymentsDB, seqNr uint64) { _, err := fetchPaymentIndexEntry(t, p, seqNr) require.Equal(t, errNoSequenceNrIndex, err) } @@ -1272,7 +1321,7 @@ type payment struct { // 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 *PaymentControl, payments []*payment) { +func createTestPayments(t *testing.T, p *KVPaymentsDB, payments []*payment) { attemptID := uint64(0) for i := 0; i < len(payments); i++ { @@ -1356,15 +1405,19 @@ func createTestPayments(t *testing.T, p *PaymentControl, payments []*payment) { // 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, db *DB, payments []*payment) { +func assertPayments(t *testing.T, paymentDB *KVPaymentsDB, + payments []*payment) { + t.Helper() - dbPayments, err := db.FetchPayments() + 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") + 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)) @@ -1379,3 +1432,364 @@ func assertPayments(t *testing.T, db *DB, payments []*payment) { // 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 := NewKVPaymentsDB(db) + + // 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: ErrDuplicateNotFound, + }, + { + name: "lookup duplicate, no duplicates " + + "bucket", + paymentHash: noDuplicates.PaymentIdentifier, + sequenceNumber: duplicateTwoSeqNr, + expectedErr: 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 *DB, 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 index b2a0292a4..1d5e81465 100644 --- a/channeldb/payments_test.go +++ b/channeldb/payments_test.go @@ -1,7 +1,6 @@ package channeldb import ( - "bytes" "fmt" "math" "reflect" @@ -9,14 +8,9 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" - "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" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" - "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -97,98 +91,6 @@ var ( } ) -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) -} - // assertRouteEquals compares to routes for equality and returns an error if // they are not equal. func assertRouteEqual(a, b *route.Route) error { @@ -200,53 +102,6 @@ func assertRouteEqual(a, b *route.Route) error { return nil } -// 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) - } -} - // TestQueryPayments tests retrieval of payments with forwards and reversed // queries. func TestQueryPayments(t *testing.T) { @@ -502,9 +357,11 @@ func TestQueryPayments(t *testing.T) { t.Fatalf("unable to init db: %v", err) } + paymentDB := NewKVPaymentsDB(db) + // Make a preliminary query to make sure it's ok to // query when we have no payments. - resp, err := db.QueryPayments(tt.query) + resp, err := paymentDB.QueryPayments(tt.query) require.NoError(t, err) require.Len(t, resp.Payments, 0) @@ -516,7 +373,6 @@ func TestQueryPayments(t *testing.T) { // where we have duplicates in the nested duplicates // bucket. nonDuplicatePayments := 6 - pControl := NewPaymentControl(db) for i := 0; i < nonDuplicatePayments; i++ { // Generate a test payment. @@ -530,7 +386,9 @@ func TestQueryPayments(t *testing.T) { info.CreationTime = time.Unix(int64(i+1), 0) // Create a new payment entry in the database. - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment( + info.PaymentIdentifier, info, + ) if err != nil { t.Fatalf("unable to initialize "+ "payment in database: %v", err) @@ -538,7 +396,7 @@ func TestQueryPayments(t *testing.T) { // Immediately delete the payment with index 2. if i == 1 { - pmt, err := pControl.FetchPayment( + pmt, err := paymentDB.FetchPayment( info.PaymentIdentifier, ) require.NoError(t, err) @@ -552,13 +410,13 @@ func TestQueryPayments(t *testing.T) { // to the parent payment + 1. Note that // duplicate payments will always be succeeded. if i == (nonDuplicatePayments - 1) { - pmt, err := pControl.FetchPayment( + pmt, err := paymentDB.FetchPayment( info.PaymentIdentifier, ) require.NoError(t, err) appendDuplicatePayment( - t, pControl.db, + t, paymentDB.db, info.PaymentIdentifier, pmt.SequenceNum+1, preimg, @@ -567,19 +425,19 @@ func TestQueryPayments(t *testing.T) { } // Fetch all payments in the database. - allPayments, err := db.FetchPayments() + 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) + t.Fatalf("Number of payments received does "+ + "not match expected one. Got %v, "+ + "want %v.", len(allPayments), 6) } - querySlice, err := db.QueryPayments(tt.query) + querySlice, err := paymentDB.QueryPayments(tt.query) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -587,7 +445,8 @@ func TestQueryPayments(t *testing.T) { tt.lastIndex != querySlice.LastIndexOffset { t.Errorf("First or last index does not match "+ - "expected index. Want (%d, %d), got (%d, %d).", + "expected index. Want (%d, %d), "+ + "got (%d, %d).", tt.firstIndex, tt.lastIndex, querySlice.FirstIndexOffset, querySlice.LastIndexOffset) @@ -595,225 +454,18 @@ func TestQueryPayments(t *testing.T) { if len(querySlice.Payments) != len(tt.expectedSeqNrs) { t.Errorf("expected: %v payments, got: %v", - len(tt.expectedSeqNrs), len(querySlice.Payments)) + 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) + t.Errorf("sequence numbers do not "+ + "match, got %v, want %v", + q.SequenceNum, seqNr) } } }) } } - -// 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) - - pControl := NewPaymentControl(db) - - // 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 = pControl.InitPayment(noDuplicates.PaymentIdentifier, noDuplicates) - require.NoError(t, err) - - // Fetch the payment so we can get its sequence nr. - noDuplicatesPayment, err := pControl.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 = pControl.InitPayment(hasDuplicates.PaymentIdentifier, hasDuplicates) - require.NoError(t, err) - - // Fetch the payment so we can get its sequence nr. - hasDuplicatesPayment, err := pControl.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: ErrDuplicateNotFound, - }, - { - name: "lookup duplicate, no duplicates bucket", - paymentHash: noDuplicates.PaymentIdentifier, - sequenceNumber: duplicateTwoSeqNr, - expectedErr: 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, - ) - - _, 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 *DB, 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/config_builder.go b/config_builder.go index 3c8b122a8..35a44e2c6 100644 --- a/config_builder.go +++ b/config_builder.go @@ -924,6 +924,10 @@ type DatabaseInstances struct { // InvoiceDB is the database that stores information about invoices. InvoiceDB invoices.InvoiceDB + // KVPaymentsDB is the database that stores all payment related + // information. + KVPaymentsDB *channeldb.KVPaymentsDB + // MacaroonDB is the database that stores macaroon root keys. MacaroonDB kvdb.Backend @@ -1215,6 +1219,14 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( return nil, nil, err } + // Mount the payments DB which is only KV for now. + // + // TODO(ziggie): Add support for SQL payments DB. + kvPaymentsDB := channeldb.NewKVPaymentsDB( + dbs.ChanStateDB, + ) + dbs.KVPaymentsDB = kvPaymentsDB + // Wrap the watchtower client DB and make sure we clean up. if cfg.WtClient.Active { dbs.TowerClientDB, err = wtdb.OpenClientDB( diff --git a/routing/control_tower.go b/routing/control_tower.go index 61ab33bd2..b2e4b7bd5 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -151,7 +151,7 @@ func (s *controlTowerSubscriberImpl) Updates() <-chan interface{} { // controlTower is persistent implementation of ControlTower to restrict // double payment sending. type controlTower struct { - db *channeldb.PaymentControl + db *channeldb.KVPaymentsDB // subscriberIndex is used to provide a unique id for each subscriber // to all payments. This is used to easily remove the subscriber when @@ -168,7 +168,7 @@ type controlTower struct { } // NewControlTower creates a new instance of the controlTower. -func NewControlTower(db *channeldb.PaymentControl) ControlTower { +func NewControlTower(db *channeldb.KVPaymentsDB) ControlTower { return &controlTower{ db: db, subscribersAllPayments: make( diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 532e639d6..697fa74d5 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -49,7 +49,7 @@ func TestControlTowerSubscribeUnknown(t *testing.T) { db := initDB(t, false) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) // Subscription should fail when the payment is not known. _, err := pControl.SubscribePayment(lntypes.Hash{1}) @@ -63,7 +63,7 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { db := initDB(t, false) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) // Initiate a payment. info, attempt, preimg, err := genInfo() @@ -156,33 +156,33 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { } } -// TestPaymentControlSubscribeFail tests that payment updates for a +// TestKVPaymentsDBSubscribeFail tests that payment updates for a // failed payment are properly sent to subscribers. -func TestPaymentControlSubscribeFail(t *testing.T) { +func TestKVPaymentsDBSubscribeFail(t *testing.T) { t.Parallel() t.Run("register attempt, keep failed payments", func(t *testing.T) { - testPaymentControlSubscribeFail(t, true, true) + testKVPaymentsDBSubscribeFail(t, true, true) }) t.Run("register attempt, delete failed payments", func(t *testing.T) { - testPaymentControlSubscribeFail(t, true, false) + testKVPaymentsDBSubscribeFail(t, true, false) }) t.Run("no register attempt, keep failed payments", func(t *testing.T) { - testPaymentControlSubscribeFail(t, false, true) + testKVPaymentsDBSubscribeFail(t, false, true) }) t.Run("no register attempt, delete failed payments", func(t *testing.T) { - testPaymentControlSubscribeFail(t, false, false) + testKVPaymentsDBSubscribeFail(t, false, false) }) } -// TestPaymentControlSubscribeAllSuccess tests that multiple payments are +// TestKVPaymentsDBSubscribeAllSuccess tests that multiple payments are // properly sent to subscribers of TrackPayments. -func TestPaymentControlSubscribeAllSuccess(t *testing.T) { +func TestKVPaymentsDBSubscribeAllSuccess(t *testing.T) { t.Parallel() db := initDB(t, true) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) // Initiate a payment. info1, attempt1, preimg1, err := genInfo() @@ -288,14 +288,14 @@ func TestPaymentControlSubscribeAllSuccess(t *testing.T) { require.Equal(t, attempt2.Route, htlc2.Route, "unexpected htlc route.") } -// TestPaymentControlSubscribeAllImmediate tests whether already inflight +// TestKVPaymentsDBSubscribeAllImmediate tests whether already inflight // payments are reported at the start of the SubscribeAllPayments subscription. -func TestPaymentControlSubscribeAllImmediate(t *testing.T) { +func TestKVPaymentsDBSubscribeAllImmediate(t *testing.T) { t.Parallel() db := initDB(t, true) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) // Initiate a payment. info, attempt, _, err := genInfo() @@ -325,14 +325,14 @@ func TestPaymentControlSubscribeAllImmediate(t *testing.T) { } } -// TestPaymentControlUnsubscribeSuccess tests that when unsubscribed, there are +// TestKVPaymentsDBUnsubscribeSuccess tests that when unsubscribed, there are // no more notifications to that specific subscription. -func TestPaymentControlUnsubscribeSuccess(t *testing.T) { +func TestKVPaymentsDBUnsubscribeSuccess(t *testing.T) { t.Parallel() db := initDB(t, true) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) subscription1, err := pControl.SubscribeAllPayments() require.NoError(t, err, "expected subscribe to succeed, but got: %v") @@ -396,12 +396,12 @@ func TestPaymentControlUnsubscribeSuccess(t *testing.T) { require.Len(t, subscription2.Updates(), 0) } -func testPaymentControlSubscribeFail(t *testing.T, registerAttempt, +func testKVPaymentsDBSubscribeFail(t *testing.T, registerAttempt, keepFailedPaymentAttempts bool) { db := initDB(t, keepFailedPaymentAttempts) - pControl := NewControlTower(channeldb.NewPaymentControl(db)) + pControl := NewControlTower(channeldb.NewKVPaymentsDB(db)) // Initiate a payment. info, attempt, _, err := genInfo() diff --git a/rpcserver.go b/rpcserver.go index ac4e98e27..9b8bedf69 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -7527,7 +7527,7 @@ func (r *rpcServer) ListPayments(ctx context.Context, query.MaxPayments = math.MaxUint64 } - paymentsQuerySlice, err := r.server.miscDB.QueryPayments(query) + paymentsQuerySlice, err := r.server.kvPaymentsDB.QueryPayments(query) if err != nil { return nil, err } @@ -7608,7 +7608,7 @@ func (r *rpcServer) DeletePayment(ctx context.Context, rpcsLog.Infof("[DeletePayment] payment_identifier=%v, "+ "failed_htlcs_only=%v", hash, req.FailedHtlcsOnly) - err = r.server.miscDB.DeletePayment(hash, req.FailedHtlcsOnly) + err = r.server.kvPaymentsDB.DeletePayment(hash, req.FailedHtlcsOnly) if err != nil { return nil, err } @@ -7648,7 +7648,7 @@ func (r *rpcServer) DeleteAllPayments(ctx context.Context, "failed_htlcs_only=%v", req.FailedPaymentsOnly, req.FailedHtlcsOnly) - numDeletedPayments, err := r.server.miscDB.DeletePayments( + numDeletedPayments, err := r.server.kvPaymentsDB.DeletePayments( req.FailedPaymentsOnly, req.FailedHtlcsOnly, ) if err != nil { diff --git a/server.go b/server.go index 9af5ac128..d79cc3547 100644 --- a/server.go +++ b/server.go @@ -335,6 +335,12 @@ type server struct { invoicesDB invoices.InvoiceDB + // kvPaymentsDB is the DB that contains all functions for managing + // payments. + // + // TODO(ziggie): Replace with interface. + kvPaymentsDB *channeldb.KVPaymentsDB + aliasMgr *aliasmgr.Manager htlcSwitch *htlcswitch.Switch @@ -678,6 +684,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, addrSource: addrSource, miscDB: dbs.ChanStateDB, invoicesDB: dbs.InvoiceDB, + kvPaymentsDB: dbs.KVPaymentsDB, cc: cc, sigPool: lnwallet.NewSigPool(cfg.Workers.Sig, cc.Signer), writePool: writePool, @@ -1127,9 +1134,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, PathFindingConfig: pathFindingConfig, } - paymentControl := channeldb.NewPaymentControl(dbs.ChanStateDB) - - s.controlTower = routing.NewControlTower(paymentControl) + s.controlTower = routing.NewControlTower(dbs.KVPaymentsDB) strictPruning := cfg.Bitcoin.Node == "neutrino" || cfg.Routing.StrictZombiePruning