mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-30 17:50:58 +02:00
invoices: accept mpp payments
This commit is contained in:
parent
56958493fe
commit
b2f43858c3
@ -117,6 +117,7 @@ const (
|
||||
resolveTimeType tlv.Type = 11
|
||||
expiryHeightType tlv.Type = 13
|
||||
htlcStateType tlv.Type = 15
|
||||
mppTotalAmtType tlv.Type = 17
|
||||
|
||||
// A set of tlv type definitions used to serialize invoice bodiees.
|
||||
//
|
||||
@ -289,6 +290,10 @@ type InvoiceHTLC struct {
|
||||
// Amt is the amount that is carried by this htlc.
|
||||
Amt lnwire.MilliSatoshi
|
||||
|
||||
// MppTotalAmt is a field for mpp that indicates the expected total
|
||||
// amount.
|
||||
MppTotalAmt lnwire.MilliSatoshi
|
||||
|
||||
// AcceptHeight is the block height at which the invoice registry
|
||||
// decided to accept this htlc as a payment to the invoice. At this
|
||||
// height, the invoice cltv delay must have been met.
|
||||
@ -323,6 +328,10 @@ type HtlcAcceptDesc struct {
|
||||
// Amt is the amount that is carried by this htlc.
|
||||
Amt lnwire.MilliSatoshi
|
||||
|
||||
// MppTotalAmt is a field for mpp that indicates the expected total
|
||||
// amount.
|
||||
MppTotalAmt lnwire.MilliSatoshi
|
||||
|
||||
// Expiry is the expiry height of this htlc.
|
||||
Expiry uint32
|
||||
|
||||
@ -1018,6 +1027,7 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
||||
// Encode the htlc in a tlv stream.
|
||||
chanID := key.ChanID.ToUint64()
|
||||
amt := uint64(htlc.Amt)
|
||||
mppTotalAmt := uint64(htlc.MppTotalAmt)
|
||||
acceptTime := uint64(htlc.AcceptTime.UnixNano())
|
||||
resolveTime := uint64(htlc.ResolveTime.UnixNano())
|
||||
state := uint8(htlc.State)
|
||||
@ -1034,6 +1044,7 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
||||
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
||||
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
||||
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
||||
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||
)
|
||||
|
||||
// Convert the custom records to tlv.Record types that are ready
|
||||
@ -1193,7 +1204,7 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
||||
chanID uint64
|
||||
state uint8
|
||||
acceptTime, resolveTime uint64
|
||||
amt uint64
|
||||
amt, mppTotalAmt uint64
|
||||
)
|
||||
tlvStream, err := tlv.NewStream(
|
||||
tlv.MakePrimitiveRecord(chanIDType, &chanID),
|
||||
@ -1206,6 +1217,7 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
||||
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
||||
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
||||
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
||||
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1221,6 +1233,7 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
||||
htlc.ResolveTime = time.Unix(0, int64(resolveTime))
|
||||
htlc.State = HtlcState(state)
|
||||
htlc.Amt = lnwire.MilliSatoshi(amt)
|
||||
htlc.MppTotalAmt = lnwire.MilliSatoshi(mppTotalAmt)
|
||||
|
||||
// Reconstruct the custom records fields from the parsed types
|
||||
// map return from the tlv parser.
|
||||
@ -1324,6 +1337,7 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
||||
|
||||
htlc := &InvoiceHTLC{
|
||||
Amt: htlcUpdate.Amt,
|
||||
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
||||
Expiry: htlcUpdate.Expiry,
|
||||
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
||||
AcceptTime: now,
|
||||
|
@ -2,8 +2,10 @@ package invoices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
@ -26,6 +28,12 @@ var (
|
||||
ErrShuttingDown = errors.New("invoice registry shutting down")
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHtlcHoldDuration defines the default for how long mpp htlcs
|
||||
// are held while waiting for the other set members to arrive.
|
||||
DefaultHtlcHoldDuration = 120 * time.Second
|
||||
)
|
||||
|
||||
// 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.
|
||||
@ -49,6 +57,38 @@ type RegistryConfig struct {
|
||||
// expiry of any invoice we create and the code effectuating this should
|
||||
// not be hit.
|
||||
FinalCltvRejectDelta int32
|
||||
|
||||
// HtlcHoldDuration defines for how long mpp htlcs are held while
|
||||
// waiting for the other set members to arrive.
|
||||
HtlcHoldDuration time.Duration
|
||||
|
||||
// Now returns the current time.
|
||||
Now func() time.Time
|
||||
|
||||
// TickAfter returns a channel that is sent on after the specified
|
||||
// duration as passed.
|
||||
TickAfter func(duration time.Duration) <-chan time.Time
|
||||
}
|
||||
|
||||
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
||||
// mpp htlcs for which the complete set didn't arrive in time.
|
||||
type htlcReleaseEvent struct {
|
||||
// hash is the payment hash of the htlc to release.
|
||||
hash lntypes.Hash
|
||||
|
||||
// key is the circuit key of the htlc to release.
|
||||
key channeldb.CircuitKey
|
||||
|
||||
// releaseTime is the time at which to release the htlc.
|
||||
releaseTime time.Time
|
||||
}
|
||||
|
||||
// Less is used to order PriorityQueueItem's by their release time such that
|
||||
// items with the older release time are at the top of the queue.
|
||||
//
|
||||
// NOTE: Part of the queue.PriorityQueueItem interface.
|
||||
func (r *htlcReleaseEvent) Less(other queue.PriorityQueueItem) bool {
|
||||
return r.releaseTime.Before(other.(*htlcReleaseEvent).releaseTime)
|
||||
}
|
||||
|
||||
// InvoiceRegistry is a central registry of all the outstanding invoices
|
||||
@ -82,6 +122,10 @@ type InvoiceRegistry struct {
|
||||
// subscriber. This is used to unsubscribe from all hashes efficiently.
|
||||
hodlReverseSubscriptions map[chan<- interface{}]map[channeldb.CircuitKey]struct{}
|
||||
|
||||
// htlcAutoReleaseChan contains the new htlcs that need to be
|
||||
// auto-released.
|
||||
htlcAutoReleaseChan chan *htlcReleaseEvent
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
}
|
||||
@ -91,7 +135,6 @@ 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, cfg *RegistryConfig) *InvoiceRegistry {
|
||||
|
||||
return &InvoiceRegistry{
|
||||
cdb: cdb,
|
||||
notificationClients: make(map[uint32]*InvoiceSubscription),
|
||||
@ -102,6 +145,7 @@ func NewRegistry(cdb *channeldb.DB, cfg *RegistryConfig) *InvoiceRegistry {
|
||||
hodlSubscriptions: make(map[channeldb.CircuitKey]map[chan<- interface{}]struct{}),
|
||||
hodlReverseSubscriptions: make(map[chan<- interface{}]map[channeldb.CircuitKey]struct{}),
|
||||
cfg: cfg,
|
||||
htlcAutoReleaseChan: make(chan *htlcReleaseEvent),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@ -110,7 +154,7 @@ func NewRegistry(cdb *channeldb.DB, cfg *RegistryConfig) *InvoiceRegistry {
|
||||
func (i *InvoiceRegistry) Start() error {
|
||||
i.wg.Add(1)
|
||||
|
||||
go i.invoiceEventNotifier()
|
||||
go i.invoiceEventLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -130,13 +174,31 @@ type invoiceEvent struct {
|
||||
invoice *channeldb.Invoice
|
||||
}
|
||||
|
||||
// invoiceEventNotifier is the dedicated goroutine responsible for accepting
|
||||
// tickAt returns a channel that ticks at the specified time. If the time has
|
||||
// already passed, it will tick immediately.
|
||||
func (i *InvoiceRegistry) tickAt(t time.Time) <-chan time.Time {
|
||||
now := i.cfg.Now()
|
||||
return i.cfg.TickAfter(t.Sub(now))
|
||||
}
|
||||
|
||||
// invoiceEventLoop is the dedicated goroutine responsible for accepting
|
||||
// new notification subscriptions, cancelling old subscriptions, and
|
||||
// dispatching new invoice events.
|
||||
func (i *InvoiceRegistry) invoiceEventNotifier() {
|
||||
func (i *InvoiceRegistry) invoiceEventLoop() {
|
||||
defer i.wg.Done()
|
||||
|
||||
// Set up a heap for htlc auto-releases.
|
||||
autoReleaseHeap := &queue.PriorityQueue{}
|
||||
|
||||
for {
|
||||
// If there is something to release, set up a release tick
|
||||
// channel.
|
||||
var nextReleaseTick <-chan time.Time
|
||||
if autoReleaseHeap.Len() > 0 {
|
||||
head := autoReleaseHeap.Top().(*htlcReleaseEvent)
|
||||
nextReleaseTick = i.tickAt(head.releaseTime)
|
||||
}
|
||||
|
||||
select {
|
||||
// A new invoice subscription for all invoices has just arrived!
|
||||
// We'll query for any backlog notifications, then add it to the
|
||||
@ -202,6 +264,29 @@ func (i *InvoiceRegistry) invoiceEventNotifier() {
|
||||
i.singleNotificationClients[e.id] = e
|
||||
}
|
||||
|
||||
// A new htlc came in for auto-release.
|
||||
case event := <-i.htlcAutoReleaseChan:
|
||||
log.Debugf("Scheduling auto-release for htlc: "+
|
||||
"hash=%v, key=%v at %v",
|
||||
event.hash, event.key, event.releaseTime)
|
||||
|
||||
// We use an independent timer for every htlc rather
|
||||
// than a set timer that is reset with every htlc coming
|
||||
// in. Otherwise the sender could keep resetting the
|
||||
// timer until the broadcast window is entered and our
|
||||
// channel is force closed.
|
||||
autoReleaseHeap.Push(event)
|
||||
|
||||
// The htlc at the top of the heap needs to be auto-released.
|
||||
case <-nextReleaseTick:
|
||||
event := autoReleaseHeap.Pop().(*htlcReleaseEvent)
|
||||
err := i.cancelSingleHtlc(
|
||||
event.hash, event.key,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("HTLC timer: %v", err)
|
||||
}
|
||||
|
||||
case <-i.quit:
|
||||
return
|
||||
}
|
||||
@ -418,6 +503,114 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
||||
return i.cdb.LookupInvoice(rHash)
|
||||
}
|
||||
|
||||
// startHtlcTimer starts a new timer via the invoice registry main loop that
|
||||
// cancels a single htlc on an invoice when the htlc hold duration has passed.
|
||||
func (i *InvoiceRegistry) startHtlcTimer(hash lntypes.Hash,
|
||||
key channeldb.CircuitKey, acceptTime time.Time) error {
|
||||
|
||||
releaseTime := acceptTime.Add(i.cfg.HtlcHoldDuration)
|
||||
event := &htlcReleaseEvent{
|
||||
hash: hash,
|
||||
key: key,
|
||||
releaseTime: releaseTime,
|
||||
}
|
||||
|
||||
select {
|
||||
case i.htlcAutoReleaseChan <- event:
|
||||
return nil
|
||||
|
||||
case <-i.quit:
|
||||
return ErrShuttingDown
|
||||
}
|
||||
}
|
||||
|
||||
// cancelSingleHtlc cancels a single accepted htlc on an invoice.
|
||||
func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
||||
key channeldb.CircuitKey) error {
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
updateInvoice := func(invoice *channeldb.Invoice) (
|
||||
*channeldb.InvoiceUpdateDesc, error) {
|
||||
|
||||
// Only allow individual htlc cancelation on open invoices.
|
||||
if invoice.State != channeldb.ContractOpen {
|
||||
log.Debugf("cancelSingleHtlc: invoice %v no longer "+
|
||||
"open", hash)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Lookup the current status of the htlc in the database.
|
||||
htlc, ok := invoice.Htlcs[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("htlc %v not found", key)
|
||||
}
|
||||
|
||||
// Cancelation is only possible if the htlc wasn't already
|
||||
// resolved.
|
||||
if htlc.State != channeldb.HtlcStateAccepted {
|
||||
log.Debugf("cancelSingleHtlc: htlc %v on invoice %v "+
|
||||
"is already resolved", key, hash)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Debugf("cancelSingleHtlc: cancelling htlc %v on invoice %v",
|
||||
key, hash)
|
||||
|
||||
// Return an update descriptor that cancels htlc and keeps
|
||||
// invoice open.
|
||||
canceledHtlcs := map[channeldb.CircuitKey]struct{}{
|
||||
key: {},
|
||||
}
|
||||
|
||||
return &channeldb.InvoiceUpdateDesc{
|
||||
CancelHtlcs: canceledHtlcs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Try to mark the specified htlc as canceled in the invoice database.
|
||||
// Intercept the update descriptor to set the local updated variable. If
|
||||
// no invoice update is performed, we can return early.
|
||||
var updated bool
|
||||
invoice, err := i.cdb.UpdateInvoice(hash,
|
||||
func(invoice *channeldb.Invoice) (
|
||||
*channeldb.InvoiceUpdateDesc, error) {
|
||||
|
||||
updateDesc, err := updateInvoice(invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated = updateDesc != nil
|
||||
|
||||
return updateDesc, err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The invoice has been updated. Notify subscribers of the htlc
|
||||
// resolution.
|
||||
htlc, ok := invoice.Htlcs[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("htlc %v not found", key)
|
||||
}
|
||||
if htlc.State == channeldb.HtlcStateCanceled {
|
||||
i.notifyHodlSubscribers(HodlEvent{
|
||||
CircuitKey: key,
|
||||
AcceptHeight: int32(htlc.AcceptHeight),
|
||||
Preimage: nil,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyExitHopHtlc attempts to mark an invoice as settled. The return value
|
||||
// describes how the htlc should be resolved.
|
||||
//
|
||||
@ -428,6 +621,11 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
||||
// 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.
|
||||
//
|
||||
// In the case that the htlc is part of a larger set of htlcs that pay to the
|
||||
// same invoice (multi-path payment), the htlc is held until the set is
|
||||
// complete. If the set doesn't fully arrive in time, a timer will cancel the
|
||||
// held htlc.
|
||||
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||
@ -436,9 +634,11 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
mpp := payload.MultiPath()
|
||||
|
||||
debugLog := func(s string) {
|
||||
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v",
|
||||
rHash[:], s, amtPaid, expiry, circuitKey)
|
||||
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v, "+
|
||||
"mpp=%v", rHash[:], s, amtPaid, expiry, circuitKey, mpp)
|
||||
}
|
||||
|
||||
// Create the update context containing the relevant details of the
|
||||
@ -450,6 +650,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
currentHeight: currentHeight,
|
||||
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
||||
customRecords: payload.CustomRecords(),
|
||||
mpp: mpp,
|
||||
}
|
||||
|
||||
// We'll attempt to settle an invoice matching this rHash on disk (if
|
||||
@ -514,6 +715,21 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
}, nil
|
||||
|
||||
case channeldb.HtlcStateSettled:
|
||||
// Also settle any previously accepted htlcs. The invoice state
|
||||
// is leading. If an htlc is marked as settled, we should follow
|
||||
// now and settle the htlc with our peer.
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
if htlc.State != channeldb.HtlcStateSettled {
|
||||
continue
|
||||
}
|
||||
|
||||
i.notifyHodlSubscribers(HodlEvent{
|
||||
CircuitKey: key,
|
||||
Preimage: &invoice.Terms.PaymentPreimage,
|
||||
AcceptHeight: int32(htlc.AcceptHeight),
|
||||
})
|
||||
}
|
||||
|
||||
return &HodlEvent{
|
||||
CircuitKey: circuitKey,
|
||||
Preimage: &invoice.Terms.PaymentPreimage,
|
||||
@ -521,6 +737,19 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
}, nil
|
||||
|
||||
case channeldb.HtlcStateAccepted:
|
||||
// (Re)start the htlc timer if the invoice is still open. It can
|
||||
// only happen for mpp payments that there are htlcs in state
|
||||
// Accepted while the invoice is Open.
|
||||
if invoice.State == channeldb.ContractOpen {
|
||||
err := i.startHtlcTimer(
|
||||
rHash, circuitKey,
|
||||
invoiceHtlc.AcceptTime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i.hodlSubscribe(hodlChan, circuitKey)
|
||||
return nil, nil
|
||||
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
var (
|
||||
testTimeout = 5 * time.Second
|
||||
|
||||
testTime = time.Date(2018, time.February, 2, 14, 0, 0, 0, time.UTC)
|
||||
|
||||
preimage = lntypes.Preimage{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
|
||||
@ -59,20 +61,27 @@ var (
|
||||
|
||||
type testContext struct {
|
||||
registry *InvoiceRegistry
|
||||
clock *testClock
|
||||
|
||||
cleanup func()
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func newTestContext(t *testing.T) *testContext {
|
||||
clock := newTestClock(testTime)
|
||||
|
||||
cdb, cleanup, err := newDB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cdb.Now = clock.now
|
||||
|
||||
// Instantiate and start the invoice ctx.registry.
|
||||
cfg := RegistryConfig{
|
||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
HtlcHoldDuration: 30 * time.Second,
|
||||
Now: clock.now,
|
||||
TickAfter: clock.tickAfter,
|
||||
}
|
||||
registry := NewRegistry(cdb, &cfg)
|
||||
|
||||
@ -84,6 +93,7 @@ func newTestContext(t *testing.T) *testContext {
|
||||
|
||||
ctx := testContext{
|
||||
registry: registry,
|
||||
clock: clock,
|
||||
t: t,
|
||||
cleanup: func() {
|
||||
registry.Stop()
|
||||
@ -683,3 +693,85 @@ func (p *mockPayload) MultiPath() *record.MPP {
|
||||
func (p *mockPayload) CustomRecords() hop.CustomRecordSet {
|
||||
return make(hop.CustomRecordSet)
|
||||
}
|
||||
|
||||
// TestSettleMpp tests settling of an invoice with multiple partial payments.
|
||||
func TestSettleMpp(t *testing.T) {
|
||||
defer timeout(t)()
|
||||
|
||||
ctx := newTestContext(t)
|
||||
defer ctx.cleanup()
|
||||
|
||||
// Add the invoice.
|
||||
_, err := ctx.registry.AddInvoice(testInvoice, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mppPayload := &mockPayload{
|
||||
mpp: record.NewMPP(testInvoiceAmt, [32]byte{}),
|
||||
}
|
||||
|
||||
// Send htlc 1.
|
||||
hodlChan1 := make(chan interface{}, 1)
|
||||
event, err := ctx.registry.NotifyExitHopHtlc(
|
||||
hash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event != nil {
|
||||
t.Fatal("expected no direct resolution")
|
||||
}
|
||||
|
||||
// Simulate mpp timeout releasing htlc 1.
|
||||
ctx.clock.setTime(testTime.Add(30 * time.Second))
|
||||
|
||||
hodlEvent := (<-hodlChan1).(HodlEvent)
|
||||
if hodlEvent.Preimage != nil {
|
||||
t.Fatal("expected cancel event")
|
||||
}
|
||||
|
||||
// Send htlc 2.
|
||||
hodlChan2 := make(chan interface{}, 1)
|
||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
||||
hash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event != nil {
|
||||
t.Fatal("expected no direct resolution")
|
||||
}
|
||||
|
||||
// Send htlc 3.
|
||||
hodlChan3 := make(chan interface{}, 1)
|
||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
||||
hash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event == nil {
|
||||
t.Fatal("expected a settle event")
|
||||
}
|
||||
|
||||
// Check that settled amount is equal to the sum of values of the htlcs
|
||||
// 0 and 1.
|
||||
inv, err := ctx.registry.LookupInvoice(hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inv.State != channeldb.ContractSettled {
|
||||
t.Fatal("expected invoice to be settled")
|
||||
}
|
||||
if inv.AmtPaid != testInvoice.Terms.Value {
|
||||
t.Fatalf("amount incorrect, expected %v but got %v",
|
||||
testInvoice.Terms.Value, inv.AmtPaid)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
)
|
||||
|
||||
// updateResult is the result of the invoice update call.
|
||||
@ -24,6 +25,13 @@ const (
|
||||
resultDuplicateToSettled
|
||||
resultAccepted
|
||||
resultSettled
|
||||
resultInvoiceNotOpen
|
||||
resultPartialAccepted
|
||||
resultMppInProgress
|
||||
resultAddressMismatch
|
||||
resultHtlcSetTotalMismatch
|
||||
resultHtlcSetTotalTooLow
|
||||
resultHtlcSetOverpayment
|
||||
)
|
||||
|
||||
// String returns a human-readable representation of the invoice update result.
|
||||
@ -63,6 +71,27 @@ func (u updateResult) String() string {
|
||||
case resultSettled:
|
||||
return "settled"
|
||||
|
||||
case resultInvoiceNotOpen:
|
||||
return "invoice no longer open"
|
||||
|
||||
case resultPartialAccepted:
|
||||
return "partial payment accepted"
|
||||
|
||||
case resultMppInProgress:
|
||||
return "mpp reception in progress"
|
||||
|
||||
case resultAddressMismatch:
|
||||
return "payment address mismatch"
|
||||
|
||||
case resultHtlcSetTotalMismatch:
|
||||
return "htlc total amt doesn't match set total"
|
||||
|
||||
case resultHtlcSetTotalTooLow:
|
||||
return "set total too low for invoice"
|
||||
|
||||
case resultHtlcSetOverpayment:
|
||||
return "mpp is overpaying set total"
|
||||
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@ -77,6 +106,7 @@ type invoiceUpdateCtx struct {
|
||||
currentHeight int32
|
||||
finalCltvRejectDelta int32
|
||||
customRecords hop.CustomRecordSet
|
||||
mpp *record.MPP
|
||||
}
|
||||
|
||||
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
|
||||
@ -102,8 +132,125 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||
}
|
||||
}
|
||||
|
||||
// If the invoice is already canceled, there is no further checking to
|
||||
// do.
|
||||
if ctx.mpp == nil {
|
||||
return updateLegacy(ctx, inv)
|
||||
}
|
||||
|
||||
return updateMpp(ctx, inv)
|
||||
}
|
||||
|
||||
// updateMpp is a callback for DB.UpdateInvoice that contains the invoice
|
||||
// settlement logic for mpp payments.
|
||||
func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||
*channeldb.InvoiceUpdateDesc, updateResult, error) {
|
||||
|
||||
// Start building the accept descriptor.
|
||||
acceptDesc := &channeldb.HtlcAcceptDesc{
|
||||
Amt: ctx.amtPaid,
|
||||
Expiry: ctx.expiry,
|
||||
AcceptHeight: ctx.currentHeight,
|
||||
MppTotalAmt: ctx.mpp.TotalMsat(),
|
||||
CustomRecords: ctx.customRecords,
|
||||
}
|
||||
|
||||
// Only accept payments to open invoices. This behaviour differs from
|
||||
// non-mpp payments that are accepted even after the invoice is settled.
|
||||
// Because non-mpp payments don't have a payment address, this is needed
|
||||
// to thwart probing.
|
||||
if inv.State != channeldb.ContractOpen {
|
||||
return nil, resultInvoiceNotOpen, nil
|
||||
}
|
||||
|
||||
// Check the payment address that authorizes the payment.
|
||||
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
|
||||
return nil, resultAddressMismatch, nil
|
||||
}
|
||||
|
||||
// Don't accept zero-valued sets.
|
||||
if ctx.mpp.TotalMsat() == 0 {
|
||||
return nil, resultHtlcSetTotalTooLow, nil
|
||||
}
|
||||
|
||||
// Check that the total amt of the htlc set is high enough. In case this
|
||||
// is a zero-valued invoice, it will always be enough.
|
||||
if ctx.mpp.TotalMsat() < inv.Terms.Value {
|
||||
return nil, resultHtlcSetTotalTooLow, nil
|
||||
}
|
||||
|
||||
// Check whether total amt matches other htlcs in the set.
|
||||
var newSetTotal lnwire.MilliSatoshi
|
||||
for _, htlc := range inv.Htlcs {
|
||||
// Only consider accepted mpp htlcs. It is possible that there
|
||||
// are htlcs registered in the invoice database that previously
|
||||
// timed out and are in the canceled state now.
|
||||
if htlc.State != channeldb.HtlcStateAccepted {
|
||||
continue
|
||||
}
|
||||
|
||||
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
|
||||
return nil, resultHtlcSetTotalMismatch, nil
|
||||
}
|
||||
|
||||
newSetTotal += htlc.Amt
|
||||
}
|
||||
|
||||
// Add amount of new htlc.
|
||||
newSetTotal += ctx.amtPaid
|
||||
|
||||
// Make sure the communicated set total isn't overpaid.
|
||||
if newSetTotal > ctx.mpp.TotalMsat() {
|
||||
return nil, resultHtlcSetOverpayment, nil
|
||||
}
|
||||
|
||||
// The invoice is still open. Check the expiry.
|
||||
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
||||
return nil, resultExpiryTooSoon, nil
|
||||
}
|
||||
|
||||
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
|
||||
return nil, resultExpiryTooSoon, nil
|
||||
}
|
||||
|
||||
// Record HTLC in the invoice database.
|
||||
newHtlcs := map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc{
|
||||
ctx.circuitKey: acceptDesc,
|
||||
}
|
||||
|
||||
update := channeldb.InvoiceUpdateDesc{
|
||||
AddHtlcs: newHtlcs,
|
||||
}
|
||||
|
||||
// If the invoice cannot be settled yet, only record the htlc.
|
||||
setComplete := newSetTotal == ctx.mpp.TotalMsat()
|
||||
if !setComplete {
|
||||
return &update, resultPartialAccepted, nil
|
||||
}
|
||||
|
||||
// Check to see if we can settle or this is an hold invoice and
|
||||
// we need to wait for the preimage.
|
||||
holdInvoice := inv.Terms.PaymentPreimage == channeldb.UnknownPreimage
|
||||
if holdInvoice {
|
||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||
NewState: channeldb.ContractAccepted,
|
||||
}
|
||||
return &update, resultAccepted, nil
|
||||
}
|
||||
|
||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||
NewState: channeldb.ContractSettled,
|
||||
Preimage: inv.Terms.PaymentPreimage,
|
||||
}
|
||||
|
||||
return &update, resultSettled, nil
|
||||
}
|
||||
|
||||
// updateLegacy is a callback for DB.UpdateInvoice that contains the invoice
|
||||
// settlement logic for legacy payments.
|
||||
func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||
*channeldb.InvoiceUpdateDesc, updateResult, error) {
|
||||
|
||||
// If the invoice is already canceled, there is no further
|
||||
// checking to do.
|
||||
if inv.State == channeldb.ContractCanceled {
|
||||
return nil, resultInvoiceAlreadyCanceled, nil
|
||||
}
|
||||
@ -116,6 +263,20 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||
return nil, resultAmountTooLow, nil
|
||||
}
|
||||
|
||||
// TODO(joostjager): Check invoice mpp required feature
|
||||
// bit when feature becomes mandatory.
|
||||
|
||||
// Don't allow settling the invoice with an old style
|
||||
// htlc if we are already in the process of gathering an
|
||||
// mpp set.
|
||||
for _, htlc := range inv.Htlcs {
|
||||
if htlc.State == channeldb.HtlcStateAccepted &&
|
||||
htlc.MppTotalAmt > 0 {
|
||||
|
||||
return nil, resultMppInProgress, nil
|
||||
}
|
||||
}
|
||||
|
||||
// The invoice is still open. Check the expiry.
|
||||
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
||||
return nil, resultExpiryTooSoon, nil
|
||||
|
@ -75,14 +75,15 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
||||
}
|
||||
|
||||
rpcHtlc := lnrpc.InvoiceHTLC{
|
||||
ChanId: key.ChanID.ToUint64(),
|
||||
HtlcIndex: key.HtlcID,
|
||||
AcceptHeight: int32(htlc.AcceptHeight),
|
||||
AcceptTime: htlc.AcceptTime.Unix(),
|
||||
ExpiryHeight: int32(htlc.Expiry),
|
||||
AmtMsat: uint64(htlc.Amt),
|
||||
State: state,
|
||||
CustomRecords: htlc.CustomRecords,
|
||||
ChanId: key.ChanID.ToUint64(),
|
||||
HtlcIndex: key.HtlcID,
|
||||
AcceptHeight: int32(htlc.AcceptHeight),
|
||||
AcceptTime: htlc.AcceptTime.Unix(),
|
||||
ExpiryHeight: int32(htlc.Expiry),
|
||||
AmtMsat: uint64(htlc.Amt),
|
||||
State: state,
|
||||
CustomRecords: htlc.CustomRecords,
|
||||
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
|
||||
}
|
||||
|
||||
// Only report resolved times if htlc is resolved.
|
||||
|
1151
lnrpc/rpc.pb.go
1151
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -2407,6 +2407,9 @@ message InvoiceHTLC {
|
||||
|
||||
/// Custom tlv records.
|
||||
map<uint64, bytes> custom_records = 9 [json_name = "custom_records"];
|
||||
|
||||
/// The total amount of the mpp payment in msat.
|
||||
uint64 mpp_total_amt_msat = 10 [json_name = "mpp_total_amt_msat"];
|
||||
}
|
||||
|
||||
message AddInvoiceResponse {
|
||||
|
@ -2854,6 +2854,11 @@
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "/ Custom tlv records."
|
||||
},
|
||||
"mpp_total_amt_msat": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "/ The total amount of the mpp payment in msat."
|
||||
}
|
||||
},
|
||||
"title": "/ Details of an HTLC that paid to an invoice"
|
||||
|
@ -4712,13 +4712,29 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
||||
}
|
||||
|
||||
// Create invoices for Dave, which expect a payment from Carol.
|
||||
_, rHashes, _, err := createPayReqs(
|
||||
payReqs, rHashes, _, err := createPayReqs(
|
||||
dave, paymentAmtSat, numPayments,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create pay reqs: %v", err)
|
||||
}
|
||||
|
||||
// Reconstruct payment addresses.
|
||||
var payAddrs [][]byte
|
||||
for _, payReq := range payReqs {
|
||||
ctx, _ := context.WithTimeout(
|
||||
context.Background(), defaultTimeout,
|
||||
)
|
||||
resp, err := dave.DecodePayReq(
|
||||
ctx,
|
||||
&lnrpc.PayReqString{PayReq: payReq},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("decode pay req: %v", err)
|
||||
}
|
||||
payAddrs = append(payAddrs, resp.PaymentAddr)
|
||||
}
|
||||
|
||||
// Query for routes to pay from Carol to Dave.
|
||||
// We set FinalCltvDelta to 40 since by default QueryRoutes returns
|
||||
// the last hop with a final cltv delta of 9 where as the default in
|
||||
@ -4741,12 +4757,10 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
||||
// Construct a closure that will set MPP fields on the route, which
|
||||
// allows us to test MPP payments.
|
||||
setMPPFields := func(i int) {
|
||||
addr := [32]byte{byte(i)}
|
||||
|
||||
hop := r.Hops[len(r.Hops)-1]
|
||||
hop.TlvPayload = true
|
||||
hop.MppRecord = &lnrpc.MPPRecord{
|
||||
PaymentAddr: addr[:],
|
||||
PaymentAddr: payAddrs[i],
|
||||
TotalAmtMsat: paymentAmtSat * 1000,
|
||||
}
|
||||
}
|
||||
@ -4930,8 +4944,8 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
||||
hop.MppRecord.TotalAmtMsat)
|
||||
}
|
||||
|
||||
expAddr := [32]byte{byte(i)}
|
||||
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr[:]) {
|
||||
expAddr := payAddrs[i]
|
||||
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr) {
|
||||
t.Fatalf("incorrect mpp payment addr for payment %d "+
|
||||
"want: %x, got: %x",
|
||||
i, expAddr, hop.MppRecord.PaymentAddr)
|
||||
|
@ -380,6 +380,9 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
||||
|
||||
registryConfig := invoices.RegistryConfig{
|
||||
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||
Now: time.Now,
|
||||
TickAfter: time.After,
|
||||
}
|
||||
|
||||
s := &server{
|
||||
|
Loading…
x
Reference in New Issue
Block a user