From 3d5f20b70ff824363e188698d1add667d7e3be3d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 May 2024 14:38:53 +0200 Subject: [PATCH] multi: introduce BlindedPaymentPathSet This commit introduces a new type, `BlindedPaymentPathSet`. For now, it holds only a single `BlindedPayment` but eventually it will hold and manage a set of blinded payments provided for a specific payment. To make the PR easier to follow though, we start off just letting it hold a single one and do some basic replacements. --- lnrpc/routerrpc/router_backend.go | 102 ++++++++++++---------- routing/blinding.go | 136 ++++++++++++++++++++++++++++++ rpcserver.go | 28 +++--- 3 files changed, 207 insertions(+), 59 deletions(-) diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 970fb04cf..487da5356 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) ( var ( targetPubKey *route.Vertex routeHintEdges map[route.Vertex][]routing.AdditionalEdge - blindedPmt *routing.BlindedPayment + blindedPathSet *routing.BlindedPaymentPathSet // finalCLTVDelta varies depending on whether we're sending to // a blinded route or an unblinded node. For blinded paths, @@ -297,13 +297,14 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) ( // Validate that the fields provided in the request are sane depending // on whether it is using a blinded path or not. if len(in.BlindedPaymentPaths) > 0 { - blindedPmt, err = parseBlindedPayment(in) + blindedPathSet, err = parseBlindedPaymentPaths(in) if err != nil { return nil, err } - if blindedPmt.Features != nil { - destinationFeatures = blindedPmt.Features.Clone() + pathFeatures := blindedPathSet.Features() + if pathFeatures != nil { + destinationFeatures = pathFeatures.Clone() } } else { // If we do not have a blinded path, a target pubkey must be @@ -390,7 +391,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) ( DestCustomRecords: record.CustomSet(in.DestCustomRecords), CltvLimit: cltvLimit, DestFeatures: destinationFeatures, - BlindedPayment: blindedPmt, + BlindedPayment: blindedPathSet.GetPath(), } // Pass along an outgoing channel restriction if specified. @@ -419,39 +420,24 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) ( return routing.NewRouteRequest( sourcePubKey, targetPubKey, amt, in.TimePref, restrictions, - customRecords, routeHintEdges, blindedPmt, finalCLTVDelta, + customRecords, routeHintEdges, blindedPathSet.GetPath(), + finalCLTVDelta, ) } -func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) ( - *routing.BlindedPayment, error) { +func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) ( + *routing.BlindedPaymentPathSet, error) { if len(in.PubKey) != 0 { return nil, fmt.Errorf("target pubkey: %x should not be set "+ "when blinded path is provided", in.PubKey) } - if len(in.BlindedPaymentPaths) != 1 { - return nil, errors.New("query routes only supports a single " + - "blinded path") - } - - blindedPath := in.BlindedPaymentPaths[0] - if len(in.RouteHints) > 0 { return nil, errors.New("route hints and blinded path can't " + "both be set") } - blindedPmt, err := unmarshalBlindedPayment(blindedPath) - if err != nil { - return nil, fmt.Errorf("parse blinded payment: %w", err) - } - - if err := blindedPmt.Validate(); err != nil { - return nil, fmt.Errorf("invalid blinded path: %w", err) - } - if in.FinalCltvDelta != 0 { return nil, errors.New("final cltv delta should be " + "zero for blinded paths") @@ -466,7 +452,21 @@ func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) ( "be populated in blinded path") } - return blindedPmt, nil + paths := make([]*routing.BlindedPayment, len(in.BlindedPaymentPaths)) + for i, paymentPath := range in.BlindedPaymentPaths { + blindedPmt, err := unmarshalBlindedPayment(paymentPath) + if err != nil { + return nil, fmt.Errorf("parse blinded payment: %w", err) + } + + if err := blindedPmt.Validate(); err != nil { + return nil, fmt.Errorf("invalid blinded path: %w", err) + } + + paths[i] = blindedPmt + } + + return routing.NewBlindedPaymentPathSet(paths) } func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) ( @@ -1001,28 +1001,24 @@ func (r *RouterBackend) extractIntentFromSendRequest( payIntent.Metadata = payReq.Metadata if len(payReq.BlindedPaymentPaths) > 0 { - // NOTE: Currently we only choose a single payment path. - // This will be updated in a future PR to handle - // multiple blinded payment paths. - path := payReq.BlindedPaymentPaths[0] - if len(path.Hops) == 0 { - return nil, fmt.Errorf("a blinded payment " + - "must have at least 1 hop") + pathSet, err := BuildBlindedPathSet( + payReq.BlindedPaymentPaths, + ) + if err != nil { + return nil, err } + payIntent.BlindedPayment = pathSet.GetPath() - finalHop := path.Hops[len(path.Hops)-1] - - payIntent.BlindedPayment = MarshalBlindedPayment(path) - - // Replace the target node with the blinded public key - // of the blinded path's final node. + // Replace the target node with the target public key + // of the blinded path set. copy( payIntent.Target[:], - finalHop.BlindedNodePub.SerializeCompressed(), + pathSet.TargetPubKey().SerializeCompressed(), ) - if !path.Features.IsEmpty() { - payIntent.DestFeatures = path.Features.Clone() + pathFeatures := pathSet.Features() + if !pathFeatures.IsEmpty() { + payIntent.DestFeatures = pathFeatures.Clone() } } } else { @@ -1163,9 +1159,29 @@ func (r *RouterBackend) extractIntentFromSendRequest( return payIntent, nil } -// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a +// BuildBlindedPathSet marshals a set of zpay32.BlindedPaymentPath and uses +// the result to build a new routing.BlindedPaymentPathSet. +func BuildBlindedPathSet(paths []*zpay32.BlindedPaymentPath) ( + *routing.BlindedPaymentPathSet, error) { + + marshalledPaths := make([]*routing.BlindedPayment, len(paths)) + for i, path := range paths { + paymentPath := marshalBlindedPayment(path) + + err := paymentPath.Validate() + if err != nil { + return nil, err + } + + marshalledPaths[i] = paymentPath + } + + return routing.NewBlindedPaymentPathSet(marshalledPaths) +} + +// marshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a // routing.BlindedPayment. -func MarshalBlindedPayment( +func marshalBlindedPayment( path *zpay32.BlindedPaymentPath) *routing.BlindedPayment { return &routing.BlindedPayment{ diff --git a/routing/blinding.go b/routing/blinding.go index 788fb7b77..c491f4d85 100644 --- a/routing/blinding.go +++ b/routing/blinding.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" @@ -25,6 +26,141 @@ var ( ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum") ) +// BlindedPaymentPathSet groups the data we need to handle sending to a set of +// blinded paths provided by the recipient of a payment. +// +// NOTE: for now this only holds a single BlindedPayment. By the end of the PR +// series, it will handle multiple paths. +type BlindedPaymentPathSet struct { + // paths is the set of blinded payment paths for a single payment. + // NOTE: For now this will always only have a single entry. By the end + // of this PR, it can hold multiple. + paths []*BlindedPayment + + // targetPubKey is the ephemeral node pub key that we will inject into + // each path as the last hop. This is only for the sake of path finding. + // Once the path has been found, the original destination pub key is + // used again. In the edge case where there is only a single hop in the + // path (the introduction node is the destination node), then this will + // just be the introduction node's real public key. + targetPubKey *btcec.PublicKey + + // features is the set of relay features available for the payment. + // This is extracted from the set of blinded payment paths. At the + // moment we require that all paths for the same payment have the + // same feature set. + features *lnwire.FeatureVector +} + +// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of +// BlindedPayments. +func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet, + error) { + + if len(paths) == 0 { + return nil, ErrNoBlindedPath + } + + // For now, we assert that all the paths have the same set of features. + features := paths[0].Features + noFeatures := features == nil || features.IsEmpty() + for i := 1; i < len(paths); i++ { + noFeats := paths[i].Features == nil || + paths[i].Features.IsEmpty() + + if noFeatures && !noFeats { + return nil, fmt.Errorf("all blinded paths must have " + + "the same set of features") + } + + if noFeatures { + continue + } + + if !features.RawFeatureVector.Equals( + paths[i].Features.RawFeatureVector, + ) { + + return nil, fmt.Errorf("all blinded paths must have " + + "the same set of features") + } + } + + // NOTE: for now, we just take a single path. By the end of this PR + // series, all paths will be kept. + path := paths[0] + + finalHop := path.BlindedPath. + BlindedHops[len(path.BlindedPath.BlindedHops)-1] + + return &BlindedPaymentPathSet{ + paths: paths, + targetPubKey: finalHop.BlindedNodePub, + features: features, + }, nil +} + +// TargetPubKey returns the public key to be used as the destination node's +// public key during pathfinding. +func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey { + return s.targetPubKey +} + +// Features returns the set of relay features available for the payment. +func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector { + return s.features +} + +// GetPath is a temporary getter for the single path that the set holds. +// This will be removed later on in this PR. +func (s *BlindedPaymentPathSet) GetPath() *BlindedPayment { + return s.paths[0] +} + +// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the +// largest last-hop payload. This is to be used for onion size estimation in +// path finding. +func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment { + var ( + largestPath *BlindedPayment + currentMax int + ) + for _, path := range s.paths { + numHops := len(path.BlindedPath.BlindedHops) + lastHop := path.BlindedPath.BlindedHops[numHops-1] + + if len(lastHop.CipherText) > currentMax { + largestPath = path + } + } + + return largestPath +} + +// ToRouteHints converts the blinded path payment set into a RouteHints map so +// that the blinded payment paths can be treated like route hints throughout the +// code base. +func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) { + hints := make(RouteHints) + + for _, path := range s.paths { + pathHints, err := path.toRouteHints() + if err != nil { + return nil, err + } + + for from, edges := range pathHints { + hints[from] = append(hints[from], edges...) + } + } + + if len(hints) == 0 { + return nil, nil + } + + return hints, nil +} + // BlindedPayment provides the path and payment parameters required to send a // payment along a blinded path. type BlindedPayment struct { diff --git a/rpcserver.go b/rpcserver.go index 9ab014b3d..ff7aa613a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -5242,28 +5242,24 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme payIntent.metadata = payReq.Metadata if len(payReq.BlindedPaymentPaths) > 0 { - // NOTE: Currently we only choose a single payment path. - // This will be updated in a future PR to handle - // multiple blinded payment paths. - path := payReq.BlindedPaymentPaths[0] - if len(path.Hops) == 0 { - return payIntent, fmt.Errorf("a blinded " + - "payment must have at least 1 hop") + pathSet, err := routerrpc.BuildBlindedPathSet( + payReq.BlindedPaymentPaths, + ) + if err != nil { + return payIntent, err } + payIntent.blindedPayment = pathSet.GetPath() - finalHop := path.Hops[len(path.Hops)-1] - payIntent.blindedPayment = - routerrpc.MarshalBlindedPayment(path) - - // Replace the target node with the blinded public key - // of the blinded path's final node. + // Replace the destination node with the target public + // key of the blinded path set. copy( payIntent.dest[:], - finalHop.BlindedNodePub.SerializeCompressed(), + pathSet.TargetPubKey().SerializeCompressed(), ) - if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() { - payIntent.destFeatures = path.Features.Clone() + pathFeatures := pathSet.Features() + if !pathFeatures.IsEmpty() { + payIntent.destFeatures = pathFeatures.Clone() } }