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" "github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "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 // Derive an ephemeral target priv key that will be injected into each
// series, all paths will be kept. // blinded path final hop.
path := paths[0] targetPriv, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
targetPub := targetPriv.PubKey()
finalHop := path.BlindedPath. // If any provided blinded path only has a single hop (ie, the
BlindedHops[len(path.BlindedPath.BlindedHops)-1] // 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{ return &BlindedPaymentPathSet{
paths: paths, paths: pathSet,
targetPubKey: finalHop.BlindedNodePub, targetPubKey: targetPub,
features: features, features: features,
}, nil }, nil
} }
@ -144,7 +164,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
hints := make(RouteHints) hints := make(RouteHints)
for _, path := range s.paths { for _, path := range s.paths {
pathHints, err := path.toRouteHints() pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -223,8 +243,11 @@ func (b *BlindedPayment) Validate() error {
// effectively the final_cltv_delta for the receiving introduction node). In // 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 // 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 // hints (both for intermediate hops and the final_cltv_delta for the receiving
// node). // node). The pseudoTarget, if provided, will be used to override the pub key
func (b *BlindedPayment) toRouteHints() (RouteHints, error) { // 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 // 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). // 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 // 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, ToNodeFeatures: features,
} }
edge, err := NewBlindedEdge(edgePolicy, b, 0) lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
if err != nil { if err != nil {
return nil, err 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 // 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 // is the introduction node and terminate at the second-last node
@ -304,13 +327,24 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
ToNodeFeatures: features, ToNodeFeatures: features,
} }
edge, err := NewBlindedEdge(edgePolicy, b, i) lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
if err != nil { if err != nil {
return nil, err 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 return hints, nil
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -128,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
HtlcMaximum: htlcMax, HtlcMaximum: htlcMax,
Features: features, Features: features,
} }
hints, err := blindedPayment.toRouteHints() hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, hints) 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.NoError(t, err)
require.Equal(t, len(expected), len(actual)) require.Equal(t, len(expected), len(actual))

View File

@ -153,19 +153,24 @@ func newRoute(sourceVertex route.Vertex,
// sender of the payment. // sender of the payment.
nextIncomingAmount lnwire.MilliSatoshi nextIncomingAmount lnwire.MilliSatoshi
blindedPath *sphinx.BlindedPath blindedPayment *BlindedPayment
) )
if blindedPathSet != nil {
blindedPath = blindedPathSet.GetPath().BlindedPath
}
pathLength := len(pathEdges) pathLength := len(pathEdges)
for i := pathLength - 1; i >= 0; i-- { for i := pathLength - 1; i >= 0; i-- {
// Now we'll start to calculate the items within the per-hop // Now we'll start to calculate the items within the per-hop
// payload for the hop this edge is leading to. // payload for the hop this edge is leading to.
edge := pathEdges[i].policy 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 // We'll calculate the amounts, timelocks, and fees for each hop
// in the route. The base case is the final hop which includes // in the route. The base case is the final hop which includes
// their amount and timelocks. These values will accumulate // 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 // node's CLTV delta. The exception is for the case
// where the final hop is the blinded path introduction // where the final hop is the blinded path introduction
// node. // node.
if blindedPath == nil || if blindedPathSet == nil ||
len(blindedPath.BlindedHops) == 1 { len(blindedPathSet.GetPath().BlindedPath.
BlindedHops) == 1 {
// As this is the last hop, we'll use the // As this is the last hop, we'll use the
// specified final CLTV delta value instead of // specified final CLTV delta value instead of
@ -245,7 +251,7 @@ func newRoute(sourceVertex route.Vertex,
metadata = finalHop.metadata metadata = finalHop.metadata
if blindedPath != nil { if blindedPathSet != nil {
totalAmtMsatBlinded = finalHop.totalAmt totalAmtMsatBlinded = finalHop.totalAmt
} }
} else { } 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 // 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. // additional data to the route that is required for blinded forwarding.
// We do another pass on our edges to append this data. // 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 ( var (
inBlindedRoute bool inBlindedRoute bool
dataIndex = 0 dataIndex = 0
blindedPath = blindedPayment.BlindedPath
numHops = len(blindedPath.BlindedHops)
realFinal = blindedPath.BlindedHops[numHops-1].
BlindedNodePub
introVertex = route.NewVertex( introVertex = route.NewVertex(
blindedPath.IntroductionPoint, blindedPath.IntroductionPoint,
) )
@ -337,6 +357,11 @@ func newRoute(sourceVertex route.Vertex,
if i != len(hops)-1 { if i != len(hops)-1 {
hop.AmtToForward = 0 hop.AmtToForward = 0
hop.OutgoingTimeLock = 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++ dataIndex++

View File

@ -23,6 +23,7 @@ import (
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop" switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/kvdb" "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 // 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 // map is keyed by source node, so we can retrieve our blinded edges
// accordingly. // accordingly.
blindedEdges, err := blindedPayment.toRouteHints() blindedEdges, err := blindedPayment.toRouteHints(
fn.None[*btcec.PublicKey](),
)
require.NoError(t, err) require.NoError(t, err)
carolDaveEdge := blindedEdges[carolVertex][0] carolDaveEdge := blindedEdges[carolVertex][0]

View File

@ -573,20 +573,7 @@ func getTargetNode(target *route.Vertex,
return route.Vertex{}, ErrTargetAndBlinded return route.Vertex{}, ErrTargetAndBlinded
case blinded: case blinded:
blindedPayment := blindedPathSet.GetPath() return route.NewVertex(blindedPathSet.TargetPubKey()), nil
// 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
case targetSet: case targetSet:
return *target, nil return *target, nil

View File

@ -2231,7 +2231,10 @@ func TestNewRouteRequest(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
t.Parallel() t.Parallel()
var blindedPathInfo *BlindedPaymentPathSet var (
blindedPathInfo *BlindedPaymentPathSet
expectedTarget = testCase.expectedTarget
)
if testCase.blindedPayment != nil { if testCase.blindedPayment != nil {
blindedPathInfo, err = NewBlindedPaymentPathSet( blindedPathInfo, err = NewBlindedPaymentPathSet(
[]*BlindedPayment{ []*BlindedPayment{
@ -2239,6 +2242,10 @@ func TestNewRouteRequest(t *testing.T) {
}, },
) )
require.NoError(t, err) require.NoError(t, err)
expectedTarget = route.NewVertex(
blindedPathInfo.TargetPubKey(),
)
} }
req, err := NewRouteRequest( req, err := NewRouteRequest(
@ -2253,7 +2260,7 @@ func TestNewRouteRequest(t *testing.T) {
return return
} }
require.Equal(t, req.Target, testCase.expectedTarget) require.Equal(t, req.Target, expectedTarget)
require.Equal( require.Equal(
t, req.FinalExpiry, testCase.expectedCltv, t, req.FinalExpiry, testCase.expectedCltv,
) )