mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-14 22:00:07 +02:00
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:
parent
1f41a2abce
commit
32f2b047e8
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
26
invoices/utils_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
1144
lnrpc/rpc.pb.go
1144
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -1892,6 +1892,7 @@ message Invoice {
|
|||||||
OPEN = 0;
|
OPEN = 0;
|
||||||
SETTLED = 1;
|
SETTLED = 1;
|
||||||
CANCELED = 2;
|
CANCELED = 2;
|
||||||
|
ACCEPTED = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1162,7 +1162,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"OPEN",
|
"OPEN",
|
||||||
"SETTLED",
|
"SETTLED",
|
||||||
"CANCELED"
|
"CANCELED",
|
||||||
|
"ACCEPTED"
|
||||||
],
|
],
|
||||||
"default": "OPEN"
|
"default": "OPEN"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user