mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-11-10 14:17:56 +01:00
zpay: encoding and decoding of a BlindedPaymentPath
In this commit, the ability is added to encode blinded payment paths and add them to a Bolt 11 invoice.
This commit is contained in:
246
zpay32/blinded_path.go
Normal file
246
zpay32/blinded_path.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package zpay32
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// relayInfoSize is the number of bytes that the relay info of a blinded
|
||||||
|
// payment will occupy.
|
||||||
|
// base fee: 4 bytes
|
||||||
|
// prop fee: 4 bytes
|
||||||
|
// cltv delta: 2 bytes
|
||||||
|
// min htlc: 8 bytes
|
||||||
|
// max htlc: 8 bytes
|
||||||
|
relayInfoSize = 26
|
||||||
|
|
||||||
|
// maxNumHopsPerPath is the maximum number of blinded path hops that can
|
||||||
|
// be included in a single encoded blinded path. This is calculated
|
||||||
|
// based on the `data_length` limit of 638 bytes for any tagged field in
|
||||||
|
// a BOLT 11 invoice along with the estimated number of bytes required
|
||||||
|
// for encoding the most minimal blinded path hop. See the [bLIP
|
||||||
|
// proposal](https://github.com/lightning/blips/pull/39) for a detailed
|
||||||
|
// calculation.
|
||||||
|
maxNumHopsPerPath = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlindedPaymentPath holds all the information a payer needs to know about a
|
||||||
|
// blinded path to a receiver of a payment.
|
||||||
|
type BlindedPaymentPath struct {
|
||||||
|
// FeeBaseMsat is the total base fee for the path in milli-satoshis.
|
||||||
|
FeeBaseMsat uint32
|
||||||
|
|
||||||
|
// FeeRate is the total fee rate for the path in parts per million.
|
||||||
|
FeeRate uint32
|
||||||
|
|
||||||
|
// CltvExpiryDelta is the total CLTV delta to apply to the path.
|
||||||
|
CltvExpiryDelta uint16
|
||||||
|
|
||||||
|
// HTLCMinMsat is the minimum number of milli-satoshis that any hop in
|
||||||
|
// the path will route.
|
||||||
|
HTLCMinMsat uint64
|
||||||
|
|
||||||
|
// HTLCMaxMsat is the maximum number of milli-satoshis that a hop in the
|
||||||
|
// path will route.
|
||||||
|
HTLCMaxMsat uint64
|
||||||
|
|
||||||
|
// Features is the feature bit vector for the path.
|
||||||
|
Features *lnwire.FeatureVector
|
||||||
|
|
||||||
|
// FirstEphemeralBlindingPoint is the blinding point to send to the
|
||||||
|
// introduction node. It will be used by the introduction node to derive
|
||||||
|
// a shared secret with the receiver which can then be used to decode
|
||||||
|
// the encrypted payload from the receiver.
|
||||||
|
FirstEphemeralBlindingPoint *btcec.PublicKey
|
||||||
|
|
||||||
|
// Hops is the blinded path. The first hop is the introduction node and
|
||||||
|
// so the BlindedNodeID of this hop will be the real node ID.
|
||||||
|
Hops []*sphinx.BlindedHopInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBlindedPayment attempts to parse a BlindedPaymentPath from the passed
|
||||||
|
// reader.
|
||||||
|
func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) {
|
||||||
|
var relayInfo [relayInfoSize]byte
|
||||||
|
n, err := r.Read(relayInfo[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n != relayInfoSize {
|
||||||
|
return nil, fmt.Errorf("unable to read %d relay info bytes "+
|
||||||
|
"off of the given stream: %w", relayInfoSize, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment BlindedPaymentPath
|
||||||
|
|
||||||
|
// Parse the relay info fields.
|
||||||
|
payment.FeeBaseMsat = binary.BigEndian.Uint32(relayInfo[:4])
|
||||||
|
payment.FeeRate = binary.BigEndian.Uint32(relayInfo[4:8])
|
||||||
|
payment.CltvExpiryDelta = binary.BigEndian.Uint16(relayInfo[8:10])
|
||||||
|
payment.HTLCMinMsat = binary.BigEndian.Uint64(relayInfo[10:18])
|
||||||
|
payment.HTLCMaxMsat = binary.BigEndian.Uint64(relayInfo[18:])
|
||||||
|
|
||||||
|
// Parse the feature bit vector.
|
||||||
|
f := lnwire.EmptyFeatureVector()
|
||||||
|
err = f.Decode(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payment.Features = f
|
||||||
|
|
||||||
|
// Parse the first ephemeral blinding point.
|
||||||
|
var blindingPointBytes [btcec.PubKeyBytesLenCompressed]byte
|
||||||
|
_, err = r.Read(blindingPointBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blinding, err := btcec.ParsePubKey(blindingPointBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payment.FirstEphemeralBlindingPoint = blinding
|
||||||
|
|
||||||
|
// Read the one byte hop number.
|
||||||
|
var numHops [1]byte
|
||||||
|
_, err = r.Read(numHops[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.Hops = make([]*sphinx.BlindedHopInfo, int(numHops[0]))
|
||||||
|
|
||||||
|
// Parse each hop.
|
||||||
|
for i := 0; i < len(payment.Hops); i++ {
|
||||||
|
hop, err := DecodeBlindedHop(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.Hops[i] = hop
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode serialises the BlindedPaymentPath and writes the bytes to the passed
|
||||||
|
// writer.
|
||||||
|
// 1) The first 26 bytes contain the relay info:
|
||||||
|
// - Base Fee in msat: uint32 (4 bytes).
|
||||||
|
// - Proportional Fee in PPM: uint32 (4 bytes).
|
||||||
|
// - CLTV expiry delta: uint16 (2 bytes).
|
||||||
|
// - HTLC min msat: uint64 (8 bytes).
|
||||||
|
// - HTLC max msat: uint64 (8 bytes).
|
||||||
|
//
|
||||||
|
// 2) Feature bit vector length (2 bytes).
|
||||||
|
// 3) Feature bit vector (can be zero length).
|
||||||
|
// 4) First blinding point: 33 bytes.
|
||||||
|
// 5) Number of hops: 1 byte.
|
||||||
|
// 6) Encoded BlindedHops.
|
||||||
|
func (p *BlindedPaymentPath) Encode(w io.Writer) error {
|
||||||
|
var relayInfo [26]byte
|
||||||
|
binary.BigEndian.PutUint32(relayInfo[:4], p.FeeBaseMsat)
|
||||||
|
binary.BigEndian.PutUint32(relayInfo[4:8], p.FeeRate)
|
||||||
|
binary.BigEndian.PutUint16(relayInfo[8:10], p.CltvExpiryDelta)
|
||||||
|
binary.BigEndian.PutUint64(relayInfo[10:18], p.HTLCMinMsat)
|
||||||
|
binary.BigEndian.PutUint64(relayInfo[18:], p.HTLCMaxMsat)
|
||||||
|
|
||||||
|
_, err := w.Write(relayInfo[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Features.Encode(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
numHops := len(p.Hops)
|
||||||
|
if numHops > maxNumHopsPerPath {
|
||||||
|
return fmt.Errorf("the number of hops, %d, exceeds the "+
|
||||||
|
"maximum of %d", numHops, maxNumHopsPerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte{byte(numHops)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hop := range p.Hops {
|
||||||
|
err = EncodeBlindedHop(w, hop)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBlindedHop reads a sphinx.BlindedHopInfo from the passed reader.
|
||||||
|
func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) {
|
||||||
|
var nodeIDBytes [btcec.PubKeyBytesLenCompressed]byte
|
||||||
|
_, err := r.Read(nodeIDBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeID, err := btcec.ParsePubKey(nodeIDBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLen, err := tlv.ReadVarInt(r, &[8]byte{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData := make([]byte, dataLen)
|
||||||
|
_, err = r.Read(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sphinx.BlindedHopInfo{
|
||||||
|
BlindedNodePub: nodeID,
|
||||||
|
CipherText: encryptedData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBlindedHop writes the passed BlindedHopInfo to the given writer.
|
||||||
|
//
|
||||||
|
// 1) Blinded node pub key: 33 bytes
|
||||||
|
// 2) Cipher text length: BigSize
|
||||||
|
// 3) Cipher text.
|
||||||
|
func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error {
|
||||||
|
_, err := w.Write(hop.BlindedNodePub.SerializeCompressed())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hop.CipherText) > math.MaxUint16 {
|
||||||
|
return fmt.Errorf("encrypted recipient data can not exceed a "+
|
||||||
|
"length of %d bytes", math.MaxUint16)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(hop.CipherText)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -215,6 +215,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.PaymentHash, err = parse32Bytes(base32Data)
|
invoice.PaymentHash, err = parse32Bytes(base32Data)
|
||||||
|
|
||||||
case fieldTypeS:
|
case fieldTypeS:
|
||||||
if invoice.PaymentAddr != nil {
|
if invoice.PaymentAddr != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -223,6 +224,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.PaymentAddr, err = parse32Bytes(base32Data)
|
invoice.PaymentAddr, err = parse32Bytes(base32Data)
|
||||||
|
|
||||||
case fieldTypeD:
|
case fieldTypeD:
|
||||||
if invoice.Description != nil {
|
if invoice.Description != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -231,6 +233,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.Description, err = parseDescription(base32Data)
|
invoice.Description, err = parseDescription(base32Data)
|
||||||
|
|
||||||
case fieldTypeM:
|
case fieldTypeM:
|
||||||
if invoice.Metadata != nil {
|
if invoice.Metadata != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -248,6 +251,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.Destination, err = parseDestination(base32Data)
|
invoice.Destination, err = parseDestination(base32Data)
|
||||||
|
|
||||||
case fieldTypeH:
|
case fieldTypeH:
|
||||||
if invoice.DescriptionHash != nil {
|
if invoice.DescriptionHash != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -256,6 +260,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.DescriptionHash, err = parse32Bytes(base32Data)
|
invoice.DescriptionHash, err = parse32Bytes(base32Data)
|
||||||
|
|
||||||
case fieldTypeX:
|
case fieldTypeX:
|
||||||
if invoice.expiry != nil {
|
if invoice.expiry != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -264,6 +269,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.expiry, err = parseExpiry(base32Data)
|
invoice.expiry, err = parseExpiry(base32Data)
|
||||||
|
|
||||||
case fieldTypeC:
|
case fieldTypeC:
|
||||||
if invoice.minFinalCLTVExpiry != nil {
|
if invoice.minFinalCLTVExpiry != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -271,7 +277,9 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data)
|
invoice.minFinalCLTVExpiry, err =
|
||||||
|
parseMinFinalCLTVExpiry(base32Data)
|
||||||
|
|
||||||
case fieldTypeF:
|
case fieldTypeF:
|
||||||
if invoice.FallbackAddr != nil {
|
if invoice.FallbackAddr != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -279,7 +287,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net)
|
invoice.FallbackAddr, err = parseFallbackAddr(
|
||||||
|
base32Data, net,
|
||||||
|
)
|
||||||
|
|
||||||
case fieldTypeR:
|
case fieldTypeR:
|
||||||
// An `r` field can be included in an invoice multiple
|
// An `r` field can be included in an invoice multiple
|
||||||
// times, so we won't skip it if we have already seen
|
// times, so we won't skip it if we have already seen
|
||||||
@@ -289,7 +300,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.RouteHints = append(invoice.RouteHints, routeHint)
|
invoice.RouteHints = append(
|
||||||
|
invoice.RouteHints, routeHint,
|
||||||
|
)
|
||||||
|
|
||||||
case fieldType9:
|
case fieldType9:
|
||||||
if invoice.Features != nil {
|
if invoice.Features != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -298,6 +312,19 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice.Features, err = parseFeatures(base32Data)
|
invoice.Features, err = parseFeatures(base32Data)
|
||||||
|
|
||||||
|
case fieldTypeB:
|
||||||
|
blindedPaymentPath, err := parseBlindedPaymentPath(
|
||||||
|
base32Data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.BlindedPaymentPaths = append(
|
||||||
|
invoice.BlindedPaymentPaths, blindedPaymentPath,
|
||||||
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Ignore unknown type.
|
// Ignore unknown type.
|
||||||
}
|
}
|
||||||
@@ -495,6 +522,17 @@ func parseRouteHint(data []byte) ([]HopHint, error) {
|
|||||||
return routeHint, nil
|
return routeHint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseBlindedPaymentPath attempts to parse a BlindedPaymentPath from the given
|
||||||
|
// byte slice.
|
||||||
|
func parseBlindedPaymentPath(data []byte) (*BlindedPaymentPath, error) {
|
||||||
|
base256Data, err := bech32.ConvertBits(data, 5, 8, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecodeBlindedPayment(bytes.NewReader(base256Data))
|
||||||
|
}
|
||||||
|
|
||||||
// parseFeatures decodes any feature bits directly from the base32
|
// parseFeatures decodes any feature bits directly from the base32
|
||||||
// representation.
|
// representation.
|
||||||
func parseFeatures(data []byte) (*lnwire.FeatureVector, error) {
|
func parseFeatures(data []byte) (*lnwire.FeatureVector, error) {
|
||||||
|
|||||||
@@ -260,6 +260,29 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, path := range invoice.BlindedPaymentPaths {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
err := path.Encode(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
blindedPathBase32, err := bech32.ConvertBits(
|
||||||
|
buf.Bytes(), 8, 5, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeTaggedField(
|
||||||
|
bufferBase32, fieldTypeB, blindedPathBase32,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if invoice.Destination != nil {
|
if invoice.Destination != nil {
|
||||||
// Convert 33 byte pubkey to 53 5-bit groups.
|
// Convert 33 byte pubkey to 53 5-bit groups.
|
||||||
pubKeyBase32, err := bech32.ConvertBits(
|
pubKeyBase32, err := bech32.ConvertBits(
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ const (
|
|||||||
// probing the recipient.
|
// probing the recipient.
|
||||||
fieldTypeS = 16
|
fieldTypeS = 16
|
||||||
|
|
||||||
|
// fieldTypeB contains blinded payment path information. This field may
|
||||||
|
// be repeated to include multiple blinded payment paths in the invoice.
|
||||||
|
fieldTypeB = 20
|
||||||
|
|
||||||
// maxInvoiceLength is the maximum total length an invoice can have.
|
// maxInvoiceLength is the maximum total length an invoice can have.
|
||||||
// This is chosen to be the maximum number of bytes that can fit into a
|
// This is chosen to be the maximum number of bytes that can fit into a
|
||||||
// single QR code: https://en.wikipedia.org/wiki/QR_code#Storage
|
// single QR code: https://en.wikipedia.org/wiki/QR_code#Storage
|
||||||
@@ -180,9 +184,17 @@ type Invoice struct {
|
|||||||
// hint can be individually used to reach the destination. These usually
|
// hint can be individually used to reach the destination. These usually
|
||||||
// represent private routes.
|
// represent private routes.
|
||||||
//
|
//
|
||||||
// NOTE: This is optional.
|
// NOTE: This is optional and should not be set at the same time as
|
||||||
|
// BlindedPaymentPaths.
|
||||||
RouteHints [][]HopHint
|
RouteHints [][]HopHint
|
||||||
|
|
||||||
|
// BlindedPaymentPaths is a set of blinded payment paths that can be
|
||||||
|
// used to find the payment receiver.
|
||||||
|
//
|
||||||
|
// NOTE: This is optional and should not be set at the same time as
|
||||||
|
// RouteHints.
|
||||||
|
BlindedPaymentPaths []*BlindedPaymentPath
|
||||||
|
|
||||||
// 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
|
||||||
@@ -263,6 +275,15 @@ func RouteHint(routeHint []HopHint) func(*Invoice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBlindedPaymentPath is a functional option that allows a caller of
|
||||||
|
// NewInvoice to attach a blinded payment path to the invoice. The option can
|
||||||
|
// be used multiple times to attach multiple paths.
|
||||||
|
func WithBlindedPaymentPath(p *BlindedPaymentPath) func(*Invoice) {
|
||||||
|
return func(i *Invoice) {
|
||||||
|
i.BlindedPaymentPaths = append(i.BlindedPaymentPaths, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Features is a functional option that allows callers of NewInvoice to set the
|
// Features is a functional option that allows callers of NewInvoice to set the
|
||||||
// desired feature bits that are advertised on the invoice. If this option is
|
// desired feature bits that are advertised on the invoice. If this option is
|
||||||
// not used, an empty feature vector will automatically be populated.
|
// not used, an empty feature vector will automatically be populated.
|
||||||
@@ -355,6 +376,13 @@ func validateInvoice(invoice *Invoice) error {
|
|||||||
return fmt.Errorf("no payment hash found")
|
return fmt.Errorf("no payment hash found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(invoice.RouteHints) != 0 &&
|
||||||
|
len(invoice.BlindedPaymentPaths) != 0 {
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot have both route hints and blinded " +
|
||||||
|
"payment paths")
|
||||||
|
}
|
||||||
|
|
||||||
// Either Description or DescriptionHash must be set, not both.
|
// Either Description or DescriptionHash must be set, not both.
|
||||||
if invoice.Description != nil && invoice.DescriptionHash != nil {
|
if invoice.Description != nil && invoice.DescriptionHash != nil {
|
||||||
return fmt.Errorf("both description and description hash set")
|
return fmt.Errorf("both description and description hash set")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,7 +16,9 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -116,6 +117,62 @@ var (
|
|||||||
|
|
||||||
// Must be initialized in init().
|
// Must be initialized in init().
|
||||||
testDescriptionHash [32]byte
|
testDescriptionHash [32]byte
|
||||||
|
|
||||||
|
testBlindedPK1Bytes, _ = hex.DecodeString("03f3311e948feb5115242c4e39" +
|
||||||
|
"6c81c448ab7ee5fd24c4e24e66c73533cc4f98b8")
|
||||||
|
testBlindedHopPK1, _ = btcec.ParsePubKey(testBlindedPK1Bytes)
|
||||||
|
testBlindedPK2Bytes, _ = hex.DecodeString("03a8c97ed5cd40d474e4ef18c8" +
|
||||||
|
"99854b25e5070106504cb225e6d2c112d61a805e")
|
||||||
|
testBlindedHopPK2, _ = btcec.ParsePubKey(testBlindedPK2Bytes)
|
||||||
|
testBlindedPK3Bytes, _ = hex.DecodeString("0220293926219d8efe733336e2" +
|
||||||
|
"b674570dd96aa763acb3564e6e367b384d861a0a")
|
||||||
|
testBlindedHopPK3, _ = btcec.ParsePubKey(testBlindedPK3Bytes)
|
||||||
|
testBlindedPK4Bytes, _ = hex.DecodeString("02c75eb336a038294eaaf76015" +
|
||||||
|
"8b2e851c3c0937262e35401ae64a1bee71a2e40c")
|
||||||
|
testBlindedHopPK4, _ = btcec.ParsePubKey(testBlindedPK4Bytes)
|
||||||
|
|
||||||
|
blindedPath1 = &BlindedPaymentPath{
|
||||||
|
FeeBaseMsat: 40,
|
||||||
|
FeeRate: 20,
|
||||||
|
CltvExpiryDelta: 130,
|
||||||
|
HTLCMinMsat: 2,
|
||||||
|
HTLCMaxMsat: 100,
|
||||||
|
Features: lnwire.EmptyFeatureVector(),
|
||||||
|
FirstEphemeralBlindingPoint: testBlindedHopPK1,
|
||||||
|
Hops: []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
BlindedNodePub: testBlindedHopPK2,
|
||||||
|
CipherText: []byte{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: testBlindedHopPK3,
|
||||||
|
CipherText: []byte{5, 4, 3, 2, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: testBlindedHopPK4,
|
||||||
|
CipherText: []byte{
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||||
|
11, 12, 13, 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
blindedPath2 = &BlindedPaymentPath{
|
||||||
|
FeeBaseMsat: 4,
|
||||||
|
FeeRate: 2,
|
||||||
|
CltvExpiryDelta: 10,
|
||||||
|
HTLCMinMsat: 0,
|
||||||
|
HTLCMaxMsat: 10,
|
||||||
|
Features: lnwire.EmptyFeatureVector(),
|
||||||
|
FirstEphemeralBlindingPoint: testBlindedHopPK4,
|
||||||
|
Hops: []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
BlindedNodePub: testBlindedHopPK3,
|
||||||
|
CipherText: []byte{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -125,6 +182,8 @@ func init() {
|
|||||||
// TestDecodeEncode tests that an encoded invoice gets decoded into the expected
|
// TestDecodeEncode tests that an encoded invoice gets decoded into the expected
|
||||||
// Invoice object, and that reencoding the decoded invoice gets us back to the
|
// Invoice object, and that reencoding the decoded invoice gets us back to the
|
||||||
// original encoded string.
|
// original encoded string.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
func TestDecodeEncode(t *testing.T) {
|
func TestDecodeEncode(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -673,9 +732,40 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
i.Destination = nil
|
i.Destination = nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Invoice with blinded payment paths.
|
||||||
|
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f",
|
||||||
|
valid: true,
|
||||||
|
decodedInvoice: func() *Invoice {
|
||||||
|
return &Invoice{
|
||||||
|
Net: &chaincfg.MainNetParams,
|
||||||
|
MilliSat: &testMillisat20mBTC,
|
||||||
|
Timestamp: time.Unix(1496314658, 0),
|
||||||
|
PaymentHash: &testPaymentHash,
|
||||||
|
Description: &testCupOfCoffee,
|
||||||
|
Destination: testPubKey,
|
||||||
|
Features: emptyFeatures,
|
||||||
|
BlindedPaymentPaths: []*BlindedPaymentPath{
|
||||||
|
blindedPath1,
|
||||||
|
blindedPath2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
var decodedInvoice *Invoice
|
var decodedInvoice *Invoice
|
||||||
net := &chaincfg.MainNetParams
|
net := &chaincfg.MainNetParams
|
||||||
if test.decodedInvoice != nil {
|
if test.decodedInvoice != nil {
|
||||||
@@ -684,41 +774,35 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoice, err := Decode(test.encodedInvoice, net)
|
invoice, err := Decode(test.encodedInvoice, net)
|
||||||
if (err == nil) != test.valid {
|
if !test.valid {
|
||||||
t.Errorf("Decoding test %d failed: %v", i, err)
|
require.Error(t, err)
|
||||||
return
|
} else {
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, decodedInvoice, invoice)
|
||||||
if test.valid {
|
|
||||||
if err := compareInvoices(decodedInvoice, invoice); err != nil {
|
|
||||||
t.Errorf("Invoice decoding result %d not as expected: %v", i, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.skipEncoding {
|
if test.skipEncoding {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.beforeEncoding != nil {
|
if test.beforeEncoding != nil {
|
||||||
test.beforeEncoding(decodedInvoice)
|
test.beforeEncoding(decodedInvoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
if decodedInvoice != nil {
|
if decodedInvoice == nil {
|
||||||
reencoded, err := decodedInvoice.Encode(
|
|
||||||
testMessageSigner,
|
|
||||||
)
|
|
||||||
if (err == nil) != test.valid {
|
|
||||||
t.Errorf("Encoding test %d failed: %v", i, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.valid && test.encodedInvoice != reencoded {
|
reencoded, err := decodedInvoice.Encode(
|
||||||
t.Errorf("Encoding %d failed, expected %v, got %v",
|
testMessageSigner,
|
||||||
i, test.encodedInvoice, reencoded)
|
)
|
||||||
|
if !test.valid {
|
||||||
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.encodedInvoice, reencoded)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,25 +889,42 @@ func TestNewInvoice(t *testing.T) {
|
|||||||
valid: true,
|
valid: true,
|
||||||
encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6",
|
encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Mainnet invoice with two blinded paths.
|
||||||
|
newInvoice: func() (*Invoice, error) {
|
||||||
|
return NewInvoice(&chaincfg.MainNetParams,
|
||||||
|
testPaymentHash,
|
||||||
|
time.Unix(1496314658, 0),
|
||||||
|
Amount(testMillisat20mBTC),
|
||||||
|
Description(testCupOfCoffee),
|
||||||
|
WithBlindedPaymentPath(blindedPath1),
|
||||||
|
WithBlindedPaymentPath(blindedPath2),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
//nolint:lll
|
||||||
|
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
invoice, err := test.newInvoice()
|
invoice, err := test.newInvoice()
|
||||||
if err != nil && !test.valid {
|
if !test.valid {
|
||||||
continue
|
require.Error(t, err)
|
||||||
}
|
|
||||||
encoded, err := invoice.Encode(testMessageSigner)
|
|
||||||
if (err == nil) != test.valid {
|
|
||||||
t.Errorf("NewInvoice test %d failed: %v", i, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
if test.valid && test.encodedInvoice != encoded {
|
encoded, err := invoice.Encode(testMessageSigner)
|
||||||
t.Errorf("Encoding %d failed, expected %v, got %v",
|
require.NoError(t, err)
|
||||||
i, test.encodedInvoice, encoded)
|
|
||||||
return
|
require.Equal(t, test.encodedInvoice, encoded)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,73 +1010,6 @@ func TestInvoiceChecksumMalleability(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareInvoices(expected, actual *Invoice) error {
|
|
||||||
if !reflect.DeepEqual(expected.Net, actual.Net) {
|
|
||||||
return fmt.Errorf("expected net %v, got %v",
|
|
||||||
expected.Net, actual.Net)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expected.MilliSat, actual.MilliSat) {
|
|
||||||
return fmt.Errorf("expected milli sat %d, got %d",
|
|
||||||
*expected.MilliSat, *actual.MilliSat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.Timestamp != actual.Timestamp {
|
|
||||||
return fmt.Errorf("expected timestamp %v, got %v",
|
|
||||||
expected.Timestamp, actual.Timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !compareHashes(expected.PaymentHash, actual.PaymentHash) {
|
|
||||||
return fmt.Errorf("expected payment hash %x, got %x",
|
|
||||||
*expected.PaymentHash, *actual.PaymentHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expected.Description, actual.Description) {
|
|
||||||
return fmt.Errorf("expected description \"%s\", got \"%s\"",
|
|
||||||
*expected.Description, *actual.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !comparePubkeys(expected.Destination, actual.Destination) {
|
|
||||||
return fmt.Errorf("expected destination pubkey %x, got %x",
|
|
||||||
expected.Destination.SerializeCompressed(),
|
|
||||||
actual.Destination.SerializeCompressed())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !compareHashes(expected.DescriptionHash, actual.DescriptionHash) {
|
|
||||||
return fmt.Errorf("expected description hash %x, got %x",
|
|
||||||
*expected.DescriptionHash, *actual.DescriptionHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.Expiry() != actual.Expiry() {
|
|
||||||
return fmt.Errorf("expected expiry %d, got %d",
|
|
||||||
expected.Expiry(), actual.Expiry())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expected.FallbackAddr, actual.FallbackAddr) {
|
|
||||||
return fmt.Errorf("expected FallbackAddr %v, got %v",
|
|
||||||
expected.FallbackAddr, actual.FallbackAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(expected.RouteHints) != len(actual.RouteHints) {
|
|
||||||
return fmt.Errorf("expected %d RouteHints, got %d",
|
|
||||||
len(expected.RouteHints), len(actual.RouteHints))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, routeHint := range expected.RouteHints {
|
|
||||||
err := compareRouteHints(routeHint, actual.RouteHints[i])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expected.Features, actual.Features) {
|
|
||||||
return fmt.Errorf("expected features %v, got %v",
|
|
||||||
expected.Features, actual.Features)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func comparePubkeys(a, b *btcec.PublicKey) bool {
|
func comparePubkeys(a, b *btcec.PublicKey) bool {
|
||||||
if a == b {
|
if a == b {
|
||||||
return true
|
return true
|
||||||
|
|||||||
Reference in New Issue
Block a user