Merge pull request #5253 from cfromknecht/amp-invoice
Support paying AMP invoices via SendPaymentV2
This commit is contained in:
30 files changed
+1528
-1023
No files matched your search
@@ -1294,9 +1294,9 @@ func TestHTLCSet(t *testing.T) {
|
|||||||
expSet2 := make(map[CircuitKey]*InvoiceHTLC)
|
expSet2 := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
|
||||||
checkHTLCSets := func() {
|
checkHTLCSets := func() {
|
||||||
require.Equal(t, expSetNil, inv.HTLCSet(nil))
|
require.Equal(t, expSetNil, inv.HTLCSet(nil, HtlcStateAccepted))
|
||||||
require.Equal(t, expSet1, inv.HTLCSet(setID1))
|
require.Equal(t, expSet1, inv.HTLCSet(setID1, HtlcStateAccepted))
|
||||||
require.Equal(t, expSet2, inv.HTLCSet(setID2))
|
require.Equal(t, expSet2, inv.HTLCSet(setID2, HtlcStateAccepted))
|
||||||
}
|
}
|
||||||
|
|
||||||
// All HTLC sets should be empty initially.
|
// All HTLC sets should be empty initially.
|
||||||
|
|||||||
+36
-12
@@ -471,17 +471,16 @@ type Invoice struct {
|
|||||||
HodlInvoice bool
|
HodlInvoice bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTLCSet returns the set of accepted HTLCs belonging to an invoice. Passing a
|
// HTLCSet returns the set of HTLCs belonging to setID and in the provided
|
||||||
// nil setID will return all accepted HTLCs in the case of legacy or MPP, and no
|
// state. Passing a nil setID will return all HTLCs in the provided state in the
|
||||||
// HTLCs in the case of AMP. Otherwise, the returned set will be filtered by
|
// case of legacy or MPP, and no HTLCs in the case of AMP. Otherwise, the
|
||||||
// the populated setID which is used to retrieve AMP HTLC sets.
|
// returned set will be filtered by the populated setID which is used to
|
||||||
func (i *Invoice) HTLCSet(setID *[32]byte) map[CircuitKey]*InvoiceHTLC {
|
// retrieve AMP HTLC sets.
|
||||||
|
func (i *Invoice) HTLCSet(setID *[32]byte, state HtlcState) map[CircuitKey]*InvoiceHTLC {
|
||||||
htlcSet := make(map[CircuitKey]*InvoiceHTLC)
|
htlcSet := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
for key, htlc := range i.Htlcs {
|
for key, htlc := range i.Htlcs {
|
||||||
// Only consider accepted mpp htlcs. It is possible that there
|
// Only add HTLCs that are in the requested HtlcState.
|
||||||
// are htlcs registered in the invoice database that previously
|
if htlc.State != state {
|
||||||
// timed out and are in the canceled state now.
|
|
||||||
if htlc.State != HtlcStateAccepted {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,6 +494,31 @@ func (i *Invoice) HTLCSet(setID *[32]byte) map[CircuitKey]*InvoiceHTLC {
|
|||||||
return htlcSet
|
return htlcSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTLCSetCompliment returns the set of all HTLCs not belonging to setID that
|
||||||
|
// are in the target state. Passing a nil setID will return no invoices, since
|
||||||
|
// all MPP HTLCs are part of the same HTLC set.
|
||||||
|
func (i *Invoice) HTLCSetCompliment(setID *[32]byte,
|
||||||
|
state HtlcState) map[CircuitKey]*InvoiceHTLC {
|
||||||
|
|
||||||
|
htlcSet := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
for key, htlc := range i.Htlcs {
|
||||||
|
// Only add HTLCs that are in the requested HtlcState.
|
||||||
|
if htlc.State != state {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are constructing the compliment, so filter anything that
|
||||||
|
// matches this set id.
|
||||||
|
if htlc.IsInHTLCSet(setID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
htlcSet[key] = htlc
|
||||||
|
}
|
||||||
|
|
||||||
|
return htlcSet
|
||||||
|
}
|
||||||
|
|
||||||
// HtlcState defines the states an htlc paying to an invoice can be in.
|
// HtlcState defines the states an htlc paying to an invoice can be in.
|
||||||
type HtlcState uint8
|
type HtlcState uint8
|
||||||
|
|
||||||
@@ -2039,7 +2063,7 @@ func updateInvoiceState(invoice *Invoice, hash *lntypes.Hash,
|
|||||||
|
|
||||||
// Sanity check that the user isn't trying to settle or accept a
|
// Sanity check that the user isn't trying to settle or accept a
|
||||||
// non-existent HTLC set.
|
// non-existent HTLC set.
|
||||||
if len(invoice.HTLCSet(update.SetID)) == 0 {
|
if len(invoice.HTLCSet(update.SetID, HtlcStateAccepted)) == 0 {
|
||||||
return ErrEmptyHTLCSet
|
return ErrEmptyHTLCSet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2329,8 +2353,8 @@ func (d *DB) DeleteInvoice(invoicesToDelete []InvoiceDeleteRef) error {
|
|||||||
// invoice key.
|
// invoice key.
|
||||||
key := invoiceAddIndex.Get(addIndexKey[:])
|
key := invoiceAddIndex.Get(addIndexKey[:])
|
||||||
if !bytes.Equal(key, invoiceKey) {
|
if !bytes.Equal(key, invoiceKey) {
|
||||||
return fmt.Errorf("unknown invoice in " +
|
return fmt.Errorf("unknown invoice " +
|
||||||
"add index")
|
"in add index")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from the add index.
|
// Remove from the add index.
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ var addInvoiceCommand = cli.Command{
|
|||||||
"private channels in order to assist the " +
|
"private channels in order to assist the " +
|
||||||
"payer in reaching you",
|
"payer in reaching you",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "amp",
|
||||||
|
Usage: "creates an AMP invoice. If true, preimage " +
|
||||||
|
"should not be set.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: actionDecorator(addInvoice),
|
Action: actionDecorator(addInvoice),
|
||||||
}
|
}
|
||||||
@@ -119,6 +124,7 @@ func addInvoice(ctx *cli.Context) error {
|
|||||||
FallbackAddr: ctx.String("fallback_addr"),
|
FallbackAddr: ctx.String("fallback_addr"),
|
||||||
Expiry: ctx.Int64("expiry"),
|
Expiry: ctx.Int64("expiry"),
|
||||||
Private: ctx.Bool("private"),
|
Private: ctx.Bool("private"),
|
||||||
|
IsAmp: ctx.Bool("amp"),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.AddInvoice(ctxc, invoice)
|
resp, err := client.AddInvoice(ctxc, invoice)
|
||||||
|
|||||||
+15
-4
@@ -87,6 +87,12 @@ var (
|
|||||||
"payment splitting is required to attempt a payment, " +
|
"payment splitting is required to attempt a payment, " +
|
||||||
"specified in milli-satoshis",
|
"specified in milli-satoshis",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ampFlag = cli.BoolFlag{
|
||||||
|
Name: "amp",
|
||||||
|
Usage: "if set to true, then AMP will be used to complete the " +
|
||||||
|
"payment",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
// paymentFlags returns common flags for sendpayment and payinvoice.
|
||||||
@@ -131,7 +137,7 @@ func paymentFlags() []cli.Flag {
|
|||||||
Usage: "allow sending a circular payment to self",
|
Usage: "allow sending a circular payment to self",
|
||||||
},
|
},
|
||||||
dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
|
dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
|
||||||
maxShardSizeSatFlag, maxShardSizeMsatFlag,
|
maxShardSizeSatFlag, maxShardSizeMsatFlag, ampFlag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,11 +293,16 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
Dest: destNode,
|
Dest: destNode,
|
||||||
Amt: amount,
|
Amt: amount,
|
||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
|
Amp: ctx.Bool(ampFlag.Name),
|
||||||
}
|
}
|
||||||
|
|
||||||
var rHash []byte
|
var rHash []byte
|
||||||
|
|
||||||
if ctx.Bool("keysend") {
|
switch {
|
||||||
|
case ctx.Bool("keysend") && ctx.Bool(ampFlag.Name):
|
||||||
|
return errors.New("either keysend or amp may be set, but not both")
|
||||||
|
|
||||||
|
case ctx.Bool("keysend"):
|
||||||
if ctx.IsSet("payment_hash") {
|
if ctx.IsSet("payment_hash") {
|
||||||
return errors.New("cannot set payment hash when using " +
|
return errors.New("cannot set payment hash when using " +
|
||||||
"keysend")
|
"keysend")
|
||||||
@@ -308,7 +319,7 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
hash := preimage.Hash()
|
hash := preimage.Hash()
|
||||||
rHash = hash[:]
|
rHash = hash[:]
|
||||||
} else {
|
case !ctx.Bool(ampFlag.Name):
|
||||||
switch {
|
switch {
|
||||||
case ctx.IsSet("payment_hash"):
|
case ctx.IsSet("payment_hash"):
|
||||||
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
||||||
@@ -323,7 +334,7 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(rHash) != 32 {
|
if !req.Amp && len(rHash) != 32 {
|
||||||
return fmt.Errorf("payment hash must be exactly 32 "+
|
return fmt.Errorf("payment hash must be exactly 32 "+
|
||||||
"bytes, is instead %v", len(rHash))
|
"bytes, is instead %v", len(rHash))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,8 @@ type Config struct {
|
|||||||
|
|
||||||
AcceptKeySend bool `long:"accept-keysend" description:"If true, spontaneous payments through keysend will be accepted. [experimental]"`
|
AcceptKeySend bool `long:"accept-keysend" description:"If true, spontaneous payments through keysend will be accepted. [experimental]"`
|
||||||
|
|
||||||
|
AcceptAMP bool `long:"accept-amp" description:"If true, spontaneous payments via AMP will be accepted."`
|
||||||
|
|
||||||
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]"`
|
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]"`
|
||||||
|
|
||||||
GcCanceledInvoicesOnStartup bool `long:"gc-canceled-invoices-on-startup" description:"If true, we'll attempt to garbage collect canceled invoices upon start."`
|
GcCanceledInvoicesOnStartup bool `long:"gc-canceled-invoices-on-startup" description:"If true, we'll attempt to garbage collect canceled invoices upon start."`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var defaultSetDesc = setDesc{
|
|||||||
SetInit: {}, // I
|
SetInit: {}, // I
|
||||||
SetNodeAnn: {}, // N
|
SetNodeAnn: {}, // N
|
||||||
SetInvoice: {}, // 9
|
SetInvoice: {}, // 9
|
||||||
|
SetInvoiceAmp: {}, // 9A
|
||||||
SetLegacyGlobal: {},
|
SetLegacyGlobal: {},
|
||||||
},
|
},
|
||||||
lnwire.StaticRemoteKeyRequired: {
|
lnwire.StaticRemoteKeyRequired: {
|
||||||
@@ -34,9 +35,10 @@ var defaultSetDesc = setDesc{
|
|||||||
SetNodeAnn: {}, // N
|
SetNodeAnn: {}, // N
|
||||||
},
|
},
|
||||||
lnwire.PaymentAddrRequired: {
|
lnwire.PaymentAddrRequired: {
|
||||||
SetInit: {}, // I
|
SetInit: {}, // I
|
||||||
SetNodeAnn: {}, // N
|
SetNodeAnn: {}, // N
|
||||||
SetInvoice: {}, // 9
|
SetInvoice: {}, // 9
|
||||||
|
SetInvoiceAmp: {}, // 9A
|
||||||
},
|
},
|
||||||
lnwire.MPPOptional: {
|
lnwire.MPPOptional: {
|
||||||
SetInit: {}, // I
|
SetInit: {}, // I
|
||||||
@@ -54,6 +56,8 @@ var defaultSetDesc = setDesc{
|
|||||||
lnwire.AMPOptional: {
|
lnwire.AMPOptional: {
|
||||||
SetInit: {}, // I
|
SetInit: {}, // I
|
||||||
SetNodeAnn: {}, // N
|
SetNodeAnn: {}, // N
|
||||||
SetInvoice: {}, // 9
|
},
|
||||||
|
lnwire.AMPRequired: {
|
||||||
|
SetInvoiceAmp: {}, // 9A
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,10 @@ const (
|
|||||||
// SetInvoice identifies features that should be advertised on invoices
|
// SetInvoice identifies features that should be advertised on invoices
|
||||||
// generated by the daemon.
|
// generated by the daemon.
|
||||||
SetInvoice
|
SetInvoice
|
||||||
|
|
||||||
|
// SetInvoiceAmp identifies the features that should be advertised on
|
||||||
|
// AMP invoices generated by the daemon.
|
||||||
|
SetInvoiceAmp
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable description of a Set.
|
// String returns a human-readable description of a Set.
|
||||||
@@ -35,6 +39,8 @@ func (s Set) String() string {
|
|||||||
return "SetNodeAnn"
|
return "SetNodeAnn"
|
||||||
case SetInvoice:
|
case SetInvoice:
|
||||||
return "SetInvoice"
|
return "SetInvoice"
|
||||||
|
case SetInvoiceAmp:
|
||||||
|
return "SetInvoiceAmp"
|
||||||
default:
|
default:
|
||||||
return "SetUnknown"
|
return "SetUnknown"
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-34
@@ -57,6 +57,10 @@ type RegistryConfig struct {
|
|||||||
// send payments.
|
// send payments.
|
||||||
AcceptKeySend bool
|
AcceptKeySend bool
|
||||||
|
|
||||||
|
// AcceptAMP indicates whether we want to accept spontaneous AMP
|
||||||
|
// payments.
|
||||||
|
AcceptAMP bool
|
||||||
|
|
||||||
// GcCanceledInvoicesOnStartup if set, we'll attempt to garbage collect
|
// GcCanceledInvoicesOnStartup if set, we'll attempt to garbage collect
|
||||||
// all canceled invoices upon start.
|
// all canceled invoices upon start.
|
||||||
GcCanceledInvoicesOnStartup bool
|
GcCanceledInvoicesOnStartup bool
|
||||||
@@ -884,28 +888,33 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
amp: payload.AMPRecord(),
|
amp: payload.AMPRecord(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process keysend if present. Do this outside of the lock, because
|
switch {
|
||||||
// AddInvoice obtains its own lock. This is no problem, because the
|
|
||||||
// operation is idempotent.
|
|
||||||
if i.cfg.AcceptKeySend {
|
|
||||||
if ctx.amp != nil {
|
|
||||||
err := i.processAMP(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.log(fmt.Sprintf("amp error: %v", err))
|
|
||||||
|
|
||||||
return NewFailResolution(
|
// If we are accepting spontaneous AMP payments and this payload
|
||||||
circuitKey, currentHeight, ResultAmpError,
|
// contains an AMP record, create an AMP invoice that will be settled
|
||||||
), nil
|
// below.
|
||||||
}
|
case i.cfg.AcceptAMP && ctx.amp != nil:
|
||||||
} else {
|
err := i.processAMP(ctx)
|
||||||
err := i.processKeySend(ctx)
|
if err != nil {
|
||||||
if err != nil {
|
ctx.log(fmt.Sprintf("amp error: %v", err))
|
||||||
ctx.log(fmt.Sprintf("keysend error: %v", err))
|
|
||||||
|
|
||||||
return NewFailResolution(
|
return NewFailResolution(
|
||||||
circuitKey, currentHeight, ResultKeySendError,
|
circuitKey, currentHeight, ResultAmpError,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are accepting spontaneous keysend payments, create a regular
|
||||||
|
// invoice that will be settled below. We also enforce that this is only
|
||||||
|
// done when no AMP payload is present since it will only be settle-able
|
||||||
|
// by regular HTLCs.
|
||||||
|
case i.cfg.AcceptKeySend && ctx.amp == nil:
|
||||||
|
err := i.processKeySend(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.log(fmt.Sprintf("keysend error: %v", err))
|
||||||
|
|
||||||
|
return NewFailResolution(
|
||||||
|
circuitKey, currentHeight, ResultKeySendError,
|
||||||
|
), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,15 +1030,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||||||
// Also cancel any HTLCs in the HTLC set that are also in the
|
// Also cancel any HTLCs in the HTLC set that are also in the
|
||||||
// canceled state with the same failure result.
|
// canceled state with the same failure result.
|
||||||
setID := ctx.setID()
|
setID := ctx.setID()
|
||||||
for key, htlc := range invoice.Htlcs {
|
canceledHtlcSet := invoice.HTLCSet(setID, channeldb.HtlcStateCanceled)
|
||||||
if htlc.State != channeldb.HtlcStateCanceled {
|
for key, htlc := range canceledHtlcSet {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !htlc.IsInHTLCSet(setID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
htlcFailResolution := NewFailResolution(
|
htlcFailResolution := NewFailResolution(
|
||||||
key, int32(htlc.AcceptHeight), res.Outcome,
|
key, int32(htlc.AcceptHeight), res.Outcome,
|
||||||
)
|
)
|
||||||
@@ -1047,11 +1049,9 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||||||
// Also settle any previously accepted htlcs. If a htlc is
|
// Also settle any previously accepted htlcs. If a htlc is
|
||||||
// marked as settled, we should follow now and settle the htlc
|
// marked as settled, we should follow now and settle the htlc
|
||||||
// with our peer.
|
// with our peer.
|
||||||
for key, htlc := range invoice.Htlcs {
|
setID := ctx.setID()
|
||||||
if htlc.State != channeldb.HtlcStateSettled {
|
settledHtlcSet := invoice.HTLCSet(setID, channeldb.HtlcStateSettled)
|
||||||
continue
|
for key, htlc := range settledHtlcSet {
|
||||||
}
|
|
||||||
|
|
||||||
preimage := res.Preimage
|
preimage := res.Preimage
|
||||||
if htlc.AMP != nil && htlc.AMP.Preimage != nil {
|
if htlc.AMP != nil && htlc.AMP.Preimage != nil {
|
||||||
preimage = *htlc.AMP.Preimage
|
preimage = *htlc.AMP.Preimage
|
||||||
@@ -1072,6 +1072,23 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||||||
i.notifyHodlSubscribers(htlcSettleResolution)
|
i.notifyHodlSubscribers(htlcSettleResolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If concurrent payments were attempted to this invoice before
|
||||||
|
// the current one was ultimately settled, cancel back any of
|
||||||
|
// the HTLCs immediately. As a result of the settle, the HTLCs
|
||||||
|
// in other HTLC sets are automatically converted to a canceled
|
||||||
|
// state when updating the invoice.
|
||||||
|
canceledHtlcSet := invoice.HTLCSetCompliment(
|
||||||
|
setID, channeldb.HtlcStateCanceled,
|
||||||
|
)
|
||||||
|
for key, htlc := range canceledHtlcSet {
|
||||||
|
htlcFailResolution := NewFailResolution(
|
||||||
|
key, int32(htlc.AcceptHeight),
|
||||||
|
ResultInvoiceAlreadySettled,
|
||||||
|
)
|
||||||
|
|
||||||
|
i.notifyHodlSubscribers(htlcFailResolution)
|
||||||
|
}
|
||||||
|
|
||||||
// If we accepted the htlc, subscribe to the hodl invoice and return
|
// If we accepted the htlc, subscribe to the hodl invoice and return
|
||||||
// an accept resolution with the htlc's accept time on it.
|
// an accept resolution with the htlc's accept time on it.
|
||||||
case *htlcAcceptResolution:
|
case *htlcAcceptResolution:
|
||||||
|
|||||||
@@ -1290,7 +1290,7 @@ func TestAMPWithoutMPPPayload(t *testing.T) {
|
|||||||
ctx := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
ctx.registry.cfg.AcceptKeySend = true
|
ctx.registry.cfg.AcceptAMP = true
|
||||||
|
|
||||||
const (
|
const (
|
||||||
shardAmt = lnwire.MilliSatoshi(10)
|
shardAmt = lnwire.MilliSatoshi(10)
|
||||||
@@ -1320,37 +1320,37 @@ func TestAMPWithoutMPPPayload(t *testing.T) {
|
|||||||
func TestSpontaneousAmpPayment(t *testing.T) {
|
func TestSpontaneousAmpPayment(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
keySendEnabled bool
|
ampEnabled bool
|
||||||
failReconstruction bool
|
failReconstruction bool
|
||||||
numShards int
|
numShards int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "enabled valid one shard",
|
name: "enabled valid one shard",
|
||||||
keySendEnabled: true,
|
ampEnabled: true,
|
||||||
failReconstruction: false,
|
failReconstruction: false,
|
||||||
numShards: 1,
|
numShards: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "enabled valid multiple shards",
|
name: "enabled valid multiple shards",
|
||||||
keySendEnabled: true,
|
ampEnabled: true,
|
||||||
failReconstruction: false,
|
failReconstruction: false,
|
||||||
numShards: 3,
|
numShards: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "enabled invalid one shard",
|
name: "enabled invalid one shard",
|
||||||
keySendEnabled: true,
|
ampEnabled: true,
|
||||||
failReconstruction: true,
|
failReconstruction: true,
|
||||||
numShards: 1,
|
numShards: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "enabled invalid multiple shards",
|
name: "enabled invalid multiple shards",
|
||||||
keySendEnabled: true,
|
ampEnabled: true,
|
||||||
failReconstruction: true,
|
failReconstruction: true,
|
||||||
numShards: 3,
|
numShards: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disabled valid multiple shards",
|
name: "disabled valid multiple shards",
|
||||||
keySendEnabled: false,
|
ampEnabled: false,
|
||||||
failReconstruction: false,
|
failReconstruction: false,
|
||||||
numShards: 3,
|
numShards: 3,
|
||||||
},
|
},
|
||||||
@@ -1360,7 +1360,7 @@ func TestSpontaneousAmpPayment(t *testing.T) {
|
|||||||
test := test
|
test := test
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
testSpontaneousAmpPayment(
|
testSpontaneousAmpPayment(
|
||||||
t, test.keySendEnabled, test.failReconstruction,
|
t, test.ampEnabled, test.failReconstruction,
|
||||||
test.numShards,
|
test.numShards,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1369,14 +1369,14 @@ func TestSpontaneousAmpPayment(t *testing.T) {
|
|||||||
|
|
||||||
// testSpontaneousAmpPayment runs a specific spontaneous AMP test case.
|
// testSpontaneousAmpPayment runs a specific spontaneous AMP test case.
|
||||||
func testSpontaneousAmpPayment(
|
func testSpontaneousAmpPayment(
|
||||||
t *testing.T, keySendEnabled, failReconstruction bool, numShards int) {
|
t *testing.T, ampEnabled, failReconstruction bool, numShards int) {
|
||||||
|
|
||||||
defer timeout()()
|
defer timeout()()
|
||||||
|
|
||||||
ctx := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
ctx.registry.cfg.AcceptKeySend = keySendEnabled
|
ctx.registry.cfg.AcceptAMP = ampEnabled
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@@ -1471,7 +1471,7 @@ func testSpontaneousAmpPayment(
|
|||||||
// When keysend is disabled all HTLC should fail with invoice
|
// When keysend is disabled all HTLC should fail with invoice
|
||||||
// not found, since one is not inserted before executing
|
// not found, since one is not inserted before executing
|
||||||
// UpdateInvoice.
|
// UpdateInvoice.
|
||||||
if !keySendEnabled {
|
if !ampEnabled {
|
||||||
require.NotNil(t, resolution)
|
require.NotNil(t, resolution)
|
||||||
checkFailResolution(t, resolution, ResultInvoiceNotFound)
|
checkFailResolution(t, resolution, ResultInvoiceNotFound)
|
||||||
continue
|
continue
|
||||||
@@ -1515,7 +1515,7 @@ func testSpontaneousAmpPayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No need to check the hodl chans when keysend is not enabled.
|
// No need to check the hodl chans when keysend is not enabled.
|
||||||
if !keySendEnabled {
|
if !ampEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ const (
|
|||||||
// invoice that is already canceled.
|
// invoice that is already canceled.
|
||||||
ResultInvoiceAlreadyCanceled
|
ResultInvoiceAlreadyCanceled
|
||||||
|
|
||||||
|
// ResultInvoiceAlreadySettled is returned when trying to pay an invoice
|
||||||
|
// that is already settled.
|
||||||
|
ResultInvoiceAlreadySettled
|
||||||
|
|
||||||
// ResultAmountTooLow is returned when an invoice is underpaid.
|
// ResultAmountTooLow is returned when an invoice is underpaid.
|
||||||
ResultAmountTooLow
|
ResultAmountTooLow
|
||||||
|
|
||||||
@@ -106,6 +110,10 @@ const (
|
|||||||
// payment.
|
// payment.
|
||||||
ResultMppInProgress
|
ResultMppInProgress
|
||||||
|
|
||||||
|
// ResultHtlcInvoiceTypeMismatch is returned when an AMP HTLC targets a
|
||||||
|
// non-AMP invoice and vice versa.
|
||||||
|
ResultHtlcInvoiceTypeMismatch
|
||||||
|
|
||||||
// ResultAmpError is returned when we receive invalid AMP parameters.
|
// ResultAmpError is returned when we receive invalid AMP parameters.
|
||||||
ResultAmpError
|
ResultAmpError
|
||||||
|
|
||||||
@@ -133,6 +141,9 @@ func (f FailResolutionResult) FailureString() string {
|
|||||||
case ResultInvoiceAlreadyCanceled:
|
case ResultInvoiceAlreadyCanceled:
|
||||||
return "invoice already canceled"
|
return "invoice already canceled"
|
||||||
|
|
||||||
|
case ResultInvoiceAlreadySettled:
|
||||||
|
return "invoice alread settled"
|
||||||
|
|
||||||
case ResultAmountTooLow:
|
case ResultAmountTooLow:
|
||||||
return "amount too low"
|
return "amount too low"
|
||||||
|
|
||||||
@@ -169,6 +180,9 @@ func (f FailResolutionResult) FailureString() string {
|
|||||||
case ResultMppInProgress:
|
case ResultMppInProgress:
|
||||||
return "mpp reception in progress"
|
return "mpp reception in progress"
|
||||||
|
|
||||||
|
case ResultHtlcInvoiceTypeMismatch:
|
||||||
|
return "htlc invoice type mismatch"
|
||||||
|
|
||||||
case ResultAmpError:
|
case ResultAmpError:
|
||||||
return "invalid amp parameters"
|
return "invalid amp parameters"
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -125,6 +125,21 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
|
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
|
||||||
HtlcResolution, error) {
|
HtlcResolution, error) {
|
||||||
|
|
||||||
|
// Reject HTLCs to AMP invoices if they are missing an AMP payload, and
|
||||||
|
// HTLCs to MPP invoices if they have an AMP payload.
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case inv.Terms.Features.RequiresFeature(lnwire.AMPRequired) &&
|
||||||
|
ctx.amp == nil:
|
||||||
|
|
||||||
|
return nil, ctx.failRes(ResultHtlcInvoiceTypeMismatch), nil
|
||||||
|
|
||||||
|
case !inv.Terms.Features.RequiresFeature(lnwire.AMPRequired) &&
|
||||||
|
ctx.amp != nil:
|
||||||
|
|
||||||
|
return nil, ctx.failRes(ResultHtlcInvoiceTypeMismatch), nil
|
||||||
|
}
|
||||||
|
|
||||||
setID := ctx.setID()
|
setID := ctx.setID()
|
||||||
|
|
||||||
// Start building the accept descriptor.
|
// Start building the accept descriptor.
|
||||||
@@ -168,7 +183,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
|
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
htlcSet := inv.HTLCSet(setID)
|
htlcSet := inv.HTLCSet(setID, channeldb.HtlcStateAccepted)
|
||||||
|
|
||||||
// Check whether total amt matches other htlcs in the set.
|
// Check whether total amt matches other htlcs in the set.
|
||||||
var newSetTotal lnwire.MilliSatoshi
|
var newSetTotal lnwire.MilliSatoshi
|
||||||
@@ -373,7 +388,7 @@ func updateLegacy(ctx *invoiceUpdateCtx,
|
|||||||
// Don't allow settling the invoice with an old style
|
// Don't allow settling the invoice with an old style
|
||||||
// htlc if we are already in the process of gathering an
|
// htlc if we are already in the process of gathering an
|
||||||
// mpp set.
|
// mpp set.
|
||||||
for _, htlc := range inv.HTLCSet(nil) {
|
for _, htlc := range inv.HTLCSet(nil, channeldb.HtlcStateAccepted) {
|
||||||
if htlc.MppTotalAmt > 0 {
|
if htlc.MppTotalAmt > 0 {
|
||||||
return nil, ctx.failRes(ResultMppInProgress), nil
|
return nil, ctx.failRes(ResultMppInProgress), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ type AddInvoiceConfig struct {
|
|||||||
// GenInvoiceFeatures returns a feature containing feature bits that
|
// GenInvoiceFeatures returns a feature containing feature bits that
|
||||||
// should be advertised on freshly generated invoices.
|
// should be advertised on freshly generated invoices.
|
||||||
GenInvoiceFeatures func() *lnwire.FeatureVector
|
GenInvoiceFeatures func() *lnwire.FeatureVector
|
||||||
|
|
||||||
|
// GenAmpInvoiceFeatures returns a feature containing feature bits that
|
||||||
|
// should be advertised on freshly generated AMP invoices.
|
||||||
|
GenAmpInvoiceFeatures func() *lnwire.FeatureVector
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInvoiceData contains the required data to create a new invoice.
|
// AddInvoiceData contains the required data to create a new invoice.
|
||||||
@@ -99,17 +103,71 @@ type AddInvoiceData struct {
|
|||||||
// immediately upon receiving the payment.
|
// immediately upon receiving the payment.
|
||||||
HodlInvoice bool
|
HodlInvoice bool
|
||||||
|
|
||||||
|
// Amp signals whether or not to create an AMP invoice.
|
||||||
|
//
|
||||||
|
// NOTE: Preimage should always be set to nil when this value is true.
|
||||||
|
Amp bool
|
||||||
|
|
||||||
// RouteHints are optional route hints that can each be individually used
|
// RouteHints are optional route hints that can each be individually used
|
||||||
// to assist in reaching the invoice's destination.
|
// to assist in reaching the invoice's destination.
|
||||||
RouteHints [][]zpay32.HopHint
|
RouteHints [][]zpay32.HopHint
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInvoice attempts to add a new invoice to the invoice database. Any
|
// paymentHashAndPreimage returns the payment hash and preimage for this invoice
|
||||||
// duplicated invoices are rejected, therefore all invoices *must* have a
|
// depending on the configuration.
|
||||||
// unique payment preimage.
|
//
|
||||||
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
// For AMP invoices (when Amp flag is true), this method always returns a nil
|
||||||
invoice *AddInvoiceData) (*lntypes.Hash, *channeldb.Invoice, error) {
|
// preimage. The hash value can be set externally by the user using the Hash
|
||||||
|
// field, or one will be generated randomly. The payment hash here only serves
|
||||||
|
// as a unique identifier for insertion into the invoice index, as there is
|
||||||
|
// no universal preimage for an AMP payment.
|
||||||
|
//
|
||||||
|
// For MPP invoices (when Amp flag is false), this method may return nil
|
||||||
|
// preimage when create a hodl invoice, but otherwise will always return a
|
||||||
|
// non-nil preimage and the corresponding payment hash. The valid combinations
|
||||||
|
// are parsed as follows:
|
||||||
|
// - Preimage == nil && Hash == nil -> (random preimage, H(random preimage))
|
||||||
|
// - Preimage != nil && Hash == nil -> (Preimage, H(Preimage))
|
||||||
|
// - Preimage == nil && Hash != nil -> (nil, Hash)
|
||||||
|
func (d *AddInvoiceData) paymentHashAndPreimage() (
|
||||||
|
*lntypes.Preimage, lntypes.Hash, error) {
|
||||||
|
|
||||||
|
if d.Amp {
|
||||||
|
return d.ampPaymentHashAndPreimage()
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.mppPaymentHashAndPreimage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ampPaymentHashAndPreimage returns the payment hash to use for an AMP invoice.
|
||||||
|
// The preimage will always be nil.
|
||||||
|
func (d *AddInvoiceData) ampPaymentHashAndPreimage() (*lntypes.Preimage, lntypes.Hash, error) {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// Preimages cannot be set on AMP invoice.
|
||||||
|
case d.Preimage != nil:
|
||||||
|
return nil, lntypes.Hash{},
|
||||||
|
errors.New("preimage set on AMP invoice")
|
||||||
|
|
||||||
|
// If a specific hash was requested, use that.
|
||||||
|
case d.Hash != nil:
|
||||||
|
return nil, *d.Hash, nil
|
||||||
|
|
||||||
|
// Otherwise generate a random hash value, just needs to be unique to be
|
||||||
|
// added to the invoice index.
|
||||||
|
default:
|
||||||
|
var paymentHash lntypes.Hash
|
||||||
|
if _, err := rand.Read(paymentHash[:]); err != nil {
|
||||||
|
return nil, lntypes.Hash{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, paymentHash, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mppPaymentHashAndPreimage returns the payment hash and preimage to use for an
|
||||||
|
// MPP invoice.
|
||||||
|
func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage, lntypes.Hash, error) {
|
||||||
var (
|
var (
|
||||||
paymentPreimage *lntypes.Preimage
|
paymentPreimage *lntypes.Preimage
|
||||||
paymentHash lntypes.Hash
|
paymentHash lntypes.Hash
|
||||||
@@ -118,28 +176,42 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
switch {
|
switch {
|
||||||
|
|
||||||
// Only either preimage or hash can be set.
|
// Only either preimage or hash can be set.
|
||||||
case invoice.Preimage != nil && invoice.Hash != nil:
|
case d.Preimage != nil && d.Hash != nil:
|
||||||
return nil, nil,
|
return nil, lntypes.Hash{},
|
||||||
errors.New("preimage and hash both set")
|
errors.New("preimage and hash both set")
|
||||||
|
|
||||||
// If no hash or preimage is given, generate a random preimage.
|
// If no hash or preimage is given, generate a random preimage.
|
||||||
case invoice.Preimage == nil && invoice.Hash == nil:
|
case d.Preimage == nil && d.Hash == nil:
|
||||||
paymentPreimage = &lntypes.Preimage{}
|
paymentPreimage = &lntypes.Preimage{}
|
||||||
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
||||||
return nil, nil, err
|
return nil, lntypes.Hash{}, err
|
||||||
}
|
}
|
||||||
paymentHash = paymentPreimage.Hash()
|
paymentHash = paymentPreimage.Hash()
|
||||||
|
|
||||||
// If just a hash is given, we create a hold invoice by setting the
|
// If just a hash is given, we create a hold invoice by setting the
|
||||||
// preimage to unknown.
|
// preimage to unknown.
|
||||||
case invoice.Preimage == nil && invoice.Hash != nil:
|
case d.Preimage == nil && d.Hash != nil:
|
||||||
paymentHash = *invoice.Hash
|
paymentHash = *d.Hash
|
||||||
|
|
||||||
// A specific preimage was supplied. Use that for the invoice.
|
// A specific preimage was supplied. Use that for the invoice.
|
||||||
case invoice.Preimage != nil && invoice.Hash == nil:
|
case d.Preimage != nil && d.Hash == nil:
|
||||||
preimage := *invoice.Preimage
|
preimage := *d.Preimage
|
||||||
paymentPreimage = &preimage
|
paymentPreimage = &preimage
|
||||||
paymentHash = invoice.Preimage.Hash()
|
paymentHash = d.Preimage.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentPreimage, paymentHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddInvoice attempts to add a new invoice to the invoice database. Any
|
||||||
|
// duplicated invoices are rejected, therefore all invoices *must* have a
|
||||||
|
// unique payment preimage.
|
||||||
|
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
||||||
|
invoice *AddInvoiceData) (*lntypes.Hash, *channeldb.Invoice, error) {
|
||||||
|
|
||||||
|
paymentPreimage, paymentHash, err := invoice.paymentHashAndPreimage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The size of the memo, receipt and description hash attached must not
|
// The size of the memo, receipt and description hash attached must not
|
||||||
@@ -307,7 +379,12 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set our desired invoice features and add them to our list of options.
|
// Set our desired invoice features and add them to our list of options.
|
||||||
invoiceFeatures := cfg.GenInvoiceFeatures()
|
var invoiceFeatures *lnwire.FeatureVector
|
||||||
|
if invoice.Amp {
|
||||||
|
invoiceFeatures = cfg.GenAmpInvoiceFeatures()
|
||||||
|
} else {
|
||||||
|
invoiceFeatures = cfg.GenInvoiceFeatures()
|
||||||
|
}
|
||||||
options = append(options, zpay32.Features(invoiceFeatures))
|
options = append(options, zpay32.Features(invoiceFeatures))
|
||||||
|
|
||||||
// Generate and set a random payment address for this invoice. If the
|
// Generate and set a random payment address for this invoice. If the
|
||||||
|
|||||||
@@ -55,4 +55,8 @@ type Config struct {
|
|||||||
// GenInvoiceFeatures returns a feature containing feature bits that
|
// GenInvoiceFeatures returns a feature containing feature bits that
|
||||||
// should be advertised on freshly generated invoices.
|
// should be advertised on freshly generated invoices.
|
||||||
GenInvoiceFeatures func() *lnwire.FeatureVector
|
GenInvoiceFeatures func() *lnwire.FeatureVector
|
||||||
|
|
||||||
|
// GenAmpInvoiceFeatures returns a feature containing feature bits that
|
||||||
|
// should be advertised on freshly generated AMP invoices.
|
||||||
|
GenAmpInvoiceFeatures func() *lnwire.FeatureVector
|
||||||
}
|
}
|
||||||
@@ -463,6 +463,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "byte",
|
"format": "byte",
|
||||||
"description": "The payment address of this invoice. This value will be used in MPP\npayments, and also for newer invoies that always require the MPP paylaod\nfor added end-to-end security."
|
"description": "The payment address of this invoice. This value will be used in MPP\npayments, and also for newer invoies that always require the MPP paylaod\nfor added end-to-end security."
|
||||||
|
},
|
||||||
|
"is_amp": {
|
||||||
|
"type": "boolean",
|
||||||
|
"format": "boolean",
|
||||||
|
"description": "Signals whether or not this is an AMP invoice."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -308,14 +308,15 @@ func (s *Server) AddHoldInvoice(ctx context.Context,
|
|||||||
invoice *AddHoldInvoiceRequest) (*AddHoldInvoiceResp, error) {
|
invoice *AddHoldInvoiceRequest) (*AddHoldInvoiceResp, error) {
|
||||||
|
|
||||||
addInvoiceCfg := &AddInvoiceConfig{
|
addInvoiceCfg := &AddInvoiceConfig{
|
||||||
AddInvoice: s.cfg.InvoiceRegistry.AddInvoice,
|
AddInvoice: s.cfg.InvoiceRegistry.AddInvoice,
|
||||||
IsChannelActive: s.cfg.IsChannelActive,
|
IsChannelActive: s.cfg.IsChannelActive,
|
||||||
ChainParams: s.cfg.ChainParams,
|
ChainParams: s.cfg.ChainParams,
|
||||||
NodeSigner: s.cfg.NodeSigner,
|
NodeSigner: s.cfg.NodeSigner,
|
||||||
DefaultCLTVExpiry: s.cfg.DefaultCLTVExpiry,
|
DefaultCLTVExpiry: s.cfg.DefaultCLTVExpiry,
|
||||||
ChanDB: s.cfg.RemoteChanDB,
|
ChanDB: s.cfg.RemoteChanDB,
|
||||||
Graph: s.cfg.LocalChanDB.ChannelGraph(),
|
Graph: s.cfg.LocalChanDB.ChannelGraph(),
|
||||||
GenInvoiceFeatures: s.cfg.GenInvoiceFeatures,
|
GenInvoiceFeatures: s.cfg.GenInvoiceFeatures,
|
||||||
|
GenAmpInvoiceFeatures: s.cfg.GenAmpInvoiceFeatures,
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := lntypes.MakeHash(invoice.Hash)
|
hash, err := lntypes.MakeHash(invoice.Hash)
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
|||||||
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
|
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAmp := invoice.Terms.Features.HasFeature(lnwire.AMPOptional)
|
||||||
|
|
||||||
rpcInvoice := &lnrpc.Invoice{
|
rpcInvoice := &lnrpc.Invoice{
|
||||||
Memo: string(invoice.Memo),
|
Memo: string(invoice.Memo),
|
||||||
RHash: rHash,
|
RHash: rHash,
|
||||||
@@ -170,8 +172,9 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
|||||||
State: state,
|
State: state,
|
||||||
Htlcs: rpcHtlcs,
|
Htlcs: rpcHtlcs,
|
||||||
Features: CreateRPCFeatures(invoice.Terms.Features),
|
Features: CreateRPCFeatures(invoice.Terms.Features),
|
||||||
IsKeysend: len(invoice.PaymentRequest) == 0,
|
IsKeysend: len(invoice.PaymentRequest) == 0 && !isAmp,
|
||||||
PaymentAddr: invoice.Terms.PaymentAddr[:],
|
PaymentAddr: invoice.Terms.PaymentAddr[:],
|
||||||
|
IsAmp: isAmp,
|
||||||
}
|
}
|
||||||
|
|
||||||
if preimage != nil {
|
if preimage != nil {
|
||||||
|
|||||||
@@ -696,13 +696,37 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
|||||||
payIntent.Amount = *payReq.MilliSat
|
payIntent.Amount = *payReq.MilliSat
|
||||||
}
|
}
|
||||||
|
|
||||||
if !payReq.Features.HasFeature(lnwire.MPPOptional) {
|
if !payReq.Features.HasFeature(lnwire.MPPOptional) &&
|
||||||
|
!payReq.Features.HasFeature(lnwire.AMPOptional) {
|
||||||
|
|
||||||
payIntent.MaxParts = 1
|
payIntent.MaxParts = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
err = payIntent.SetPaymentHash(*payReq.PaymentHash)
|
if payReq.Features.HasFeature(lnwire.AMPOptional) {
|
||||||
if err != nil {
|
// Generate random SetID and root share.
|
||||||
return nil, err
|
var setID [32]byte
|
||||||
|
_, err = rand.Read(setID[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootShare [32]byte
|
||||||
|
_, err = rand.Read(rootShare[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err := payIntent.SetAMP(&routing.AMPOptions{
|
||||||
|
SetID: setID,
|
||||||
|
RootShare: rootShare,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = payIntent.SetPaymentHash(*payReq.PaymentHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destKey := payReq.Destination.SerializeCompressed()
|
destKey := payReq.Destination.SerializeCompressed()
|
||||||
@@ -765,7 +789,6 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
|||||||
ampFeatures := []lnrpc.FeatureBit{
|
ampFeatures := []lnrpc.FeatureBit{
|
||||||
lnrpc.FeatureBit_TLV_ONION_OPT,
|
lnrpc.FeatureBit_TLV_ONION_OPT,
|
||||||
lnrpc.FeatureBit_PAYMENT_ADDR_OPT,
|
lnrpc.FeatureBit_PAYMENT_ADDR_OPT,
|
||||||
lnrpc.FeatureBit_MPP_OPT,
|
|
||||||
lnrpc.FeatureBit_AMP_OPT,
|
lnrpc.FeatureBit_AMP_OPT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+906
-895
File diff suppressed because it is too large.
Load diff
@@ -3003,6 +3003,11 @@ message Invoice {
|
|||||||
for added end-to-end security.
|
for added end-to-end security.
|
||||||
*/
|
*/
|
||||||
bytes payment_addr = 26;
|
bytes payment_addr = 26;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Signals whether or not this is an AMP invoice.
|
||||||
|
*/
|
||||||
|
bool is_amp = 27;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InvoiceHTLCState {
|
enum InvoiceHTLCState {
|
||||||
|
|||||||
@@ -4387,6 +4387,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "byte",
|
"format": "byte",
|
||||||
"description": "The payment address of this invoice. This value will be used in MPP\npayments, and also for newer invoies that always require the MPP paylaod\nfor added end-to-end security."
|
"description": "The payment address of this invoice. This value will be used in MPP\npayments, and also for newer invoies that always require the MPP paylaod\nfor added end-to-end security."
|
||||||
|
},
|
||||||
|
"is_amp": {
|
||||||
|
"type": "boolean",
|
||||||
|
"format": "boolean",
|
||||||
|
"description": "Signals whether or not this is an AMP invoice."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+247
-13
@@ -3,6 +3,8 @@ package itest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
@@ -15,6 +17,152 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testSendPaymentAMPInvoice tests that we can send an AMP payment to a
|
||||||
|
// specified AMP invoice using SendPaymentV2.
|
||||||
|
func testSendPaymentAMPInvoice(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
ctx := newMppTestContext(t, net)
|
||||||
|
defer ctx.shutdownNodes()
|
||||||
|
|
||||||
|
const paymentAmt = btcutil.Amount(300000)
|
||||||
|
|
||||||
|
// Set up a network with three different paths Alice <-> Bob. Channel
|
||||||
|
// capacities are set such that the payment can only succeed if (at
|
||||||
|
// least) three paths are used.
|
||||||
|
//
|
||||||
|
// _ Eve _
|
||||||
|
// / \
|
||||||
|
// Alice -- Carol ---- Bob
|
||||||
|
// \ /
|
||||||
|
// \__ Dave ____/
|
||||||
|
//
|
||||||
|
ctx.openChannel(ctx.carol, ctx.bob, 135000)
|
||||||
|
ctx.openChannel(ctx.alice, ctx.carol, 235000)
|
||||||
|
ctx.openChannel(ctx.dave, ctx.bob, 135000)
|
||||||
|
ctx.openChannel(ctx.alice, ctx.dave, 135000)
|
||||||
|
ctx.openChannel(ctx.eve, ctx.bob, 135000)
|
||||||
|
ctx.openChannel(ctx.carol, ctx.eve, 135000)
|
||||||
|
|
||||||
|
defer ctx.closeChannels()
|
||||||
|
|
||||||
|
ctx.waitForChannels()
|
||||||
|
|
||||||
|
// Subscribe to bob's invoices.
|
||||||
|
req := &lnrpc.InvoiceSubscription{}
|
||||||
|
ctxc, cancelSubscription := context.WithCancel(ctxb)
|
||||||
|
bobInvoiceSubscription, err := ctx.bob.SubscribeInvoices(ctxc, req)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
defer cancelSubscription()
|
||||||
|
|
||||||
|
addInvoiceResp, err := ctx.bob.AddInvoice(context.Background(), &lnrpc.Invoice{
|
||||||
|
Value: int64(paymentAmt),
|
||||||
|
IsAmp: true,
|
||||||
|
})
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Ensure we get a notification of the invoice being added by Bob.
|
||||||
|
rpcInvoice, err := bobInvoiceSubscription.Recv()
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
require.False(t.t, rpcInvoice.Settled) // nolint:staticcheck
|
||||||
|
require.Equal(t.t, lnrpc.Invoice_OPEN, rpcInvoice.State)
|
||||||
|
require.Equal(t.t, int64(0), rpcInvoice.AmtPaidSat)
|
||||||
|
require.Equal(t.t, int64(0), rpcInvoice.AmtPaidMsat)
|
||||||
|
|
||||||
|
require.Equal(t.t, 0, len(rpcInvoice.Htlcs))
|
||||||
|
|
||||||
|
// Increase Dave's fee to make the test deterministic. Otherwise it
|
||||||
|
// would be unpredictable whether pathfinding would go through Charlie
|
||||||
|
// or Dave for the first shard.
|
||||||
|
_, err = ctx.dave.UpdateChannelPolicy(
|
||||||
|
context.Background(),
|
||||||
|
&lnrpc.PolicyUpdateRequest{
|
||||||
|
Scope: &lnrpc.PolicyUpdateRequest_Global{Global: true},
|
||||||
|
BaseFeeMsat: 500000,
|
||||||
|
FeeRate: 0.001,
|
||||||
|
TimeLockDelta: 40,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dave policy update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(context.Background(), 4*defaultTimeout)
|
||||||
|
payment := sendAndAssertSuccess(
|
||||||
|
ctxt, t, ctx.alice,
|
||||||
|
&routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: addInvoiceResp.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check that Alice split the payment in at least three shards. Because
|
||||||
|
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
||||||
|
// there is some non-determinism in the process. Depending on whether
|
||||||
|
// the new pathfinding round is started before or after the htlc is
|
||||||
|
// locked into the channel, different sharding may occur. Therefore we
|
||||||
|
// can only check if the number of shards isn't below the theoretical
|
||||||
|
// minimum.
|
||||||
|
succeeded := 0
|
||||||
|
for _, htlc := range payment.Htlcs {
|
||||||
|
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
||||||
|
succeeded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const minExpectedShards = 3
|
||||||
|
if succeeded < minExpectedShards {
|
||||||
|
t.Fatalf("expected at least %v shards, but got %v",
|
||||||
|
minExpectedShards, succeeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There should now be a settle event for the invoice.
|
||||||
|
rpcInvoice, err = bobInvoiceSubscription.Recv()
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Also fetch Bob's invoice from ListInvoices and assert it is equal to
|
||||||
|
// the one recevied via the subscription.
|
||||||
|
invoiceResp, err := ctx.bob.ListInvoices(
|
||||||
|
ctxb, &lnrpc.ListInvoiceRequest{},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
require.Equal(t.t, 1, len(invoiceResp.Invoices))
|
||||||
|
assertInvoiceEqual(t.t, rpcInvoice, invoiceResp.Invoices[0])
|
||||||
|
|
||||||
|
// Assert that the invoice is settled for the total payment amount and
|
||||||
|
// has the correct payment address.
|
||||||
|
require.True(t.t, rpcInvoice.Settled) // nolint:staticcheck
|
||||||
|
require.Equal(t.t, lnrpc.Invoice_SETTLED, rpcInvoice.State)
|
||||||
|
require.Equal(t.t, int64(paymentAmt), rpcInvoice.AmtPaidSat)
|
||||||
|
require.Equal(t.t, int64(paymentAmt*1000), rpcInvoice.AmtPaidMsat)
|
||||||
|
|
||||||
|
// Finally, assert that the same set id is recorded for each htlc, and
|
||||||
|
// that the preimage hash pair is valid.
|
||||||
|
var setID []byte
|
||||||
|
require.Equal(t.t, succeeded, len(rpcInvoice.Htlcs))
|
||||||
|
for _, htlc := range rpcInvoice.Htlcs {
|
||||||
|
require.NotNil(t.t, htlc.Amp)
|
||||||
|
if setID == nil {
|
||||||
|
setID = make([]byte, 32)
|
||||||
|
copy(setID, htlc.Amp.SetId)
|
||||||
|
}
|
||||||
|
require.Equal(t.t, setID, htlc.Amp.SetId)
|
||||||
|
|
||||||
|
// Parse the child hash and child preimage, and assert they are
|
||||||
|
// well-formed.
|
||||||
|
childHash, err := lntypes.MakeHash(htlc.Amp.Hash)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
childPreimage, err := lntypes.MakePreimage(htlc.Amp.Preimage)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Assert that the preimage actually matches the hashes.
|
||||||
|
validPreimage := childPreimage.Matches(childHash)
|
||||||
|
require.True(t.t, validPreimage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testSendPaymentAMP tests that we can send an AMP payment to a specified
|
// testSendPaymentAMP tests that we can send an AMP payment to a specified
|
||||||
// destination using SendPaymentV2.
|
// destination using SendPaymentV2.
|
||||||
func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
@@ -64,12 +212,11 @@ func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
ctxt, _ := context.WithTimeout(context.Background(), 4*defaultTimeout)
|
ctxt, _ := context.WithTimeout(context.Background(), 4*defaultTimeout)
|
||||||
payment := sendAndAssertSuccess(
|
payment := sendAndAssertSuccess(
|
||||||
ctxt, t, net.Alice,
|
ctxt, t, ctx.alice,
|
||||||
&routerrpc.SendPaymentRequest{
|
&routerrpc.SendPaymentRequest{
|
||||||
Dest: net.Bob.PubKey[:],
|
Dest: ctx.bob.PubKey[:],
|
||||||
Amt: int64(paymentAmt),
|
Amt: int64(paymentAmt),
|
||||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||||
MaxParts: 10,
|
|
||||||
TimeoutSeconds: 60,
|
TimeoutSeconds: 60,
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
Amp: true,
|
Amp: true,
|
||||||
@@ -97,7 +244,7 @@ func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Bob's invoices.
|
// Fetch Bob's invoices.
|
||||||
invoiceResp, err := net.Bob.ListInvoices(
|
invoiceResp, err := ctx.bob.ListInvoices(
|
||||||
ctxb, &lnrpc.ListInvoiceRequest{},
|
ctxb, &lnrpc.ListInvoiceRequest{},
|
||||||
)
|
)
|
||||||
require.NoError(t.t, err)
|
require.NoError(t.t, err)
|
||||||
@@ -172,6 +319,13 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
ctx.waitForChannels()
|
ctx.waitForChannels()
|
||||||
|
|
||||||
|
// Subscribe to bob's invoices.
|
||||||
|
req := &lnrpc.InvoiceSubscription{}
|
||||||
|
ctxc, cancelSubscription := context.WithCancel(ctxb)
|
||||||
|
bobInvoiceSubscription, err := ctx.bob.SubscribeInvoices(ctxc, req)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
defer cancelSubscription()
|
||||||
|
|
||||||
// We'll send shards along three routes from Alice.
|
// We'll send shards along three routes from Alice.
|
||||||
sendRoutes := [numShards][]*lntest.HarnessNode{
|
sendRoutes := [numShards][]*lntest.HarnessNode{
|
||||||
{ctx.carol, ctx.bob},
|
{ctx.carol, ctx.bob},
|
||||||
@@ -180,7 +334,7 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payAddr := make([]byte, 32)
|
payAddr := make([]byte, 32)
|
||||||
_, err := rand.Read(payAddr)
|
_, err = rand.Read(payAddr)
|
||||||
require.NoError(t.t, err)
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
setID := make([]byte, 32)
|
setID := make([]byte, 32)
|
||||||
@@ -193,9 +347,11 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
childPreimages := make(map[lntypes.Preimage]uint32)
|
childPreimages := make(map[lntypes.Preimage]uint32)
|
||||||
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
||||||
for i, hops := range sendRoutes {
|
|
||||||
|
// Define a closure for sending each of the three shards.
|
||||||
|
sendShard := func(i int, hops []*lntest.HarnessNode) {
|
||||||
// Build a route for the specified hops.
|
// Build a route for the specified hops.
|
||||||
r, err := ctx.buildRoute(ctxb, shardAmt, net.Alice, hops)
|
r, err := ctx.buildRoute(ctxb, shardAmt, ctx.alice, hops)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to build route: %v", err)
|
t.Fatalf("unable to build route: %v", err)
|
||||||
}
|
}
|
||||||
@@ -236,7 +392,7 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// block as long as the payment is in flight.
|
// block as long as the payment is in flight.
|
||||||
go func() {
|
go func() {
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
resp, err := ctx.alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
}
|
}
|
||||||
@@ -245,6 +401,24 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send the first shard, this cause Bob to JIT add an invoice.
|
||||||
|
sendShard(0, sendRoutes[0])
|
||||||
|
|
||||||
|
// Ensure we get a notification of the invoice being added by Bob.
|
||||||
|
rpcInvoice, err := bobInvoiceSubscription.Recv()
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
require.False(t.t, rpcInvoice.Settled) // nolint:staticcheck
|
||||||
|
require.Equal(t.t, lnrpc.Invoice_OPEN, rpcInvoice.State)
|
||||||
|
require.Equal(t.t, int64(0), rpcInvoice.AmtPaidSat)
|
||||||
|
require.Equal(t.t, int64(0), rpcInvoice.AmtPaidMsat)
|
||||||
|
require.Equal(t.t, payAddr, rpcInvoice.PaymentAddr)
|
||||||
|
|
||||||
|
require.Equal(t.t, 0, len(rpcInvoice.Htlcs))
|
||||||
|
|
||||||
|
sendShard(1, sendRoutes[1])
|
||||||
|
sendShard(2, sendRoutes[2])
|
||||||
|
|
||||||
// Assert that all of the child preimages are unique.
|
// Assert that all of the child preimages are unique.
|
||||||
require.Equal(t.t, len(sendRoutes), len(childPreimages))
|
require.Equal(t.t, len(sendRoutes), len(childPreimages))
|
||||||
|
|
||||||
@@ -282,15 +456,18 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
childPreimages = childPreimagesCopy
|
childPreimages = childPreimagesCopy
|
||||||
|
|
||||||
// Fetch Bob's invoices.
|
// There should now be a settle event for the invoice.
|
||||||
invoiceResp, err := net.Bob.ListInvoices(
|
rpcInvoice, err = bobInvoiceSubscription.Recv()
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Also fetch Bob's invoice from ListInvoices and assert it is equal to
|
||||||
|
// the one recevied via the subscription.
|
||||||
|
invoiceResp, err := ctx.bob.ListInvoices(
|
||||||
ctxb, &lnrpc.ListInvoiceRequest{},
|
ctxb, &lnrpc.ListInvoiceRequest{},
|
||||||
)
|
)
|
||||||
require.NoError(t.t, err)
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
// There should only be one invoice.
|
|
||||||
require.Equal(t.t, 1, len(invoiceResp.Invoices))
|
require.Equal(t.t, 1, len(invoiceResp.Invoices))
|
||||||
rpcInvoice := invoiceResp.Invoices[0]
|
assertInvoiceEqual(t.t, rpcInvoice, invoiceResp.Invoices[0])
|
||||||
|
|
||||||
// Assert that the invoice is settled for the total payment amount and
|
// Assert that the invoice is settled for the total payment amount and
|
||||||
// has the correct payment address.
|
// has the correct payment address.
|
||||||
@@ -330,3 +507,60 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
delete(childPreimages, childPreimage)
|
delete(childPreimages, childPreimage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertInvoiceEqual asserts that two lnrpc.Invoices are equivalent. A custom
|
||||||
|
// comparison function is defined for these tests, since proto message returned
|
||||||
|
// from unary and streaming RPCs (as of protobuf 1.23.0 and grpc 1.29.1) aren't
|
||||||
|
// consistent with the private fields set on the messages. As a result, we avoid
|
||||||
|
// using require.Equal and test only the actual data members.
|
||||||
|
func assertInvoiceEqual(t *testing.T, a, b *lnrpc.Invoice) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Ensure the HTLCs are sorted properly before attempting to compare.
|
||||||
|
sort.Slice(a.Htlcs, func(i, j int) bool {
|
||||||
|
return a.Htlcs[i].ChanId < a.Htlcs[j].ChanId
|
||||||
|
})
|
||||||
|
sort.Slice(b.Htlcs, func(i, j int) bool {
|
||||||
|
return b.Htlcs[i].ChanId < b.Htlcs[j].ChanId
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, a.Memo, b.Memo)
|
||||||
|
require.Equal(t, a.RPreimage, b.RPreimage)
|
||||||
|
require.Equal(t, a.RHash, b.RHash)
|
||||||
|
require.Equal(t, a.Value, b.Value)
|
||||||
|
require.Equal(t, a.ValueMsat, b.ValueMsat)
|
||||||
|
require.Equal(t, a.CreationDate, b.CreationDate)
|
||||||
|
require.Equal(t, a.SettleDate, b.SettleDate)
|
||||||
|
require.Equal(t, a.PaymentRequest, b.PaymentRequest)
|
||||||
|
require.Equal(t, a.DescriptionHash, b.DescriptionHash)
|
||||||
|
require.Equal(t, a.Expiry, b.Expiry)
|
||||||
|
require.Equal(t, a.FallbackAddr, b.FallbackAddr)
|
||||||
|
require.Equal(t, a.CltvExpiry, b.CltvExpiry)
|
||||||
|
require.Equal(t, a.RouteHints, b.RouteHints)
|
||||||
|
require.Equal(t, a.Private, b.Private)
|
||||||
|
require.Equal(t, a.AddIndex, b.AddIndex)
|
||||||
|
require.Equal(t, a.SettleIndex, b.SettleIndex)
|
||||||
|
require.Equal(t, a.AmtPaidSat, b.AmtPaidSat)
|
||||||
|
require.Equal(t, a.AmtPaidMsat, b.AmtPaidMsat)
|
||||||
|
require.Equal(t, a.State, b.State)
|
||||||
|
require.Equal(t, a.Features, b.Features)
|
||||||
|
require.Equal(t, a.IsKeysend, b.IsKeysend)
|
||||||
|
require.Equal(t, a.PaymentAddr, b.PaymentAddr)
|
||||||
|
require.Equal(t, a.IsAmp, b.IsAmp)
|
||||||
|
|
||||||
|
require.Equal(t, len(a.Htlcs), len(b.Htlcs))
|
||||||
|
for i := range a.Htlcs {
|
||||||
|
htlcA, htlcB := a.Htlcs[i], b.Htlcs[i]
|
||||||
|
require.Equal(t, htlcA.ChanId, htlcB.ChanId)
|
||||||
|
require.Equal(t, htlcA.HtlcIndex, htlcB.HtlcIndex)
|
||||||
|
require.Equal(t, htlcA.AmtMsat, htlcB.AmtMsat)
|
||||||
|
require.Equal(t, htlcA.AcceptHeight, htlcB.AcceptHeight)
|
||||||
|
require.Equal(t, htlcA.AcceptTime, htlcB.AcceptTime)
|
||||||
|
require.Equal(t, htlcA.ResolveTime, htlcB.ResolveTime)
|
||||||
|
require.Equal(t, htlcA.ExpiryHeight, htlcB.ExpiryHeight)
|
||||||
|
require.Equal(t, htlcA.State, htlcB.State)
|
||||||
|
require.Equal(t, htlcA.CustomRecords, htlcB.CustomRecords)
|
||||||
|
require.Equal(t, htlcA.MppTotalAmtMsat, htlcB.MppTotalAmtMsat)
|
||||||
|
require.Equal(t, htlcA.Amp, htlcB.Amp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
// Make Bob create an invoice for Alice to pay.
|
// Make Bob create an invoice for Alice to pay.
|
||||||
payReqs, rHashes, invoices, err := createPayReqs(
|
payReqs, rHashes, invoices, err := createPayReqs(
|
||||||
net.Bob, paymentAmt, 1,
|
ctx.bob, paymentAmt, 1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
@@ -65,7 +65,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
payReq := payReqs[0]
|
payReq := payReqs[0]
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
decodeResp, err := net.Bob.DecodePayReq(
|
decodeResp, err := ctx.bob.DecodePayReq(
|
||||||
ctxt, &lnrpc.PayReqString{PayReq: payReq},
|
ctxt, &lnrpc.PayReqString{PayReq: payReq},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,7 +84,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
||||||
for _, hops := range sendRoutes {
|
for _, hops := range sendRoutes {
|
||||||
// Build a route for the specified hops.
|
// Build a route for the specified hops.
|
||||||
r, err := ctx.buildRoute(ctxb, shardAmt, net.Alice, hops)
|
r, err := ctx.buildRoute(ctxb, shardAmt, ctx.alice, hops)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to build route: %v", err)
|
t.Fatalf("unable to build route: %v", err)
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// block as long as the payment is in flight.
|
// block as long as the payment is in flight.
|
||||||
go func() {
|
go func() {
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
resp, err := ctx.alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
}
|
}
|
||||||
@@ -234,10 +234,10 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
// Finally check that the payment shows up with three settled HTLCs in
|
// Finally check that the payment shows up with three settled HTLCs in
|
||||||
// Alice's list of payments...
|
// Alice's list of payments...
|
||||||
assertNumHtlcs(net.Alice, 3)
|
assertNumHtlcs(ctx.alice, 3)
|
||||||
|
|
||||||
// ...and in Bob's list of paid invoices.
|
// ...and in Bob's list of paid invoices.
|
||||||
assertSettledInvoice(net.Bob, rHash, 3)
|
assertSettledInvoice(ctx.bob, rHash, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mppTestContext struct {
|
type mppTestContext struct {
|
||||||
@@ -257,6 +257,16 @@ func newMppTestContext(t *harnessTest,
|
|||||||
|
|
||||||
ctxb := context.Background()
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
alice, err := net.NewNode("alice", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create alice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bob, err := net.NewNode("bob", []string{"--accept-amp"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create bob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a five-node context consisting of Alice, Bob and three new
|
// Create a five-node context consisting of Alice, Bob and three new
|
||||||
// nodes.
|
// nodes.
|
||||||
carol, err := net.NewNode("carol", nil)
|
carol, err := net.NewNode("carol", nil)
|
||||||
@@ -275,7 +285,7 @@ func newMppTestContext(t *harnessTest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect nodes to ensure propagation of channels.
|
// Connect nodes to ensure propagation of channels.
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave, eve}
|
nodes := []*lntest.HarnessNode{alice, bob, carol, dave, eve}
|
||||||
for i := 0; i < len(nodes); i++ {
|
for i := 0; i < len(nodes); i++ {
|
||||||
for j := i + 1; j < len(nodes); j++ {
|
for j := i + 1; j < len(nodes); j++ {
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
@@ -288,8 +298,8 @@ func newMppTestContext(t *harnessTest,
|
|||||||
ctx := mppTestContext{
|
ctx := mppTestContext{
|
||||||
t: t,
|
t: t,
|
||||||
net: net,
|
net: net,
|
||||||
alice: net.Alice,
|
alice: alice,
|
||||||
bob: net.Bob,
|
bob: bob,
|
||||||
carol: carol,
|
carol: carol,
|
||||||
dave: dave,
|
dave: dave,
|
||||||
eve: eve,
|
eve: eve,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func testSendMultiPathPayment(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// Our first test will be Alice paying Bob using a SendPayment call.
|
// Our first test will be Alice paying Bob using a SendPayment call.
|
||||||
// Let Bob create an invoice for Alice to pay.
|
// Let Bob create an invoice for Alice to pay.
|
||||||
payReqs, rHashes, invoices, err := createPayReqs(
|
payReqs, rHashes, invoices, err := createPayReqs(
|
||||||
net.Bob, paymentAmt, 1,
|
ctx.bob, paymentAmt, 1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
@@ -70,7 +70,7 @@ func testSendMultiPathPayment(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
payment := sendAndAssertSuccess(
|
payment := sendAndAssertSuccess(
|
||||||
ctxt, t, net.Alice,
|
ctxt, t, ctx.alice,
|
||||||
&routerrpc.SendPaymentRequest{
|
&routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: payReq,
|
PaymentRequest: payReq,
|
||||||
MaxParts: 10,
|
MaxParts: 10,
|
||||||
|
|||||||
@@ -278,7 +278,10 @@ var allTestCases = []*testCase{
|
|||||||
name: "sendpayment amp",
|
name: "sendpayment amp",
|
||||||
test: testSendPaymentAMP,
|
test: testSendPaymentAMP,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sendpayment amp invoice",
|
||||||
|
test: testSendPaymentAMPInvoice,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "send multi path payment",
|
name: "send multi path payment",
|
||||||
test: testSendMultiPathPayment,
|
test: testSendMultiPathPayment,
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ type NodeConfig struct {
|
|||||||
ProfilePort int
|
ProfilePort int
|
||||||
|
|
||||||
AcceptKeySend bool
|
AcceptKeySend bool
|
||||||
|
AcceptAMP bool
|
||||||
|
|
||||||
FeeURL string
|
FeeURL string
|
||||||
|
|
||||||
@@ -297,6 +298,10 @@ func (cfg NodeConfig) genArgs() []string {
|
|||||||
args = append(args, "--accept-keysend")
|
args = append(args, "--accept-keysend")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.AcceptAMP {
|
||||||
|
args = append(args, "--accept-amp")
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Etcd {
|
if cfg.Etcd {
|
||||||
args = append(args, "--db.backend=etcd")
|
args = append(args, "--db.backend=etcd")
|
||||||
args = append(args, "--db.etcd.embedded")
|
args = append(args, "--db.etcd.embedded")
|
||||||
|
|||||||
@@ -296,9 +296,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
return nil, errNoPathFound
|
return nil, errNoPathFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.payment.DestFeatures.HasFeature(lnwire.MPPOptional) {
|
destFeatures := p.payment.DestFeatures
|
||||||
|
if !destFeatures.HasFeature(lnwire.MPPOptional) &&
|
||||||
|
!destFeatures.HasFeature(lnwire.AMPOptional) {
|
||||||
|
|
||||||
p.log.Debug("not splitting because " +
|
p.log.Debug("not splitting because " +
|
||||||
"destination doesn't declare MPP")
|
"destination doesn't declare MPP or AMP")
|
||||||
|
|
||||||
return nil, errNoPathFound
|
return nil, errNoPathFound
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -661,6 +661,9 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
|||||||
genInvoiceFeatures := func() *lnwire.FeatureVector {
|
genInvoiceFeatures := func() *lnwire.FeatureVector {
|
||||||
return s.featureMgr.Get(feature.SetInvoice)
|
return s.featureMgr.Get(feature.SetInvoice)
|
||||||
}
|
}
|
||||||
|
genAmpInvoiceFeatures := func() *lnwire.FeatureVector {
|
||||||
|
return s.featureMgr.Get(feature.SetInvoiceAmp)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
subServers []lnrpc.SubServer
|
subServers []lnrpc.SubServer
|
||||||
@@ -677,7 +680,8 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
|||||||
s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter,
|
s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter,
|
||||||
routerBackend, s.nodeSigner, s.localChanDB, s.remoteChanDB,
|
routerBackend, s.nodeSigner, s.localChanDB, s.remoteChanDB,
|
||||||
s.sweeper, tower, s.towerClient, s.anchorTowerClient,
|
s.sweeper, tower, s.towerClient, s.anchorTowerClient,
|
||||||
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, rpcsLog,
|
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures,
|
||||||
|
genAmpInvoiceFeatures, rpcsLog,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -4796,6 +4800,9 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||||||
GenInvoiceFeatures: func() *lnwire.FeatureVector {
|
GenInvoiceFeatures: func() *lnwire.FeatureVector {
|
||||||
return r.server.featureMgr.Get(feature.SetInvoice)
|
return r.server.featureMgr.Get(feature.SetInvoice)
|
||||||
},
|
},
|
||||||
|
GenAmpInvoiceFeatures: func() *lnwire.FeatureVector {
|
||||||
|
return r.server.featureMgr.Get(feature.SetInvoiceAmp)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
|
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
|
||||||
@@ -4817,6 +4824,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||||||
CltvExpiry: invoice.CltvExpiry,
|
CltvExpiry: invoice.CltvExpiry,
|
||||||
Private: invoice.Private,
|
Private: invoice.Private,
|
||||||
RouteHints: routeHints,
|
RouteHints: routeHints,
|
||||||
|
Amp: invoice.IsAmp,
|
||||||
}
|
}
|
||||||
|
|
||||||
if invoice.RPreimage != nil {
|
if invoice.RPreimage != nil {
|
||||||
|
|||||||
@@ -375,6 +375,10 @@
|
|||||||
; automatically. [experimental]
|
; automatically. [experimental]
|
||||||
; keysend-hold-time=true
|
; keysend-hold-time=true
|
||||||
|
|
||||||
|
; If true, spontaneous payments through AMP will be accepted. Payments to AMP
|
||||||
|
; invoices will be accepted regardless of this setting.
|
||||||
|
; accept-amp=true
|
||||||
|
|
||||||
; If set, lnd will use anchor channels by default if the remote channel party
|
; If set, lnd will use anchor channels by default if the remote channel party
|
||||||
; supports them. Note that lnd will require 1 UTXO to be reserved for this
|
; supports them. Note that lnd will require 1 UTXO to be reserved for this
|
||||||
; channel type if it is enabled.
|
; channel type if it is enabled.
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||||
Clock: clock.NewDefaultClock(),
|
Clock: clock.NewDefaultClock(),
|
||||||
AcceptKeySend: cfg.AcceptKeySend,
|
AcceptKeySend: cfg.AcceptKeySend,
|
||||||
|
AcceptAMP: cfg.AcceptAMP,
|
||||||
GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup,
|
GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup,
|
||||||
GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly,
|
GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly,
|
||||||
KeysendHoldTime: cfg.KeysendHoldTime,
|
KeysendHoldTime: cfg.KeysendHoldTime,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
|||||||
anchorTowerClient wtclient.Client,
|
anchorTowerClient wtclient.Client,
|
||||||
tcpResolver lncfg.TCPResolver,
|
tcpResolver lncfg.TCPResolver,
|
||||||
genInvoiceFeatures func() *lnwire.FeatureVector,
|
genInvoiceFeatures func() *lnwire.FeatureVector,
|
||||||
|
genAmpInvoiceFeatures func() *lnwire.FeatureVector,
|
||||||
rpcLogger btclog.Logger) error {
|
rpcLogger btclog.Logger) error {
|
||||||
|
|
||||||
// First, we'll use reflect to obtain a version of the config struct
|
// First, we'll use reflect to obtain a version of the config struct
|
||||||
@@ -230,6 +231,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
|||||||
subCfgValue.FieldByName("GenInvoiceFeatures").Set(
|
subCfgValue.FieldByName("GenInvoiceFeatures").Set(
|
||||||
reflect.ValueOf(genInvoiceFeatures),
|
reflect.ValueOf(genInvoiceFeatures),
|
||||||
)
|
)
|
||||||
|
subCfgValue.FieldByName("GenAmpInvoiceFeatures").Set(
|
||||||
|
reflect.ValueOf(genAmpInvoiceFeatures),
|
||||||
|
)
|
||||||
|
|
||||||
// RouterRPC isn't conditionally compiled and doesn't need to be
|
// RouterRPC isn't conditionally compiled and doesn't need to be
|
||||||
// populated using reflection.
|
// populated using reflection.
|
||||||
|
|||||||
Reference in new issue
Block a user