@ -5,6 +5,8 @@ import (
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes"
@ -34,6 +36,28 @@ func (e invoiceExpiryTs) Less(other queue.PriorityQueueItem) bool {
return e . Expiry . Before ( other . ( * invoiceExpiryTs ) . Expiry )
}
// Compile time assertion that invoiceExpiryHeight implements invoiceExpiry.
var _ invoiceExpiry = ( * invoiceExpiryHeight ) ( nil )
// invoiceExpiryHeight holds information about an invoice which can be used to
// cancel it based on its expiry height.
type invoiceExpiryHeight struct {
paymentHash lntypes . Hash
expiryHeight uint32
}
// Less implements PriorityQueueItem.Less such that the top item in the
// priority queue is the lowest block height.
func ( b invoiceExpiryHeight ) Less ( other queue . PriorityQueueItem ) bool {
return b . expiryHeight < other . ( * invoiceExpiryHeight ) . expiryHeight
}
// expired returns a boolean that indicates whether this entry has expired,
// taking our expiry delta into account.
func ( b invoiceExpiryHeight ) expired ( currentHeight , delta uint32 ) bool {
return currentHeight + delta >= b . expiryHeight
}
// 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
@ -49,6 +73,21 @@ type InvoiceExpiryWatcher struct {
// It is useful for testing.
clock clock . Clock
// notifier provides us with block height updates.
notifier chainntnfs . ChainNotifier
// blockExpiryDelta is the number of blocks before a htlc's expiry that
// we expire the invoice based on expiry height. We use a delta because
// we will go to some delta before our expiry, so we want to cancel
// before this to prevent force closes.
blockExpiryDelta uint32
// currentHeight is the current block height.
currentHeight uint32
// currentHash is the block hash for our current height.
currentHash * chainhash . Hash
// cancelInvoice is a template method that cancels an expired invoice.
cancelInvoice func ( lntypes . Hash , bool ) error
@ -56,6 +95,15 @@ type InvoiceExpiryWatcher struct {
// the next invoice to expire.
timestampExpiryQueue queue . PriorityQueue
// blockExpiryQueue holds blockExpiry items and is used to find the
// next invoice to expire based on block height. Only hold invoices
// with active htlcs are added to this queue, because they require
// manual cancellation when the hltc is going to time out. Items in
// this queue may already be in the timestampExpiryQueue, this is ok
// because they will not be expired based on timestamp if they have
// active htlcs.
blockExpiryQueue queue . PriorityQueue
// newInvoices channel is used to wake up the main loop when a new
// invoices is added.
newInvoices chan [ ] invoiceExpiry
@ -67,11 +115,18 @@ type InvoiceExpiryWatcher struct {
}
// NewInvoiceExpiryWatcher creates a new InvoiceExpiryWatcher instance.
func NewInvoiceExpiryWatcher ( clock clock . Clock ) * InvoiceExpiryWatcher {
func NewInvoiceExpiryWatcher ( clock clock . Clock ,
expiryDelta , startHeight uint32 , startHash * chainhash . Hash ,
notifier chainntnfs . ChainNotifier ) * InvoiceExpiryWatcher {
return & InvoiceExpiryWatcher {
clock : clock ,
newInvoices : make ( chan [ ] invoiceExpiry ) ,
quit : make ( chan struct { } ) ,
clock : clock ,
notifier : notifier ,
blockExpiryDelta : expiryDelta ,
currentHeight : startHeight ,
currentHash : startHash ,
newInvoices : make ( chan [ ] invoiceExpiry ) ,
quit : make ( chan struct { } ) ,
}
}
@ -91,8 +146,17 @@ func (ew *InvoiceExpiryWatcher) Start(
ew . started = true
ew . cancelInvoice = cancelInvoice
ntfn , err := ew . notifier . RegisterBlockEpochNtfn ( & chainntnfs . BlockEpoch {
Height : int32 ( ew . currentHeight ) ,
Hash : ew . currentHash ,
} )
if err != nil {
return err
}
ew . wg . Add ( 1 )
go ew . mainLoop ( )
go ew . mainLoop ( ntfn )
return nil
}
@ -122,6 +186,32 @@ func makeInvoiceExpiry(paymentHash lntypes.Hash,
case channeldb . ContractOpen :
return makeTimestampExpiry ( paymentHash , invoice )
// If an invoice has active htlcs, we want to expire it based on block
// height. We only do this for hodl invoices, since regular invoices
// should resolve themselves automatically.
case channeldb . ContractAccepted :
if ! invoice . HodlInvoice {
log . Debugf ( "Invoice in accepted state not added to " +
"expiry watcher: %v" , paymentHash )
return nil
}
var minHeight uint32
for _ , htlc := range invoice . Htlcs {
// We only care about accepted htlcs, since they will
// trigger force-closes.
if htlc . State != channeldb . HtlcStateAccepted {
continue
}
if minHeight == 0 || htlc . Expiry < minHeight {
minHeight = htlc . Expiry
}
}
return makeHeightExpiry ( paymentHash , minHeight )
default :
log . Debugf ( "Invoice not added to expiry watcher: %v" ,
paymentHash )
@ -151,18 +241,36 @@ func makeTimestampExpiry(paymentHash lntypes.Hash,
}
}
// makeHeightExpiry creates height-based expiry for an invoice based on its
// lowest htlc expiry height.
func makeHeightExpiry ( paymentHash lntypes . Hash ,
minHeight uint32 ) * invoiceExpiryHeight {
if minHeight == 0 {
log . Warnf ( "make height expiry called with 0 height" )
return nil
}
return & invoiceExpiryHeight {
paymentHash : paymentHash ,
expiryHeight : minHeight ,
}
}
// AddInvoices adds invoices to the InvoiceExpiryWatcher.
func ( ew * InvoiceExpiryWatcher ) AddInvoices ( invoices ... invoiceExpiry ) {
if len ( invoices ) > 0 {
select {
case ew . newInvoices <- invoices :
log . Debugf ( "Added %d invoices to the expiry watcher" ,
len ( invoices ) )
if len ( invoices ) == 0 {
return
}
// Select on quit too so that callers won't get blocked in case
// of concurrent shutdown.
case <- ew . quit :
}
select {
case ew . newInvoices <- invoices :
log . Debugf ( "Added %d invoices to the expiry watcher" ,
len ( invoices ) )
// Select on quit too so that callers won't get blocked in case
// of concurrent shutdown.
case <- ew . quit :
}
}
@ -178,6 +286,23 @@ func (ew *InvoiceExpiryWatcher) nextTimestampExpiry() <-chan time.Time {
return nil
}
// nextHeightExpiry returns a channel that will immediately be read from if
// the top item on our queue has expired.
func ( ew * InvoiceExpiryWatcher ) nextHeightExpiry ( ) <- chan uint32 {
if ew . blockExpiryQueue . Empty ( ) {
return nil
}
top := ew . blockExpiryQueue . Top ( ) . ( * invoiceExpiryHeight )
if ! top . expired ( ew . currentHeight , ew . blockExpiryDelta ) {
return nil
}
blockChan := make ( chan uint32 , 1 )
blockChan <- top . expiryHeight
return blockChan
}
// cancelNextExpiredInvoice will cancel the next expired invoice and removes
// it from the expiry queue.
func ( ew * InvoiceExpiryWatcher ) cancelNextExpiredInvoice ( ) {
@ -198,6 +323,25 @@ func (ew *InvoiceExpiryWatcher) cancelNextExpiredInvoice() {
}
}
// cancelNextHeightExpiredInvoice looks at our height based queue and expires
// the next invoice if we have reached its expiry block.
func ( ew * InvoiceExpiryWatcher ) cancelNextHeightExpiredInvoice ( ) {
if ew . blockExpiryQueue . Empty ( ) {
return
}
top := ew . blockExpiryQueue . Top ( ) . ( * invoiceExpiryHeight )
if ! top . expired ( ew . currentHeight , ew . blockExpiryDelta ) {
return
}
// We always force-cancel block-based expiry so that we can
// cancel invoices that have been accepted but not yet resolved.
// This helps us avoid force closes.
ew . expireInvoice ( top . paymentHash , true )
ew . blockExpiryQueue . Pop ( )
}
// expireInvoice attempts to expire an invoice and logs an error if we get an
// unexpected error.
func ( ew * InvoiceExpiryWatcher ) expireInvoice ( hash lntypes . Hash , force bool ) {
@ -226,6 +370,11 @@ func (ew *InvoiceExpiryWatcher) pushInvoices(invoices []invoiceExpiry) {
ew . timestampExpiryQueue . Push ( expiry )
}
case * invoiceExpiryHeight :
if expiry != nil {
ew . blockExpiryQueue . Push ( expiry )
}
default :
log . Errorf ( "unexpected queue item: %T" , inv )
}
@ -234,12 +383,20 @@ func (ew *InvoiceExpiryWatcher) pushInvoices(invoices []invoiceExpiry) {
// mainLoop is a goroutine that receives new invoices and handles cancellation
// of expired invoices.
func ( ew * InvoiceExpiryWatcher ) mainLoop ( ) {
defer ew . wg . Done ( )
func ( ew * InvoiceExpiryWatcher ) mainLoop ( blockNtfns * chainntnfs . BlockEpochEvent ) {
defer func ( ) {
blockNtfns . Cancel ( )
ew . wg . Done ( )
} ( )
// We have two different queues, so we use a different cancel method
// depending on which expiry condition we have hit. Starting with time
// based expiry is an arbitrary choice to start off.
cancelNext := ew . cancelNextExpiredInvoice
for {
// Cancel any invoices that may have expired.
ew . cancelNextExpiredInvoice ( )
cancelNext ( )
select {
@ -252,13 +409,29 @@ func (ew *InvoiceExpiryWatcher) mainLoop() {
default :
select {
// Wait until the next invoice expires.
case <- ew . nextTimestampExpiry ( ) :
// Wait until the next invoice expires.
cancelNext = ew . cancelNextExpiredInvoice
continue
case <- ew . nextHeightExpiry ( ) :
cancelNext = ew . cancelNextHeightExpiredInvoice
continue
case newInvoices := <- ew . newInvoices :
ew . pushInvoices ( newInvoices )
// Consume new blocks.
case block , ok := <- blockNtfns . Epochs :
if ! ok {
log . Debugf ( "block notifications " +
"canceled" )
return
}
ew . currentHeight = uint32 ( block . Height )
ew . currentHash = block . Hash
case <- ew . quit :
return
}