lnd.xprv/channeldb/control_tower.go

243 lines
7.2 KiB
Go
Raw Normal View History

package channeldb
import (
"errors"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
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")
// ErrPaymentNotInitiated is returned if payment wasn't initiated in
// switch.
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
// ErrPaymentAlreadyCompleted is returned in the event we attempt to
// recomplete a completed payment.
ErrPaymentAlreadyCompleted = errors.New("payment is already completed")
2019-05-23 21:05:28 +03:00
// 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")
)
// 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.
type ControlTower interface {
// 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.
Success(paymentHash [32]byte) error
// 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.
Fail(paymentHash [32]byte) error
}
// paymentControl is persistent implementation of ControlTower to restrict
// double payment sending.
type paymentControl struct {
db *DB
}
// NewPaymentControl creates a new instance of the paymentControl.
func NewPaymentControl(db *DB) ControlTower {
return &paymentControl{
db: db,
}
}
// 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 {
var takeoffErr error
err := p.db.Batch(func(tx *bbolt.Tx) error {
bucket, err := fetchPaymentBucket(tx, htlc.PaymentHash)
if err != nil {
return err
}
// Get the existing status of this payment, if any.
paymentStatus := fetchPaymentStatus(bucket)
// 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:28 +03:00
// 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.
case StatusGrounded:
return bucket.Put(paymentStatusKey, StatusInFlight.Bytes())
// We already have an InFlight payment on the network. We will
// disallow any more payment until a response is received.
case StatusInFlight:
takeoffErr = ErrPaymentInFlight
// We've already completed a payment to this payment hash,
// forbid the switch from sending another.
case StatusCompleted:
takeoffErr = ErrAlreadyPaid
default:
takeoffErr = ErrUnknownPaymentStatus
}
return nil
})
if err != nil {
return err
}
return takeoffErr
}
// 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.
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
2019-05-23 21:05:28 +03:00
bucket, err := fetchPaymentBucket(tx, paymentHash)
if err != nil {
return err
}
2019-05-23 21:05:28 +03:00
// We can only mark in-flight payments as succeeded.
if err := ensureInFlight(bucket); err != nil {
updateErr = err
return nil
}
2019-05-23 21:05:28 +03:00
return bucket.Put(paymentStatusKey, StatusCompleted.Bytes())
})
if err != nil {
return err
}
return updateErr
}
// 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.
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
2019-05-23 21:05:28 +03:00
bucket, err := fetchPaymentBucket(tx, paymentHash)
if err != nil {
return err
}
2019-05-23 21:05:28 +03:00
// We can only mark in-flight payments as failed.
if err := ensureInFlight(bucket); err != nil {
updateErr = err
return nil
}
2019-05-23 21:05:28 +03:00
return bucket.Put(paymentStatusKey, StatusGrounded.Bytes())
})
if err != nil {
return err
}
return updateErr
}
// 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
}
2019-05-23 21:05:28 +03:00
// 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
}
}