htlcswitch: hodl invoice

This commit modifies the invoice registry to handle invoices for which
the preimage is not known yet (hodl invoices). In that case, the
resolution channel passed in from links and resolvers is stored until we
either learn the preimage or want to cancel the htlc.
This commit is contained in:
Joost Jager 2019-02-11 12:01:05 +01:00
parent 1f41a2abce
commit 32f2b047e8
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
14 changed files with 1192 additions and 646 deletions

View File

@ -99,7 +99,7 @@ func TestInvoiceWorkflow(t *testing.T) {
// now have the settled bit toggle to true and a non-default // now have the settled bit toggle to true and a non-default
// SettledDate // SettledDate
payAmt := fakeInvoice.Terms.Value * 2 payAmt := fakeInvoice.Terms.Value * 2
if _, err := db.SettleInvoice(paymentHash, payAmt); err != nil { if _, err := db.AcceptOrSettleInvoice(paymentHash, payAmt); err != nil {
t.Fatalf("unable to settle invoice: %v", err) t.Fatalf("unable to settle invoice: %v", err)
} }
dbInvoice2, err := db.LookupInvoice(paymentHash) dbInvoice2, err := db.LookupInvoice(paymentHash)
@ -261,7 +261,7 @@ func TestInvoiceAddTimeSeries(t *testing.T) {
paymentHash := invoice.Terms.PaymentPreimage.Hash() paymentHash := invoice.Terms.PaymentPreimage.Hash()
_, err := db.SettleInvoice(paymentHash, 0) _, err := db.AcceptOrSettleInvoice(paymentHash, 0)
if err != nil { if err != nil {
t.Fatalf("unable to settle invoice: %v", err) t.Fatalf("unable to settle invoice: %v", err)
} }
@ -342,7 +342,7 @@ func TestDuplicateSettleInvoice(t *testing.T) {
} }
// With the invoice in the DB, we'll now attempt to settle the invoice. // With the invoice in the DB, we'll now attempt to settle the invoice.
dbInvoice, err := db.SettleInvoice(payHash, amt) dbInvoice, err := db.AcceptOrSettleInvoice(payHash, amt)
if err != nil { if err != nil {
t.Fatalf("unable to settle invoice: %v", err) t.Fatalf("unable to settle invoice: %v", err)
} }
@ -362,7 +362,7 @@ func TestDuplicateSettleInvoice(t *testing.T) {
// If we try to settle the invoice again, then we should get the very // If we try to settle the invoice again, then we should get the very
// same invoice back, but with an error this time. // same invoice back, but with an error this time.
dbInvoice, err = db.SettleInvoice(payHash, amt) dbInvoice, err = db.AcceptOrSettleInvoice(payHash, amt)
if err != ErrInvoiceAlreadySettled { if err != ErrInvoiceAlreadySettled {
t.Fatalf("expected ErrInvoiceAlreadySettled") t.Fatalf("expected ErrInvoiceAlreadySettled")
} }
@ -407,7 +407,7 @@ func TestQueryInvoices(t *testing.T) {
// We'll only settle half of all invoices created. // We'll only settle half of all invoices created.
if i%2 == 0 { if i%2 == 0 {
if _, err := db.SettleInvoice(paymentHash, i); err != nil { if _, err := db.AcceptOrSettleInvoice(paymentHash, i); err != nil {
t.Fatalf("unable to settle invoice: %v", err) t.Fatalf("unable to settle invoice: %v", err)
} }
} }

View File

@ -69,6 +69,13 @@ var (
// ErrInvoiceAlreadyCanceled is returned when the invoice is already // ErrInvoiceAlreadyCanceled is returned when the invoice is already
// canceled. // canceled.
ErrInvoiceAlreadyCanceled = errors.New("invoice already canceled") ErrInvoiceAlreadyCanceled = errors.New("invoice already canceled")
// ErrInvoiceAlreadyAccepted is returned when the invoice is already
// accepted.
ErrInvoiceAlreadyAccepted = errors.New("invoice already accepted")
// ErrInvoiceStillOpen is returned when the invoice is still open.
ErrInvoiceStillOpen = errors.New("invoice still open")
) )
const ( const (
@ -100,6 +107,10 @@ const (
// ContractCanceled means the invoice has been canceled. // ContractCanceled means the invoice has been canceled.
ContractCanceled ContractState = 2 ContractCanceled ContractState = 2
// ContractAccepted means the HTLC has been accepted but not settled
// yet.
ContractAccepted ContractState = 3
) )
// String returns a human readable identifier for the ContractState type. // String returns a human readable identifier for the ContractState type.
@ -111,6 +122,8 @@ func (c ContractState) String() string {
return "Settled" return "Settled"
case ContractCanceled: case ContractCanceled:
return "Canceled" return "Canceled"
case ContractAccepted:
return "Accepted"
} }
return "Unknown" return "Unknown"
@ -611,11 +624,14 @@ func (d *DB) QueryInvoices(q InvoiceQuery) (InvoiceSlice, error) {
return resp, nil return resp, nil
} }
// SettleInvoice attempts to mark an invoice corresponding to the passed // AcceptOrSettleInvoice attempts to mark an invoice corresponding to the passed
// payment hash as fully settled. If an invoice matching the passed payment // payment hash as settled. If an invoice matching the passed payment hash
// hash doesn't existing within the database, then the action will fail with a // doesn't existing within the database, then the action will fail with a "not
// "not found" error. // found" error.
func (d *DB) SettleInvoice(paymentHash [32]byte, //
// When the preimage for the invoice is unknown (hold invoice), the invoice is
// marked as accepted.
func (d *DB) AcceptOrSettleInvoice(paymentHash [32]byte,
amtPaid lnwire.MilliSatoshi) (*Invoice, error) { amtPaid lnwire.MilliSatoshi) (*Invoice, error) {
var settledInvoice *Invoice var settledInvoice *Invoice
@ -644,7 +660,7 @@ func (d *DB) SettleInvoice(paymentHash [32]byte,
return ErrInvoiceNotFound return ErrInvoiceNotFound
} }
settledInvoice, err = settleInvoice( settledInvoice, err = acceptOrSettleInvoice(
invoices, settleIndex, invoiceNum, amtPaid, invoices, settleIndex, invoiceNum, amtPaid,
) )
@ -654,6 +670,46 @@ func (d *DB) SettleInvoice(paymentHash [32]byte,
return settledInvoice, err return settledInvoice, err
} }
// SettleHoldInvoice sets the preimage of a hodl invoice and marks the invoice
// as settled.
func (d *DB) SettleHoldInvoice(preimage lntypes.Preimage) (*Invoice, error) {
var updatedInvoice *Invoice
hash := preimage.Hash()
err := d.Update(func(tx *bbolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(
invoiceIndexBucket,
)
if err != nil {
return err
}
settleIndex, err := invoices.CreateBucketIfNotExists(
settleIndexBucket,
)
if err != nil {
return err
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(hash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
updatedInvoice, err = settleHoldInvoice(
invoices, settleIndex, invoiceNum, preimage,
)
return err
})
return updatedInvoice, err
}
// CancelInvoice attempts to cancel the invoice corresponding to the passed // CancelInvoice attempts to cancel the invoice corresponding to the passed
// payment hash. // payment hash.
func (d *DB) CancelInvoice(paymentHash lntypes.Hash) (*Invoice, error) { func (d *DB) CancelInvoice(paymentHash lntypes.Hash) (*Invoice, error) {
@ -932,7 +988,7 @@ func deserializeInvoice(r io.Reader) (Invoice, error) {
return invoice, nil return invoice, nil
} }
func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte, func acceptOrSettleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
amtPaid lnwire.MilliSatoshi) (*Invoice, error) { amtPaid lnwire.MilliSatoshi) (*Invoice, error) {
invoice, err := fetchInvoice(invoiceNum, invoices) invoice, err := fetchInvoice(invoiceNum, invoices)
@ -940,32 +996,90 @@ func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
return nil, err return nil, err
} }
switch invoice.Terms.State { state := invoice.Terms.State
case ContractSettled:
switch {
case state == ContractAccepted:
return &invoice, ErrInvoiceAlreadyAccepted
case state == ContractSettled:
return &invoice, ErrInvoiceAlreadySettled return &invoice, ErrInvoiceAlreadySettled
case ContractCanceled: case state == ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled return &invoice, ErrInvoiceAlreadyCanceled
} }
holdInvoice := invoice.Terms.PaymentPreimage == UnknownPreimage
if holdInvoice {
invoice.Terms.State = ContractAccepted
} else {
err := setSettleFields(settleIndex, invoiceNum, &invoice)
if err != nil {
return nil, err
}
}
invoice.AmtPaid = amtPaid
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err
}
if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
return nil, err
}
return &invoice, nil
}
func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte,
invoice *Invoice) error {
// Now that we know the invoice hasn't already been settled, we'll // Now that we know the invoice hasn't already been settled, we'll
// update the settle index so we can place this settle event in the // update the settle index so we can place this settle event in the
// proper location within our time series. // proper location within our time series.
nextSettleSeqNo, err := settleIndex.NextSequence() nextSettleSeqNo, err := settleIndex.NextSequence()
if err != nil { if err != nil {
return nil, err return err
} }
var seqNoBytes [8]byte var seqNoBytes [8]byte
byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo) byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo)
if err := settleIndex.Put(seqNoBytes[:], invoiceNum); err != nil { if err := settleIndex.Put(seqNoBytes[:], invoiceNum); err != nil {
return nil, err return err
} }
invoice.AmtPaid = amtPaid
invoice.Terms.State = ContractSettled invoice.Terms.State = ContractSettled
invoice.SettleDate = time.Now() invoice.SettleDate = time.Now()
invoice.SettleIndex = nextSettleSeqNo invoice.SettleIndex = nextSettleSeqNo
return nil
}
func settleHoldInvoice(invoices, settleIndex *bbolt.Bucket,
invoiceNum []byte, preimage lntypes.Preimage) (*Invoice,
error) {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return nil, err
}
switch invoice.Terms.State {
case ContractOpen:
return &invoice, ErrInvoiceStillOpen
case ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled
case ContractSettled:
return &invoice, ErrInvoiceAlreadySettled
}
invoice.Terms.PaymentPreimage = preimage
err = setSettleFields(settleIndex, invoiceNum, &invoice)
if err != nil {
return nil, err
}
var buf bytes.Buffer var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil { if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err return nil, err
@ -995,6 +1109,9 @@ func cancelInvoice(invoices *bbolt.Bucket, invoiceNum []byte) (
invoice.Terms.State = ContractCanceled invoice.Terms.State = ContractCanceled
// Set AmtPaid back to 0, in case the invoice was already accepted.
invoice.AmtPaid = 0
var buf bytes.Buffer var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil { if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err return nil, err

View File

@ -1,11 +1,13 @@
package contractcourt package contractcourt
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/invoices"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
) )
@ -70,11 +72,18 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
return nil, h.Checkpoint(h) return nil, h.Checkpoint(h)
} }
// applyPreimage is a helper function that will populate our internal // tryApplyPreimage is a helper function that will populate our internal
// resolver with the preimage we learn of. This should be called once // resolver with the preimage we learn of. This should be called once
// the preimage is revealed so the inner resolver can properly complete // the preimage is revealed so the inner resolver can properly complete
// its duties. // its duties. The boolean return value indicates whether the preimage
applyPreimage := func(preimage lntypes.Preimage) { // was properly applied.
tryApplyPreimage := func(preimage lntypes.Preimage) bool {
// Check to see if this preimage matches our htlc.
if !preimage.Matches(h.payHash) {
return false
}
// Update htlcResolution with the matching preimage.
h.htlcResolution.Preimage = preimage h.htlcResolution.Preimage = preimage
log.Infof("%T(%v): extracted preimage=%v from beacon!", h, log.Infof("%T(%v): extracted preimage=%v from beacon!", h,
@ -93,6 +102,8 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
// preimage. // preimage.
h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:] h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:]
} }
return true
} }
// If the HTLC hasn't expired yet, then we may still be able to claim // If the HTLC hasn't expired yet, then we may still be able to claim
@ -112,6 +123,26 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
blockEpochs.Cancel() blockEpochs.Cancel()
}() }()
// Create a buffered hodl chan to prevent deadlock.
hodlChan := make(chan interface{}, 1)
// Notify registry that we are potentially settling as exit hop
// on-chain, so that we will get a hodl event when a corresponding hodl
// invoice is settled.
event, err := h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt, hodlChan)
if err != nil && err != channeldb.ErrInvoiceNotFound {
return nil, err
}
defer h.Registry.HodlUnsubscribeAll(hodlChan)
// If the htlc can be settled directly, we can progress to the inner
// resolver immediately.
if event != nil && event.Preimage != nil {
if tryApplyPreimage(*event.Preimage) {
return &h.htlcSuccessResolver, nil
}
}
// With the epochs and preimage subscriptions initialized, we'll query // With the epochs and preimage subscriptions initialized, we'll query
// to see if we already know the preimage. // to see if we already know the preimage.
preimage, ok := h.PreimageDB.LookupPreimage(h.payHash) preimage, ok := h.PreimageDB.LookupPreimage(h.payHash)
@ -119,26 +150,35 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
// If we do, then this means we can claim the HTLC! However, // If we do, then this means we can claim the HTLC! However,
// we don't know how to ourselves, so we'll return our inner // we don't know how to ourselves, so we'll return our inner
// resolver which has the knowledge to do so. // resolver which has the knowledge to do so.
applyPreimage(preimage) if tryApplyPreimage(preimage) {
return &h.htlcSuccessResolver, nil return &h.htlcSuccessResolver, nil
}
} }
for { for {
select { select {
case preimage := <-preimageSubscription.WitnessUpdates: case preimage := <-preimageSubscription.WitnessUpdates:
// If this isn't our preimage, then we'll continue if !tryApplyPreimage(preimage) {
// onwards.
hash := preimage.Hash()
preimageMatches := bytes.Equal(hash[:], h.payHash[:])
if !preimageMatches {
continue continue
} }
// Otherwise, we've learned of the preimage! We'll add // We've learned of the preimage and this information
// this information to our inner resolver, then return // has been added to our inner resolver. We return it so
// it so it can continue contract resolution. // it can continue contract resolution.
applyPreimage(preimage) return &h.htlcSuccessResolver, nil
case hodlItem := <-hodlChan:
hodlEvent := hodlItem.(invoices.HodlEvent)
// Only process settle events.
if hodlEvent.Preimage == nil {
continue
}
if !tryApplyPreimage(*hodlEvent.Preimage) {
continue
}
return &h.htlcSuccessResolver, nil return &h.htlcSuccessResolver, nil
case newBlock, ok := <-blockEpochs.Epochs: case newBlock, ok := <-blockEpochs.Epochs:

View File

@ -180,8 +180,11 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
// With the HTLC claimed, we can attempt to settle its // With the HTLC claimed, we can attempt to settle its
// corresponding invoice if we were the original destination. As // corresponding invoice if we were the original destination. As
// the htlc is already settled at this point, we don't need to // the htlc is already settled at this point, we don't need to
// process the result. // read on the hodl channel.
_, err = h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt) hodlChan := make(chan interface{}, 1)
_, err = h.Registry.NotifyExitHopHtlc(
h.payHash, h.htlcAmt, hodlChan,
)
if err != nil && err != channeldb.ErrInvoiceNotFound { if err != nil && err != channeldb.ErrInvoiceNotFound {
log.Errorf("Unable to settle invoice with payment "+ log.Errorf("Unable to settle invoice with payment "+
"hash %x: %v", h.payHash, err) "hash %x: %v", h.payHash, err)
@ -254,8 +257,10 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
// With the HTLC claimed, we can attempt to settle its corresponding // With the HTLC claimed, we can attempt to settle its corresponding
// invoice if we were the original destination. As the htlc is already // invoice if we were the original destination. As the htlc is already
// settled at this point, we don't need to read the result. // settled at this point, we don't need to read on the hodl
_, err = h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt) // channel.
hodlChan := make(chan interface{}, 1)
_, err = h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt, hodlChan)
if err != nil && err != channeldb.ErrInvoiceNotFound { if err != nil && err != channeldb.ErrInvoiceNotFound {
log.Errorf("Unable to settle invoice with payment "+ log.Errorf("Unable to settle invoice with payment "+
"hash %x: %v", h.payHash, err) "hash %x: %v", h.payHash, err)

View File

@ -21,13 +21,20 @@ type InvoiceDatabase interface {
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the // NotifyExitHopHtlc attempts to mark an invoice as settled. If the
// invoice is a debug invoice, then this method is a noop as debug // invoice is a debug invoice, then this method is a noop as debug
// invoices are never fully settled. The return value describes how the // invoices are never fully settled. The return value describes how the
// htlc should be resolved. // htlc should be resolved. If the htlc cannot be resolved immediately,
NotifyExitHopHtlc(rhash lntypes.Hash, amt lnwire.MilliSatoshi) ( // the resolution is sent on the passed in hodlChan later.
*invoices.HodlEvent, error) NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
hodlChan chan<- interface{}) (*invoices.HodlEvent, error)
// CancelInvoice attempts to cancel the invoice corresponding to the // CancelInvoice attempts to cancel the invoice corresponding to the
// passed payment hash. // passed payment hash.
CancelInvoice(payHash lntypes.Hash) error CancelInvoice(payHash lntypes.Hash) error
// SettleHodlInvoice settles a hold invoice.
SettleHodlInvoice(preimage lntypes.Preimage) error
// HodlUnsubscribeAll unsubscribes from all hodl events.
HodlUnsubscribeAll(subscriber chan<- interface{})
} }
// ChannelLink is an interface which represents the subsystem for managing the // ChannelLink is an interface which represents the subsystem for managing the

View File

@ -21,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/ticker"
) )
@ -345,6 +346,14 @@ type channelLink struct {
sync.RWMutex sync.RWMutex
// hodlQueue is used to receive exit hop htlc resolutions from invoice
// registry.
hodlQueue *queue.ConcurrentQueue
// hodlMap stores a list of htlc data structs per hash. It allows
// resolving those htlcs when we receive a message on hodlQueue.
hodlMap map[lntypes.Hash][]hodlHtlc
wg sync.WaitGroup wg sync.WaitGroup
quit chan struct{} quit chan struct{}
} }
@ -368,6 +377,8 @@ func NewChannelLink(cfg ChannelLinkConfig,
logCommitTimer: time.NewTimer(300 * time.Millisecond), logCommitTimer: time.NewTimer(300 * time.Millisecond),
overflowQueue: newPacketQueue(input.MaxHTLCNumber / 2), overflowQueue: newPacketQueue(input.MaxHTLCNumber / 2),
htlcUpdates: make(chan []channeldb.HTLC), htlcUpdates: make(chan []channeldb.HTLC),
hodlMap: make(map[lntypes.Hash][]hodlHtlc),
hodlQueue: queue.NewConcurrentQueue(10),
quit: make(chan struct{}), quit: make(chan struct{}),
} }
} }
@ -391,6 +402,7 @@ func (l *channelLink) Start() error {
l.mailBox.ResetMessages() l.mailBox.ResetMessages()
l.overflowQueue.Start() l.overflowQueue.Start()
l.hodlQueue.Start()
// Before launching the htlcManager messages, revert any circuits that // Before launching the htlcManager messages, revert any circuits that
// were marked open in the switch's circuit map, but did not make it // were marked open in the switch's circuit map, but did not make it
@ -456,12 +468,17 @@ func (l *channelLink) Stop() {
log.Infof("ChannelLink(%v) is stopping", l) log.Infof("ChannelLink(%v) is stopping", l)
// As the link is stopping, we are no longer interested in hodl events
// coming from the invoice registry.
l.cfg.Registry.HodlUnsubscribeAll(l.hodlQueue.ChanIn())
if l.cfg.ChainEvents.Cancel != nil { if l.cfg.ChainEvents.Cancel != nil {
l.cfg.ChainEvents.Cancel() l.cfg.ChainEvents.Cancel()
} }
l.updateFeeTimer.Stop() l.updateFeeTimer.Stop()
l.overflowQueue.Stop() l.overflowQueue.Stop()
l.hodlQueue.Stop()
close(l.quit) close(l.quit)
l.wg.Wait() l.wg.Wait()
@ -1065,12 +1082,75 @@ out:
case msg := <-l.upstream: case msg := <-l.upstream:
l.handleUpstreamMsg(msg) l.handleUpstreamMsg(msg)
// A hodl event is received. This means that we now have a
// resolution for a previously accepted htlc.
case hodlItem := <-l.hodlQueue.ChanOut():
hodlEvent := hodlItem.(invoices.HodlEvent)
err := l.processHodlQueue(hodlEvent)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
fmt.Sprintf("process hodl queue: %v",
err.Error()),
)
break out
}
case <-l.quit: case <-l.quit:
break out break out
} }
} }
} }
// processHodlQueue processes a received hodl event and continues reading from
// the hodl queue until no more events remain. When this function returns
// without an error, the commit tx should be updated.
func (l *channelLink) processHodlQueue(firstHodlEvent invoices.HodlEvent) error {
// Try to read all waiting resolution messages, so that they can all be
// processed in a single commitment tx update.
hodlEvent := firstHodlEvent
loop:
for {
if err := l.processHodlMapEvent(hodlEvent); err != nil {
return err
}
select {
case item := <-l.hodlQueue.ChanOut():
hodlEvent = item.(invoices.HodlEvent)
default:
break loop
}
}
// Update the commitment tx.
if err := l.updateCommitTx(); err != nil {
return fmt.Errorf("unable to update commitment: %v", err)
}
return nil
}
// processHodlMapEvent resolves stored hodl htlcs based using the information in
// hodlEvent.
func (l *channelLink) processHodlMapEvent(hodlEvent invoices.HodlEvent) error {
// Lookup all hodl htlcs that can be failed or settled with this event.
// The hodl htlc must be present in the map.
hash := hodlEvent.Hash
hodlHtlcs, ok := l.hodlMap[hash]
if !ok {
return fmt.Errorf("hodl htlc not found: %v", hash)
}
if err := l.processHodlEvent(hodlEvent, hodlHtlcs...); err != nil {
return err
}
// Clean up hodl map.
delete(l.hodlMap, hash)
return nil
}
// processHodlEvent applies a received hodl event to the provided htlc. When // processHodlEvent applies a received hodl event to the provided htlc. When
// this function returns without an error, the commit tx should be updated. // this function returns without an error, the commit tx should be updated.
func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent, func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
@ -2620,19 +2700,6 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
return true, nil return true, nil
} }
// Reject invoices with unknown preimages.
if invoice.Terms.PaymentPreimage == channeldb.UnknownPreimage {
log.Errorf("rejecting htlc because preimage is unknown")
failure := lnwire.NewFailUnknownPaymentHash(pd.Amount)
l.sendHTLCError(
pd.HtlcIndex, failure, obfuscator,
pd.SourceRef,
)
return true, nil
}
// If the invoice is already settled, we choose to accept the payment to // If the invoice is already settled, we choose to accept the payment to
// simplify failure recovery. // simplify failure recovery.
// //
@ -2729,22 +2796,31 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
// after this, this code will be re-executed after restart. We will // after this, this code will be re-executed after restart. We will
// receive back a resolution event. // receive back a resolution event.
event, err := l.cfg.Registry.NotifyExitHopHtlc( event, err := l.cfg.Registry.NotifyExitHopHtlc(
invoiceHash, pd.Amount, invoiceHash, pd.Amount, l.hodlQueue.ChanIn(),
) )
if err != nil { if err != nil {
return false, err return false, err
} }
// Process the received resolution. // Create a hodlHtlc struct and decide either resolved now or later.
htlc := hodlHtlc{ htlc := hodlHtlc{
pd: pd, pd: pd,
obfuscator: obfuscator, obfuscator: obfuscator,
} }
if event == nil {
// Save payment descriptor for future reference.
hodlHtlcs := l.hodlMap[invoiceHash]
l.hodlMap[invoiceHash] = append(hodlHtlcs, htlc)
return false, nil
}
// Process the received resolution.
err = l.processHodlEvent(*event, htlc) err = l.processHodlEvent(*event, htlc)
if err != nil { if err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }

