mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
Merge pull request #5253 from cfromknecht/amp-invoice
Support paying AMP invoices via SendPaymentV2
This commit is contained in:
commit
dc73a23e81
@ -1294,9 +1294,9 @@ func TestHTLCSet(t *testing.T) {
|
||||
expSet2 := make(map[CircuitKey]*InvoiceHTLC)
|
||||
|
||||
checkHTLCSets := func() {
|
||||
require.Equal(t, expSetNil, inv.HTLCSet(nil))
|
||||
require.Equal(t, expSet1, inv.HTLCSet(setID1))
|
||||
require.Equal(t, expSet2, inv.HTLCSet(setID2))
|
||||
require.Equal(t, expSetNil, inv.HTLCSet(nil, HtlcStateAccepted))
|
||||
require.Equal(t, expSet1, inv.HTLCSet(setID1, HtlcStateAccepted))
|
||||
require.Equal(t, expSet2, inv.HTLCSet(setID2, HtlcStateAccepted))
|
||||
}
|
||||
|
||||
// All HTLC sets should be empty initially.
|
||||
|
@ -471,17 +471,16 @@ type Invoice struct {
|
||||
HodlInvoice bool
|
||||
}
|
||||
|
||||
// HTLCSet returns the set of accepted HTLCs belonging to an invoice. Passing a
|
||||
// nil setID will return all accepted HTLCs in the case of legacy or MPP, and no
|
||||
// HTLCs in the case of AMP. Otherwise, the returned set will be filtered by
|
||||
// the populated setID which is used to retrieve AMP HTLC sets.
|
||||
func (i *Invoice) HTLCSet(setID *[32]byte) map[CircuitKey]*InvoiceHTLC {
|
||||
// HTLCSet returns the set of HTLCs belonging to setID and in the provided
|
||||
// state. Passing a nil setID will return all HTLCs in the provided state in the
|
||||
// case of legacy or MPP, and no HTLCs in the case of AMP. Otherwise, the
|
||||
// returned set will be filtered by the populated setID which is used to
|
||||
// retrieve AMP HTLC sets.
|
||||
func (i *Invoice) HTLCSet(setID *[32]byte, state HtlcState) map[CircuitKey]*InvoiceHTLC {
|
||||
htlcSet := make(map[CircuitKey]*InvoiceHTLC)
|
||||
for key, htlc := range i.Htlcs {
|
||||
// Only consider accepted mpp htlcs. It is possible that there
|
||||
// are htlcs registered in the invoice database that previously
|
||||
// timed out and are in the canceled state now.
|
||||
if htlc.State != HtlcStateAccepted {
|
||||
// Only add HTLCs that are in the requested HtlcState.
|
||||
if htlc.State != state {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -495,6 +494,31 @@ func (i *Invoice) HTLCSet(setID *[32]byte) map[CircuitKey]*InvoiceHTLC {
|
||||
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.
|
||||
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
|
||||
// non-existent HTLC set.
|
||||
if len(invoice.HTLCSet(update.SetID)) == 0 {
|
||||
if len(invoice.HTLCSet(update.SetID, HtlcStateAccepted)) == 0 {
|
||||
return ErrEmptyHTLCSet
|
||||
}
|
||||
|
||||
@ -2329,8 +2353,8 @@ func (d *DB) DeleteInvoice(invoicesToDelete []InvoiceDeleteRef) error {
|
||||
// invoice key.
|
||||
key := invoiceAddIndex.Get(addIndexKey[:])
|
||||
if !bytes.Equal(key, invoiceKey) {
|
||||
return fmt.Errorf("unknown invoice in " +
|
||||
"add index")
|
||||
return fmt.Errorf("unknown invoice " +
|
||||
"in add index")
|
||||
}
|
||||
|
||||
// Remove from the add index.
|
||||
|
@ -66,6 +66,11 @@ var addInvoiceCommand = cli.Command{
|
||||
"private channels in order to assist the " +
|
||||
"payer in reaching you",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "amp",
|
||||
Usage: "creates an AMP invoice. If true, preimage " +
|
||||
"should not be set.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(addInvoice),
|
||||
}
|
||||
@ -119,6 +124,7 @@ func addInvoice(ctx *cli.Context) error {
|
||||
FallbackAddr: ctx.String("fallback_addr"),
|
||||
Expiry: ctx.Int64("expiry"),
|
||||
Private: ctx.Bool("private"),
|
||||
IsAmp: ctx.Bool("amp"),
|
||||
}
|
||||
|
||||
resp, err := client.AddInvoice(ctxc, invoice)
|
||||
|
@ -87,6 +87,12 @@ var (
|
||||
"payment splitting is required to attempt a payment, " +
|
||||
"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.
|
||||
@ -131,7 +137,7 @@ func paymentFlags() []cli.Flag {
|
||||
Usage: "allow sending a circular payment to self",
|
||||
},
|
||||
dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
|
||||
maxShardSizeSatFlag, maxShardSizeMsatFlag,
|
||||
maxShardSizeSatFlag, maxShardSizeMsatFlag, ampFlag,
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,11 +293,16 @@ func sendPayment(ctx *cli.Context) error {
|
||||
Dest: destNode,
|
||||
Amt: amount,
|
||||
DestCustomRecords: make(map[uint64][]byte),
|
||||
Amp: ctx.Bool(ampFlag.Name),
|
||||
}
|
||||
|
||||
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") {
|
||||
return errors.New("cannot set payment hash when using " +
|
||||
"keysend")
|
||||
@ -308,7 +319,7 @@ func sendPayment(ctx *cli.Context) error {
|
||||
|
||||
hash := preimage.Hash()
|
||||
rHash = hash[:]
|
||||
} else {
|
||||
case !ctx.Bool(ampFlag.Name):
|
||||
switch {
|
||||
case ctx.IsSet("payment_hash"):
|
||||
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
||||
@ -323,7 +334,7 @@ func sendPayment(ctx *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rHash) != 32 {
|
||||
if !req.Amp && len(rHash) != 32 {
|
||||
return fmt.Errorf("payment hash must be exactly 32 "+
|
||||
"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]"`
|
||||
|
||||
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]"`
|
||||
|
||||
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
|
||||
SetNodeAnn: {}, // N
|
||||
SetInvoice: {}, // 9
|
||||
SetInvoiceAmp: {}, // 9A
|
||||
SetLegacyGlobal: {},
|
||||
},
|
||||
lnwire.StaticRemoteKeyRequired: {
|
||||
@ -34,9 +35,10 @@ var defaultSetDesc = setDesc{
|
||||
SetNodeAnn: {}, // N
|
||||
},
|
||||
lnwire.PaymentAddrRequired: {
|
||||
SetInit: {}, // I
|
||||
SetNodeAnn: {}, // N
|
||||
SetInvoice: {}, // 9
|
||||
SetInit: {}, // I
|
||||
SetNodeAnn: {}, // N
|
||||
SetInvoice: {}, // 9
|
||||
SetInvoiceAmp: {}, // 9A
|
||||
},
|
||||
lnwire.MPPOptional: {
|
||||
SetInit: {}, // I
|
||||
@ -54,6 +56,8 @@ var defaultSetDesc = setDesc{
|
||||
lnwire.AMPOptional: {
|
||||
SetInit: {}, // I
|
||||
SetNodeAnn: {}, // N
|
||||
SetInvoice: {}, // 9
|
||||
},
|
||||
lnwire.AMPRequired: {
|
||||
SetInvoiceAmp: {}, // 9A
|
||||
},
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ const (
|
||||
// SetInvoice identifies features that should be advertised on invoices
|
||||
// generated by the daemon.
|
||||
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.
|
||||
@ -35,6 +39,8 @@ func (s Set) String() string {
|
||||
return "SetNodeAnn"
|
||||
case SetInvoice:
|
||||
return "SetInvoice"
|
||||
case SetInvoiceAmp:
|
||||
return "SetInvoiceAmp"
|
||||
default:
|
||||
return "SetUnknown"
|
||||
}
|
||||
|
@ -57,6 +57,10 @@ type RegistryConfig struct {
|
||||
// send payments.
|
||||
AcceptKeySend bool
|
||||
|
||||
// AcceptAMP indicates whether we want to accept spontaneous AMP
|
||||
// payments.
|
||||
AcceptAMP bool
|
||||
|
||||
// GcCanceledInvoicesOnStartup if set, we'll attempt to garbage collect
|
||||
// all canceled invoices upon start.
|
||||
GcCanceledInvoicesOnStartup bool
|
||||
@ -884,28 +888,33 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
amp: payload.AMPRecord(),
|
||||
}
|
||||
|
||||
// Process keysend 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 {
|
||||
if ctx.amp != nil {
|
||||
err := i.processAMP(ctx)
|
||||
if err != nil {
|
||||
ctx.log(fmt.Sprintf("amp error: %v", err))
|
||||
switch {
|
||||
|
||||
return NewFailResolution(
|
||||
circuitKey, currentHeight, ResultAmpError,
|
||||
), nil
|
||||
}
|
||||
} else {
|
||||
err := i.processKeySend(ctx)
|
||||
if err != nil {
|
||||
ctx.log(fmt.Sprintf("keysend error: %v", err))
|
||||
// If we are accepting spontaneous AMP payments and this payload
|
||||
// contains an AMP record, create an AMP invoice that will be settled
|
||||
// below.
|
||||
case i.cfg.AcceptAMP && ctx.amp != nil:
|
||||
err := i.processAMP(ctx)
|
||||
if err != nil {
|
||||
ctx.log(fmt.Sprintf("amp error: %v", err))
|
||||
|
||||
return NewFailResolution(
|
||||
circuitKey, currentHeight, ResultKeySendError,
|
||||
), nil
|
||||
}
|
||||
return NewFailResolution(
|
||||
circuitKey, currentHeight, ResultAmpError,
|
||||
), 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
|
||||
// canceled state with the same failure result.
|
||||
setID := ctx.setID()
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
if htlc.State != channeldb.HtlcStateCanceled {
|
||||
continue
|
||||
}
|
||||
|
||||
if !htlc.IsInHTLCSet(setID) {
|
||||
continue
|
||||
}
|
||||
|
||||
canceledHtlcSet := invoice.HTLCSet(setID, channeldb.HtlcStateCanceled)
|
||||
for key, htlc := range canceledHtlcSet {
|
||||
htlcFailResolution := NewFailResolution(
|
||||
key, int32(htlc.AcceptHeight), res.Outcome,
|
||||
)
|
||||
@ -1047,11 +1049,9 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
||||
// Also settle any previously accepted htlcs. If a htlc is
|
||||
// marked as settled, we should follow now and settle the htlc
|
||||
// with our peer.
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
if htlc.State != channeldb.HtlcStateSettled {
|
||||
continue
|
||||
}
|
||||
|
||||
setID := ctx.setID()
|
||||
settledHtlcSet := invoice.HTLCSet(setID, channeldb.HtlcStateSettled)
|
||||
for key, htlc := range settledHtlcSet {
|
||||
preimage := res.Preimage
|
||||
if htlc.AMP != nil && htlc.AMP.Preimage != nil {
|
||||
preimage = *htlc.AMP.Preimage
|
||||
@ -1072,6 +1072,23 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
||||
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
|
||||
// an accept resolution with the htlc's accept time on it.
|
||||
case *htlcAcceptResolution:
|
||||
|
@ -1290,7 +1290,7 @@ func TestAMPWithoutMPPPayload(t *testing.T) {
|
||||
ctx := newTestContext(t)
|
||||
defer ctx.cleanup()
|
||||
|
||||
ctx.registry.cfg.AcceptKeySend = true
|
||||
ctx.registry.cfg.AcceptAMP = true
|
||||
|
||||
const (
|
||||
shardAmt = lnwire.MilliSatoshi(10)
|
||||
@ -1320,37 +1320,37 @@ func TestAMPWithoutMPPPayload(t *testing.T) {
|
||||
func TestSpontaneousAmpPayment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keySendEnabled bool
|
||||
ampEnabled bool
|
||||
failReconstruction bool
|
||||
numShards int
|
||||
}{
|
||||
{
|
||||
name: "enabled valid one shard",
|
||||
keySendEnabled: true,
|
||||
ampEnabled: true,
|
||||
failReconstruction: false,
|
||||
numShards: 1,
|
||||
},
|
||||
{
|
||||
name: "enabled valid multiple shards",
|
||||
keySendEnabled: true,
|
||||
ampEnabled: true,
|
||||
failReconstruction: false,
|
||||
numShards: 3,
|
||||
},
|
||||
{
|
||||
name: "enabled invalid one shard",
|
||||
keySendEnabled: true,
|
||||
ampEnabled: true,
|
||||
failReconstruction: true,
|
||||
numShards: 1,
|
||||
},
|
||||
{
|
||||
name: "enabled invalid multiple shards",
|
||||
keySendEnabled: true,
|
||||
ampEnabled: true,
|
||||
failReconstruction: true,
|
||||
numShards: 3,
|
||||
},
|
||||
{
|
||||
name: "disabled valid multiple shards",
|
||||
keySendEnabled: false,
|
||||
ampEnabled: false,
|
||||
failReconstruction: false,
|
||||
numShards: 3,
|
||||
},
|
||||
@ -1360,7 +1360,7 @@ func TestSpontaneousAmpPayment(t *testing.T) {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testSpontaneousAmpPayment(
|
||||
t, test.keySendEnabled, test.failReconstruction,
|
||||
t, test.ampEnabled, test.failReconstruction,
|
||||
test.numShards,
|
||||
)
|
||||
})
|
||||
@ -1369,14 +1369,14 @@ func TestSpontaneousAmpPayment(t *testing.T) {
|
||||
|
||||
// testSpontaneousAmpPayment runs a specific spontaneous AMP test case.
|
||||
func testSpontaneousAmpPayment(
|
||||
t *testing.T, keySendEnabled, failReconstruction bool, numShards int) {
|
||||
t *testing.T, ampEnabled, failReconstruction bool, numShards int) {
|
||||
|
||||
defer timeout()()
|
||||
|
||||
ctx := newTestContext(t)
|
||||
defer ctx.cleanup()
|
||||
|
||||
ctx.registry.cfg.AcceptKeySend = keySendEnabled
|
||||
ctx.registry.cfg.AcceptAMP = ampEnabled
|
||||
|
||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||
require.Nil(t, err)
|
||||
@ -1471,7 +1471,7 @@ func testSpontaneousAmpPayment(
|
||||
// When keysend is disabled all HTLC should fail with invoice
|
||||
// not found, since one is not inserted before executing
|
||||
// UpdateInvoice.
|
||||
if !keySendEnabled {
|
||||
if !ampEnabled {
|
||||
require.NotNil(t, resolution)
|
||||
checkFailResolution(t, resolution, ResultInvoiceNotFound)
|
||||
continue
|
||||
@ -1515,7 +1515,7 @@ func testSpontaneousAmpPayment(
|
||||
}
|
||||
|
||||
// No need to check the hodl chans when keysend is not enabled.
|
||||
if !keySendEnabled {
|
||||
if !ampEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,10 @@ const (
|
||||
// invoice that is already canceled.
|
||||
ResultInvoiceAlreadyCanceled
|
||||
|
||||
// ResultInvoiceAlreadySettled is returned when trying to pay an invoice
|
||||
// that is already settled.
|
||||
ResultInvoiceAlreadySettled
|
||||
|
||||
// ResultAmountTooLow is returned when an invoice is underpaid.
|
||||
ResultAmountTooLow
|
||||
|
||||
@ -106,6 +110,10 @@ const (
|
||||
// payment.
|
||||
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
|
||||
|
||||
@ -133,6 +141,9 @@ func (f FailResolutionResult) FailureString() string {
|
||||
case ResultInvoiceAlreadyCanceled:
|
||||
return "invoice already canceled"
|
||||
|
||||
case ResultInvoiceAlreadySettled:
|
||||
return "invoice alread settled"
|
||||
|
||||
case ResultAmountTooLow:
|
||||
return "amount too low"
|
||||
|
||||
@ -169,6 +180,9 @@ func (f FailResolutionResult) FailureString() string {
|
||||
case ResultMppInProgress:
|
||||
return "mpp reception in progress"
|
||||
|
||||
case ResultHtlcInvoiceTypeMismatch:
|
||||
return "htlc invoice type mismatch"
|
||||
|
||||
case ResultAmpError:
|
||||
return "invalid amp parameters"
|
||||
|
||||
|
@ -125,6 +125,21 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
||||
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
|
||||
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()
|
||||
|
||||
// Start building the accept descriptor.
|
||||
@ -168,7 +183,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
||||
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.
|
||||
var newSetTotal lnwire.MilliSatoshi
|
||||
@ -373,7 +388,7 @@ func updateLegacy(ctx *invoiceUpdateCtx,
|
||||
// Don't allow settling the invoice with an old style
|
||||
// htlc if we are already in the process of gathering an
|
||||
// mpp set.
|
||||
for _, htlc := range inv.HTLCSet(nil) {
|
||||
for _, htlc := range inv.HTLCSet(nil, channeldb.HtlcStateAccepted) {
|
||||
if htlc.MppTotalAmt > 0 {
|
||||
return nil, ctx.failRes(ResultMppInProgress), nil
|
||||
}
|
||||
|
@ -54,6 +54,10 @@ type AddInvoiceConfig struct {
|
||||
// GenInvoiceFeatures returns a feature containing feature bits that
|
||||
// should be advertised on freshly generated invoices.
|
||||
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.
|
||||
@ -99,17 +103,71 @@ type AddInvoiceData struct {
|
||||
// immediately upon receiving the payment.
|
||||
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
|
||||
// to assist in reaching the invoice's destination.
|
||||
RouteHints [][]zpay32.HopHint
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// paymentHashAndPreimage returns the payment hash and preimage for this invoice
|
||||
// depending on the configuration.
|
||||
//
|
||||
// For AMP invoices (when Amp flag is true), this method always returns a nil
|
||||
// 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 (
|
||||
paymentPreimage *lntypes.Preimage
|
||||
paymentHash lntypes.Hash
|
||||
@ -118,28 +176,42 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
||||
switch {
|
||||
|
||||
// Only either preimage or hash can be set.
|
||||
case invoice.Preimage != nil && invoice.Hash != nil:
|
||||
return nil, nil,
|
||||
case d.Preimage != nil && d.Hash != nil:
|
||||
return nil, lntypes.Hash{},
|
||||
errors.New("preimage and hash both set")
|
||||
|
||||
// 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{}
|
||||
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, lntypes.Hash{}, err
|
||||
}
|
||||
paymentHash = paymentPreimage.Hash()
|
||||
|
||||
// If just a hash is given, we create a hold invoice by setting the
|
||||
// preimage to unknown.
|
||||
case invoice.Preimage == nil && invoice.Hash != nil:
|
||||
paymentHash = *invoice.Hash
|
||||
case d.Preimage == nil && d.Hash != nil:
|
||||
paymentHash = *d.Hash
|
||||
|
||||
// A specific preimage was supplied. Use that for the invoice.
|
||||
case invoice.Preimage != nil && invoice.Hash == nil:
|
||||
preimage := *invoice.Preimage
|
||||
case d.Preimage != nil && d.Hash == nil:
|
||||
preimage := *d.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
|
||||
@ -307,7 +379,12 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
||||
// 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
|
||||
// should be advertised on freshly generated invoices.
|
||||
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",
|
||||
"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."
|
||||
},
|
||||
"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) {
|
||||
|
||||
addInvoiceCfg := &AddInvoiceConfig{
|
||||
AddInvoice: s.cfg.InvoiceRegistry.AddInvoice,
|
||||
IsChannelActive: s.cfg.IsChannelActive,
|
||||
ChainParams: s.cfg.ChainParams,
|
||||
NodeSigner: s.cfg.NodeSigner,
|
||||
DefaultCLTVExpiry: s.cfg.DefaultCLTVExpiry,
|
||||
ChanDB: s.cfg.RemoteChanDB,
|
||||
Graph: s.cfg.LocalChanDB.ChannelGraph(),
|
||||
GenInvoiceFeatures: s.cfg.GenInvoiceFeatures,
|
||||
AddInvoice: s.cfg.InvoiceRegistry.AddInvoice,
|
||||
IsChannelActive: s.cfg.IsChannelActive,
|
||||
ChainParams: s.cfg.ChainParams,
|
||||
NodeSigner: s.cfg.NodeSigner,
|
||||
DefaultCLTVExpiry: s.cfg.DefaultCLTVExpiry,
|
||||
ChanDB: s.cfg.RemoteChanDB,
|
||||
Graph: s.cfg.LocalChanDB.ChannelGraph(),
|
||||
GenInvoiceFeatures: s.cfg.GenInvoiceFeatures,
|
||||
GenAmpInvoiceFeatures: s.cfg.GenAmpInvoiceFeatures,
|
||||
}
|
||||
|
||||
hash, err := lntypes.MakeHash(invoice.Hash)
|
||||
|
@ -147,6 +147,8 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
||||
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
|
||||
}
|
||||
|
||||
isAmp := invoice.Terms.Features.HasFeature(lnwire.AMPOptional)
|
||||
|
||||
rpcInvoice := &lnrpc.Invoice{
|
||||
Memo: string(invoice.Memo),
|
||||
RHash: rHash,
|
||||
@ -170,8 +172,9 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
||||
State: state,
|
||||
Htlcs: rpcHtlcs,
|
||||
Features: CreateRPCFeatures(invoice.Terms.Features),
|
||||
IsKeysend: len(invoice.PaymentRequest) == 0,
|
||||
IsKeysend: len(invoice.PaymentRequest) == 0 && !isAmp,
|
||||
PaymentAddr: invoice.Terms.PaymentAddr[:],
|
||||
IsAmp: isAmp,
|
||||
}
|
||||
|
||||
if preimage != nil {
|
||||
|
@ -696,13 +696,37 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
||||
payIntent.Amount = *payReq.MilliSat
|
||||
}
|
||||
|
||||
if !payReq.Features.HasFeature(lnwire.MPPOptional) {
|
||||
if !payReq.Features.HasFeature(lnwire.MPPOptional) &&
|
||||
!payReq.Features.HasFeature(lnwire.AMPOptional) {
|
||||
|
||||
payIntent.MaxParts = 1
|
||||
}
|
||||
|
||||
err = payIntent.SetPaymentHash(*payReq.PaymentHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if payReq.Features.HasFeature(lnwire.AMPOptional) {
|
||||
// Generate random SetID and root share.
|
||||
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()
|
||||
@ -765,7 +789,6 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
||||
ampFeatures := []lnrpc.FeatureBit{
|
||||
lnrpc.FeatureBit_TLV_ONION_OPT,
|
||||
lnrpc.FeatureBit_PAYMENT_ADDR_OPT,
|
||||
lnrpc.FeatureBit_MPP_OPT,
|
||||
lnrpc.FeatureBit_AMP_OPT,
|
||||
}
|
||||
|
||||
|
1801
lnrpc/rpc.pb.go
1801
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -3003,6 +3003,11 @@ message Invoice {
|
||||
for added end-to-end security.
|
||||
*/
|
||||
bytes payment_addr = 26;
|
||||
|
||||
/*
|
||||
Signals whether or not this is an AMP invoice.
|
||||
*/
|
||||
bool is_amp = 27;
|
||||
}
|
||||
|
||||
enum InvoiceHTLCState {
|
||||
|
@ -4387,6 +4387,11 @@
|
||||
"type": "string",
|
||||
"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."
|
||||
},
|
||||
"is_amp": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Signals whether or not this is an AMP invoice."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3,6 +3,8 @@ package itest
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
@ -15,6 +17,152 @@ import (
|
||||
"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
|
||||
// destination using SendPaymentV2.
|
||||
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)
|
||||
payment := sendAndAssertSuccess(
|
||||
ctxt, t, net.Alice,
|
||||
ctxt, t, ctx.alice,
|
||||
&routerrpc.SendPaymentRequest{
|
||||
Dest: net.Bob.PubKey[:],
|
||||
Dest: ctx.bob.PubKey[:],
|
||||
Amt: int64(paymentAmt),
|
||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
MaxParts: 10,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
Amp: true,
|
||||
@ -97,7 +244,7 @@ func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
}
|
||||
|
||||
// Fetch Bob's invoices.
|
||||
invoiceResp, err := net.Bob.ListInvoices(
|
||||
invoiceResp, err := ctx.bob.ListInvoices(
|
||||
ctxb, &lnrpc.ListInvoiceRequest{},
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
@ -172,6 +319,13 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
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.
|
||||
sendRoutes := [numShards][]*lntest.HarnessNode{
|
||||
{ctx.carol, ctx.bob},
|
||||
@ -180,7 +334,7 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
}
|
||||
|
||||
payAddr := make([]byte, 32)
|
||||
_, err := rand.Read(payAddr)
|
||||
_, err = rand.Read(payAddr)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
setID := make([]byte, 32)
|
||||
@ -193,9 +347,11 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
childPreimages := make(map[lntypes.Preimage]uint32)
|
||||
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.
|
||||
r, err := ctx.buildRoute(ctxb, shardAmt, net.Alice, hops)
|
||||
r, err := ctx.buildRoute(ctxb, shardAmt, ctx.alice, hops)
|
||||
if err != nil {
|
||||
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.
|
||||
go func() {
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||
resp, err := ctx.alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||
if err != nil {
|
||||
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.
|
||||
require.Equal(t.t, len(sendRoutes), len(childPreimages))
|
||||
|
||||
@ -282,15 +456,18 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
}
|
||||
childPreimages = childPreimagesCopy
|
||||
|
||||
// Fetch Bob's invoices.
|
||||
invoiceResp, err := net.Bob.ListInvoices(
|
||||
// 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)
|
||||
|
||||
// There should only be one invoice.
|
||||
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
|
||||
// has the correct payment address.
|
||||
@ -330,3 +507,60 @@ func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
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.
|
||||
payReqs, rHashes, invoices, err := createPayReqs(
|
||||
net.Bob, paymentAmt, 1,
|
||||
ctx.bob, paymentAmt, 1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create pay reqs: %v", err)
|
||||
@ -65,7 +65,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
payReq := payReqs[0]
|
||||
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
decodeResp, err := net.Bob.DecodePayReq(
|
||||
decodeResp, err := ctx.bob.DecodePayReq(
|
||||
ctxt, &lnrpc.PayReqString{PayReq: payReq},
|
||||
)
|
||||
if err != nil {
|
||||
@ -84,7 +84,7 @@ func testSendToRouteMultiPath(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
||||
for _, hops := range sendRoutes {
|
||||
// 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 {
|
||||
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.
|
||||
go func() {
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||
resp, err := ctx.alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||
if err != nil {
|
||||
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
|
||||
// Alice's list of payments...
|
||||
assertNumHtlcs(net.Alice, 3)
|
||||
assertNumHtlcs(ctx.alice, 3)
|
||||
|
||||
// ...and in Bob's list of paid invoices.
|
||||
assertSettledInvoice(net.Bob, rHash, 3)
|
||||
assertSettledInvoice(ctx.bob, rHash, 3)
|
||||
}
|
||||
|
||||
type mppTestContext struct {
|
||||
@ -257,6 +257,16 @@ func newMppTestContext(t *harnessTest,
|
||||
|
||||
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
|
||||
// nodes.
|
||||
carol, err := net.NewNode("carol", nil)
|
||||
@ -275,7 +285,7 @@ func newMppTestContext(t *harnessTest,
|
||||
}
|
||||
|
||||
// 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 j := i + 1; j < len(nodes); j++ {
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
@ -288,8 +298,8 @@ func newMppTestContext(t *harnessTest,
|
||||
ctx := mppTestContext{
|
||||
t: t,
|
||||
net: net,
|
||||
alice: net.Alice,
|
||||
bob: net.Bob,
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
carol: carol,
|
||||
dave: dave,
|
||||
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.
|
||||
// Let Bob create an invoice for Alice to pay.
|
||||
payReqs, rHashes, invoices, err := createPayReqs(
|
||||
net.Bob, paymentAmt, 1,
|
||||
ctx.bob, paymentAmt, 1,
|
||||
)
|
||||
if err != nil {
|
||||
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)
|
||||
payment := sendAndAssertSuccess(
|
||||
ctxt, t, net.Alice,
|
||||
ctxt, t, ctx.alice,
|
||||
&routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
MaxParts: 10,
|
||||
|
@ -278,7 +278,10 @@ var allTestCases = []*testCase{
|
||||
name: "sendpayment amp",
|
||||
test: testSendPaymentAMP,
|
||||
},
|
||||
|
||||
{
|
||||
name: "sendpayment amp invoice",
|
||||
test: testSendPaymentAMPInvoice,
|
||||
},
|
||||
{
|
||||
name: "send multi path payment",
|
||||
test: testSendMultiPathPayment,
|
||||
|
@ -210,6 +210,7 @@ type NodeConfig struct {
|
||||
ProfilePort int
|
||||
|
||||
AcceptKeySend bool
|
||||
AcceptAMP bool
|
||||
|
||||
FeeURL string
|
||||
|
||||
@ -297,6 +298,10 @@ func (cfg NodeConfig) genArgs() []string {
|
||||
args = append(args, "--accept-keysend")
|
||||
}
|
||||
|
||||
if cfg.AcceptAMP {
|
||||
args = append(args, "--accept-amp")
|
||||
}
|
||||
|
||||
if cfg.Etcd {
|
||||
args = append(args, "--db.backend=etcd")
|
||||
args = append(args, "--db.etcd.embedded")
|
||||
|
@ -296,9 +296,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
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 " +
|
||||
"destination doesn't declare MPP")
|
||||
"destination doesn't declare MPP or AMP")
|
||||
|
||||
return nil, errNoPathFound
|
||||
}
|
||||
|
10
rpcserver.go
10
rpcserver.go
@ -661,6 +661,9 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
||||
genInvoiceFeatures := func() *lnwire.FeatureVector {
|
||||
return s.featureMgr.Get(feature.SetInvoice)
|
||||
}
|
||||
genAmpInvoiceFeatures := func() *lnwire.FeatureVector {
|
||||
return s.featureMgr.Get(feature.SetInvoiceAmp)
|
||||
}
|
||||
|
||||
var (
|
||||
subServers []lnrpc.SubServer
|
||||
@ -677,7 +680,8 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
||||
s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter,
|
||||
routerBackend, s.nodeSigner, s.localChanDB, s.remoteChanDB,
|
||||
s.sweeper, tower, s.towerClient, s.anchorTowerClient,
|
||||
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, rpcsLog,
|
||||
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures,
|
||||
genAmpInvoiceFeatures, rpcsLog,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -4796,6 +4800,9 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
||||
GenInvoiceFeatures: func() *lnwire.FeatureVector {
|
||||
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)
|
||||
@ -4817,6 +4824,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
||||
CltvExpiry: invoice.CltvExpiry,
|
||||
Private: invoice.Private,
|
||||
RouteHints: routeHints,
|
||||
Amp: invoice.IsAmp,
|
||||
}
|
||||
|
||||
if invoice.RPreimage != nil {
|
||||
|
@ -375,6 +375,10 @@
|
||||
; automatically. [experimental]
|
||||
; 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
|
||||
; supports them. Note that lnd will require 1 UTXO to be reserved for this
|
||||
; channel type if it is enabled.
|
||||
|
@ -427,6 +427,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||
Clock: clock.NewDefaultClock(),
|
||||
AcceptKeySend: cfg.AcceptKeySend,
|
||||
AcceptAMP: cfg.AcceptAMP,
|
||||
GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup,
|
||||
GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly,
|
||||
KeysendHoldTime: cfg.KeysendHoldTime,
|
||||
|
@ -100,6 +100,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
||||
anchorTowerClient wtclient.Client,
|
||||
tcpResolver lncfg.TCPResolver,
|
||||
genInvoiceFeatures func() *lnwire.FeatureVector,
|
||||
genAmpInvoiceFeatures func() *lnwire.FeatureVector,
|
||||
rpcLogger btclog.Logger) error {
|
||||
|
||||
// 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(
|
||||
reflect.ValueOf(genInvoiceFeatures),
|
||||
)
|
||||
subCfgValue.FieldByName("GenAmpInvoiceFeatures").Set(
|
||||
reflect.ValueOf(genAmpInvoiceFeatures),
|
||||
)
|
||||
|
||||
// RouterRPC isn't conditionally compiled and doesn't need to be
|
||||
// populated using reflection.
|
||||
|
Loading…
Reference in New Issue
Block a user