From 8df03de3e9c87afd7ffcd8ea6c9b9aba2d0c5db2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 May 2024 15:52:17 +0200 Subject: [PATCH] routing: swap out final hop blinded route pub keys If multiple blinded paths are provided, they will each have a different pub key for the destination node. This makes using our existing pathfinding logic tricky since it depends on having a single destination node (characterised by a single pub key). We want to re-use this logic. So what we do is swap out the pub keys of the destinaion hop with a pseudo target pub key. This will then be used during pathfinding. Later on once a path is found, we will swap the real destination keys back in so that onion creation can be done. --- routing/blinding.go | 62 +++++++++++++++++++++++++++++++--------- routing/blinding_test.go | 5 ++-- routing/pathfind.go | 43 ++++++++++++++++++++++------ routing/pathfind_test.go | 5 +++- routing/router.go | 15 +--------- routing/router_test.go | 11 +++++-- 6 files changed, 99 insertions(+), 42 deletions(-) diff --git a/routing/blinding.go b/routing/blinding.go index c491f4d85..32bcfa3ff 100644 --- a/routing/blinding.go +++ b/routing/blinding.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -86,16 +87,35 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet, } } - // NOTE: for now, we just take a single path. By the end of this PR - // series, all paths will be kept. - path := paths[0] + // Derive an ephemeral target priv key that will be injected into each + // blinded path final hop. + targetPriv, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + targetPub := targetPriv.PubKey() - finalHop := path.BlindedPath. - BlindedHops[len(path.BlindedPath.BlindedHops)-1] + // If any provided blinded path only has a single hop (ie, the + // destination node is also the introduction node), then we discard all + // other paths since we know the real pub key of the destination node. + // For a single hop path, there is also no need for the pseudo target + // pub key replacement, so our target pub key in this case just remains + // the real introduction node ID. + var pathSet = paths + for _, path := range paths { + if len(path.BlindedPath.BlindedHops) != 1 { + continue + } + + pathSet = []*BlindedPayment{path} + targetPub = path.BlindedPath.IntroductionPoint + + break + } return &BlindedPaymentPathSet{ - paths: paths, - targetPubKey: finalHop.BlindedNodePub, + paths: pathSet, + targetPubKey: targetPub, features: features, }, nil } @@ -144,7 +164,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) { hints := make(RouteHints) for _, path := range s.paths { - pathHints, err := path.toRouteHints() + pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey)) if err != nil { return nil, err } @@ -223,8 +243,11 @@ func (b *BlindedPayment) Validate() error { // effectively the final_cltv_delta for the receiving introduction node). In // the case of multiple blinded hops, CLTV delta is fully accounted for in the // hints (both for intermediate hops and the final_cltv_delta for the receiving -// node). -func (b *BlindedPayment) toRouteHints() (RouteHints, error) { +// node). The pseudoTarget, if provided, will be used to override the pub key +// of the destination node in the path. +func (b *BlindedPayment) toRouteHints( + pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) { + // If we just have a single hop in our blinded route, it just contains // an introduction node (this is a valid path according to the spec). // Since we have the un-blinded node ID for the introduction node, we @@ -272,12 +295,12 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) { ToNodeFeatures: features, } - edge, err := NewBlindedEdge(edgePolicy, b, 0) + lastEdge, err := NewBlindedEdge(edgePolicy, b, 0) if err != nil { return nil, err } - hints[fromNode] = []AdditionalEdge{edge} + hints[fromNode] = []AdditionalEdge{lastEdge} // Start at an offset of 1 because the first node in our blinded hops // is the introduction node and terminate at the second-last node @@ -304,13 +327,24 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) { ToNodeFeatures: features, } - edge, err := NewBlindedEdge(edgePolicy, b, i) + lastEdge, err = NewBlindedEdge(edgePolicy, b, i) if err != nil { return nil, err } - hints[fromNode] = []AdditionalEdge{edge} + hints[fromNode] = []AdditionalEdge{lastEdge} } + pseudoTarget.WhenSome(func(key *btcec.PublicKey) { + // For the very last hop on the path, switch out the ToNodePub + // for the pseudo target pub key. + lastEdge.policy.ToNodePubKey = func() route.Vertex { + return route.NewVertex(key) + } + + // Then override the final hint with this updated edge. + hints[fromNode] = []AdditionalEdge{lastEdge} + }) + return hints, nil } diff --git a/routing/blinding_test.go b/routing/blinding_test.go index 58ad56594..950cb0210 100644 --- a/routing/blinding_test.go +++ b/routing/blinding_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" @@ -128,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) { HtlcMaximum: htlcMax, Features: features, } - hints, err := blindedPayment.toRouteHints() + hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]()) require.NoError(t, err) require.Nil(t, hints) @@ -183,7 +184,7 @@ func TestBlindedPaymentToHints(t *testing.T) { }, } - actual, err := blindedPayment.toRouteHints() + actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]()) require.NoError(t, err) require.Equal(t, len(expected), len(actual)) diff --git a/routing/pathfind.go b/routing/pathfind.go index ee920c564..bf1d3bf4f 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -153,19 +153,24 @@ func newRoute(sourceVertex route.Vertex, // sender of the payment. nextIncomingAmount lnwire.MilliSatoshi - blindedPath *sphinx.BlindedPath + blindedPayment *BlindedPayment ) - if blindedPathSet != nil { - blindedPath = blindedPathSet.GetPath().BlindedPath - } - pathLength := len(pathEdges) for i := pathLength - 1; i >= 0; i-- { // Now we'll start to calculate the items within the per-hop // payload for the hop this edge is leading to. edge := pathEdges[i].policy + // If this is an edge from a blinded path and the + // blindedPayment variable has not been set yet, then set it now + // by extracting the corresponding blinded payment from the + // edge. + isBlindedEdge := pathEdges[i].blindedPayment != nil + if isBlindedEdge && blindedPayment == nil { + blindedPayment = pathEdges[i].blindedPayment + } + // We'll calculate the amounts, timelocks, and fees for each hop // in the route. The base case is the final hop which includes // their amount and timelocks. These values will accumulate @@ -212,8 +217,9 @@ func newRoute(sourceVertex route.Vertex, // node's CLTV delta. The exception is for the case // where the final hop is the blinded path introduction // node. - if blindedPath == nil || - len(blindedPath.BlindedHops) == 1 { + if blindedPathSet == nil || + len(blindedPathSet.GetPath().BlindedPath. + BlindedHops) == 1 { // As this is the last hop, we'll use the // specified final CLTV delta value instead of @@ -245,7 +251,7 @@ func newRoute(sourceVertex route.Vertex, metadata = finalHop.metadata - if blindedPath != nil { + if blindedPathSet != nil { totalAmtMsatBlinded = finalHop.totalAmt } } else { @@ -305,11 +311,25 @@ func newRoute(sourceVertex route.Vertex, // If we are creating a route to a blinded path, we need to add some // additional data to the route that is required for blinded forwarding. // We do another pass on our edges to append this data. - if blindedPath != nil { + if blindedPathSet != nil { + // If the passed in BlindedPaymentPathSet is non-nil but no + // edge had a BlindedPayment attached, it means that the path + // chosen was an introduction-node-only path. So in this case, + // we can assume the relevant payment is the only one in the + // payment set. + if blindedPayment == nil { + blindedPayment = blindedPathSet.GetPath() + } + var ( inBlindedRoute bool dataIndex = 0 + blindedPath = blindedPayment.BlindedPath + numHops = len(blindedPath.BlindedHops) + realFinal = blindedPath.BlindedHops[numHops-1]. + BlindedNodePub + introVertex = route.NewVertex( blindedPath.IntroductionPoint, ) @@ -337,6 +357,11 @@ func newRoute(sourceVertex route.Vertex, if i != len(hops)-1 { hop.AmtToForward = 0 hop.OutgoingTimeLock = 0 + } else { + // For the final hop, we swap out the pub key + // bytes to the original destination node pub + // key for that payment path. + hop.PubKeyBytes = route.NewVertex(realFinal) } dataIndex++ diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 1200035bb..802385351 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -23,6 +23,7 @@ import ( sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch" switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/kvdb" @@ -3286,7 +3287,9 @@ func TestBlindedRouteConstruction(t *testing.T) { // that make up the graph we'll give to route construction. The hints // map is keyed by source node, so we can retrieve our blinded edges // accordingly. - blindedEdges, err := blindedPayment.toRouteHints() + blindedEdges, err := blindedPayment.toRouteHints( + fn.None[*btcec.PublicKey](), + ) require.NoError(t, err) carolDaveEdge := blindedEdges[carolVertex][0] diff --git a/routing/router.go b/routing/router.go index 1bf6a56c7..13e36f304 100644 --- a/routing/router.go +++ b/routing/router.go @@ -573,20 +573,7 @@ func getTargetNode(target *route.Vertex, return route.Vertex{}, ErrTargetAndBlinded case blinded: - blindedPayment := blindedPathSet.GetPath() - - // If we're dealing with an edge-case blinded path that just - // has an introduction node (first hop expected to be the intro - // hop), then we return the unblinded introduction node as our - // target. - hops := blindedPayment.BlindedPath.BlindedHops - if len(hops) == 1 { - return route.NewVertex( - blindedPayment.BlindedPath.IntroductionPoint, - ), nil - } - - return route.NewVertex(hops[len(hops)-1].BlindedNodePub), nil + return route.NewVertex(blindedPathSet.TargetPubKey()), nil case targetSet: return *target, nil diff --git a/routing/router_test.go b/routing/router_test.go index 0ffd75db2..28cced4e6 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2231,7 +2231,10 @@ func TestNewRouteRequest(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - var blindedPathInfo *BlindedPaymentPathSet + var ( + blindedPathInfo *BlindedPaymentPathSet + expectedTarget = testCase.expectedTarget + ) if testCase.blindedPayment != nil { blindedPathInfo, err = NewBlindedPaymentPathSet( []*BlindedPayment{ @@ -2239,6 +2242,10 @@ func TestNewRouteRequest(t *testing.T) { }, ) require.NoError(t, err) + + expectedTarget = route.NewVertex( + blindedPathInfo.TargetPubKey(), + ) } req, err := NewRouteRequest( @@ -2253,7 +2260,7 @@ func TestNewRouteRequest(t *testing.T) { return } - require.Equal(t, req.Target, testCase.expectedTarget) + require.Equal(t, req.Target, expectedTarget) require.Equal( t, req.FinalExpiry, testCase.expectedCltv, )