mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-31 18:21:42 +02:00
invoices: accept mpp payments
This commit is contained in:
parent
56958493fe
commit
b2f43858c3
@ -117,6 +117,7 @@ const (
|
|||||||
resolveTimeType tlv.Type = 11
|
resolveTimeType tlv.Type = 11
|
||||||
expiryHeightType tlv.Type = 13
|
expiryHeightType tlv.Type = 13
|
||||||
htlcStateType tlv.Type = 15
|
htlcStateType tlv.Type = 15
|
||||||
|
mppTotalAmtType tlv.Type = 17
|
||||||
|
|
||||||
// A set of tlv type definitions used to serialize invoice bodiees.
|
// 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 is the amount that is carried by this htlc.
|
||||||
Amt lnwire.MilliSatoshi
|
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
|
// AcceptHeight is the block height at which the invoice registry
|
||||||
// decided to accept this htlc as a payment to the invoice. At this
|
// decided to accept this htlc as a payment to the invoice. At this
|
||||||
// height, the invoice cltv delay must have been met.
|
// 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 is the amount that is carried by this htlc.
|
||||||
Amt lnwire.MilliSatoshi
|
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 is the expiry height of this htlc.
|
||||||
Expiry uint32
|
Expiry uint32
|
||||||
|
|
||||||
@ -1018,6 +1027,7 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
|||||||
// Encode the htlc in a tlv stream.
|
// Encode the htlc in a tlv stream.
|
||||||
chanID := key.ChanID.ToUint64()
|
chanID := key.ChanID.ToUint64()
|
||||||
amt := uint64(htlc.Amt)
|
amt := uint64(htlc.Amt)
|
||||||
|
mppTotalAmt := uint64(htlc.MppTotalAmt)
|
||||||
acceptTime := uint64(htlc.AcceptTime.UnixNano())
|
acceptTime := uint64(htlc.AcceptTime.UnixNano())
|
||||||
resolveTime := uint64(htlc.ResolveTime.UnixNano())
|
resolveTime := uint64(htlc.ResolveTime.UnixNano())
|
||||||
state := uint8(htlc.State)
|
state := uint8(htlc.State)
|
||||||
@ -1034,6 +1044,7 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
|||||||
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
||||||
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
||||||
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
||||||
|
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Convert the custom records to tlv.Record types that are ready
|
// 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
|
chanID uint64
|
||||||
state uint8
|
state uint8
|
||||||
acceptTime, resolveTime uint64
|
acceptTime, resolveTime uint64
|
||||||
amt uint64
|
amt, mppTotalAmt uint64
|
||||||
)
|
)
|
||||||
tlvStream, err := tlv.NewStream(
|
tlvStream, err := tlv.NewStream(
|
||||||
tlv.MakePrimitiveRecord(chanIDType, &chanID),
|
tlv.MakePrimitiveRecord(chanIDType, &chanID),
|
||||||
@ -1206,6 +1217,7 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
|||||||
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
|
||||||
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
||||||
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
||||||
|
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1221,6 +1233,7 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
|||||||
htlc.ResolveTime = time.Unix(0, int64(resolveTime))
|
htlc.ResolveTime = time.Unix(0, int64(resolveTime))
|
||||||
htlc.State = HtlcState(state)
|
htlc.State = HtlcState(state)
|
||||||
htlc.Amt = lnwire.MilliSatoshi(amt)
|
htlc.Amt = lnwire.MilliSatoshi(amt)
|
||||||
|
htlc.MppTotalAmt = lnwire.MilliSatoshi(mppTotalAmt)
|
||||||
|
|
||||||
// Reconstruct the custom records fields from the parsed types
|
// Reconstruct the custom records fields from the parsed types
|
||||||
// map return from the tlv parser.
|
// map return from the tlv parser.
|
||||||
@ -1324,6 +1337,7 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
|||||||
|
|
||||||
htlc := &InvoiceHTLC{
|
htlc := &InvoiceHTLC{
|
||||||
Amt: htlcUpdate.Amt,
|
Amt: htlcUpdate.Amt,
|
||||||
|
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
||||||
Expiry: htlcUpdate.Expiry,
|
Expiry: htlcUpdate.Expiry,
|
||||||
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
||||||
AcceptTime: now,
|
AcceptTime: now,
|
||||||
|
@ -2,8 +2,10 @@ package invoices
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
@ -26,6 +28,12 @@ var (
|
|||||||
ErrShuttingDown = errors.New("invoice registry shutting down")
|
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
|
// 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
|
// set, the event indicates a settle event. If Preimage is nil, it is a cancel
|
||||||
// event.
|
// event.
|
||||||
@ -49,6 +57,38 @@ type RegistryConfig struct {
|
|||||||
// expiry of any invoice we create and the code effectuating this should
|
// expiry of any invoice we create and the code effectuating this should
|
||||||
// not be hit.
|
// not be hit.
|
||||||
FinalCltvRejectDelta int32
|
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
|
// 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.
|
// subscriber. This is used to unsubscribe from all hashes efficiently.
|
||||||
hodlReverseSubscriptions map[chan<- interface{}]map[channeldb.CircuitKey]struct{}
|
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
|
wg sync.WaitGroup
|
||||||
quit chan struct{}
|
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
|
// 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.
|
// which are volatile yet available system wide within the daemon.
|
||||||
func NewRegistry(cdb *channeldb.DB, cfg *RegistryConfig) *InvoiceRegistry {
|
func NewRegistry(cdb *channeldb.DB, cfg *RegistryConfig) *InvoiceRegistry {
|
||||||
|
|
||||||
return &InvoiceRegistry{
|
return &InvoiceRegistry{
|
||||||
cdb: cdb,
|
cdb: cdb,
|
||||||
notificationClients: make(map[uint32]*InvoiceSubscription),
|
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{}),
|
hodlSubscriptions: make(map[channeldb.CircuitKey]map[chan<- interface{}]struct{}),
|
||||||
hodlReverseSubscriptions: make(map[chan<- interface{}]map[channeldb.CircuitKey]struct{}),
|
hodlReverseSubscriptions: make(map[chan<- interface{}]map[channeldb.CircuitKey]struct{}),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
htlcAutoReleaseChan: make(chan *htlcReleaseEvent),
|
||||||
quit: make(chan struct{}),
|
quit: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,7 +154,7 @@ func NewRegistry(cdb *channeldb.DB, cfg *RegistryConfig) *InvoiceRegistry {
|
|||||||
func (i *InvoiceRegistry) Start() error {
|
func (i *InvoiceRegistry) Start() error {
|
||||||
i.wg.Add(1)
|
i.wg.Add(1)
|
||||||
|
|
||||||
go i.invoiceEventNotifier()
|
go i.invoiceEventLoop()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -130,13 +174,31 @@ type invoiceEvent struct {
|
|||||||
invoice *channeldb.Invoice
|
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
|
// new notification subscriptions, cancelling old subscriptions, and
|
||||||
// dispatching new invoice events.
|
// dispatching new invoice events.
|
||||||
func (i *InvoiceRegistry) invoiceEventNotifier() {
|
func (i *InvoiceRegistry) invoiceEventLoop() {
|
||||||
defer i.wg.Done()
|
defer i.wg.Done()
|
||||||
|
|
||||||
|
// Set up a heap for htlc auto-releases.
|
||||||
|
autoReleaseHeap := &queue.PriorityQueue{}
|
||||||
|
|
||||||
for {
|
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 {
|
select {
|
||||||
// A new invoice subscription for all invoices has just arrived!
|
// A new invoice subscription for all invoices has just arrived!
|
||||||
// We'll query for any backlog notifications, then add it to the
|
// 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
|
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:
|
case <-i.quit:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -418,6 +503,114 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
|||||||
return i.cdb.LookupInvoice(rHash)
|
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
|
// NotifyExitHopHtlc attempts to mark an invoice as settled. The return value
|
||||||
// describes how the htlc should be resolved.
|
// 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
|
// 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
|
// the channel is either buffered or received on from another goroutine to
|
||||||
// prevent deadlock.
|
// 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,
|
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||||
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
@ -436,9 +634,11 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
i.Lock()
|
i.Lock()
|
||||||
defer i.Unlock()
|
defer i.Unlock()
|
||||||
|
|
||||||
|
mpp := payload.MultiPath()
|
||||||
|
|
||||||
debugLog := func(s string) {
|
debugLog := func(s string) {
|
||||||
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v",
|
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v, "+
|
||||||
rHash[:], s, amtPaid, expiry, circuitKey)
|
"mpp=%v", rHash[:], s, amtPaid, expiry, circuitKey, mpp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the update context containing the relevant details of the
|
// Create the update context containing the relevant details of the
|
||||||
@ -450,6 +650,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
currentHeight: currentHeight,
|
currentHeight: currentHeight,
|
||||||
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
||||||
customRecords: payload.CustomRecords(),
|
customRecords: payload.CustomRecords(),
|
||||||
|
mpp: mpp,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll attempt to settle an invoice matching this rHash on disk (if
|
// 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
|
}, nil
|
||||||
|
|
||||||
case channeldb.HtlcStateSettled:
|
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{
|
return &HodlEvent{
|
||||||
CircuitKey: circuitKey,
|
CircuitKey: circuitKey,
|
||||||
Preimage: &invoice.Terms.PaymentPreimage,
|
Preimage: &invoice.Terms.PaymentPreimage,
|
||||||
@ -521,6 +737,19 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case channeldb.HtlcStateAccepted:
|
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)
|
i.hodlSubscribe(hodlChan, circuitKey)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
testTimeout = 5 * time.Second
|
testTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
testTime = time.Date(2018, time.February, 2, 14, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
preimage = lntypes.Preimage{
|
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, 0,
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
|
||||||
@ -59,20 +61,27 @@ var (
|
|||||||
|
|
||||||
type testContext struct {
|
type testContext struct {
|
||||||
registry *InvoiceRegistry
|
registry *InvoiceRegistry
|
||||||
|
clock *testClock
|
||||||
|
|
||||||
cleanup func()
|
cleanup func()
|
||||||
t *testing.T
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestContext(t *testing.T) *testContext {
|
func newTestContext(t *testing.T) *testContext {
|
||||||
|
clock := newTestClock(testTime)
|
||||||
|
|
||||||
cdb, cleanup, err := newDB()
|
cdb, cleanup, err := newDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
cdb.Now = clock.now
|
||||||
|
|
||||||
// Instantiate and start the invoice ctx.registry.
|
// Instantiate and start the invoice ctx.registry.
|
||||||
cfg := RegistryConfig{
|
cfg := RegistryConfig{
|
||||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||||
|
HtlcHoldDuration: 30 * time.Second,
|
||||||
|
Now: clock.now,
|
||||||
|
TickAfter: clock.tickAfter,
|
||||||
}
|
}
|
||||||
registry := NewRegistry(cdb, &cfg)
|
registry := NewRegistry(cdb, &cfg)
|
||||||
|
|
||||||
@ -84,6 +93,7 @@ func newTestContext(t *testing.T) *testContext {
|
|||||||
|
|
||||||
ctx := testContext{
|
ctx := testContext{
|
||||||
registry: registry,
|
registry: registry,
|
||||||
|
clock: clock,
|
||||||
t: t,
|
t: t,
|
||||||
cleanup: func() {
|
cleanup: func() {
|
||||||
registry.Stop()
|
registry.Stop()
|
||||||
@ -683,3 +693,85 @@ func (p *mockPayload) MultiPath() *record.MPP {
|
|||||||
func (p *mockPayload) CustomRecords() hop.CustomRecordSet {
|
func (p *mockPayload) CustomRecords() hop.CustomRecordSet {
|
||||||
return make(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/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
// updateResult is the result of the invoice update call.
|
// updateResult is the result of the invoice update call.
|
||||||
@ -24,6 +25,13 @@ const (
|
|||||||
resultDuplicateToSettled
|
resultDuplicateToSettled
|
||||||
resultAccepted
|
resultAccepted
|
||||||
resultSettled
|
resultSettled
|
||||||
|
resultInvoiceNotOpen
|
||||||
|
resultPartialAccepted
|
||||||
|
resultMppInProgress
|
||||||
|
resultAddressMismatch
|
||||||
|
resultHtlcSetTotalMismatch
|
||||||
|
resultHtlcSetTotalTooLow
|
||||||
|
resultHtlcSetOverpayment
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable representation of the invoice update result.
|
// String returns a human-readable representation of the invoice update result.
|
||||||
@ -63,6 +71,27 @@ func (u updateResult) String() string {
|
|||||||
case resultSettled:
|
case resultSettled:
|
||||||
return "settled"
|
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:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
@ -77,6 +106,7 @@ type invoiceUpdateCtx struct {
|
|||||||
currentHeight int32
|
currentHeight int32
|
||||||
finalCltvRejectDelta int32
|
finalCltvRejectDelta int32
|
||||||
customRecords hop.CustomRecordSet
|
customRecords hop.CustomRecordSet
|
||||||
|
mpp *record.MPP
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
|
// 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
|
if ctx.mpp == nil {
|
||||||
// do.
|
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 {
|
if inv.State == channeldb.ContractCanceled {
|
||||||
return nil, resultInvoiceAlreadyCanceled, nil
|
return nil, resultInvoiceAlreadyCanceled, nil
|
||||||
}
|
}
|
||||||
@ -116,6 +263,20 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
return nil, resultAmountTooLow, nil
|
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.
|
// The invoice is still open. Check the expiry.
|
||||||
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
||||||
return nil, resultExpiryTooSoon, nil
|
return nil, resultExpiryTooSoon, nil
|
||||||
|
@ -75,14 +75,15 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
|||||||
}
|
}
|
||||||
|
|
||||||
rpcHtlc := lnrpc.InvoiceHTLC{
|
rpcHtlc := lnrpc.InvoiceHTLC{
|
||||||
ChanId: key.ChanID.ToUint64(),
|
ChanId: key.ChanID.ToUint64(),
|
||||||
HtlcIndex: key.HtlcID,
|
HtlcIndex: key.HtlcID,
|
||||||
AcceptHeight: int32(htlc.AcceptHeight),
|
AcceptHeight: int32(htlc.AcceptHeight),
|
||||||
AcceptTime: htlc.AcceptTime.Unix(),
|
AcceptTime: htlc.AcceptTime.Unix(),
|
||||||
ExpiryHeight: int32(htlc.Expiry),
|
ExpiryHeight: int32(htlc.Expiry),
|
||||||
AmtMsat: uint64(htlc.Amt),
|
AmtMsat: uint64(htlc.Amt),
|
||||||
State: state,
|
State: state,
|
||||||
CustomRecords: htlc.CustomRecords,
|
CustomRecords: htlc.CustomRecords,
|
||||||
|
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only report resolved times if htlc is resolved.
|
// 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.
|
/// Custom tlv records.
|
||||||
map<uint64, bytes> custom_records = 9 [json_name = "custom_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 {
|
message AddInvoiceResponse {
|
||||||
|
@ -2854,6 +2854,11 @@
|
|||||||
"format": "byte"
|
"format": "byte"
|
||||||
},
|
},
|
||||||
"description": "/ Custom tlv records."
|
"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"
|
"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.
|
// Create invoices for Dave, which expect a payment from Carol.
|
||||||
_, rHashes, _, err := createPayReqs(
|
payReqs, rHashes, _, err := createPayReqs(
|
||||||
dave, paymentAmtSat, numPayments,
|
dave, paymentAmtSat, numPayments,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
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.
|
// Query for routes to pay from Carol to Dave.
|
||||||
// We set FinalCltvDelta to 40 since by default QueryRoutes returns
|
// 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
|
// 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
|
// Construct a closure that will set MPP fields on the route, which
|
||||||
// allows us to test MPP payments.
|
// allows us to test MPP payments.
|
||||||
setMPPFields := func(i int) {
|
setMPPFields := func(i int) {
|
||||||
addr := [32]byte{byte(i)}
|
|
||||||
|
|
||||||
hop := r.Hops[len(r.Hops)-1]
|
hop := r.Hops[len(r.Hops)-1]
|
||||||
hop.TlvPayload = true
|
hop.TlvPayload = true
|
||||||
hop.MppRecord = &lnrpc.MPPRecord{
|
hop.MppRecord = &lnrpc.MPPRecord{
|
||||||
PaymentAddr: addr[:],
|
PaymentAddr: payAddrs[i],
|
||||||
TotalAmtMsat: paymentAmtSat * 1000,
|
TotalAmtMsat: paymentAmtSat * 1000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4930,8 +4944,8 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
hop.MppRecord.TotalAmtMsat)
|
hop.MppRecord.TotalAmtMsat)
|
||||||
}
|
}
|
||||||
|
|
||||||
expAddr := [32]byte{byte(i)}
|
expAddr := payAddrs[i]
|
||||||
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr[:]) {
|
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr) {
|
||||||
t.Fatalf("incorrect mpp payment addr for payment %d "+
|
t.Fatalf("incorrect mpp payment addr for payment %d "+
|
||||||
"want: %x, got: %x",
|
"want: %x, got: %x",
|
||||||
i, expAddr, hop.MppRecord.PaymentAddr)
|
i, expAddr, hop.MppRecord.PaymentAddr)
|
||||||
|
@ -380,6 +380,9 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
|||||||
|
|
||||||
registryConfig := invoices.RegistryConfig{
|
registryConfig := invoices.RegistryConfig{
|
||||||
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
||||||
|
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||||
|
Now: time.Now,
|
||||||
|
TickAfter: time.After,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &server{
|
s := &server{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user