mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
routing+channeldb: send payment metadata from invoice
This commit is contained in:
parent
135e27ddd3
commit
9195f29e61
@ -1141,6 +1141,10 @@ func serializeHop(w io.Writer, h *route.Hop) error {
|
||||
records = append(records, h.MPP.Record())
|
||||
}
|
||||
|
||||
if h.Metadata != nil {
|
||||
records = append(records, record.NewMetadataRecord(&h.Metadata))
|
||||
}
|
||||
|
||||
// Final sanity check to absolutely rule out custom records that are not
|
||||
// custom and write into the standard range.
|
||||
if err := h.CustomRecords.Validate(); err != nil {
|
||||
@ -1255,6 +1259,13 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
|
||||
h.MPP = mpp
|
||||
}
|
||||
|
||||
metadataType := uint64(record.MetadataOnionType)
|
||||
if metadata, ok := tlvMap[metadataType]; ok {
|
||||
delete(tlvMap, metadataType)
|
||||
|
||||
h.Metadata = metadata
|
||||
}
|
||||
|
||||
h.CustomRecords = tlvMap
|
||||
|
||||
return h, nil
|
||||
|
@ -31,7 +31,8 @@ var (
|
||||
65536: []byte{},
|
||||
80001: []byte{},
|
||||
},
|
||||
MPP: record.NewMPP(32, [32]byte{0x42}),
|
||||
MPP: record.NewMPP(32, [32]byte{0x42}),
|
||||
Metadata: []byte{1, 2, 3},
|
||||
}
|
||||
|
||||
testHop2 = &route.Hop{
|
||||
|
@ -1,5 +1,16 @@
|
||||
# Release Notes
|
||||
|
||||
## Payments
|
||||
|
||||
Support according to the
|
||||
[spec](https://github.com/lightningnetwork/lightning-rfc/pull/912) has been
|
||||
added for [payment metadata in
|
||||
invoices](https://github.com/lightningnetwork/lnd/pull/5810). If metadata is
|
||||
present in the invoice, it is encoded as a tlv record for the receiver.
|
||||
|
||||
This functionality unlocks future features such as [stateless
|
||||
invoices](https://lists.linuxfoundation.org/pipermail/lightning-dev/2021-September/003236.html).
|
||||
|
||||
## Security
|
||||
|
||||
* [Misconfigured ZMQ
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2792,6 +2792,9 @@ message Hop {
|
||||
to drop off at each hop within the onion.
|
||||
*/
|
||||
map<uint64, bytes> custom_records = 11;
|
||||
|
||||
// The payment metadata to send along with the payment to the payee.
|
||||
bytes metadata = 13;
|
||||
}
|
||||
|
||||
message MPPRecord {
|
||||
|
@ -4616,6 +4616,11 @@
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "An optional set of key-value TLV records. This is useful within the context\nof the SendToRoute call as it allows callers to specify arbitrary K-V pairs\nto drop off at each hop within the onion."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The payment metadata to send along with the payment to the payee."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -819,6 +819,11 @@
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "An optional set of key-value TLV records. This is useful within the context\nof the SendToRoute call as it allows callers to specify arbitrary K-V pairs\nto drop off at each hop within the onion."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The payment metadata to send along with the payment to the payee."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -444,6 +444,7 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
|
||||
CustomRecords: hop.CustomRecords,
|
||||
TlvPayload: !hop.LegacyPayload,
|
||||
MppRecord: mpp,
|
||||
Metadata: hop.Metadata,
|
||||
}
|
||||
incomingAmt = hop.AmtToForward
|
||||
}
|
||||
@ -766,6 +767,7 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
||||
payIntent.DestFeatures = payReq.Features
|
||||
payIntent.PaymentAddr = payAddr
|
||||
payIntent.PaymentRequest = []byte(rpcPayReq.PaymentRequest)
|
||||
payIntent.Metadata = payReq.Metadata
|
||||
} else {
|
||||
// Otherwise, If the payment request field was not specified
|
||||
// (and a custom route wasn't specified), construct the payment
|
||||
|
@ -155,6 +155,16 @@ const (
|
||||
// TODO: Decide on actual feature bit value.
|
||||
ExplicitChannelTypeOptional = 45
|
||||
|
||||
// PaymentMetadataRequired is a required bit that denotes that if an
|
||||
// invoice contains metadata, it must be passed along with the payment
|
||||
// htlc(s).
|
||||
PaymentMetadataRequired = 48
|
||||
|
||||
// PaymentMetadataOptional is an optional bit that denotes that if an
|
||||
// invoice contains metadata, it may be passed along with the payment
|
||||
// htlc(s).
|
||||
PaymentMetadataOptional = 49
|
||||
|
||||
// ScriptEnforcedLeaseOptional is an optional feature bit that signals
|
||||
// that the node requires channels having zero-fee second-level HTLC
|
||||
// transactions, which also imply anchor commitments, along with an
|
||||
@ -218,6 +228,8 @@ var Features = map[FeatureBit]string{
|
||||
WumboChannelsOptional: "wumbo-channels",
|
||||
AMPRequired: "amp",
|
||||
AMPOptional: "amp",
|
||||
PaymentMetadataOptional: "payment-metadata",
|
||||
PaymentMetadataRequired: "payment-metadata",
|
||||
ExplicitChannelTypeOptional: "explicit-commitment-type",
|
||||
ExplicitChannelTypeRequired: "explicit-commitment-type",
|
||||
ScriptEnforcedLeaseRequired: "script-enforced-lease",
|
||||
|
@ -16,6 +16,10 @@ const (
|
||||
// NextHopOnionType is the type used in the onion to reference the ID
|
||||
// of the next hop.
|
||||
NextHopOnionType tlv.Type = 6
|
||||
|
||||
// MetadataOnionType is the type used in the onion for the payment
|
||||
// metadata.
|
||||
MetadataOnionType tlv.Type = 16
|
||||
)
|
||||
|
||||
// NewAmtToFwdRecord creates a tlv.Record that encodes the amount_to_forward
|
||||
@ -45,3 +49,15 @@ func NewLockTimeRecord(lockTime *uint32) tlv.Record {
|
||||
func NewNextHopIDRecord(cid *uint64) tlv.Record {
|
||||
return tlv.MakePrimitiveRecord(NextHopOnionType, cid)
|
||||
}
|
||||
|
||||
// NewMetadataRecord creates a tlv.Record that encodes the metadata (type 10)
|
||||
// for an onion payload.
|
||||
func NewMetadataRecord(metadata *[]byte) tlv.Record {
|
||||
return tlv.MakeDynamicRecord(
|
||||
MetadataOnionType, metadata,
|
||||
func() uint64 {
|
||||
return uint64(len(*metadata))
|
||||
},
|
||||
tlv.EVarBytes, tlv.DVarBytes,
|
||||
)
|
||||
}
|
||||
|
@ -89,6 +89,10 @@ type finalHopParams struct {
|
||||
cltvDelta uint16
|
||||
records record.CustomSet
|
||||
paymentAddr *[32]byte
|
||||
|
||||
// metadata is additional data that is sent along with the payment to
|
||||
// the payee.
|
||||
metadata []byte
|
||||
}
|
||||
|
||||
// newRoute constructs a route using the provided path and final hop constraints.
|
||||
@ -138,6 +142,7 @@ func newRoute(sourceVertex route.Vertex,
|
||||
tlvPayload bool
|
||||
customRecords record.CustomSet
|
||||
mpp *record.MPP
|
||||
metadata []byte
|
||||
)
|
||||
|
||||
// Define a helper function that checks this edge's feature
|
||||
@ -202,6 +207,8 @@ func newRoute(sourceVertex route.Vertex,
|
||||
*finalHop.paymentAddr,
|
||||
)
|
||||
}
|
||||
|
||||
metadata = finalHop.metadata
|
||||
} else {
|
||||
// The amount that the current hop needs to forward is
|
||||
// equal to the incoming amount of the next hop.
|
||||
@ -232,6 +239,7 @@ func newRoute(sourceVertex route.Vertex,
|
||||
LegacyPayload: !tlvPayload,
|
||||
CustomRecords: customRecords,
|
||||
MPP: mpp,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
hops = append([]*route.Hop{currentHop}, hops...)
|
||||
@ -330,6 +338,10 @@ type RestrictParams struct {
|
||||
// mitigate probing vectors and payment sniping attacks on overpaid
|
||||
// invoices.
|
||||
PaymentAddr *[32]byte
|
||||
|
||||
// Metadata is additional data that is sent along with the payment to
|
||||
// the payee.
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
// PathFindingConfig defines global parameters that control the trade-off in
|
||||
@ -474,6 +486,14 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||
return nil, errNoPaymentAddr
|
||||
}
|
||||
|
||||
// If the caller needs to send custom records, check that our
|
||||
// destination feature vector supports TLV.
|
||||
if r.Metadata != nil &&
|
||||
!features.HasFeature(lnwire.TLVOnionPayloadOptional) {
|
||||
|
||||
return nil, errNoTlvPayload
|
||||
}
|
||||
|
||||
// Set up outgoing channel map for quicker access.
|
||||
var outgoingChanMap map[uint64]struct{}
|
||||
if len(r.OutgoingChannelIDs) > 0 {
|
||||
@ -547,7 +567,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||
LegacyPayload: !features.HasFeature(
|
||||
lnwire.TLVOnionPayloadOptional,
|
||||
),
|
||||
MPP: mpp,
|
||||
MPP: mpp,
|
||||
Metadata: r.Metadata,
|
||||
}
|
||||
|
||||
// We can't always assume that the end destination is publicly
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -840,6 +841,9 @@ func TestPathFinding(t *testing.T) {
|
||||
}, {
|
||||
name: "route to self",
|
||||
fn: runRouteToSelf,
|
||||
}, {
|
||||
name: "with metadata",
|
||||
fn: runFindPathWithMetadata,
|
||||
}}
|
||||
|
||||
// Run with graph cache enabled.
|
||||
@ -866,6 +870,46 @@ func TestPathFinding(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// runFindPathWithMetadata tests that metadata is taken into account during
|
||||
// pathfinding.
|
||||
func runFindPathWithMetadata(t *testing.T, useCache bool) {
|
||||
testChannels := []*testChannel{
|
||||
symmetricTestChannel("alice", "bob", 100000, &testChannelPolicy{
|
||||
Expiry: 144,
|
||||
FeeRate: 400,
|
||||
MinHTLC: 1,
|
||||
MaxHTLC: 100000000,
|
||||
}),
|
||||
}
|
||||
|
||||
ctx := newPathFindingTestContext(t, useCache, testChannels, "alice")
|
||||
defer ctx.cleanup()
|
||||
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
target := ctx.keyFromAlias("bob")
|
||||
|
||||
// Assert that a path is found when metadata is specified.
|
||||
ctx.restrictParams.Metadata = []byte{1, 2, 3}
|
||||
ctx.restrictParams.DestFeatures = tlvFeatures
|
||||
|
||||
path, err := ctx.findPath(target, paymentAmt)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, path, 1)
|
||||
|
||||
// Assert that no path is found when metadata is too large.
|
||||
ctx.restrictParams.Metadata = make([]byte, 2000)
|
||||
|
||||
_, err = ctx.findPath(target, paymentAmt)
|
||||
require.ErrorIs(t, errNoPathFound, err)
|
||||
|
||||
// Assert that tlv payload support takes precedence over metadata
|
||||
// issues.
|
||||
ctx.restrictParams.DestFeatures = lnwire.EmptyFeatureVector()
|
||||
|
||||
_, err = ctx.findPath(target, paymentAmt)
|
||||
require.ErrorIs(t, errNoTlvPayload, err)
|
||||
}
|
||||
|
||||
// runFindLowestFeePath tests that out of two routes with identical total
|
||||
// time lock values, the route with the lowest total fee should be returned.
|
||||
// The fee rates are chosen such that the test failed on the previous edge
|
||||
@ -1340,6 +1384,9 @@ func TestNewRoute(t *testing.T) {
|
||||
|
||||
paymentAddr *[32]byte
|
||||
|
||||
// metadata is the payment metadata to attach to the route.
|
||||
metadata []byte
|
||||
|
||||
// expectedFees is a list of fees that every hop is expected
|
||||
// to charge for forwarding.
|
||||
expectedFees []lnwire.MilliSatoshi
|
||||
@ -1380,6 +1427,7 @@ func TestNewRoute(t *testing.T) {
|
||||
hops: []*channeldb.CachedEdgePolicy{
|
||||
createHop(100, 1000, 1000000, 10),
|
||||
},
|
||||
metadata: []byte{1, 2, 3},
|
||||
expectedFees: []lnwire.MilliSatoshi{0},
|
||||
expectedTimeLocks: []uint32{1},
|
||||
expectedTotalAmount: 100000,
|
||||
@ -1561,6 +1609,12 @@ func TestNewRoute(t *testing.T) {
|
||||
" but got: %v instead",
|
||||
testCase.expectedMPP, finalHop.MPP)
|
||||
}
|
||||
|
||||
if !bytes.Equal(finalHop.Metadata, testCase.metadata) {
|
||||
t.Errorf("Expected final metadata field: %v, "+
|
||||
" but got: %v instead",
|
||||
testCase.metadata, finalHop.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
@ -1572,6 +1626,7 @@ func TestNewRoute(t *testing.T) {
|
||||
cltvDelta: finalHopCLTV,
|
||||
records: nil,
|
||||
paymentAddr: testCase.paymentAddr,
|
||||
metadata: testCase.metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -257,6 +257,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
DestCustomRecords: p.payment.DestCustomRecords,
|
||||
DestFeatures: p.payment.DestFeatures,
|
||||
PaymentAddr: p.payment.PaymentAddr,
|
||||
Metadata: p.payment.Metadata,
|
||||
}
|
||||
|
||||
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
|
||||
@ -388,6 +389,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
cltvDelta: finalCltvDelta,
|
||||
records: p.payment.DestCustomRecords,
|
||||
paymentAddr: p.payment.PaymentAddr,
|
||||
metadata: p.payment.Metadata,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -127,6 +127,10 @@ type Hop struct {
|
||||
// understand the new TLV payload, so we must instead use the legacy
|
||||
// payload.
|
||||
LegacyPayload bool
|
||||
|
||||
// Metadata is additional data that is sent along with the payment to
|
||||
// the payee.
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the Hop.
|
||||
@ -205,6 +209,13 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If metadata is specified, generate a tlv record for it.
|
||||
if h.Metadata != nil {
|
||||
records = append(records,
|
||||
record.NewMetadataRecord(&h.Metadata),
|
||||
)
|
||||
}
|
||||
|
||||
// Append any custom types destined for this hop.
|
||||
tlvRecords := tlv.MapToRecords(h.CustomRecords)
|
||||
records = append(records, tlvRecords...)
|
||||
@ -259,6 +270,11 @@ func (h *Hop) PayloadSize(nextChanID uint64) uint64 {
|
||||
addRecord(record.AMPOnionType, h.AMP.PayloadSize())
|
||||
}
|
||||
|
||||
// Add metadata if present.
|
||||
if h.Metadata != nil {
|
||||
addRecord(record.MetadataOnionType, uint64(len(h.Metadata)))
|
||||
}
|
||||
|
||||
// Add custom records.
|
||||
for k, v := range h.CustomRecords {
|
||||
addRecord(tlv.Type(k), uint64(len(v)))
|
||||
|
@ -186,6 +186,7 @@ func TestPayloadSize(t *testing.T) {
|
||||
100000: {1, 2, 3},
|
||||
1000000: {4, 5},
|
||||
},
|
||||
Metadata: []byte{10, 11},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1971,6 +1971,10 @@ type LightningPayment struct {
|
||||
// optimize for fees only, to 1 to optimize for reliability only or a
|
||||
// value in between for a mix.
|
||||
TimePref float64
|
||||
|
||||
// Metadata is additional data that is sent along with the payment to
|
||||
// the payee.
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
// AMPOptions houses information that must be known in order to send an AMP
|
||||
|
@ -4602,6 +4602,7 @@ type rpcPaymentIntent struct {
|
||||
destFeatures *lnwire.FeatureVector
|
||||
paymentAddr *[32]byte
|
||||
payReq []byte
|
||||
metadata []byte
|
||||
|
||||
destCustomRecords record.CustomSet
|
||||
|
||||
@ -4735,6 +4736,7 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
|
||||
payIntent.payReq = []byte(rpcPayReq.PaymentRequest)
|
||||
payIntent.destFeatures = payReq.Features
|
||||
payIntent.paymentAddr = payReq.PaymentAddr
|
||||
payIntent.metadata = payReq.Metadata
|
||||
|
||||
if err := validateDest(payIntent.dest); err != nil {
|
||||
return payIntent, err
|
||||
@ -4889,6 +4891,7 @@ func (r *rpcServer) dispatchPaymentIntent(
|
||||
DestCustomRecords: payIntent.destCustomRecords,
|
||||
DestFeatures: payIntent.destFeatures,
|
||||
PaymentAddr: payIntent.paymentAddr,
|
||||
Metadata: payIntent.metadata,
|
||||
|
||||
// Don't enable multi-part payments on the main rpc.
|
||||
// Users need to use routerrpc for that.
|
||||
|
Loading…
Reference in New Issue
Block a user