Merge pull request #5810 from bottlepay/payment-metadata
routing: send payment metadata
This commit is contained in:
27 files changed
+1730
-1420
No files matched your search
@@ -1141,6 +1141,10 @@ func serializeHop(w io.Writer, h *route.Hop) error {
|
|||||||
records = append(records, h.MPP.Record())
|
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
|
// Final sanity check to absolutely rule out custom records that are not
|
||||||
// custom and write into the standard range.
|
// custom and write into the standard range.
|
||||||
if err := h.CustomRecords.Validate(); err != nil {
|
if err := h.CustomRecords.Validate(); err != nil {
|
||||||
@@ -1255,6 +1259,13 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
|
|||||||
h.MPP = mpp
|
h.MPP = mpp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadataType := uint64(record.MetadataOnionType)
|
||||||
|
if metadata, ok := tlvMap[metadataType]; ok {
|
||||||
|
delete(tlvMap, metadataType)
|
||||||
|
|
||||||
|
h.Metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
h.CustomRecords = tlvMap
|
h.CustomRecords = tlvMap
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ var (
|
|||||||
65536: []byte{},
|
65536: []byte{},
|
||||||
80001: []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{
|
testHop2 = &route.Hop{
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Release Notes
|
# 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
|
## Security
|
||||||
|
|
||||||
* [Misconfigured ZMQ
|
* [Misconfigured ZMQ
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ type Payload struct {
|
|||||||
// customRecords are user-defined records in the custom type range that
|
// customRecords are user-defined records in the custom type range that
|
||||||
// were included in the payload.
|
// were included in the payload.
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
|
|
||||||
|
// metadata is additional data that is sent along with the payment to
|
||||||
|
// the payee.
|
||||||
|
metadata []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
|
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
|
||||||
@@ -115,11 +119,12 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload {
|
|||||||
// should correspond to the bytes encapsulated in a TLV onion payload.
|
// should correspond to the bytes encapsulated in a TLV onion payload.
|
||||||
func NewPayloadFromReader(r io.Reader) (*Payload, error) {
|
func NewPayloadFromReader(r io.Reader) (*Payload, error) {
|
||||||
var (
|
var (
|
||||||
cid uint64
|
cid uint64
|
||||||
amt uint64
|
amt uint64
|
||||||
cltv uint32
|
cltv uint32
|
||||||
mpp = &record.MPP{}
|
mpp = &record.MPP{}
|
||||||
amp = &record.AMP{}
|
amp = &record.AMP{}
|
||||||
|
metadata []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
tlvStream, err := tlv.NewStream(
|
tlvStream, err := tlv.NewStream(
|
||||||
@@ -128,6 +133,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
|
|||||||
record.NewNextHopIDRecord(&cid),
|
record.NewNextHopIDRecord(&cid),
|
||||||
mpp.Record(),
|
mpp.Record(),
|
||||||
amp.Record(),
|
amp.Record(),
|
||||||
|
record.NewMetadataRecord(&metadata),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -168,6 +174,12 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
|
|||||||
amp = nil
|
amp = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no metadata field was parsed, set the metadata field on the
|
||||||
|
// resulting payload to nil.
|
||||||
|
if _, ok := parsedTypes[record.MetadataOnionType]; !ok {
|
||||||
|
metadata = nil
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out the custom records.
|
// Filter out the custom records.
|
||||||
customRecords := NewCustomRecords(parsedTypes)
|
customRecords := NewCustomRecords(parsedTypes)
|
||||||
|
|
||||||
@@ -180,6 +192,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
|
|||||||
},
|
},
|
||||||
MPP: mpp,
|
MPP: mpp,
|
||||||
AMP: amp,
|
AMP: amp,
|
||||||
|
metadata: metadata,
|
||||||
customRecords: customRecords,
|
customRecords: customRecords,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -284,6 +297,12 @@ func (h *Payload) CustomRecords() record.CustomSet {
|
|||||||
return h.customRecords
|
return h.customRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata returns the additional data that is sent along with the
|
||||||
|
// payment to the payee.
|
||||||
|
func (h *Payload) Metadata() []byte {
|
||||||
|
return h.metadata
|
||||||
|
}
|
||||||
|
|
||||||
// getMinRequiredViolation checks for unrecognized required (even) fields in the
|
// getMinRequiredViolation checks for unrecognized required (even) fields in the
|
||||||
// standard range and returns the lowest required type. Always returning the
|
// standard range and returns the lowest required type. Always returning the
|
||||||
// lowest required type allows a failure message to be deterministic.
|
// lowest required type allows a failure message to be deterministic.
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testUnknownRequiredType = 0x10
|
const testUnknownRequiredType = 0x80
|
||||||
|
|
||||||
type decodePayloadTest struct {
|
type decodePayloadTest struct {
|
||||||
name string
|
name string
|
||||||
payload []byte
|
payload []byte
|
||||||
expErr error
|
expErr error
|
||||||
expCustomRecords map[uint64][]byte
|
expCustomRecords map[uint64][]byte
|
||||||
shouldHaveMPP bool
|
shouldHaveMPP bool
|
||||||
shouldHaveAMP bool
|
shouldHaveAMP bool
|
||||||
|
shouldHaveMetadata bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var decodePayloadTests = []decodePayloadTest{
|
var decodePayloadTests = []decodePayloadTest{
|
||||||
@@ -258,6 +259,18 @@ var decodePayloadTests = []decodePayloadTest{
|
|||||||
},
|
},
|
||||||
shouldHaveAMP: true,
|
shouldHaveAMP: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "final hop with metadata",
|
||||||
|
payload: []byte{
|
||||||
|
// amount
|
||||||
|
0x02, 0x00,
|
||||||
|
// cltv
|
||||||
|
0x04, 0x00,
|
||||||
|
// metadata
|
||||||
|
0x10, 0x03, 0x01, 0x02, 0x03,
|
||||||
|
},
|
||||||
|
shouldHaveMetadata: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
|
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
|
||||||
@@ -293,6 +306,7 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
|
|||||||
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
||||||
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
||||||
}
|
}
|
||||||
|
testMetadata = []byte{1, 2, 3}
|
||||||
testChildIndex = uint32(9)
|
testChildIndex = uint32(9)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -331,6 +345,15 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
|
|||||||
t.Fatalf("unexpected AMP payload")
|
t.Fatalf("unexpected AMP payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if test.shouldHaveMetadata {
|
||||||
|
if p.Metadata() == nil {
|
||||||
|
t.Fatalf("payload should have metadata")
|
||||||
|
}
|
||||||
|
require.Equal(t, testMetadata, p.Metadata())
|
||||||
|
} else if p.Metadata() != nil {
|
||||||
|
t.Fatalf("unexpected metadata")
|
||||||
|
}
|
||||||
|
|
||||||
// Convert expected nil map to empty map, because we always expect an
|
// Convert expected nil map to empty map, because we always expect an
|
||||||
// initiated map from the payload.
|
// initiated map from the payload.
|
||||||
expCustomRecords := make(record.CustomSet)
|
expCustomRecords := make(record.CustomSet)
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ type Payload interface {
|
|||||||
// CustomRecords returns the custom tlv type records that were parsed
|
// CustomRecords returns the custom tlv type records that were parsed
|
||||||
// from the payload.
|
// from the payload.
|
||||||
CustomRecords() record.CustomSet
|
CustomRecords() record.CustomSet
|
||||||
|
|
||||||
|
// Metadata returns the additional data that is sent along with the
|
||||||
|
// payment to the payee.
|
||||||
|
Metadata() []byte
|
||||||
}
|
}
|
||||||
@@ -906,6 +906,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
customRecords: payload.CustomRecords(),
|
customRecords: payload.CustomRecords(),
|
||||||
mpp: payload.MultiPath(),
|
mpp: payload.MultiPath(),
|
||||||
amp: payload.AMPRecord(),
|
amp: payload.AMPRecord(),
|
||||||
|
metadata: payload.Metadata(),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type mockPayload struct {
|
|||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
amp *record.AMP
|
amp *record.AMP
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
|
metadata []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *mockPayload) MultiPath() *record.MPP {
|
func (p *mockPayload) MultiPath() *record.MPP {
|
||||||
@@ -50,6 +51,10 @@ func (p *mockPayload) CustomRecords() record.CustomSet {
|
|||||||
return p.customRecords
|
return p.customRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *mockPayload) Metadata() []byte {
|
||||||
|
return p.metadata
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testHtlcExpiry = uint32(5)
|
testHtlcExpiry = uint32(5)
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -1,6 +1,7 @@
|
|||||||
package invoices
|
package invoices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/amp"
|
"github.com/lightningnetwork/lnd/amp"
|
||||||
@@ -22,6 +23,7 @@ type invoiceUpdateCtx struct {
|
|||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
amp *record.AMP
|
amp *record.AMP
|
||||||
|
metadata []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceRef returns an identifier that can be used to lookup or update the
|
// invoiceRef returns an identifier that can be used to lookup or update the
|
||||||
@@ -52,9 +54,16 @@ func (i invoiceUpdateCtx) setID() *[32]byte {
|
|||||||
|
|
||||||
// log logs a message specific to this update context.
|
// log logs a message specific to this update context.
|
||||||
func (i *invoiceUpdateCtx) log(s string) {
|
func (i *invoiceUpdateCtx) log(s string) {
|
||||||
|
// Don't use %x in the log statement below, because it doesn't
|
||||||
|
// distinguish between nil and empty metadata.
|
||||||
|
metadata := "<nil>"
|
||||||
|
if i.metadata != nil {
|
||||||
|
metadata = hex.EncodeToString(i.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v, "+
|
log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v, "+
|
||||||
"amp=%v", i.invoiceRef(), s, i.amtPaid, i.expiry, i.circuitKey,
|
"amp=%v, metadata=%v", i.invoiceRef(), s, i.amtPaid, i.expiry,
|
||||||
i.mpp, i.amp)
|
i.circuitKey, i.mpp, i.amp, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// failRes is a helper function which creates a failure resolution with
|
// failRes is a helper function which creates a failure resolution with
|
||||||
|
|||||||
+1410
-1399
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.
|
to drop off at each hop within the onion.
|
||||||
*/
|
*/
|
||||||
map<uint64, bytes> custom_records = 11;
|
map<uint64, bytes> custom_records = 11;
|
||||||
|
|
||||||
|
// The payment metadata to send along with the payment to the payee.
|
||||||
|
bytes metadata = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MPPRecord {
|
message MPPRecord {
|
||||||
|
|||||||
@@ -4616,6 +4616,11 @@
|
|||||||
"format": "byte"
|
"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."
|
"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"
|
"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."
|
"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,
|
CustomRecords: hop.CustomRecords,
|
||||||
TlvPayload: !hop.LegacyPayload,
|
TlvPayload: !hop.LegacyPayload,
|
||||||
MppRecord: mpp,
|
MppRecord: mpp,
|
||||||
|
Metadata: hop.Metadata,
|
||||||
}
|
}
|
||||||
incomingAmt = hop.AmtToForward
|
incomingAmt = hop.AmtToForward
|
||||||
}
|
}
|
||||||
@@ -766,6 +767,7 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
|||||||
payIntent.DestFeatures = payReq.Features
|
payIntent.DestFeatures = payReq.Features
|
||||||
payIntent.PaymentAddr = payAddr
|
payIntent.PaymentAddr = payAddr
|
||||||
payIntent.PaymentRequest = []byte(rpcPayReq.PaymentRequest)
|
payIntent.PaymentRequest = []byte(rpcPayReq.PaymentRequest)
|
||||||
|
payIntent.Metadata = payReq.Metadata
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, If the payment request field was not specified
|
// Otherwise, If the payment request field was not specified
|
||||||
// (and a custom route wasn't specified), construct the payment
|
// (and a custom route wasn't specified), construct the payment
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ const (
|
|||||||
// TODO: Decide on actual feature bit value.
|
// TODO: Decide on actual feature bit value.
|
||||||
ExplicitChannelTypeOptional = 45
|
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
|
// ScriptEnforcedLeaseOptional is an optional feature bit that signals
|
||||||
// that the node requires channels having zero-fee second-level HTLC
|
// that the node requires channels having zero-fee second-level HTLC
|
||||||
// transactions, which also imply anchor commitments, along with an
|
// transactions, which also imply anchor commitments, along with an
|
||||||
@@ -218,6 +228,8 @@ var Features = map[FeatureBit]string{
|
|||||||
WumboChannelsOptional: "wumbo-channels",
|
WumboChannelsOptional: "wumbo-channels",
|
||||||
AMPRequired: "amp",
|
AMPRequired: "amp",
|
||||||
AMPOptional: "amp",
|
AMPOptional: "amp",
|
||||||
|
PaymentMetadataOptional: "payment-metadata",
|
||||||
|
PaymentMetadataRequired: "payment-metadata",
|
||||||
ExplicitChannelTypeOptional: "explicit-commitment-type",
|
ExplicitChannelTypeOptional: "explicit-commitment-type",
|
||||||
ExplicitChannelTypeRequired: "explicit-commitment-type",
|
ExplicitChannelTypeRequired: "explicit-commitment-type",
|
||||||
ScriptEnforcedLeaseRequired: "script-enforced-lease",
|
ScriptEnforcedLeaseRequired: "script-enforced-lease",
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ const (
|
|||||||
// NextHopOnionType is the type used in the onion to reference the ID
|
// NextHopOnionType is the type used in the onion to reference the ID
|
||||||
// of the next hop.
|
// of the next hop.
|
||||||
NextHopOnionType tlv.Type = 6
|
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
|
// 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 {
|
func NewNextHopIDRecord(cid *uint64) tlv.Record {
|
||||||
return tlv.MakePrimitiveRecord(NextHopOnionType, cid)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
-1
@@ -89,6 +89,10 @@ type finalHopParams struct {
|
|||||||
cltvDelta uint16
|
cltvDelta uint16
|
||||||
records record.CustomSet
|
records record.CustomSet
|
||||||
paymentAddr *[32]byte
|
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.
|
// newRoute constructs a route using the provided path and final hop constraints.
|
||||||
@@ -138,6 +142,7 @@ func newRoute(sourceVertex route.Vertex,
|
|||||||
tlvPayload bool
|
tlvPayload bool
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
|
metadata []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define a helper function that checks this edge's feature
|
// Define a helper function that checks this edge's feature
|
||||||
@@ -202,6 +207,8 @@ func newRoute(sourceVertex route.Vertex,
|
|||||||
*finalHop.paymentAddr,
|
*finalHop.paymentAddr,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata = finalHop.metadata
|
||||||
} else {
|
} else {
|
||||||
// The amount that the current hop needs to forward is
|
// The amount that the current hop needs to forward is
|
||||||
// equal to the incoming amount of the next hop.
|
// equal to the incoming amount of the next hop.
|
||||||
@@ -232,6 +239,7 @@ func newRoute(sourceVertex route.Vertex,
|
|||||||
LegacyPayload: !tlvPayload,
|
LegacyPayload: !tlvPayload,
|
||||||
CustomRecords: customRecords,
|
CustomRecords: customRecords,
|
||||||
MPP: mpp,
|
MPP: mpp,
|
||||||
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
hops = append([]*route.Hop{currentHop}, hops...)
|
hops = append([]*route.Hop{currentHop}, hops...)
|
||||||
@@ -330,6 +338,10 @@ type RestrictParams struct {
|
|||||||
// mitigate probing vectors and payment sniping attacks on overpaid
|
// mitigate probing vectors and payment sniping attacks on overpaid
|
||||||
// invoices.
|
// invoices.
|
||||||
PaymentAddr *[32]byte
|
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
|
// 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
|
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.
|
// Set up outgoing channel map for quicker access.
|
||||||
var outgoingChanMap map[uint64]struct{}
|
var outgoingChanMap map[uint64]struct{}
|
||||||
if len(r.OutgoingChannelIDs) > 0 {
|
if len(r.OutgoingChannelIDs) > 0 {
|
||||||
@@ -547,7 +567,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
LegacyPayload: !features.HasFeature(
|
LegacyPayload: !features.HasFeature(
|
||||||
lnwire.TLVOnionPayloadOptional,
|
lnwire.TLVOnionPayloadOptional,
|
||||||
),
|
),
|
||||||
MPP: mpp,
|
MPP: mpp,
|
||||||
|
Metadata: r.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't always assume that the end destination is publicly
|
// 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/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -840,6 +841,9 @@ func TestPathFinding(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
name: "route to self",
|
name: "route to self",
|
||||||
fn: runRouteToSelf,
|
fn: runRouteToSelf,
|
||||||
|
}, {
|
||||||
|
name: "with metadata",
|
||||||
|
fn: runFindPathWithMetadata,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Run with graph cache enabled.
|
// 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
|
// runFindLowestFeePath tests that out of two routes with identical total
|
||||||
// time lock values, the route with the lowest total fee should be returned.
|
// 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
|
// 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
|
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
|
// expectedFees is a list of fees that every hop is expected
|
||||||
// to charge for forwarding.
|
// to charge for forwarding.
|
||||||
expectedFees []lnwire.MilliSatoshi
|
expectedFees []lnwire.MilliSatoshi
|
||||||
@@ -1380,6 +1427,7 @@ func TestNewRoute(t *testing.T) {
|
|||||||
hops: []*channeldb.CachedEdgePolicy{
|
hops: []*channeldb.CachedEdgePolicy{
|
||||||
createHop(100, 1000, 1000000, 10),
|
createHop(100, 1000, 1000000, 10),
|
||||||
},
|
},
|
||||||
|
metadata: []byte{1, 2, 3},
|
||||||
expectedFees: []lnwire.MilliSatoshi{0},
|
expectedFees: []lnwire.MilliSatoshi{0},
|
||||||
expectedTimeLocks: []uint32{1},
|
expectedTimeLocks: []uint32{1},
|
||||||
expectedTotalAmount: 100000,
|
expectedTotalAmount: 100000,
|
||||||
@@ -1561,6 +1609,12 @@ func TestNewRoute(t *testing.T) {
|
|||||||
" but got: %v instead",
|
" but got: %v instead",
|
||||||
testCase.expectedMPP, finalHop.MPP)
|
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) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
@@ -1572,6 +1626,7 @@ func TestNewRoute(t *testing.T) {
|
|||||||
cltvDelta: finalHopCLTV,
|
cltvDelta: finalHopCLTV,
|
||||||
records: nil,
|
records: nil,
|
||||||
paymentAddr: testCase.paymentAddr,
|
paymentAddr: testCase.paymentAddr,
|
||||||
|
metadata: testCase.metadata,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
DestCustomRecords: p.payment.DestCustomRecords,
|
DestCustomRecords: p.payment.DestCustomRecords,
|
||||||
DestFeatures: p.payment.DestFeatures,
|
DestFeatures: p.payment.DestFeatures,
|
||||||
PaymentAddr: p.payment.PaymentAddr,
|
PaymentAddr: p.payment.PaymentAddr,
|
||||||
|
Metadata: p.payment.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
|
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
|
||||||
@@ -388,6 +389,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
cltvDelta: finalCltvDelta,
|
cltvDelta: finalCltvDelta,
|
||||||
records: p.payment.DestCustomRecords,
|
records: p.payment.DestCustomRecords,
|
||||||
paymentAddr: p.payment.PaymentAddr,
|
paymentAddr: p.payment.PaymentAddr,
|
||||||
|
metadata: p.payment.Metadata,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ type Hop struct {
|
|||||||
// understand the new TLV payload, so we must instead use the legacy
|
// understand the new TLV payload, so we must instead use the legacy
|
||||||
// payload.
|
// payload.
|
||||||
LegacyPayload bool
|
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.
|
// 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.
|
// Append any custom types destined for this hop.
|
||||||
tlvRecords := tlv.MapToRecords(h.CustomRecords)
|
tlvRecords := tlv.MapToRecords(h.CustomRecords)
|
||||||
records = append(records, tlvRecords...)
|
records = append(records, tlvRecords...)
|
||||||
@@ -259,6 +270,11 @@ func (h *Hop) PayloadSize(nextChanID uint64) uint64 {
|
|||||||
addRecord(record.AMPOnionType, h.AMP.PayloadSize())
|
addRecord(record.AMPOnionType, h.AMP.PayloadSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add metadata if present.
|
||||||
|
if h.Metadata != nil {
|
||||||
|
addRecord(record.MetadataOnionType, uint64(len(h.Metadata)))
|
||||||
|
}
|
||||||
|
|
||||||
// Add custom records.
|
// Add custom records.
|
||||||
for k, v := range h.CustomRecords {
|
for k, v := range h.CustomRecords {
|
||||||
addRecord(tlv.Type(k), uint64(len(v)))
|
addRecord(tlv.Type(k), uint64(len(v)))
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ func TestPayloadSize(t *testing.T) {
|
|||||||
100000: {1, 2, 3},
|
100000: {1, 2, 3},
|
||||||
1000000: {4, 5},
|
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
|
// optimize for fees only, to 1 to optimize for reliability only or a
|
||||||
// value in between for a mix.
|
// value in between for a mix.
|
||||||
TimePref float64
|
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
|
// AMPOptions houses information that must be known in order to send an AMP
|
||||||
|
|||||||
@@ -4602,6 +4602,7 @@ type rpcPaymentIntent struct {
|
|||||||
destFeatures *lnwire.FeatureVector
|
destFeatures *lnwire.FeatureVector
|
||||||
paymentAddr *[32]byte
|
paymentAddr *[32]byte
|
||||||
payReq []byte
|
payReq []byte
|
||||||
|
metadata []byte
|
||||||
|
|
||||||
destCustomRecords record.CustomSet
|
destCustomRecords record.CustomSet
|
||||||
|
|
||||||
@@ -4735,6 +4736,7 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
|
|||||||
payIntent.payReq = []byte(rpcPayReq.PaymentRequest)
|
payIntent.payReq = []byte(rpcPayReq.PaymentRequest)
|
||||||
payIntent.destFeatures = payReq.Features
|
payIntent.destFeatures = payReq.Features
|
||||||
payIntent.paymentAddr = payReq.PaymentAddr
|
payIntent.paymentAddr = payReq.PaymentAddr
|
||||||
|
payIntent.metadata = payReq.Metadata
|
||||||
|
|
||||||
if err := validateDest(payIntent.dest); err != nil {
|
if err := validateDest(payIntent.dest); err != nil {
|
||||||
return payIntent, err
|
return payIntent, err
|
||||||
@@ -4889,6 +4891,7 @@ func (r *rpcServer) dispatchPaymentIntent(
|
|||||||
DestCustomRecords: payIntent.destCustomRecords,
|
DestCustomRecords: payIntent.destCustomRecords,
|
||||||
DestFeatures: payIntent.destFeatures,
|
DestFeatures: payIntent.destFeatures,
|
||||||
PaymentAddr: payIntent.paymentAddr,
|
PaymentAddr: payIntent.paymentAddr,
|
||||||
|
Metadata: payIntent.metadata,
|
||||||
|
|
||||||
// Don't enable multi-part payments on the main rpc.
|
// Don't enable multi-part payments on the main rpc.
|
||||||
// Users need to use routerrpc for that.
|
// Users need to use routerrpc for that.
|
||||||
|
|||||||
@@ -229,6 +229,15 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.Description, err = parseDescription(base32Data)
|
invoice.Description, err = parseDescription(base32Data)
|
||||||
|
case fieldTypeM:
|
||||||
|
if invoice.Metadata != nil {
|
||||||
|
// We skip the field if we have already seen a
|
||||||
|
// supported one.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.Metadata, err = parseMetadata(base32Data)
|
||||||
|
|
||||||
case fieldTypeN:
|
case fieldTypeN:
|
||||||
if invoice.Destination != nil {
|
if invoice.Destination != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -345,6 +354,12 @@ func parseDescription(data []byte) (*string, error) {
|
|||||||
return &description, nil
|
return &description, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseMetadata converts the data (encoded in base32) into a byte slice to use
|
||||||
|
// as the metadata.
|
||||||
|
func parseMetadata(data []byte) ([]byte, error) {
|
||||||
|
return bech32.ConvertBits(data, 5, 8, false)
|
||||||
|
}
|
||||||
|
|
||||||
// parseDestination converts the data (encoded in base32) into a 33-byte public
|
// parseDestination converts the data (encoded in base32) into a 33-byte public
|
||||||
// key of the payee node.
|
// key of the payee node.
|
||||||
func parseDestination(data []byte) (*btcec.PublicKey, error) {
|
func parseDestination(data []byte) (*btcec.PublicKey, error) {
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if invoice.Metadata != nil {
|
||||||
|
base32, err := bech32.ConvertBits(invoice.Metadata, 8, 5, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = writeTaggedField(bufferBase32, fieldTypeM, base32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if invoice.minFinalCLTVExpiry != nil {
|
if invoice.minFinalCLTVExpiry != nil {
|
||||||
finalDelta := uint64ToBase32(*invoice.minFinalCLTVExpiry)
|
finalDelta := uint64ToBase32(*invoice.minFinalCLTVExpiry)
|
||||||
err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta)
|
err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta)
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ const (
|
|||||||
// fieldTypeD contains a short description of the payment.
|
// fieldTypeD contains a short description of the payment.
|
||||||
fieldTypeD = 13
|
fieldTypeD = 13
|
||||||
|
|
||||||
|
// fieldTypeM contains the payment metadata.
|
||||||
|
fieldTypeM = 27
|
||||||
|
|
||||||
// fieldTypeN contains the pubkey of the target node.
|
// fieldTypeN contains the pubkey of the target node.
|
||||||
fieldTypeN = 19
|
fieldTypeN = 19
|
||||||
|
|
||||||
@@ -183,6 +186,10 @@ type Invoice struct {
|
|||||||
// Features represents an optional field used to signal optional or
|
// Features represents an optional field used to signal optional or
|
||||||
// required support for features by the receiver.
|
// required support for features by the receiver.
|
||||||
Features *lnwire.FeatureVector
|
Features *lnwire.FeatureVector
|
||||||
|
|
||||||
|
// Metadata is additional data that is sent along with the payment to
|
||||||
|
// the payee.
|
||||||
|
Metadata []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Amount is a functional option that allows callers of NewInvoice to set the
|
// Amount is a functional option that allows callers of NewInvoice to set the
|
||||||
@@ -273,6 +280,14 @@ func PaymentAddr(addr [32]byte) func(*Invoice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata is a functional option that allows callers of NewInvoice to set
|
||||||
|
// the desired payment Metadata tht is advertised on the invoice.
|
||||||
|
func Metadata(metadata []byte) func(*Invoice) {
|
||||||
|
return func(i *Invoice) {
|
||||||
|
i.Metadata = metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewInvoice creates a new Invoice object. The last parameter is a set of
|
// NewInvoice creates a new Invoice object. The last parameter is a set of
|
||||||
// variadic arguments for setting optional fields of the invoice.
|
// variadic arguments for setting optional fields of the invoice.
|
||||||
//
|
//
|
||||||
|
|||||||
+34
-5
@@ -27,6 +27,7 @@ var (
|
|||||||
testMillisat2500uBTC = lnwire.MilliSatoshi(250000000)
|
testMillisat2500uBTC = lnwire.MilliSatoshi(250000000)
|
||||||
testMillisat25mBTC = lnwire.MilliSatoshi(2500000000)
|
testMillisat25mBTC = lnwire.MilliSatoshi(2500000000)
|
||||||
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
||||||
|
testMillisat10mBTC = lnwire.MilliSatoshi(1000000000)
|
||||||
|
|
||||||
testPaymentHash = [32]byte{
|
testPaymentHash = [32]byte{
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
@@ -49,11 +50,12 @@ var (
|
|||||||
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||||
}
|
}
|
||||||
|
|
||||||
testEmptyString = ""
|
testEmptyString = ""
|
||||||
testCupOfCoffee = "1 cup coffee"
|
testCupOfCoffee = "1 cup coffee"
|
||||||
testCoffeeBeans = "coffee beans"
|
testCoffeeBeans = "coffee beans"
|
||||||
testCupOfNonsense = "ナンセンス 1杯"
|
testCupOfNonsense = "ナンセンス 1杯"
|
||||||
testPleaseConsider = "Please consider supporting this project"
|
testPleaseConsider = "Please consider supporting this project"
|
||||||
|
testPaymentMetadata = "payment metadata inside"
|
||||||
|
|
||||||
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
||||||
testPrivKey, testPubKey = btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
testPrivKey, testPubKey = btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
||||||
@@ -692,6 +694,33 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Please send 0.01 BTC with payment metadata 0x01fafaf0.
|
||||||
|
encodedInvoice: "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc",
|
||||||
|
valid: true,
|
||||||
|
decodedInvoice: func() *Invoice {
|
||||||
|
return &Invoice{
|
||||||
|
Net: &chaincfg.MainNetParams,
|
||||||
|
MilliSat: &testMillisat10mBTC,
|
||||||
|
Timestamp: time.Unix(1496314658, 0),
|
||||||
|
PaymentHash: &testPaymentHash,
|
||||||
|
Description: &testPaymentMetadata,
|
||||||
|
Destination: testPubKey,
|
||||||
|
PaymentAddr: &specPaymentAddr,
|
||||||
|
Features: lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(8, 14, 48),
|
||||||
|
lnwire.Features,
|
||||||
|
),
|
||||||
|
Metadata: []byte{0x01, 0xfa, 0xfa, 0xf0},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeEncoding: func(i *Invoice) {
|
||||||
|
// Since this destination pubkey was recovered
|
||||||
|
// from the signature, we must set it nil before
|
||||||
|
// encoding to get back the same invoice string.
|
||||||
|
i.Destination = nil
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|||||||
Reference in new issue
Block a user