mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-20 21:30:06 +02:00
cnct+htlcswitch+invoices: move invoice parameter check out of link
This commit is the final step in making the link unaware of invoices. It now purely offers the htlc to the invoice registry and follows instructions from the invoice registry about how and when to respond to the htlc. The change also fixes a bug where upon restart, hodl htlcs were subjected to the invoice minimum cltv delta requirement again. If the block height has increased in the mean while, the htlc would be canceled back. Furthermore the invoice registry interaction is aligned between link and contract resolvers.
This commit is contained in:
@@ -2,6 +2,7 @@ package invoices
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -27,12 +28,64 @@ var (
|
||||
DebugHash = DebugPre.Hash()
|
||||
)
|
||||
|
||||
// HtlcCancelReason defines reasons for which htlcs can be canceled.
|
||||
type HtlcCancelReason uint8
|
||||
|
||||
const (
|
||||
// CancelInvoiceUnknown is returned if the preimage is unknown.
|
||||
CancelInvoiceUnknown HtlcCancelReason = iota
|
||||
|
||||
// CancelExpiryTooSoon is returned when the timelock of the htlc
|
||||
// does not satisfy the invoice cltv expiry requirement.
|
||||
CancelExpiryTooSoon
|
||||
|
||||
// CancelInvoiceCanceled is returned when the invoice is already
|
||||
// canceled and can't be paid to anymore.
|
||||
CancelInvoiceCanceled
|
||||
|
||||
// CancelAmountTooLow is returned when the amount paid is too low.
|
||||
CancelAmountTooLow
|
||||
)
|
||||
|
||||
// String returns a human readable identifier for the cancel reason.
|
||||
func (r HtlcCancelReason) String() string {
|
||||
switch r {
|
||||
case CancelInvoiceUnknown:
|
||||
return "InvoiceUnknown"
|
||||
case CancelExpiryTooSoon:
|
||||
return "ExpiryTooSoon"
|
||||
case CancelInvoiceCanceled:
|
||||
return "InvoiceCanceled"
|
||||
case CancelAmountTooLow:
|
||||
return "CancelAmountTooLow"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvoiceExpiryTooSoon is returned when an invoice is attempted to be
|
||||
// accepted or settled with not enough blocks remaining.
|
||||
ErrInvoiceExpiryTooSoon = errors.New("invoice expiry too soon")
|
||||
|
||||
// ErrInvoiceAmountTooLow is returned when an invoice is attempted to be
|
||||
// accepted or settled with an amount that is too low.
|
||||
ErrInvoiceAmountTooLow = errors.New("paid amount less than invoice amount")
|
||||
)
|
||||
|
||||
// HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is
|
||||
// set, the event indicates a settle event. If Preimage is nil, it is a cancel
|
||||
// event.
|
||||
type HodlEvent struct {
|
||||
// Preimage is the htlc preimage. Its value is nil in case of a cancel.
|
||||
Preimage *lntypes.Preimage
|
||||
Hash lntypes.Hash
|
||||
|
||||
// Hash is the htlc hash.
|
||||
Hash lntypes.Hash
|
||||
|
||||
// CancelReason specifies the reason why invoice registry decided to
|
||||
// cancel the htlc.
|
||||
CancelReason HtlcCancelReason
|
||||
}
|
||||
|
||||
// InvoiceRegistry is a central registry of all the outstanding invoices
|
||||
@@ -70,6 +123,13 @@ type InvoiceRegistry struct {
|
||||
// is used to unsubscribe from all hashes efficiently.
|
||||
hodlReverseSubscriptions map[chan<- interface{}]map[lntypes.Hash]struct{}
|
||||
|
||||
// finalCltvRejectDelta defines the number of blocks before the expiry
|
||||
// of the htlc where we no longer settle it as an exit hop and instead
|
||||
// cancel it back. Normally this value should be lower than the cltv
|
||||
// expiry of any invoice we create and the code effectuating this should
|
||||
// not be hit.
|
||||
finalCltvRejectDelta int32
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
}
|
||||
@@ -79,7 +139,7 @@ type InvoiceRegistry struct {
|
||||
// layer. The in-memory layer is in place such that debug invoices can be added
|
||||
// which are volatile yet available system wide within the daemon.
|
||||
func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
|
||||
uint32, error)) *InvoiceRegistry {
|
||||
uint32, error), finalCltvRejectDelta int32) *InvoiceRegistry {
|
||||
|
||||
return &InvoiceRegistry{
|
||||
cdb: cdb,
|
||||
@@ -93,6 +153,7 @@ func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
|
||||
hodlSubscriptions: make(map[lntypes.Hash]map[chan<- interface{}]struct{}),
|
||||
hodlReverseSubscriptions: make(map[chan<- interface{}]map[lntypes.Hash]struct{}),
|
||||
decodeFinalCltvExpiry: decodeFinalCltvExpiry,
|
||||
finalCltvRejectDelta: finalCltvRejectDelta,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -460,6 +521,35 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
||||
return invoice, expiry, nil
|
||||
}
|
||||
|
||||
// checkHtlcParameters is a callback used inside invoice db transactions to
|
||||
// atomically check-and-update an invoice.
|
||||
func (i *InvoiceRegistry) checkHtlcParameters(invoice *channeldb.Invoice,
|
||||
amtPaid lnwire.MilliSatoshi, htlcExpiry uint32, currentHeight int32) error {
|
||||
|
||||
expiry, err := i.decodeFinalCltvExpiry(string(invoice.PaymentRequest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if htlcExpiry < uint32(currentHeight+i.finalCltvRejectDelta) {
|
||||
return ErrInvoiceExpiryTooSoon
|
||||
}
|
||||
|
||||
if htlcExpiry < uint32(currentHeight)+expiry {
|
||||
return ErrInvoiceExpiryTooSoon
|
||||
}
|
||||
|
||||
// If an invoice amount is specified, check that enough is paid. This
|
||||
// check is only performed for open invoices. Once a sufficiently large
|
||||
// payment has been made and the invoice is in the accepted or settled
|
||||
// state, any amount will be accepted on top of that.
|
||||
if invoice.Terms.Value > 0 && amtPaid < invoice.Terms.Value {
|
||||
return ErrInvoiceAmountTooLow
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// settled. The return value describes how the htlc should be resolved.
|
||||
@@ -472,59 +562,112 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
||||
// the channel is either buffered or received on from another goroutine to
|
||||
// prevent deadlock.
|
||||
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
amtPaid lnwire.MilliSatoshi, hodlChan chan<- interface{}) (
|
||||
*HodlEvent, error) {
|
||||
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
hodlChan chan<- interface{}) (*HodlEvent, error) {
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
log.Debugf("Invoice(%x): htlc accepted", rHash[:])
|
||||
|
||||
createEvent := func(preimage *lntypes.Preimage) *HodlEvent {
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
Preimage: preimage,
|
||||
}
|
||||
debugLog := func(s string) {
|
||||
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v",
|
||||
rHash[:], s, amtPaid, expiry)
|
||||
}
|
||||
|
||||
// First check the in-memory debug invoice index to see if this is an
|
||||
// existing invoice added for debugging.
|
||||
if invoice, ok := i.debugInvoices[rHash]; ok {
|
||||
debugLog("payment to debug invoice accepted")
|
||||
|
||||
// Debug invoices are never fully settled, so we just settle the
|
||||
// htlc in this case.
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
Preimage: &invoice.Terms.PaymentPreimage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If this isn't a debug invoice, then we'll attempt to settle an
|
||||
// invoice matching this rHash on disk (if one exists).
|
||||
invoice, err := i.cdb.AcceptOrSettleInvoice(rHash, amtPaid)
|
||||
invoice, err := i.cdb.AcceptOrSettleInvoice(
|
||||
rHash, amtPaid,
|
||||
func(inv *channeldb.Invoice) error {
|
||||
return i.checkHtlcParameters(
|
||||
inv, amtPaid, expiry, currentHeight,
|
||||
)
|
||||
},
|
||||
)
|
||||
switch err {
|
||||
|
||||
// If invoice is already settled, settle htlc. This means we accept more
|
||||
// payments to the same invoice hash.
|
||||
//
|
||||
// NOTE: Though our recovery and forwarding logic is predominately
|
||||
// batched, settling invoices happens iteratively. We may reject one of
|
||||
// two payments for the same rhash at first, but then restart and reject
|
||||
// both after seeing that the invoice has been settled. Without any
|
||||
// record of which one settles first, it is ambiguous as to which one
|
||||
// actually settled the invoice. Thus, by accepting all payments, we
|
||||
// eliminate the race condition that can lead to this inconsistency.
|
||||
//
|
||||
// TODO(conner): track ownership of settlements to properly recover from
|
||||
// failures? or add batch invoice settlement
|
||||
case channeldb.ErrInvoiceAlreadySettled:
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
debugLog("accepting duplicate payment to settled invoice")
|
||||
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
Preimage: &invoice.Terms.PaymentPreimage,
|
||||
}, nil
|
||||
|
||||
// If invoice is already canceled, cancel htlc.
|
||||
case channeldb.ErrInvoiceAlreadyCanceled:
|
||||
return createEvent(nil), nil
|
||||
debugLog("invoice already canceled")
|
||||
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
CancelReason: CancelInvoiceCanceled,
|
||||
}, nil
|
||||
|
||||
// If invoice is already accepted, add this htlc to the list of
|
||||
// subscribers.
|
||||
case channeldb.ErrInvoiceAlreadyAccepted:
|
||||
debugLog("accepting duplicate payment to accepted invoice")
|
||||
|
||||
i.hodlSubscribe(hodlChan, rHash)
|
||||
return nil, nil
|
||||
|
||||
// If there are not enough blocks left, cancel the htlc.
|
||||
case ErrInvoiceExpiryTooSoon:
|
||||
debugLog("expiry too soon")
|
||||
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
CancelReason: CancelExpiryTooSoon,
|
||||
}, nil
|
||||
|
||||
// If there are not enough blocks left, cancel the htlc.
|
||||
case ErrInvoiceAmountTooLow:
|
||||
debugLog("amount too low")
|
||||
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
CancelReason: CancelAmountTooLow,
|
||||
}, nil
|
||||
|
||||
// If this call settled the invoice, settle the htlc. Otherwise
|
||||
// subscribe for a future hodl event.
|
||||
case nil:
|
||||
i.notifyClients(rHash, invoice, invoice.Terms.State)
|
||||
switch invoice.Terms.State {
|
||||
case channeldb.ContractSettled:
|
||||
log.Debugf("Invoice(%x): settled", rHash[:])
|
||||
debugLog("settled")
|
||||
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
Preimage: &invoice.Terms.PaymentPreimage,
|
||||
}, nil
|
||||
case channeldb.ContractAccepted:
|
||||
debugLog("accepted")
|
||||
|
||||
// Subscribe to updates to this invoice.
|
||||
i.hodlSubscribe(hodlChan, rHash)
|
||||
return nil, nil
|
||||
@@ -534,6 +677,8 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
}
|
||||
|
||||
default:
|
||||
debugLog(err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -584,7 +729,8 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
|
||||
|
||||
log.Debugf("Invoice(%v): canceled", payHash)
|
||||
i.notifyHodlSubscribers(HodlEvent{
|
||||
Hash: payHash,
|
||||
Hash: payHash,
|
||||
CancelReason: CancelInvoiceCanceled,
|
||||
})
|
||||
i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
|
||||
|
||||
|
@@ -6,11 +6,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,18 +21,15 @@ var (
|
||||
|
||||
hash = preimage.Hash()
|
||||
|
||||
// testPayReq is a dummy payment request that does parse properly. It
|
||||
// has no relation with the real invoice parameters and isn't asserted
|
||||
// on in this test. LookupInvoice requires this to have a valid value.
|
||||
testPayReq = "lnbc500u1pwywxzwpp5nd2u9xzq02t0tuf2654as7vma42lwkcjptx4yzfq0umq4swpa7cqdqqcqzysmlpc9ewnydr8rr8dnltyxphdyf6mcqrsd6dml8zajtyhwe6a45d807kxtmzayuf0hh2d9tn478ecxkecdg7c5g85pntupug5kakm7xcpn63zqk"
|
||||
testInvoiceExpiry = uint32(3)
|
||||
|
||||
testCurrentHeight = int32(0)
|
||||
|
||||
testFinalCltvRejectDelta = int32(3)
|
||||
)
|
||||
|
||||
func decodeExpiry(payReq string) (uint32, error) {
|
||||
invoice, err := zpay32.Decode(payReq, &chaincfg.MainNetParams)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint32(invoice.MinFinalCLTVExpiry()), nil
|
||||
return uint32(testInvoiceExpiry), nil
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -43,7 +38,6 @@ var (
|
||||
PaymentPreimage: preimage,
|
||||
Value: lnwire.MilliSatoshi(100000),
|
||||
},
|
||||
PaymentRequest: []byte(testPayReq),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,7 +48,7 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
|
||||
}
|
||||
|
||||
// Instantiate and start the invoice registry.
|
||||
registry := NewRegistry(cdb, decodeExpiry)
|
||||
registry := NewRegistry(cdb, decodeExpiry, testFinalCltvRejectDelta)
|
||||
|
||||
err = registry.Start()
|
||||
if err != nil {
|
||||
@@ -121,7 +115,9 @@ func TestSettleInvoice(t *testing.T) {
|
||||
|
||||
// Settle invoice with a slightly higher amount.
|
||||
amtPaid := lnwire.MilliSatoshi(100500)
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
_, err = registry.NotifyExitHopHtlc(
|
||||
hash, amtPaid, testInvoiceExpiry, 0, hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -153,13 +149,18 @@ func TestSettleInvoice(t *testing.T) {
|
||||
}
|
||||
|
||||
// Try to settle again.
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
_, err = registry.NotifyExitHopHtlc(
|
||||
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("expected duplicate settle to succeed")
|
||||
}
|
||||
|
||||
// Try to settle again with a different amount.
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid+600, hodlChan)
|
||||
_, err = registry.NotifyExitHopHtlc(
|
||||
hash, amtPaid+600, testInvoiceExpiry, testCurrentHeight,
|
||||
hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("expected duplicate settle to succeed")
|
||||
}
|
||||
@@ -274,7 +275,9 @@ func TestCancelInvoice(t *testing.T) {
|
||||
// Notify arrival of a new htlc paying to this invoice. This should
|
||||
// succeed.
|
||||
hodlChan := make(chan interface{})
|
||||
event, err := registry.NotifyExitHopHtlc(hash, amt, hodlChan)
|
||||
event, err := registry.NotifyExitHopHtlc(
|
||||
hash, amt, testInvoiceExpiry, testCurrentHeight, hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("expected settlement of a canceled invoice to succeed")
|
||||
}
|
||||
@@ -292,7 +295,7 @@ func TestHoldInvoice(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
// Instantiate and start the invoice registry.
|
||||
registry := NewRegistry(cdb, decodeExpiry)
|
||||
registry := NewRegistry(cdb, decodeExpiry, testFinalCltvRejectDelta)
|
||||
|
||||
err = registry.Start()
|
||||
if err != nil {
|
||||
@@ -345,7 +348,9 @@ func TestHoldInvoice(t *testing.T) {
|
||||
|
||||
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
||||
// should be possible.
|
||||
event, err := registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
event, err := registry.NotifyExitHopHtlc(
|
||||
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
}
|
||||
@@ -354,7 +359,9 @@ func TestHoldInvoice(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test idempotency.
|
||||
event, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
event, err = registry.NotifyExitHopHtlc(
|
||||
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
}
|
||||
@@ -434,3 +441,24 @@ func newDB() (*channeldb.DB, func(), error) {
|
||||
|
||||
return cdb, cleanUp, nil
|
||||
}
|
||||
|
||||
// TestUnknownInvoice tests that invoice registry returns an error when the
|
||||
// invoice is unknown. This is to guard against returning a cancel hodl event
|
||||
// for forwarded htlcs. In the link, NotifyExitHopHtlc is only called if we are
|
||||
// the exit hop, but in htlcIncomingContestResolver it is called with forwarded
|
||||
// htlc hashes as well.
|
||||
func TestUnknownInvoice(t *testing.T) {
|
||||
registry, cleanup := newTestContext(t)
|
||||
defer cleanup()
|
||||
|
||||
// Notify arrival of a new htlc paying to this invoice. This should
|
||||
// succeed.
|
||||
hodlChan := make(chan interface{})
|
||||
amt := lnwire.MilliSatoshi(100000)
|
||||
_, err := registry.NotifyExitHopHtlc(
|
||||
hash, amt, testInvoiceExpiry, testCurrentHeight, hodlChan,
|
||||
)
|
||||
if err != channeldb.ErrInvoiceNotFound {
|
||||
t.Fatal("expected invoice not found error")
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user