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:
Joost Jager
2020-02-20 18:08:01 +01:00
parent 8558534417
commit 48c0e42c26
10 changed files with 550 additions and 274 deletions

View File

@@ -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
})