143 lines
4.0 KiB
Go
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
|
|
}
|