mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
24e3234dfa
Adds a new configuration flag to lnd that will keep keysend payments in the accepted state. An application can then inspect the payment parameters and decide whether to settle or cancel. The on-the-fly inserted keysend invoices get a configurable expiry time. This is a safeguard in case the application that should decide on the keysend payments isn't active.
249 lines
7.2 KiB
Go
249 lines
7.2 KiB
Go
package invoices
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/queue"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
)
|
|
|
|
// invoiceExpiry holds and invoice's payment hash and its expiry. This
|
|
// is used to order invoices by their expiry for cancellation.
|
|
type invoiceExpiry struct {
|
|
PaymentHash lntypes.Hash
|
|
Expiry time.Time
|
|
Keysend bool
|
|
}
|
|
|
|
// Less implements PriorityQueueItem.Less such that the top item in the
|
|
// priorty queue will be the one that expires next.
|
|
func (e invoiceExpiry) Less(other queue.PriorityQueueItem) bool {
|
|
return e.Expiry.Before(other.(*invoiceExpiry).Expiry)
|
|
}
|
|
|
|
// InvoiceExpiryWatcher handles automatic invoice cancellation of expried
|
|
// invoices. Upon start InvoiceExpiryWatcher will retrieve all pending (not yet
|
|
// settled or canceled) invoices invoices to its watcing queue. When a new
|
|
// invoice is added to the InvoiceRegistry, it'll be forarded to the
|
|
// InvoiceExpiryWatcher and will end up in the watching queue as well.
|
|
// If any of the watched invoices expire, they'll be removed from the watching
|
|
// queue and will be cancelled through InvoiceRegistry.CancelInvoice().
|
|
type InvoiceExpiryWatcher struct {
|
|
sync.Mutex
|
|
started bool
|
|
|
|
// clock is the clock implementation that InvoiceExpiryWatcher uses.
|
|
// It is useful for testing.
|
|
clock clock.Clock
|
|
|
|
// cancelInvoice is a template method that cancels an expired invoice.
|
|
cancelInvoice func(lntypes.Hash, bool) error
|
|
|
|
// expiryQueue holds invoiceExpiry items and is used to find the next
|
|
// invoice to expire.
|
|
expiryQueue queue.PriorityQueue
|
|
|
|
// newInvoices channel is used to wake up the main loop when a new invoices
|
|
// is added.
|
|
newInvoices chan []*invoiceExpiry
|
|
|
|
wg sync.WaitGroup
|
|
|
|
// quit signals InvoiceExpiryWatcher to stop.
|
|
quit chan struct{}
|
|
}
|
|
|
|
// NewInvoiceExpiryWatcher creates a new InvoiceExpiryWatcher instance.
|
|
func NewInvoiceExpiryWatcher(clock clock.Clock) *InvoiceExpiryWatcher {
|
|
return &InvoiceExpiryWatcher{
|
|
clock: clock,
|
|
newInvoices: make(chan []*invoiceExpiry),
|
|
quit: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start starts the the subscription handler and the main loop. Start() will
|
|
// return with error if InvoiceExpiryWatcher is already started. Start()
|
|
// expects a cancellation function passed that will be use to cancel expired
|
|
// invoices by their payment hash.
|
|
func (ew *InvoiceExpiryWatcher) Start(
|
|
cancelInvoice func(lntypes.Hash, bool) error) error {
|
|
|
|
ew.Lock()
|
|
defer ew.Unlock()
|
|
|
|
if ew.started {
|
|
return fmt.Errorf("InvoiceExpiryWatcher already started")
|
|
}
|
|
|
|
ew.started = true
|
|
ew.cancelInvoice = cancelInvoice
|
|
ew.wg.Add(1)
|
|
go ew.mainLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop quits the expiry handler loop and waits for InvoiceExpiryWatcher to
|
|
// fully stop.
|
|
func (ew *InvoiceExpiryWatcher) Stop() {
|
|
ew.Lock()
|
|
defer ew.Unlock()
|
|
|
|
if ew.started {
|
|
// Signal subscriptionHandler to quit and wait for it to return.
|
|
close(ew.quit)
|
|
ew.wg.Wait()
|
|
ew.started = false
|
|
}
|
|
}
|
|
|
|
// prepareInvoice checks if the passed invoice may be canceled and calculates
|
|
// the expiry time.
|
|
func (ew *InvoiceExpiryWatcher) prepareInvoice(
|
|
paymentHash lntypes.Hash, invoice *channeldb.Invoice) *invoiceExpiry {
|
|
|
|
if invoice.State != channeldb.ContractOpen {
|
|
log.Debugf("Invoice not added to expiry watcher: %v", paymentHash)
|
|
return nil
|
|
}
|
|
|
|
realExpiry := invoice.Terms.Expiry
|
|
if realExpiry == 0 {
|
|
realExpiry = zpay32.DefaultInvoiceExpiry
|
|
}
|
|
|
|
expiry := invoice.CreationDate.Add(realExpiry)
|
|
return &invoiceExpiry{
|
|
PaymentHash: paymentHash,
|
|
Expiry: expiry,
|
|
Keysend: len(invoice.PaymentRequest) == 0,
|
|
}
|
|
}
|
|
|
|
// AddInvoices adds multiple invoices to the InvoiceExpiryWatcher.
|
|
func (ew *InvoiceExpiryWatcher) AddInvoices(
|
|
invoices []channeldb.InvoiceWithPaymentHash) {
|
|
|
|
invoicesWithExpiry := make([]*invoiceExpiry, 0, len(invoices))
|
|
for _, invoiceWithPaymentHash := range invoices {
|
|
newInvoiceExpiry := ew.prepareInvoice(
|
|
invoiceWithPaymentHash.PaymentHash, &invoiceWithPaymentHash.Invoice,
|
|
)
|
|
if newInvoiceExpiry != nil {
|
|
invoicesWithExpiry = append(invoicesWithExpiry, newInvoiceExpiry)
|
|
}
|
|
}
|
|
|
|
if len(invoicesWithExpiry) > 0 {
|
|
log.Debugf("Added %d invoices to the expiry watcher",
|
|
len(invoicesWithExpiry))
|
|
select {
|
|
case ew.newInvoices <- invoicesWithExpiry:
|
|
// Select on quit too so that callers won't get blocked in case
|
|
// of concurrent shutdown.
|
|
case <-ew.quit:
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddInvoice adds a new invoice to the InvoiceExpiryWatcher. This won't check
|
|
// if the invoice is already added and will only add invoices with ContractOpen
|
|
// state.
|
|
func (ew *InvoiceExpiryWatcher) AddInvoice(
|
|
paymentHash lntypes.Hash, invoice *channeldb.Invoice) {
|
|
|
|
newInvoiceExpiry := ew.prepareInvoice(paymentHash, invoice)
|
|
if newInvoiceExpiry != nil {
|
|
log.Debugf("Adding invoice '%v' to expiry watcher, expiration: %v",
|
|
paymentHash, newInvoiceExpiry.Expiry)
|
|
|
|
select {
|
|
case ew.newInvoices <- []*invoiceExpiry{newInvoiceExpiry}:
|
|
// Select on quit too so that callers won't get blocked in case
|
|
// of concurrent shutdown.
|
|
case <-ew.quit:
|
|
}
|
|
}
|
|
}
|
|
|
|
// nextExpiry returns a Time chan to wait on until the next invoice expires.
|
|
// If there are no active invoices, then it'll simply wait indefinitely.
|
|
func (ew *InvoiceExpiryWatcher) nextExpiry() <-chan time.Time {
|
|
if !ew.expiryQueue.Empty() {
|
|
top := ew.expiryQueue.Top().(*invoiceExpiry)
|
|
return ew.clock.TickAfter(top.Expiry.Sub(ew.clock.Now()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// cancelNextExpiredInvoice will cancel the next expired invoice and removes
|
|
// it from the expiry queue.
|
|
func (ew *InvoiceExpiryWatcher) cancelNextExpiredInvoice() {
|
|
if !ew.expiryQueue.Empty() {
|
|
top := ew.expiryQueue.Top().(*invoiceExpiry)
|
|
if !top.Expiry.Before(ew.clock.Now()) {
|
|
return
|
|
}
|
|
|
|
// Don't force-cancel already accepted invoices. An exception to
|
|
// this are auto-generated keysend invoices. Because those move
|
|
// to the Accepted state directly after being opened, the expiry
|
|
// field would never be used. Enabling cancellation for accepted
|
|
// keysend invoices creates a safety mechanism that can prevents
|
|
// channel force-closes.
|
|
err := ew.cancelInvoice(top.PaymentHash, top.Keysend)
|
|
if err != nil && err != channeldb.ErrInvoiceAlreadySettled &&
|
|
err != channeldb.ErrInvoiceAlreadyCanceled {
|
|
|
|
log.Errorf("Unable to cancel invoice: %v", top.PaymentHash)
|
|
}
|
|
|
|
ew.expiryQueue.Pop()
|
|
}
|
|
}
|
|
|
|
// mainLoop is a goroutine that receives new invoices and handles cancellation
|
|
// of expired invoices.
|
|
func (ew *InvoiceExpiryWatcher) mainLoop() {
|
|
defer ew.wg.Done()
|
|
|
|
for {
|
|
// Cancel any invoices that may have expired.
|
|
ew.cancelNextExpiredInvoice()
|
|
|
|
select {
|
|
|
|
case invoicesWithExpiry := <-ew.newInvoices:
|
|
// Take newly forwarded invoices with higher priority
|
|
// in order to not block the newInvoices channel.
|
|
for _, invoiceWithExpiry := range invoicesWithExpiry {
|
|
ew.expiryQueue.Push(invoiceWithExpiry)
|
|
}
|
|
continue
|
|
|
|
default:
|
|
select {
|
|
|
|
case <-ew.nextExpiry():
|
|
// Wait until the next invoice expires.
|
|
continue
|
|
|
|
case invoicesWithExpiry := <-ew.newInvoices:
|
|
for _, invoiceWithExpiry := range invoicesWithExpiry {
|
|
ew.expiryQueue.Push(invoiceWithExpiry)
|
|
}
|
|
|
|
case <-ew.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|