Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lncli+invoices: experimental key send mode #3795

Merged
merged 10 commits into from
Dec 24, 2019
4 changes: 2 additions & 2 deletions channeldb/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ type Invoice struct {
// or any other message which fits within the size constraints.
Memo []byte

// PaymentRequest is an optional field where a payment request created
// for this invoice can be stored.
// PaymentRequest is the encoded payment request for this invoice. For
// spontaneous (key send) payments, this field will be empty.
PaymentRequest []byte

// CreationDate is the exact time the invoice was created.
Expand Down
71 changes: 41 additions & 30 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/rand"
joostjager marked this conversation as resolved.
Show resolved Hide resolved
"encoding/hex"
"errors"
"fmt"
Expand All @@ -24,6 +25,8 @@ import (
"github.com/lightninglabs/protobuf-hex-display/proto"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/urfave/cli"
Expand Down Expand Up @@ -2144,12 +2147,6 @@ var sendPaymentCommand = cli.Command{
* --amt=A
* --final_cltv_delta=T
* --payment_hash=H

The --debug_send flag is provided for usage *purely* in test
environments. If specified, then the payment hash isn't required, as
it'll use the hash of all zeroes. This mode allows one to quickly test
payment connectivity without having to create an invoice at the
destination.
`,
ArgsUsage: "dest amt payment_hash final_cltv_delta | --pay_req=[payment request]",
Flags: append(paymentFlags(),
Expand All @@ -2166,14 +2163,14 @@ var sendPaymentCommand = cli.Command{
Name: "payment_hash, r",
Usage: "the hash to use within the payment's HTLC",
},
cli.BoolFlag{
joostjager marked this conversation as resolved.
Show resolved Hide resolved
Name: "debug_send",
Usage: "use the debug rHash when sending the HTLC",
},
cli.Int64Flag{
Name: "final_cltv_delta",
Usage: "the number of blocks the last hop has to reveal the preimage",
},
cli.BoolFlag{
Name: "key_send",
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
},
),
Action: sendPayment,
}
Expand Down Expand Up @@ -2276,11 +2273,25 @@ func sendPayment(ctx *cli.Context) error {
Amt: amount,
}

if ctx.Bool("debug_send") && (ctx.IsSet("payment_hash") || args.Present()) {
return fmt.Errorf("do not provide a payment hash with debug send")
} else if !ctx.Bool("debug_send") {
var rHash []byte
var rHash []byte

if ctx.Bool("key_send") {
if ctx.IsSet("payment_hash") {
joostjager marked this conversation as resolved.
Show resolved Hide resolved
return errors.New("cannot set payment hash when using " +
"key send")
}
var preimage lntypes.Preimage
if _, err := rand.Read(preimage[:]); err != nil {
return err
}

req.DestCustomRecords = map[uint64][]byte{
joostjager marked this conversation as resolved.
Show resolved Hide resolved
record.KeySendType: preimage[:],
}

hash := preimage.Hash()
rHash = hash[:]
} else {
switch {
case ctx.IsSet("payment_hash"):
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
Expand All @@ -2290,26 +2301,26 @@ func sendPayment(ctx *cli.Context) error {
default:
return fmt.Errorf("payment hash argument missing")
}
}

if err != nil {
return err
}
if len(rHash) != 32 {
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %v", len(rHash))
}
req.PaymentHash = rHash

switch {
case ctx.IsSet("final_cltv_delta"):
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
case args.Present():
delta, err := strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return err
}
if len(rHash) != 32 {
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %v", len(rHash))
}
req.PaymentHash = rHash

switch {
case ctx.IsSet("final_cltv_delta"):
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
case args.Present():
delta, err := strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return err
}
req.FinalCltvDelta = int32(delta)
}
req.FinalCltvDelta = int32(delta)
}

return sendPaymentRequest(ctx, req)
Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."`

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"`

Workers *lncfg.Workers `group:"workers" namespace:"workers"`
Expand Down
87 changes: 84 additions & 3 deletions invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/record"
)

var (
Expand Down Expand Up @@ -94,6 +95,10 @@ type RegistryConfig struct {
// Now() and TickAfter() and is useful to stub out the clock functions
// during testing.
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
Expand Down Expand Up @@ -690,6 +695,68 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
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.
joostjager marked this conversation as resolved.
Show resolved Hide resolved
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
cfromknecht marked this conversation as resolved.
Show resolved Hide resolved

// 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
// describes how the htlc should be resolved.
//
Expand All @@ -710,9 +777,6 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
payload Payload) (*HtlcResolution, error) {

i.Lock()
defer i.Unlock()

mpp := payload.MultiPath()

debugLog := func(s string) {
Expand All @@ -732,6 +796,23 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
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 {
joostjager marked this conversation as resolved.
Show resolved Hide resolved
err := i.processKeySend(updateCtx, rHash)
if err != nil {
debugLog(fmt.Sprintf("key send error: %v", err))

return NewFailureResolution(
circuitKey, currentHeight, ResultKeySendError,
), nil
}
}

i.Lock()
joostjager marked this conversation as resolved.
Show resolved Hide resolved
defer i.Unlock()

// We'll attempt to settle an invoice matching this rHash on disk (if
// one exists). The callback will update the invoice state and/or htlcs.
var (
Expand Down
106 changes: 105 additions & 1 deletion invoices/invoiceregistry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
joostjager marked this conversation as resolved.
Show resolved Hide resolved
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.
// 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.
Expand Down Expand Up @@ -770,7 +874,7 @@ func TestInvoiceExpiryWithRegistry(t *testing.T) {
testClock.SetTime(testTime.Add(24 * time.Hour))

// Give some time to the watcher to cancel everything.
time.Sleep(testTimeout)
time.Sleep(500 * time.Millisecond)
registry.Stop()

// Create the expected cancellation set before the final check.
Expand Down
Loading