View File

@ -759,18 +759,23 @@ func (i *mockInvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoi
return i.registry.LookupInvoice(rHash) return i.registry.LookupInvoice(rHash)
} }
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash, func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
amt lnwire.MilliSatoshi) (*invoices.HodlEvent, error) { return i.registry.SettleHodlInvoice(preimage)
}
event, err := i.registry.NotifyExitHopHtlc(rhash, amt) func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, hodlChan chan<- interface{}) (
*invoices.HodlEvent, error) {
event, err := i.registry.NotifyExitHopHtlc(rhash, amt, hodlChan)
if err != nil { if err != nil {
return event, err return nil, err
} }
if i.settleChan != nil { if i.settleChan != nil {
i.settleChan <- rhash i.settleChan <- rhash
} }
return event, err return event, nil
} }
func (i *mockInvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error { func (i *mockInvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
@ -784,6 +789,10 @@ func (i *mockInvoiceRegistry) AddInvoice(invoice channeldb.Invoice,
return err return err
} }
func (i *mockInvoiceRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {
i.registry.HodlUnsubscribeAll(subscriber)
}
var _ InvoiceDatabase = (*mockInvoiceRegistry)(nil) var _ InvoiceDatabase = (*mockInvoiceRegistry)(nil)
type mockSigner struct { type mockSigner struct {

View File

@ -62,6 +62,14 @@ type InvoiceRegistry struct {
// value from the payment request. // value from the payment request.
decodeFinalCltvExpiry func(invoice string) (uint32, error) decodeFinalCltvExpiry func(invoice string) (uint32, error)
// subscriptions is a map from a payment hash to a list of subscribers.
// It is used for efficient notification of links.
hodlSubscriptions map[lntypes.Hash]map[chan<- interface{}]struct{}
// reverseSubscriptions tracks hashes subscribed to per subscriber. This
// is used to unsubscribe from all hashes efficiently.
hodlReverseSubscriptions map[chan<- interface{}]map[lntypes.Hash]struct{}
wg sync.WaitGroup wg sync.WaitGroup
quit chan struct{} quit chan struct{}
} }
@ -82,6 +90,8 @@ func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
newSingleSubscriptions: make(chan *SingleInvoiceSubscription), newSingleSubscriptions: make(chan *SingleInvoiceSubscription),
subscriptionCancels: make(chan uint32), subscriptionCancels: make(chan uint32),
invoiceEvents: make(chan *invoiceEvent, 100), invoiceEvents: make(chan *invoiceEvent, 100),
hodlSubscriptions: make(map[lntypes.Hash]map[chan<- interface{}]struct{}),
hodlReverseSubscriptions: make(map[chan<- interface{}]map[lntypes.Hash]struct{}),
decodeFinalCltvExpiry: decodeFinalCltvExpiry, decodeFinalCltvExpiry: decodeFinalCltvExpiry,
quit: make(chan struct{}), quit: make(chan struct{}),
} }
@ -171,8 +181,10 @@ func (i *InvoiceRegistry) invoiceEventNotifier() {
// dispatch notifications to all registered clients. // dispatch notifications to all registered clients.
case event := <-i.invoiceEvents: case event := <-i.invoiceEvents:
// For backwards compatibility, do not notify all // For backwards compatibility, do not notify all
// invoice subscribers of cancel events. // invoice subscribers of cancel and accept events.
if event.state != channeldb.ContractCanceled { if event.state != channeldb.ContractCanceled &&
event.state != channeldb.ContractAccepted {
i.dispatchToClients(event) i.dispatchToClients(event)
} }
i.dispatchToSingleClients(event) i.dispatchToSingleClients(event)
@ -449,8 +461,17 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a // NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a
// debug invoice, then this method is a noop as debug invoices are never fully // debug invoice, then this method is a noop as debug invoices are never fully
// settled. The return value describes how the htlc should be resolved. // settled. The return value describes how the htlc should be resolved.
//
// When the preimage of the invoice is not yet known (hodl invoice), this
// function moves the invoice to the accepted state. When SettleHoldInvoice is
// called later, a resolution message will be send back to the caller via the
// provided hodlChan. Invoice registry sends on this channel what action needs
// to be taken on the htlc (settle or cancel). The caller needs to ensure that
// the channel is either buffered or received on from another goroutine to
// prevent deadlock.
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi) (*HodlEvent, error) { amtPaid lnwire.MilliSatoshi, hodlChan chan<- interface{}) (
*HodlEvent, error) {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()
@ -474,7 +495,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
// If this isn't a debug invoice, then we'll attempt to settle an // If this isn't a debug invoice, then we'll attempt to settle an
// invoice matching this rHash on disk (if one exists). // invoice matching this rHash on disk (if one exists).
invoice, err := i.cdb.SettleInvoice(rHash, amtPaid) invoice, err := i.cdb.AcceptOrSettleInvoice(rHash, amtPaid)
switch err { switch err {
// If invoice is already settled, settle htlc. This means we accept more // If invoice is already settled, settle htlc. This means we accept more
@ -486,14 +507,55 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
case channeldb.ErrInvoiceAlreadyCanceled: case channeldb.ErrInvoiceAlreadyCanceled:
return createEvent(nil), nil return createEvent(nil), nil
// If this call settled the invoice, settle the htlc. // If invoice is already accepted, add this htlc to the list of
// subscribers.
case channeldb.ErrInvoiceAlreadyAccepted:
i.hodlSubscribe(hodlChan, rHash)
return nil, nil
// If this call settled the invoice, settle the htlc. Otherwise
// subscribe for a future hodl event.
case nil: case nil:
i.notifyClients(rHash, invoice, invoice.Terms.State) i.notifyClients(rHash, invoice, invoice.Terms.State)
return createEvent(&invoice.Terms.PaymentPreimage), nil switch invoice.Terms.State {
case channeldb.ContractSettled:
return createEvent(&invoice.Terms.PaymentPreimage), nil
case channeldb.ContractAccepted:
// Subscribe to updates to this invoice.
i.hodlSubscribe(hodlChan, rHash)
return nil, nil
default:
return nil, fmt.Errorf("unexpected invoice state %v",
invoice.Terms.State)
}
default:
return nil, err
}
}
// SettleHodlInvoice sets the preimage of a hodl invoice.
func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
i.Lock()
defer i.Unlock()
invoice, err := i.cdb.SettleHoldInvoice(preimage)
if err != nil {
log.Errorf("Invoice SetPreimage %v: %v", preimage, err)
return err
} }
// If another error occurred, return that. hash := preimage.Hash()
return nil, err log.Infof("Notifying clients of set preimage to %v",
invoice.Terms.PaymentPreimage)
i.notifyHodlSubscribers(HodlEvent{
Hash: hash,
Preimage: &preimage,
})
i.notifyClients(hash, invoice, invoice.Terms.State)
return nil
} }
// CancelInvoice attempts to cancel the invoice corresponding to the passed // CancelInvoice attempts to cancel the invoice corresponding to the passed
@ -512,13 +574,14 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
log.Debugf("Invoice %v already canceled", payHash) log.Debugf("Invoice %v already canceled", payHash)
return nil return nil
} }
if err != nil { if err != nil {
return err return err
} }
log.Infof("Invoice %v canceled", payHash) log.Infof("Invoice %v canceled", payHash)
i.notifyHodlSubscribers(HodlEvent{
Hash: payHash,
})
i.notifyClients(payHash, invoice, channeldb.ContractCanceled) i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
return nil return nil
@ -770,3 +833,60 @@ func (i *InvoiceRegistry) SubscribeSingleInvoice(
return client return client
} }
// notifyHodlSubscribers sends out the hodl event to all current subscribers.
func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
subscribers, ok := i.hodlSubscriptions[hodlEvent.Hash]
if !ok {
return
}
// Notify all interested subscribers and remove subscription from both
// maps. The subscription can be removed as there only ever will be a
// single resolution for each hash.
for subscriber := range subscribers {
select {
case subscriber <- hodlEvent:
case <-i.quit:
return
}
delete(i.hodlReverseSubscriptions[subscriber], hodlEvent.Hash)
}
delete(i.hodlSubscriptions, hodlEvent.Hash)
}
// hodlSubscribe adds a new invoice subscription.
func (i *InvoiceRegistry) hodlSubscribe(subscriber chan<- interface{},
hash lntypes.Hash) {
log.Debugf("Hodl subscribe for %v", hash)
subscriptions, ok := i.hodlSubscriptions[hash]
if !ok {
subscriptions = make(map[chan<- interface{}]struct{})
i.hodlSubscriptions[hash] = subscriptions
}
subscriptions[subscriber] = struct{}{}
reverseSubscriptions, ok := i.hodlReverseSubscriptions[subscriber]
if !ok {
reverseSubscriptions = make(map[lntypes.Hash]struct{})
i.hodlReverseSubscriptions[subscriber] = reverseSubscriptions
}
reverseSubscriptions[hash] = struct{}{}
}
// HodlUnsubscribeAll cancels the subscription.
func (i *InvoiceRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {
i.Lock()
defer i.Unlock()
hashes := i.hodlReverseSubscriptions[subscriber]
for hash := range hashes {
delete(i.hodlSubscriptions[hash], subscriber)
}
delete(i.hodlReverseSubscriptions, subscriber)
}

View File

@ -117,9 +117,11 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("no update received") t.Fatal("no update received")
} }
hodlChan := make(chan interface{}, 1)
// Settle invoice with a slightly higher amount. // Settle invoice with a slightly higher amount.
amtPaid := lnwire.MilliSatoshi(100500) amtPaid := lnwire.MilliSatoshi(100500)
_, err = registry.NotifyExitHopHtlc(hash, amtPaid) _, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -151,13 +153,13 @@ func TestSettleInvoice(t *testing.T) {
} }
// Try to settle again. // Try to settle again.
_, err = registry.NotifyExitHopHtlc(hash, amtPaid) _, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
if err != nil { if err != nil {
t.Fatal("expected duplicate settle to succeed") t.Fatal("expected duplicate settle to succeed")
} }
// Try to settle again with a different amount. // Try to settle again with a different amount.
_, err = registry.NotifyExitHopHtlc(hash, amtPaid+600) _, err = registry.NotifyExitHopHtlc(hash, amtPaid+600, hodlChan)
if err != nil { if err != nil {
t.Fatal("expected duplicate settle to succeed") t.Fatal("expected duplicate settle to succeed")
} }
@ -176,6 +178,13 @@ func TestSettleInvoice(t *testing.T) {
if err != channeldb.ErrInvoiceAlreadySettled { if err != channeldb.ErrInvoiceAlreadySettled {
t.Fatal("expected cancelation of a settled invoice to fail") t.Fatal("expected cancelation of a settled invoice to fail")
} }
// As this is a direct sette, we expect nothing on the hodl chan.
select {
case <-hodlChan:
t.Fatal("unexpected event")
default:
}
} }
// TestCancelInvoice tests cancelation of an invoice and related notifications. // TestCancelInvoice tests cancelation of an invoice and related notifications.
@ -264,7 +273,8 @@ func TestCancelInvoice(t *testing.T) {
// Notify arrival of a new htlc paying to this invoice. This should // Notify arrival of a new htlc paying to this invoice. This should
// succeed. // succeed.
event, err := registry.NotifyExitHopHtlc(hash, amt) hodlChan := make(chan interface{})
event, err := registry.NotifyExitHopHtlc(hash, amt, hodlChan)
if err != nil { if err != nil {
t.Fatal("expected settlement of a canceled invoice to succeed") t.Fatal("expected settlement of a canceled invoice to succeed")
} }
@ -274,6 +284,134 @@ func TestCancelInvoice(t *testing.T) {
} }
} }
// TestHoldInvoice tests settling of a hold invoice and related notifications.
func TestHoldInvoice(t *testing.T) {
defer timeout(t)()
cdb, cleanup, err := newDB()
defer cleanup()
// Instantiate and start the invoice registry.
registry := NewRegistry(cdb, decodeExpiry)
err = registry.Start()
if err != nil {
t.Fatal(err)
}
defer registry.Stop()
allSubscriptions := registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
// Subscribe to the not yet existing invoice.
subscription := registry.SubscribeSingleInvoice(hash)
defer subscription.Cancel()
if subscription.hash != hash {
t.Fatalf("expected subscription for provided hash")
}
// Add the invoice.
invoice := &channeldb.Invoice{
Terms: channeldb.ContractTerm{
PaymentPreimage: channeldb.UnknownPreimage,
Value: lnwire.MilliSatoshi(100000),
},
}
_, err = registry.AddInvoice(invoice, hash)
if err != nil {
t.Fatal(err)
}
// We expect the open state to be sent to the single invoice subscriber.
update := <-subscription.Updates
if update.Terms.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
update.Terms.State)
}
// We expect a new invoice notification to be sent out.
newInvoice := <-allSubscriptions.NewInvoices
if newInvoice.Terms.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
newInvoice.Terms.State)
}
// Use slightly higher amount for accept/settle.
amtPaid := lnwire.MilliSatoshi(100500)
hodlChan := make(chan interface{}, 1)
// NotifyExitHopHtlc without a preimage present in the invoice registry
// should be possible.
event, err := registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("unexpect direct settle")
}
// Test idempotency.
event, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("unexpect direct settle")
}
// We expect the accepted state to be sent to the single invoice
// subscriber. For all invoice subscribers, we don't expect an update.
// Those only get notified on settle.
update = <-subscription.Updates
if update.Terms.State != channeldb.ContractAccepted {
t.Fatalf("expected state ContractAccepted, but got %v",
update.Terms.State)
}
if update.AmtPaid != amtPaid {
t.Fatal("invoice AmtPaid incorrect")
}
// Settling with preimage should succeed.
err = registry.SettleHodlInvoice(preimage)
if err != nil {
t.Fatal("expected set preimage to succeed")
}
hodlEvent := (<-hodlChan).(HodlEvent)
if *hodlEvent.Preimage != preimage {
t.Fatal("unexpected preimage in hodl event")
}
// We expect a settled notification to be sent out for both all and
// single invoice subscribers.
settledInvoice := <-allSubscriptions.SettledInvoices
if settledInvoice.Terms.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractSettled, but got %v",
settledInvoice.Terms.State)
}
update = <-subscription.Updates
if update.Terms.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractSettled, but got %v",
update.Terms.State)
}
// Idempotency.
err = registry.SettleHodlInvoice(preimage)
if err != channeldb.ErrInvoiceAlreadySettled {
t.Fatalf("expected ErrInvoiceAlreadySettled but got %v", err)
}
// Try to cancel.
err = registry.CancelInvoice(hash)
if err == nil {
t.Fatal("expected cancelation of a settled invoice to fail")
}
}
func newDB() (*channeldb.DB, func(), error) { func newDB() (*channeldb.DB, func(), error) {
// First, create a temporary directory to be used for the duration of // First, create a temporary directory to be used for the duration of
// this test. // this test.

26
invoices/utils_test.go Normal file
View File

@ -0,0 +1,26 @@
package invoices
import (
"os"
"runtime/pprof"
"testing"
"time"
)
// timeout implements a test level timeout.
func timeout(t *testing.T) func() {
done := make(chan struct{})
go func() {
select {
case <-time.After(5 * time.Second):
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
panic("test timeout")
case <-done:
}
}()
return func() {
close(done)
}
}

View File

@ -60,6 +60,8 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
state = lnrpc.Invoice_SETTLED state = lnrpc.Invoice_SETTLED
case channeldb.ContractCanceled: case channeldb.ContractCanceled:
state = lnrpc.Invoice_CANCELED state = lnrpc.Invoice_CANCELED
case channeldb.ContractAccepted:
state = lnrpc.Invoice_ACCEPTED
default: default:
return nil, fmt.Errorf("unknown invoice state %v", return nil, fmt.Errorf("unknown invoice state %v",
invoice.Terms.State) invoice.Terms.State)

File diff suppressed because it is too large Load Diff

View File

@ -1892,6 +1892,7 @@ message Invoice {
OPEN = 0; OPEN = 0;
SETTLED = 1; SETTLED = 1;
CANCELED = 2; CANCELED = 2;
ACCEPTED = 3;
} }
/** /**

View File

@ -1162,7 +1162,8 @@
"enum": [ "enum": [
"OPEN", "OPEN",
"SETTLED", "SETTLED",
"CANCELED" "CANCELED",
"ACCEPTED"
], ],
"default": "OPEN" "default": "OPEN"
}, },