mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-10-03 19:34:23 +02:00
channeldb+routing: store all payment htlcs
This commit converts the database structure of a payment so that it can not just store the last htlc attempt, but all attempts that have been made. This is a preparation for mpp sending. In addition to that, we now also persist the fail time of an htlc. In a later commit, the full failure reason will be added as well. A key change is made to the control tower interface. Previously the control tower wasn't aware of individual htlc outcomes. The payment remained in-flight with the latest attempt recorded, but an outcome was only set when the payment finished. With this commit, the outcome of every htlc is expected by the control tower and recorded in the database. Co-authored-by: Johan T. Halseth <johanth@gmail.com>
This commit is contained in:
@@ -127,11 +127,11 @@ func (p *PaymentControl) InitPayment(paymentHash lntypes.Hash,
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll delete any lingering attempt info 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.Delete(paymentAttemptInfoKey)
|
||||
if err != nil {
|
||||
// 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.DeleteBucket(paymentHtlcsBucket)
|
||||
if err != nil && err != bbolt.ErrBucketNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -153,76 +153,113 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
|
||||
|
||||
// Serialize the information before opening the db transaction.
|
||||
var a bytes.Buffer
|
||||
if err := serializeHTLCAttemptInfo(&a, attempt); err != nil {
|
||||
err := serializeHTLCAttemptInfo(&a, attempt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attemptBytes := a.Bytes()
|
||||
htlcInfoBytes := a.Bytes()
|
||||
|
||||
var updateErr error
|
||||
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
||||
// Reset the update error, to avoid carrying over an error
|
||||
// from a previous execution of the batched db transaction.
|
||||
updateErr = nil
|
||||
htlcIDBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(htlcIDBytes, attempt.AttemptID)
|
||||
|
||||
return p.db.Update(func(tx *bbolt.Tx) error {
|
||||
// Get the payment bucket to register this new attempt in.
|
||||
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
||||
if err == ErrPaymentNotInitiated {
|
||||
updateErr = ErrPaymentNotInitiated
|
||||
return nil
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We can only register attempts for payments that are
|
||||
// in-flight.
|
||||
if err := ensureInFlight(bucket); err != nil {
|
||||
updateErr = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add the payment attempt to the payments bucket.
|
||||
return bucket.Put(paymentAttemptInfoKey, attemptBytes)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateErr
|
||||
}
|
||||
|
||||
// Success transitions a payment into the Succeeded state. 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) Success(paymentHash lntypes.Hash,
|
||||
preimage lntypes.Preimage) (*MPPayment, error) {
|
||||
|
||||
var (
|
||||
updateErr error
|
||||
payment *MPPayment
|
||||
)
|
||||
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
||||
// Reset the update error, to avoid carrying over an error
|
||||
// from a previous execution of the batched db transaction.
|
||||
updateErr = nil
|
||||
payment = nil
|
||||
|
||||
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
||||
if err == ErrPaymentNotInitiated {
|
||||
updateErr = ErrPaymentNotInitiated
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We can only mark in-flight payments as succeeded.
|
||||
if err := ensureInFlight(bucket); err != nil {
|
||||
updateErr = err
|
||||
return nil
|
||||
htlcsBucket, err := bucket.CreateBucketIfNotExists(
|
||||
paymentHtlcsBucket,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Record the successful payment info atomically to the
|
||||
// payments record.
|
||||
err = bucket.Put(paymentSettleInfoKey, preimage[:])
|
||||
// Create bucket for this attempt. Fail if the bucket already
|
||||
// exists.
|
||||
htlcBucket, err := htlcsBucket.CreateBucket(htlcIDBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return htlcBucket.Put(htlcAttemptInfoKey, htlcInfoBytes)
|
||||
})
|
||||
}
|
||||
|
||||
// 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) error {
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := serializeHTLCFailInfo(&b, failInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
failBytes := b.Bytes()
|
||||
|
||||
_, err := p.updateHtlcKey(hash, attemptID, htlcFailInfoKey, failBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
// updateHtlcKey updates a database key for the specified htlc.
|
||||
func (p *PaymentControl) updateHtlcKey(paymentHash lntypes.Hash,
|
||||
attemptID uint64, key, value []byte) (*MPPayment, error) {
|
||||
|
||||
htlcIDBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(htlcIDBytes, attemptID)
|
||||
|
||||
var payment *MPPayment
|
||||
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
||||
// Fetch bucket that contains all information for the payment
|
||||
// with this hash.
|
||||
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We can only update keys of in-flight payments.
|
||||
if err := ensureInFlight(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
htlcsBucket := bucket.Bucket(paymentHtlcsBucket)
|
||||
if htlcsBucket == nil {
|
||||
return fmt.Errorf("htlcs bucket not found")
|
||||
}
|
||||
|
||||
htlcBucket := htlcsBucket.Bucket(htlcIDBytes)
|
||||
if htlcBucket == nil {
|
||||
return fmt.Errorf("HTLC with ID %v not registered",
|
||||
attemptID)
|
||||
}
|
||||
|
||||
// Add or update the key for this htlc.
|
||||
err = htlcBucket.Put(key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -235,7 +272,7 @@ func (p *PaymentControl) Success(paymentHash lntypes.Hash,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payment, updateErr
|
||||
return payment, err
|
||||
}
|
||||
|
||||
// Fail transitions a payment into the Failed state, and records the reason the
|
||||
@@ -278,7 +315,19 @@ func (p *PaymentControl) Fail(paymentHash lntypes.Hash,
|
||||
|
||||
// Retrieve attempt info for the notification, if available.
|
||||
payment, err = fetchPayment(bucket)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Final sanity check to see if there are no in-flight htlcs.
|
||||
for _, htlc := range payment.HTLCs {
|
||||
if htlc.Settle == nil && htlc.Failure == nil {
|
||||
return errors.New("payment failed with " +
|
||||
"in-flight htlc(s)")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -338,7 +387,6 @@ func fetchPaymentBucket(tx *bbolt.Tx, paymentHash lntypes.Hash) (
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
|
||||
}
|
||||
|
||||
// nextPaymentSequence returns the next sequence number to store for a new
|
||||
@@ -362,8 +410,21 @@ func nextPaymentSequence(tx *bbolt.Tx) ([]byte, error) {
|
||||
// fetchPaymentStatus fetches the payment status of the payment. If the payment
|
||||
// isn't found, it will default to "StatusUnknown".
|
||||
func fetchPaymentStatus(bucket *bbolt.Bucket) (PaymentStatus, error) {
|
||||
if bucket.Get(paymentSettleInfoKey) != nil {
|
||||
return StatusSucceeded, nil
|
||||
htlcsBucket := bucket.Bucket(paymentHtlcsBucket)
|
||||
if htlcsBucket != nil {
|
||||
htlcs, err := fetchHtlcAttempts(htlcsBucket)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Go through all HTLCs, and return StatusSucceeded if any of
|
||||
// them did succeed.
|
||||
for _, h := range htlcs {
|
||||
if h.Settle != nil {
|
||||
return StatusSucceeded, nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if bucket.Get(paymentFailInfoKey) != nil {
|
||||
@@ -410,27 +471,16 @@ func ensureInFlight(bucket *bbolt.Bucket) error {
|
||||
}
|
||||
}
|
||||
|
||||
// fetchPaymentAttempt fetches the payment attempt from the bucket.
|
||||
func fetchPaymentAttempt(bucket *bbolt.Bucket) (*HTLCAttemptInfo, error) {
|
||||
attemptData := bucket.Get(paymentAttemptInfoKey)
|
||||
if attemptData == nil {
|
||||
return nil, errNoAttemptInfo
|
||||
}
|
||||
|
||||
r := bytes.NewReader(attemptData)
|
||||
return deserializeHTLCAttemptInfo(r)
|
||||
}
|
||||
|
||||
// InFlightPayment is a wrapper around a payment that has status InFlight.
|
||||
type InFlightPayment struct {
|
||||
// Info is the PaymentCreationInfo of the in-flight payment.
|
||||
Info *PaymentCreationInfo
|
||||
|
||||
// Attempt contains information about the last payment attempt that was
|
||||
// made to this payment hash.
|
||||
// Attempts is the set of payment attempts that was made to this
|
||||
// payment hash.
|
||||
//
|
||||
// NOTE: Might be nil.
|
||||
Attempt *HTLCAttemptInfo
|
||||
// NOTE: Might be empty.
|
||||
Attempts []HTLCAttemptInfo
|
||||
}
|
||||
|
||||
// FetchInFlightPayments returns all payments with status InFlight.
|
||||
@@ -473,13 +523,31 @@ func (p *PaymentControl) FetchInFlightPayments() ([]*InFlightPayment, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now get the attempt info. It could be that there is
|
||||
// no attempt info yet.
|
||||
inFlight.Attempt, err = fetchPaymentAttempt(bucket)
|
||||
if err != nil && err != errNoAttemptInfo {
|
||||
htlcsBucket := bucket.Bucket(paymentHtlcsBucket)
|
||||
if htlcsBucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch all HTLCs attempted for this payment.
|
||||
htlcs, err := fetchHtlcAttempts(htlcsBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We only care about the static info for the HTLCs
|
||||
// still in flight, so convert the result to a slice of
|
||||
// HTLCAttemptInfos.
|
||||
for _, h := range htlcs {
|
||||
// Skip HTLCs not in flight.
|
||||
if h.Settle != nil || h.Failure != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
inFlight.Attempts = append(
|
||||
inFlight.Attempts, h.HTLCAttemptInfo,
|
||||
)
|
||||
}
|
||||
|
||||
inFlights = append(inFlights, inFlight)
|
||||
return nil
|
||||
})
|
||||
|
Reference in New Issue
Block a user