Merge pull request #5253 from cfromknecht/amp-invoice

Support paying AMP invoices via SendPaymentV2
This commit is contained in:
Olaoluwa Osuntokun 2021-05-12 13:38:42 -07:00 committed by GitHub
commit dc73a23e81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1528 additions and 1023 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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))
}

View File

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

View File

@ -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
},
}

View File

@ -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"
}

View File

@ -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:

View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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."
}
}
},

View File

@ -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)

View File

@ -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 {

View File

@ -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,
}

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View File

@ -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."
}
}
},

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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")

View File

@ -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
}

View File

@ -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 {

View File

@ -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.

View File

@ -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,

View File

@ -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.