2019-05-23 21:05:26 +03:00
|
|
|
package channeldb
|
2018-08-12 16:18:35 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
"github.com/coreos/bbolt"
|
2019-05-23 21:05:26 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2018-08-12 16:18:35 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2018-08-11 00:00:50 +03:00
|
|
|
// ErrAlreadyPaid signals we have already paid this payment hash.
|
|
|
|
ErrAlreadyPaid = errors.New("invoice is already paid")
|
2018-08-12 16:18:35 +03:00
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// ErrPaymentInFlight signals that payment for this payment hash is
|
|
|
|
// already "in flight" on the network.
|
2018-08-12 16:18:35 +03:00
|
|
|
ErrPaymentInFlight = errors.New("payment is in transition")
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// ErrPaymentNotInitiated is returned if payment wasn't initiated in
|
|
|
|
// switch.
|
2018-08-12 16:18:35 +03:00
|
|
|
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// ErrPaymentAlreadyCompleted is returned in the event we attempt to
|
|
|
|
// recomplete a completed payment.
|
2018-08-12 16:18:35 +03:00
|
|
|
ErrPaymentAlreadyCompleted = errors.New("payment is already completed")
|
2018-08-11 00:00:50 +03:00
|
|
|
|
|
|
|
// ErrUnknownPaymentStatus is returned when we do not recognize the
|
|
|
|
// existing state of a payment.
|
|
|
|
ErrUnknownPaymentStatus = errors.New("unknown payment status")
|
2018-08-12 16:18:35 +03:00
|
|
|
)
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// ControlTower tracks all outgoing payments made by the switch, whose primary
|
|
|
|
// purpose is to prevent duplicate payments to the same payment hash. In
|
|
|
|
// production, a persistent implementation is preferred so that tracking can
|
|
|
|
// survive across restarts. Payments are transition through various payment
|
|
|
|
// states, and the ControlTower interface provides access to driving the state
|
|
|
|
// transitions.
|
2018-08-12 16:18:35 +03:00
|
|
|
type ControlTower interface {
|
2018-08-11 00:00:50 +03:00
|
|
|
// ClearForTakeoff atomically checks that no inflight or completed
|
|
|
|
// payments exist for this payment hash. If none are found, this method
|
|
|
|
// atomically transitions the status for this payment hash as InFlight.
|
|
|
|
ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error
|
|
|
|
|
|
|
|
// Success transitions an InFlight payment into a Completed payment.
|
|
|
|
// After invoking this method, ClearForTakeoff should always return an
|
|
|
|
// error to prevent us from making duplicate payments to the same
|
|
|
|
// payment hash.
|
2018-08-12 16:18:35 +03:00
|
|
|
Success(paymentHash [32]byte) error
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// Fail transitions an InFlight payment into a Grounded Payment. After
|
|
|
|
// invoking this method, ClearForTakeoff should return nil on its next
|
|
|
|
// call for this payment hash, allowing the switch to make a subsequent
|
|
|
|
// payment.
|
2018-08-12 16:18:35 +03:00
|
|
|
Fail(paymentHash [32]byte) error
|
|
|
|
}
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// paymentControl is persistent implementation of ControlTower to restrict
|
|
|
|
// double payment sending.
|
2018-08-12 16:18:35 +03:00
|
|
|
type paymentControl struct {
|
2018-08-14 04:46:58 +03:00
|
|
|
strict bool
|
2018-08-12 16:18:35 +03:00
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
db *DB
|
2018-08-12 16:18:35 +03:00
|
|
|
}
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
// NewPaymentControl creates a new instance of the paymentControl. The strict
|
|
|
|
// flag indicates whether the controller should require "strict" state
|
|
|
|
// transitions, which would be otherwise intolerant to older databases that may
|
|
|
|
// already have duplicate payments to the same payment hash. It should be
|
|
|
|
// enabled only after sufficient checks have been made to ensure the db does not
|
|
|
|
// contain such payments. In the meantime, non-strict mode enforces a superset
|
|
|
|
// of the state transitions that prevent additional payments to a given payment
|
|
|
|
// hash from being added.
|
2019-05-23 21:05:26 +03:00
|
|
|
func NewPaymentControl(strict bool, db *DB) ControlTower {
|
2018-08-12 16:18:35 +03:00
|
|
|
return &paymentControl{
|
2018-08-14 04:46:58 +03:00
|
|
|
strict: strict,
|
|
|
|
db: db,
|
2018-08-12 16:18:35 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// ClearForTakeoff checks that we don't already have an InFlight or Completed
|
|
|
|
// payment identified by the same payment hash.
|
|
|
|
func (p *paymentControl) ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error {
|
2018-08-21 07:14:52 +03:00
|
|
|
var takeoffErr error
|
2018-11-30 07:04:21 +03:00
|
|
|
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
2019-05-23 21:05:26 +03:00
|
|
|
bucket, err := fetchPaymentBucket(tx, htlc.PaymentHash)
|
2018-08-21 07:14:52 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
// Get the existing status of this payment, if any.
|
|
|
|
paymentStatus := fetchPaymentStatus(bucket)
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
// Reset the takeoff error, to avoid carrying over an error
|
|
|
|
// from a previous execution of the batched db transaction.
|
|
|
|
takeoffErr = nil
|
|
|
|
|
|
|
|
switch paymentStatus {
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case StatusGrounded:
|
2018-08-21 07:14:52 +03:00
|
|
|
// It is safe to reattempt a payment if we know that we
|
|
|
|
// haven't left one in flight. Since this one is
|
2019-05-23 21:05:26 +03:00
|
|
|
// grounded or failed, transition the payment status
|
|
|
|
// to InFlight to prevent others.
|
|
|
|
return bucket.Put(paymentStatusKey, StatusInFlight.Bytes())
|
2018-08-21 07:14:52 +03:00
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case StatusInFlight:
|
2018-08-21 07:14:52 +03:00
|
|
|
// We already have an InFlight payment on the network. We will
|
|
|
|
// disallow any more payment until a response is received.
|
|
|
|
takeoffErr = ErrPaymentInFlight
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case StatusCompleted:
|
2018-08-21 07:14:52 +03:00
|
|
|
// We've already completed a payment to this payment hash,
|
|
|
|
// forbid the switch from sending another.
|
|
|
|
takeoffErr = ErrAlreadyPaid
|
|
|
|
|
|
|
|
default:
|
|
|
|
takeoffErr = ErrUnknownPaymentStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2018-08-12 16:18:35 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
return takeoffErr
|
2018-08-12 16:18:35 +03:00
|
|
|
}
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// Success transitions an InFlight payment to Completed, otherwise it returns an
|
|
|
|
// error. After calling Success, ClearForTakeoff should prevent any further
|
|
|
|
// attempts for the same payment hash.
|
2018-08-12 16:18:35 +03:00
|
|
|
func (p *paymentControl) Success(paymentHash [32]byte) error {
|
2018-08-21 07:14:52 +03:00
|
|
|
var updateErr error
|
2018-11-30 07:04:21 +03:00
|
|
|
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
2019-05-23 21:05:26 +03:00
|
|
|
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
2018-08-21 07:14:52 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
// Get the existing status, if any.
|
|
|
|
paymentStatus := fetchPaymentStatus(bucket)
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
// Reset the update error, to avoid carrying over an error
|
|
|
|
// from a previous execution of the batched db transaction.
|
|
|
|
updateErr = nil
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusGrounded && p.strict:
|
2018-08-21 07:14:52 +03:00
|
|
|
// Our records show the payment as still being grounded,
|
|
|
|
// meaning it never should have left the switch.
|
|
|
|
updateErr = ErrPaymentNotInitiated
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusGrounded && !p.strict:
|
2018-08-21 07:14:52 +03:00
|
|
|
// Though our records show the payment as still being
|
|
|
|
// grounded, meaning it never should have left the
|
|
|
|
// switch, we permit this transition in non-strict mode
|
|
|
|
// to handle inconsistent db states.
|
|
|
|
fallthrough
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusInFlight:
|
2018-08-21 07:14:52 +03:00
|
|
|
// A successful response was received for an InFlight
|
|
|
|
// payment, mark it as completed to prevent sending to
|
|
|
|
// this payment hash again.
|
2019-05-23 21:05:26 +03:00
|
|
|
return bucket.Put(paymentStatusKey, StatusCompleted.Bytes())
|
2018-08-21 07:14:52 +03:00
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusCompleted:
|
2018-08-21 07:14:52 +03:00
|
|
|
// The payment was completed previously, alert the
|
|
|
|
// caller that this may be a duplicate call.
|
|
|
|
updateErr = ErrPaymentAlreadyCompleted
|
|
|
|
|
|
|
|
default:
|
|
|
|
updateErr = ErrUnknownPaymentStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2018-08-12 16:18:35 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
return updateErr
|
2018-08-12 16:18:35 +03:00
|
|
|
}
|
|
|
|
|
2018-08-11 00:00:50 +03:00
|
|
|
// Fail transitions an InFlight payment to Grounded, otherwise it returns an
|
|
|
|
// error. After calling Fail, ClearForTakeoff should fail any further attempts
|
|
|
|
// for the same payment hash.
|
2018-08-12 16:18:35 +03:00
|
|
|
func (p *paymentControl) Fail(paymentHash [32]byte) error {
|
2018-08-21 07:14:52 +03:00
|
|
|
var updateErr error
|
2018-11-30 07:04:21 +03:00
|
|
|
err := p.db.Batch(func(tx *bbolt.Tx) error {
|
2019-05-23 21:05:26 +03:00
|
|
|
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
2018-08-21 07:14:52 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
paymentStatus := fetchPaymentStatus(bucket)
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
// Reset the update error, to avoid carrying over an error
|
|
|
|
// from a previous execution of the batched db transaction.
|
|
|
|
updateErr = nil
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusGrounded && p.strict:
|
2018-08-21 07:14:52 +03:00
|
|
|
// Our records show the payment as still being grounded,
|
|
|
|
// meaning it never should have left the switch.
|
|
|
|
updateErr = ErrPaymentNotInitiated
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusGrounded && !p.strict:
|
2018-08-21 07:14:52 +03:00
|
|
|
// Though our records show the payment as still being
|
|
|
|
// grounded, meaning it never should have left the
|
|
|
|
// switch, we permit this transition in non-strict mode
|
|
|
|
// to handle inconsistent db states.
|
|
|
|
fallthrough
|
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusInFlight:
|
2018-08-21 07:14:52 +03:00
|
|
|
// A failed response was received for an InFlight
|
2019-05-23 21:05:26 +03:00
|
|
|
// payment, mark it as Failed to allow subsequent
|
|
|
|
// attempts.
|
|
|
|
return bucket.Put(paymentStatusKey, StatusGrounded.Bytes())
|
2018-08-21 07:14:52 +03:00
|
|
|
|
2019-05-23 21:05:26 +03:00
|
|
|
case paymentStatus == StatusCompleted:
|
2018-08-21 07:14:52 +03:00
|
|
|
// 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.
|
|
|
|
updateErr = ErrPaymentAlreadyCompleted
|
|
|
|
|
|
|
|
default:
|
|
|
|
updateErr = ErrUnknownPaymentStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2018-08-12 16:18:35 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-21 07:14:52 +03:00
|
|
|
return updateErr
|
2018-08-12 16:18:35 +03:00
|
|
|
}
|
2019-05-23 21:05:26 +03:00
|
|
|
|
|
|
|
// fetchPaymentBucket fetches or creates the sub-bucket assigned to this
|
|
|
|
// payment hash.
|
|
|
|
func fetchPaymentBucket(tx *bbolt.Tx, paymentHash lntypes.Hash) (
|
|
|
|
*bbolt.Bucket, error) {
|
|
|
|
|
|
|
|
payments, err := tx.CreateBucketIfNotExists(paymentsRootBucket)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return payments.CreateBucketIfNotExists(paymentHash[:])
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetchPaymentStatus fetches the payment status from the bucket. If the
|
|
|
|
// status isn't found, it will default to "StatusGrounded".
|
|
|
|
func fetchPaymentStatus(bucket *bbolt.Bucket) PaymentStatus {
|
|
|
|
// The default status for all payments that aren't recorded in
|
|
|
|
// database.
|
|
|
|
var paymentStatus = StatusGrounded
|
|
|
|
|
|
|
|
paymentStatusBytes := bucket.Get(paymentStatusKey)
|
|
|
|
if paymentStatusBytes != nil {
|
|
|
|
paymentStatus.FromBytes(paymentStatusBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
return paymentStatus
|
|
|
|
}
|