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.
This commit is contained in:
Elle Mouton 2024-05-15 15:52:17 +02:00
parent 4a22ec8413
commit 8df03de3e9
No known key found for this signature in database
GPG Key ID: D7D916376026F177
6 changed files with 99 additions and 42 deletions

View File

@ -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
}

View File

@ -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))

View File

@ -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++

View File

@ -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]

View File

@ -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

View File

@ -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,
)