From 735d7d97384283515d0eb945815fc780dc9a9242 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 6 May 2024 16:39:43 +0200 Subject: [PATCH] multi: send to a blinded path in an invoice Update the SendPayment flow so that it is able to send to an invoice containing a blinded path. --- lnrpc/routerrpc/router_backend.go | 46 +++++++++++++++++++++++++++++++ routing/payment_session.go | 20 +++++++++++++- routing/router.go | 12 +++++++- rpcserver.go | 28 +++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 9fde63411..970fb04cf 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -999,6 +999,32 @@ func (r *RouterBackend) extractIntentFromSendRequest( payIntent.PaymentAddr = payAddr payIntent.PaymentRequest = []byte(rpcPayReq.PaymentRequest) 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") + } + + 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. + copy( + payIntent.Target[:], + finalHop.BlindedNodePub.SerializeCompressed(), + ) + + if !path.Features.IsEmpty() { + payIntent.DestFeatures = path.Features.Clone() + } + } } else { // Otherwise, If the payment request field was not specified // (and a custom route wasn't specified), construct the payment @@ -1137,6 +1163,26 @@ func (r *RouterBackend) extractIntentFromSendRequest( return payIntent, nil } +// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a +// routing.BlindedPayment. +func MarshalBlindedPayment( + path *zpay32.BlindedPaymentPath) *routing.BlindedPayment { + + return &routing.BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + IntroductionPoint: path.Hops[0].BlindedNodePub, + BlindingPoint: path.FirstEphemeralBlindingPoint, + BlindedHops: path.Hops, + }, + BaseFee: path.FeeBaseMsat, + ProportionalFeeRate: path.FeeRate, + CltvExpiryDelta: path.CltvExpiryDelta, + HtlcMinimum: path.HTLCMinMsat, + HtlcMaximum: path.HTLCMaxMsat, + Features: path.Features, + } +} + // unmarshallRouteHints unmarshalls a list of route hints. func unmarshallRouteHints(rpcRouteHints []*lnrpc.RouteHint) ( [][]zpay32.HopHint, error) { diff --git a/routing/payment_session.go b/routing/payment_session.go index 710318426..14b369407 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -5,6 +5,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btclog" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" @@ -205,6 +206,18 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex, return nil, err } + if p.BlindedPayment != nil { + if len(edges) != 0 { + return nil, fmt.Errorf("cannot have both route hints " + + "and blinded path") + } + + edges, err = p.BlindedPayment.toRouteHints() + if err != nil { + return nil, err + } + } + logPrefix := fmt.Sprintf("PaymentSession(%x):", p.Identifier()) return &paymentSession{ @@ -389,6 +402,11 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, return nil, err } + var blindedPath *sphinx.BlindedPath + if p.payment.BlindedPayment != nil { + blindedPath = p.payment.BlindedPayment.BlindedPath + } + // With the next candidate path found, we'll attempt to turn // this into a route by applying the time-lock and fee // requirements. @@ -401,7 +419,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, records: p.payment.DestCustomRecords, paymentAddr: p.payment.PaymentAddr, metadata: p.payment.Metadata, - }, nil, + }, blindedPath, ) if err != nil { return nil, err diff --git a/routing/router.go b/routing/router.go index 9e183cde5..5551c5345 100644 --- a/routing/router.go +++ b/routing/router.go @@ -922,9 +922,19 @@ type LightningPayment struct { // NOTE: This is optional unless required by the payment. When providing // multiple routes, ensure the hop hints within each route are chained // together and sorted in forward order in order to reach the - // destination successfully. + // destination successfully. This is mutually exclusive to the + // BlindedPayment field. RouteHints [][]zpay32.HopHint + // BlindedPayment holds the information about a blinded path to the + // payment recipient. This is mutually exclusive to the RouteHints + // field. + // + // NOTE: a recipient may provide multiple blinded payment paths in the + // same invoice. Currently, LND will only attempt to use the first one. + // A future PR will handle multiple blinded payment paths. + BlindedPayment *BlindedPayment + // OutgoingChannelIDs is the list of channels that are allowed for the // first hop. If nil, any channel may be used. OutgoingChannelIDs []uint64 diff --git a/rpcserver.go b/rpcserver.go index e838afcd1..5d9ed2ef5 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -5110,6 +5110,7 @@ type rpcPaymentIntent struct { paymentAddr *[32]byte payReq []byte metadata []byte + blindedPayment *routing.BlindedPayment destCustomRecords record.CustomSet @@ -5245,6 +5246,32 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme payIntent.paymentAddr = payReq.PaymentAddr 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") + } + + 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. + copy( + payIntent.dest[:], + finalHop.BlindedNodePub.SerializeCompressed(), + ) + + if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() { + payIntent.destFeatures = path.Features.Clone() + } + } + if err := validateDest(payIntent.dest); err != nil { return payIntent, err } @@ -5399,6 +5426,7 @@ func (r *rpcServer) dispatchPaymentIntent( DestFeatures: payIntent.destFeatures, PaymentAddr: payIntent.paymentAddr, Metadata: payIntent.metadata, + BlindedPayment: payIntent.blindedPayment, // Don't enable multi-part payments on the main rpc. // Users need to use routerrpc for that.