From 6d80661bbb4cb33f268ba2246032e457f7a4e781 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 23 May 2019 20:05:28 +0200 Subject: [PATCH] channeldb/payments: add StatusFailed --- channeldb/control_tower.go | 111 +++++++++++++++++++------------------ channeldb/payments.go | 13 ++++- 2 files changed, 66 insertions(+), 58 deletions(-) diff --git a/channeldb/control_tower.go b/channeldb/control_tower.go index ae6c7e7c..98de1279 100644 --- a/channeldb/control_tower.go +++ b/channeldb/control_tower.go @@ -24,6 +24,10 @@ var ( // recomplete a completed payment. ErrPaymentAlreadyCompleted = errors.New("payment is already completed") + // ErrPaymentAlreadyFailed is returned in the event we attempt to + // re-fail 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") @@ -86,6 +90,10 @@ func (p *paymentControl) ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error { switch paymentStatus { + // We allow retrying failed payments. + case StatusFailed: + fallthrough + // It is safe to reattempt a payment if we know that we haven't // left one in flight. Since this one is grounded or failed, // transition the payment status to InFlight to prevent others. @@ -121,41 +129,22 @@ func (p *paymentControl) ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error { func (p *paymentControl) Success(paymentHash [32]byte) error { 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 + bucket, err := fetchPaymentBucket(tx, paymentHash) if err != nil { return err } - // Get the existing status, if any. - paymentStatus := fetchPaymentStatus(bucket) - - // Reset the update error, to avoid carrying over an error - // from a previous execution of the batched db transaction. - updateErr = nil - - switch { - - // Our records show the payment as still being grounded, - // meaning it never should have left the switch. - case paymentStatus == StatusGrounded: - updateErr = ErrPaymentNotInitiated - - // A successful response was received for an InFlight payment, - // mark it as completed to prevent sending to this payment hash - // again. - case paymentStatus == StatusInFlight: - return bucket.Put(paymentStatusKey, StatusCompleted.Bytes()) - - // The payment was completed previously, alert the caller that - // this may be a duplicate call. - case paymentStatus == StatusCompleted: - updateErr = ErrPaymentAlreadyCompleted - - default: - updateErr = ErrUnknownPaymentStatus + // We can only mark in-flight payments as succeeded. + if err := ensureInFlight(bucket); err != nil { + updateErr = err + return nil } - return nil + return bucket.Put(paymentStatusKey, StatusCompleted.Bytes()) }) if err != nil { return err @@ -170,40 +159,22 @@ func (p *paymentControl) Success(paymentHash [32]byte) error { func (p *paymentControl) Fail(paymentHash [32]byte) error { 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 + bucket, err := fetchPaymentBucket(tx, paymentHash) if err != nil { return err } - paymentStatus := fetchPaymentStatus(bucket) - - // Reset the update error, to avoid carrying over an error - // from a previous execution of the batched db transaction. - updateErr = nil - - switch { - - // Our records show the payment as still being grounded, - // meaning it never should have left the switch. - case paymentStatus == StatusGrounded: - updateErr = ErrPaymentNotInitiated - - // A failed response was received for an InFlight payment, mark - // it as Failed to allow subsequent attempts. - case paymentStatus == StatusInFlight: - return bucket.Put(paymentStatusKey, StatusGrounded.Bytes()) - - // The payment was completed previously, and we are now - // reporting that it has failed. Leave the status as completed, - // but alert the user that something is wrong. - case paymentStatus == StatusCompleted: - updateErr = ErrPaymentAlreadyCompleted - - default: - updateErr = ErrUnknownPaymentStatus + // We can only mark in-flight payments as failed. + if err := ensureInFlight(bucket); err != nil { + updateErr = err + return nil } - return nil + return bucket.Put(paymentStatusKey, StatusGrounded.Bytes()) }) if err != nil { return err @@ -239,3 +210,33 @@ func fetchPaymentStatus(bucket *bbolt.Bucket) PaymentStatus { return paymentStatus } + +// ensureInFlight checks whether the payment found in the given bucket has +// status InFlight, and returns an error otherwise. This should be used to +// ensure we only mark in-flight payments as succeeded or failed. +func ensureInFlight(bucket *bbolt.Bucket) error { + paymentStatus := fetchPaymentStatus(bucket) + + switch { + + // The payment was indeed InFlight, return. + case paymentStatus == StatusInFlight: + return nil + + // Our records show the payment as unknown, meaning it never + // should have left the switch. + case paymentStatus == StatusGrounded: + return ErrPaymentNotInitiated + + // The payment succeeded previously. + case paymentStatus == StatusCompleted: + return ErrPaymentAlreadyCompleted + + // The payment was already failed. + case paymentStatus == StatusFailed: + return ErrPaymentAlreadyFailed + + default: + return ErrUnknownPaymentStatus + } +} diff --git a/channeldb/payments.go b/channeldb/payments.go index 7f1b015b..2c134ef9 100644 --- a/channeldb/payments.go +++ b/channeldb/payments.go @@ -99,8 +99,7 @@ type PaymentStatus byte const ( // StatusGrounded is the status where a payment has never been - // initiated, or has been initiated and received an intermittent - // failure. + // initiated. StatusGrounded PaymentStatus = 0 // StatusInFlight is the status where a payment has been initiated, but @@ -110,6 +109,12 @@ const ( // StatusCompleted is the status where a payment has been initiated and // the payment was completed successfully. StatusCompleted PaymentStatus = 2 + + // StatusFailed is the status where a payment has been initiated and a + // failure result has come back. + StatusFailed PaymentStatus = 3 + + // TODO(halseth): timeout/cancel state? ) // Bytes returns status as slice of bytes. @@ -124,7 +129,7 @@ func (ps *PaymentStatus) FromBytes(status []byte) error { } switch PaymentStatus(status[0]) { - case StatusGrounded, StatusInFlight, StatusCompleted: + case StatusGrounded, StatusInFlight, StatusCompleted, StatusFailed: *ps = PaymentStatus(status[0]) default: return errors.New("unknown payment status") @@ -142,6 +147,8 @@ func (ps PaymentStatus) String() string { return "In Flight" case StatusCompleted: return "Completed" + case StatusFailed: + return "Failed" default: return "Unknown" }