Skip to content

Commit

Permalink
invoices: optionally hold and auto-cancel keysend payments
Browse files Browse the repository at this point in the history
Adds a new configuration flag to lnd that will keep keysend payments in
the accepted state. An application can then inspect the payment
parameters and decide whether to settle or cancel.

The on-the-fly inserted keysend invoices get a configurable expiry time.
This is a safeguard in case the application that should decide on the
keysend payments isn't active.
  • Loading branch information
joostjager committed Apr 9, 2020
1 parent c47cb06 commit 2464fdf
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 17 deletions.
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ type config struct {

AcceptKeySend bool `long:"accept-keysend" description:"If true, spontaneous payments through keysend will be accepted. [experimental]"`

KeySendHoldTime time.Duration `long:"keysend-hold-time" description:"If non-zero, keysend payments are accepted but not immediately settled. If the payment isn't settled manually after the specified time, it is canceled automatically. [experimental]"`

Routing *routing.Conf `group:"routing" namespace:"routing"`

Workers *lncfg.Workers `group:"workers" namespace:"workers"`
Expand Down
14 changes: 11 additions & 3 deletions invoices/invoice_expiry_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type invoiceExpiry struct {
PaymentHash lntypes.Hash
Expiry time.Time
Keysend bool
}

// Less implements PriorityQueueItem.Less such that the top item in the
Expand All @@ -41,7 +42,7 @@ type InvoiceExpiryWatcher struct {
clock clock.Clock

// cancelInvoice is a template method that cancels an expired invoice.
cancelInvoice func(lntypes.Hash) error
cancelInvoice func(lntypes.Hash, bool) error

// expiryQueue holds invoiceExpiry items and is used to find the next
// invoice to expire.
Expand Down Expand Up @@ -71,7 +72,7 @@ func NewInvoiceExpiryWatcher(clock clock.Clock) *InvoiceExpiryWatcher {
// expects a cancellation function passed that will be use to cancel expired
// invoices by their payment hash.
func (ew *InvoiceExpiryWatcher) Start(
cancelInvoice func(lntypes.Hash) error) error {
cancelInvoice func(lntypes.Hash, bool) error) error {

ew.Lock()
defer ew.Unlock()
Expand Down Expand Up @@ -121,6 +122,7 @@ func (ew *InvoiceExpiryWatcher) prepareInvoice(
return &invoiceExpiry{
PaymentHash: paymentHash,
Expiry: expiry,
Keysend: len(invoice.PaymentRequest) == 0,
}
}

Expand Down Expand Up @@ -190,7 +192,13 @@ func (ew *InvoiceExpiryWatcher) cancelNextExpiredInvoice() {
return
}

err := ew.cancelInvoice(top.PaymentHash)
// Don't force-cancel already accepted invoices. An exception to
// this are auto-generated keysend invoices. Because those start
// out in the Accepted state, the expiry field would never be
// used. Enabling cancellation for accepted keysend invoices
// creates a safety mechanism that can prevents channel
// force-closes.
err := ew.cancelInvoice(top.PaymentHash, top.Keysend)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled &&
err != channeldb.ErrInvoiceAlreadyCanceled {

Expand Down
6 changes: 4 additions & 2 deletions invoices/invoice_expiry_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ func newInvoiceExpiryWatcherTest(t *testing.T, now time.Time,
),
}

err := test.watcher.Start(func(paymentHash lntypes.Hash) error {
err := test.watcher.Start(func(paymentHash lntypes.Hash,
force bool) error {

test.canceledInvoices = append(test.canceledInvoices, paymentHash)
return nil
})
Expand Down Expand Up @@ -60,7 +62,7 @@ func (t *invoiceExpiryWatcherTest) checkExpectations() {
// Tests that InvoiceExpiryWatcher can be started and stopped.
func TestInvoiceExpiryWatcherStartStop(t *testing.T) {
watcher := NewInvoiceExpiryWatcher(clock.NewTestClock(testTime))
cancel := func(lntypes.Hash) error {
cancel := func(lntypes.Hash, bool) error {
t.Fatalf("unexpected call")
return nil
}
Expand Down
15 changes: 10 additions & 5 deletions invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ type RegistryConfig struct {
// AcceptKeySend indicates whether we want to accept spontaneous key
// send payments.
AcceptKeySend bool

// KeySendHoldTime indicates for how long we want to accept and hold
// spontaneous key send payments.
KeySendHoldTime time.Duration
}

// htlcReleaseEvent describes an htlc auto-release event. It is used to release
Expand Down Expand Up @@ -165,10 +169,7 @@ func (i *InvoiceRegistry) populateExpiryWatcher() error {
func (i *InvoiceRegistry) Start() error {
// Start InvoiceExpiryWatcher and prepopulate it with existing active
// invoices.
err := i.expiryWatcher.Start(func(paymentHash lntypes.Hash) error {
cancelIfAccepted := false
return i.cancelInvoiceImpl(paymentHash, cancelIfAccepted)
})
err := i.expiryWatcher.Start(i.cancelInvoiceImpl)

if err != nil {
return err
Expand Down Expand Up @@ -649,7 +650,6 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
// processKeySend just-in-time inserts an invoice if this htlc is a keysend
// htlc.
func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {

// Retrieve keysend record if present.
preimageSlice, ok := ctx.customRecords[record.KeySendType]
if !ok {
Expand Down Expand Up @@ -697,6 +697,11 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {
},
}

if i.cfg.KeySendHoldTime != 0 {
invoice.HodlInvoice = true
invoice.Terms.Expiry = i.cfg.KeySendHoldTime
}

// Insert invoice into database. Ignore duplicates, because this
// may be a replay.
_, err = i.AddInvoice(invoice, ctx.hash)
Expand Down
30 changes: 23 additions & 7 deletions invoices/invoiceregistry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,23 +637,29 @@ func TestUnknownInvoice(t *testing.T) {
// TestKeySend tests receiving a spontaneous payment with and without keysend
// enabled.
func TestKeySend(t *testing.T) {
t.Run("enabled hold", func(t *testing.T) {
testKeySend(t, true, time.Minute)
})
t.Run("enabled", func(t *testing.T) {
testKeySend(t, true)
testKeySend(t, true, 0)
})
t.Run("disabled", func(t *testing.T) {
testKeySend(t, false)
testKeySend(t, false, 0)
})
}

// testKeySend is the inner test function that tests keysend for a particular
// enabled state on the receiver end.
func testKeySend(t *testing.T, keySendEnabled bool) {
func testKeySend(t *testing.T, enabled bool,
holdDuration time.Duration) {

defer timeout()()

ctx := newTestContext(t)
defer ctx.cleanup()

ctx.registry.cfg.AcceptKeySend = keySendEnabled
ctx.registry.cfg.AcceptKeySend = enabled
ctx.registry.cfg.KeySendHoldTime = holdDuration

allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
Expand Down Expand Up @@ -689,10 +695,10 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
}

switch {
case !keySendEnabled && failResolution.Outcome != ResultInvoiceNotFound:
case !enabled && failResolution.Outcome != ResultInvoiceNotFound:
t.Fatal("expected invoice not found outcome")

case keySendEnabled && failResolution.Outcome != ResultKeySendError:
case enabled && failResolution.Outcome != ResultKeySendError:
t.Fatal("expected keysend error")
}

Expand All @@ -712,7 +718,7 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
}

// Expect a cancel resolution if keysend is disabled.
if !keySendEnabled {
if !enabled {
failResolution, ok = resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
Expand All @@ -724,6 +730,16 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
return
}

if holdDuration != 0 {
if resolution != nil {
t.Fatalf("expected hold resolution")
}

// TODO(joostjager): Assert manual settle or timeout.

return
}

// Otherwise we expect no error and a settle resolution for the htlc.
settleResolution, ok := resolution.(*HtlcSettleResolution)
if !ok {
Expand Down
1 change: 1 addition & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
Clock: clock.NewDefaultClock(),
AcceptKeySend: cfg.AcceptKeySend,
KeySendHoldTime: cfg.KeySendHoldTime,
}

s := &server{
Expand Down

0 comments on commit 2464fdf

Please sign in to comment.