lnd.xprv/htlcswitch/switch_control.go
2018-08-21 19:23:22 -07:00

143 lines
4.0 KiB
Go

package htlcswitch
import (
"errors"
"sync"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)
var (
// ErrAlreadyPaid is used when we have already paid
ErrAlreadyPaid = errors.New("invoice was already paid")
// ErrPaymentInFlight returns in case if payment is already "in flight"
ErrPaymentInFlight = errors.New("payment is in transition")
// ErrPaymentNotInitiated returns in case if payment wasn't initiated
// in switch
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
// ErrPaymentAlreadyCompleted returns in case of attempt to complete
// completed payment
ErrPaymentAlreadyCompleted = errors.New("payment is already completed")
)
// ControlTower is a controller interface of sending HTLC messages to switch
type ControlTower interface {
// CheckSend intercepts incoming message to provide checks
// and fail if specific message is not allowed by implementation
CheckSend(htlc *lnwire.UpdateAddHTLC) error
// Success marks message transition as successful
Success(paymentHash [32]byte) error
// Fail marks message transition as failed
Fail(paymentHash [32]byte) error
}
// paymentControl is implementation of ControlTower to restrict double payment
// sending.
type paymentControl struct {
mx sync.Mutex
db *channeldb.DB
}
// NewPaymentControl creates a new instance of the paymentControl.
func NewPaymentControl(db *channeldb.DB) ControlTower {
return &paymentControl{
db: db,
}
}
// CheckSend checks that a sending htlc wasn't triggered before for specific
// payment hash, if so, should trigger error depends on current status
func (p *paymentControl) CheckSend(htlc *lnwire.UpdateAddHTLC) error {
p.mx.Lock()
defer p.mx.Unlock()
// Retrieve current status of payment from local database.
paymentStatus, err := p.db.FetchPaymentStatus(htlc.PaymentHash)
if err != nil {
return err
}
switch paymentStatus {
case channeldb.StatusGrounded:
// It is safe to reattempt a payment if we know that we haven't
// left one in flight prior to restarting and switch.
return p.db.UpdatePaymentStatus(htlc.PaymentHash,
channeldb.StatusInFlight)
case channeldb.StatusInFlight:
// Not clear if it's safe to reinitiate a payment if there
// is already a payment in flight, so we should withhold any
// additional attempts to send to that payment hash.
return ErrPaymentInFlight
case channeldb.StatusCompleted:
// It has been already paid and don't want to pay again.
return ErrAlreadyPaid
}
return nil
}
// Success proceed status changing of payment to next successful status
func (p *paymentControl) Success(paymentHash [32]byte) error {
p.mx.Lock()
defer p.mx.Unlock()
paymentStatus, err := p.db.FetchPaymentStatus(paymentHash)
if err != nil {
return err
}
switch paymentStatus {
case channeldb.StatusGrounded:
// Payment isn't initiated but received.
return ErrPaymentNotInitiated
case channeldb.StatusInFlight:
// Successful transition from InFlight transition to Completed.
return p.db.UpdatePaymentStatus(paymentHash, channeldb.StatusCompleted)
case channeldb.StatusCompleted:
// Payment is completed before in should be ignored.
return ErrPaymentAlreadyCompleted
}
return nil
}
// Fail proceed status changing of payment to initial status in case of failure
func (p *paymentControl) Fail(paymentHash [32]byte) error {
p.mx.Lock()
defer p.mx.Unlock()
paymentStatus, err := p.db.FetchPaymentStatus(paymentHash)
if err != nil {
return err
}
switch paymentStatus {
case channeldb.StatusGrounded:
// Unpredictable behavior when payment wasn't transited to
// StatusInFlight status and was failed.
return ErrPaymentNotInitiated
case channeldb.StatusInFlight:
// If payment wasn't processed by some reason should return to
// default status to unlock retrying option for the same payment hash.
return p.db.UpdatePaymentStatus(paymentHash, channeldb.StatusGrounded)
case channeldb.StatusCompleted:
// Payment is completed before and can't be moved to another status.
return ErrPaymentAlreadyCompleted
}
return nil
}