invoices: jit insert key send invoices
This commit adds handling code for the key send custom record. If this record is present and its hash matches the payment hash, invoice registry will insert a new invoice into the database "just in time". The subsequent settle flow is unchanged. The newly inserted invoice is picked up and settled. Notifications will be broadcast as usual.
This commit is contained in:
parent
202b7c26a7
commit
1d5844c196
@ -219,7 +219,8 @@ type Invoice struct {
|
|||||||
// or any other message which fits within the size constraints.
|
// or any other message which fits within the size constraints.
|
||||||
Memo []byte
|
Memo []byte
|
||||||
|
|
||||||
// PaymentRequest is the encoded payment request for this invoice.
|
// PaymentRequest is the encoded payment request for this invoice. For
|
||||||
|
// spontaneous (key send) payments, this field will be empty.
|
||||||
PaymentRequest []byte
|
PaymentRequest []byte
|
||||||
|
|
||||||
// CreationDate is the exact time the invoice was created.
|
// CreationDate is the exact time the invoice was created.
|
||||||
|
@ -326,6 +326,8 @@ type config struct {
|
|||||||
|
|
||||||
EnableUpfrontShutdown bool `long:"enable-upfront-shutdown" description:"If true, option upfront shutdown script will be enabled. If peers that we open channels with support this feature, we will automatically set the script to which cooperative closes should be paid out to on channel open. This offers the partial protection of a channel peer disconnecting from us if cooperative close is attempted with a different script."`
|
EnableUpfrontShutdown bool `long:"enable-upfront-shutdown" description:"If true, option upfront shutdown script will be enabled. If peers that we open channels with support this feature, we will automatically set the script to which cooperative closes should be paid out to on channel open. This offers the partial protection of a channel peer disconnecting from us if cooperative close is attempted with a different script."`
|
||||||
|
|
||||||
|
AcceptKeySend bool `long:"accept-key-send" description:"If true, spontaneous payments through key send will be accepted. [experimental]"`
|
||||||
|
|
||||||
Routing *routing.Conf `group:"routing" namespace:"routing"`
|
Routing *routing.Conf `group:"routing" namespace:"routing"`
|
||||||
|
|
||||||
Workers *lncfg.Workers `group:"workers" namespace:"workers"`
|
Workers *lncfg.Workers `group:"workers" namespace:"workers"`
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -94,6 +95,10 @@ type RegistryConfig struct {
|
|||||||
// Now() and TickAfter() and is useful to stub out the clock functions
|
// Now() and TickAfter() and is useful to stub out the clock functions
|
||||||
// during testing.
|
// during testing.
|
||||||
Clock clock.Clock
|
Clock clock.Clock
|
||||||
|
|
||||||
|
// AcceptKeySend indicates whether we want to accept spontaneous key
|
||||||
|
// send payments.
|
||||||
|
AcceptKeySend bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
||||||
@ -690,6 +695,68 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processKeySend just-in-time inserts an invoice if this htlc is a key send
|
||||||
|
// htlc.
|
||||||
|
func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx,
|
||||||
|
hash lntypes.Hash) error {
|
||||||
|
|
||||||
|
// Retrieve key send record if present.
|
||||||
|
preimageSlice, ok := ctx.customRecords[record.KeySendType]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel htlc is preimage is invalid.
|
||||||
|
preimage, err := lntypes.MakePreimage(preimageSlice)
|
||||||
|
if err != nil || preimage.Hash() != hash {
|
||||||
|
return errors.New("invalid key send preimage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't accept zero preimages as those have a special meaning in our
|
||||||
|
// database for hodl invoices.
|
||||||
|
if preimage == channeldb.UnknownPreimage {
|
||||||
|
return errors.New("invalid key send preimage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow key send for non-mpp payments.
|
||||||
|
if ctx.mpp != nil {
|
||||||
|
return errors.New("no mpp key send supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an invoice for the htlc amount.
|
||||||
|
amt := ctx.amtPaid
|
||||||
|
|
||||||
|
// Set tlv optional feature vector on the invoice. Otherwise we wouldn't
|
||||||
|
// be able to pay to it with key send.
|
||||||
|
rawFeatures := lnwire.NewRawFeatureVector(
|
||||||
|
lnwire.TLVOnionPayloadOptional,
|
||||||
|
)
|
||||||
|
features := lnwire.NewFeatureVector(rawFeatures, lnwire.Features)
|
||||||
|
|
||||||
|
// Use the minimum block delta that we require for settling htlcs.
|
||||||
|
finalCltvDelta := i.cfg.FinalCltvRejectDelta
|
||||||
|
|
||||||
|
// Create placeholder invoice.
|
||||||
|
invoice := &channeldb.Invoice{
|
||||||
|
CreationDate: i.cfg.Clock.Now(),
|
||||||
|
Terms: channeldb.ContractTerm{
|
||||||
|
FinalCltvDelta: finalCltvDelta,
|
||||||
|
Value: amt,
|
||||||
|
PaymentPreimage: preimage,
|
||||||
|
Features: features,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert invoice into database. Ignore duplicates, because this
|
||||||
|
// may be a replay.
|
||||||
|
_, err = i.AddInvoice(invoice, hash)
|
||||||
|
if err != nil && err != channeldb.ErrDuplicateInvoice {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
//
|
//
|
||||||
@ -729,6 +796,20 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
mpp: mpp,
|
mpp: mpp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process key send if present. Do this outside of the lock, because
|
||||||
|
// AddInvoice obtains its own lock. This is no problem, because the
|
||||||
|
// operation is idempotent.
|
||||||
|
if i.cfg.AcceptKeySend {
|
||||||
|
err := i.processKeySend(updateCtx, rHash)
|
||||||
|
if err != nil {
|
||||||
|
debugLog(fmt.Sprintf("key send error: %v", err))
|
||||||
|
|
||||||
|
return NewFailureResolution(
|
||||||
|
circuitKey, currentHeight, ResultKeySendError,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
i.Lock()
|
i.Lock()
|
||||||
defer i.Unlock()
|
defer i.Unlock()
|
||||||
|
|
||||||
|
@ -598,6 +598,110 @@ func TestUnknownInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestKeySend tests receiving a spontaneous payment with and without key send
|
||||||
|
// enabled.
|
||||||
|
func TestKeySend(t *testing.T) {
|
||||||
|
t.Run("enabled", func(t *testing.T) {
|
||||||
|
testKeySend(t, true)
|
||||||
|
})
|
||||||
|
t.Run("disabled", func(t *testing.T) {
|
||||||
|
testKeySend(t, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// testKeySend is the inner test function that tests key send for a particular
|
||||||
|
// enabled state on the receiver end.
|
||||||
|
func testKeySend(t *testing.T, keySendEnabled bool) {
|
||||||
|
defer timeout()()
|
||||||
|
|
||||||
|
ctx := newTestContext(t)
|
||||||
|
defer ctx.cleanup()
|
||||||
|
|
||||||
|
ctx.registry.cfg.AcceptKeySend = keySendEnabled
|
||||||
|
|
||||||
|
allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
|
hodlChan := make(chan interface{}, 1)
|
||||||
|
|
||||||
|
amt := lnwire.MilliSatoshi(1000)
|
||||||
|
expiry := uint32(testCurrentHeight + 20)
|
||||||
|
|
||||||
|
// Create key for key send.
|
||||||
|
preimage := lntypes.Preimage{1, 2, 3}
|
||||||
|
hash := preimage.Hash()
|
||||||
|
|
||||||
|
// Try to settle invoice with an invalid key send htlc.
|
||||||
|
invalidKeySendPayload := &mockPayload{
|
||||||
|
customRecords: map[uint64][]byte{
|
||||||
|
record.KeySendType: {1, 2, 3},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
|
hash, amt, expiry,
|
||||||
|
testCurrentHeight, getCircuitKey(10), hodlChan,
|
||||||
|
invalidKeySendPayload,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a cancel resolution with the correct outcome.
|
||||||
|
if resolution.Preimage != nil {
|
||||||
|
t.Fatal("expected cancel resolution")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !keySendEnabled && resolution.Outcome != ResultInvoiceNotFound:
|
||||||
|
t.Fatal("expected invoice not found outcome")
|
||||||
|
|
||||||
|
case keySendEnabled && resolution.Outcome != ResultKeySendError:
|
||||||
|
t.Fatal("expected key send error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to settle invoice with a valid key send htlc.
|
||||||
|
keySendPayload := &mockPayload{
|
||||||
|
customRecords: map[uint64][]byte{
|
||||||
|
record.KeySendType: preimage[:],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
|
hash, amt, expiry,
|
||||||
|
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a cancel resolution if key send is disabled.
|
||||||
|
if !keySendEnabled {
|
||||||
|
if resolution.Outcome != ResultInvoiceNotFound {
|
||||||
|
t.Fatal("expected key send payment not to be accepted")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we expect no error and a settle resolution for the htlc.
|
||||||
|
if resolution.Preimage == nil || *resolution.Preimage != preimage {
|
||||||
|
t.Fatal("expected valid settle event")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect a new invoice notification to be sent out.
|
||||||
|
newInvoice := <-allSubscriptions.NewInvoices
|
||||||
|
if newInvoice.State != channeldb.ContractOpen {
|
||||||
|
t.Fatalf("expected state ContractOpen, but got %v",
|
||||||
|
newInvoice.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect a settled notification to be sent out.
|
||||||
|
settledInvoice := <-allSubscriptions.SettledInvoices
|
||||||
|
if settledInvoice.State != channeldb.ContractSettled {
|
||||||
|
t.Fatalf("expected state ContractOpen, but got %v",
|
||||||
|
settledInvoice.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
||||||
// It covers the case where there is a mpp timeout before the whole invoice is
|
// It covers the case where there is a mpp timeout before the whole invoice is
|
||||||
// paid and the case where the invoice is settled in time.
|
// paid and the case where the invoice is settled in time.
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
type mockPayload struct {
|
type mockPayload struct {
|
||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
|
customRecords record.CustomSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *mockPayload) MultiPath() *record.MPP {
|
func (p *mockPayload) MultiPath() *record.MPP {
|
||||||
@ -29,7 +30,13 @@ func (p *mockPayload) MultiPath() *record.MPP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *mockPayload) CustomRecords() record.CustomSet {
|
func (p *mockPayload) CustomRecords() record.CustomSet {
|
||||||
|
// This function should always return a map instance, but for mock
|
||||||
|
// configuration we do accept nil.
|
||||||
|
if p.customRecords == nil {
|
||||||
return make(record.CustomSet)
|
return make(record.CustomSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.customRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -85,6 +85,10 @@ const (
|
|||||||
// ResultInvoiceNotFound is returned when an attempt is made to pay an
|
// ResultInvoiceNotFound is returned when an attempt is made to pay an
|
||||||
// invoice that is unknown to us.
|
// invoice that is unknown to us.
|
||||||
ResultInvoiceNotFound
|
ResultInvoiceNotFound
|
||||||
|
|
||||||
|
// ResultKeySendError is returned when we receive invalid key send
|
||||||
|
// parameters.
|
||||||
|
ResultKeySendError
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable representation of the invoice update result.
|
// String returns a human-readable representation of the invoice update result.
|
||||||
@ -145,6 +149,9 @@ func (u ResolutionResult) String() string {
|
|||||||
case ResultHtlcSetOverpayment:
|
case ResultHtlcSetOverpayment:
|
||||||
return "mpp is overpaying set total"
|
return "mpp is overpaying set total"
|
||||||
|
|
||||||
|
case ResultKeySendError:
|
||||||
|
return "invalid key send parameters"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
6
record/experimental.go
Normal file
6
record/experimental.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package record
|
||||||
|
|
||||||
|
const (
|
||||||
|
// KeySendType is the custom record identifier for key send preimages.
|
||||||
|
KeySendType uint64 = 5482373484
|
||||||
|
)
|
@ -384,6 +384,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
|||||||
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
||||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||||
Clock: clock.NewDefaultClock(),
|
Clock: clock.NewDefaultClock(),
|
||||||
|
AcceptKeySend: cfg.AcceptKeySend,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &server{
|
s := &server{
|
||||||
|
Loading…
Reference in New Issue
Block a user