routing: use mission control to select blinded paths

Add a `FindBlindedPaths` method to the `ChannelRouter` which will use
the new `findBlindedPaths` function to get a set of candidate blinded
path routes. It then uses mission control to select the best of these
paths.

Note that as of this commit, the MC data we get from these queries won't
mean much since we wont have data about a channel in the direction
towards us. But we do this now in preparation for a future PR which will
start writing mission control success pairs for successful receives from
blinded route paths.
This commit is contained in:
Elle Mouton 2024-05-05 13:36:26 +02:00
parent 1039aedd0c
commit e12a226272
No known key found for this signature in database
GPG key ID: D7D916376026F177
2 changed files with 345 additions and 0 deletions

View file

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"math"
"sort"
"sync"
"sync/atomic"
"time"
@ -675,6 +676,131 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
return route, probability, nil
}
// probabilitySource defines the signature of a function that can be used to
// query the success probability of sending a given amount between the two
// given vertices.
type probabilitySource func(route.Vertex, route.Vertex, lnwire.MilliSatoshi,
btcutil.Amount) float64
// BlindedPathRestrictions are a set of constraints to adhere to when
// choosing a set of blinded paths to this node.
type BlindedPathRestrictions struct {
// MinDistanceFromIntroNode is the minimum number of _real_ (non-dummy)
// hops to include in a blinded path. Since we post-fix dummy hops, this
// is the minimum distance between our node and the introduction node
// of the path. This doesn't include our node, so if the minimum is 1,
// then the path will contain at minimum our node along with an
// introduction node hop.
MinDistanceFromIntroNode uint8
// NumHops is the number of hops that each blinded path should consist
// of. If paths are found with a number of hops less that NumHops, then
// dummy hops will be padded on to the route. This value doesn't
// include our node, so if the maximum is 1, then the path will contain
// our node along with an introduction node hop.
NumHops uint8
// MaxNumPaths is the maximum number of blinded paths to select.
MaxNumPaths uint8
}
// FindBlindedPaths finds a selection of paths to the destination node that can
// be used in blinded payment paths.
func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
amt lnwire.MilliSatoshi, probabilitySrc probabilitySource,
restrictions *BlindedPathRestrictions) ([]*route.Route, error) {
// First, find a set of candidate paths given the destination node and
// path length restrictions.
paths, err := findBlindedPaths(
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
minNumHops: restrictions.MinDistanceFromIntroNode,
maxNumHops: restrictions.NumHops,
},
)
if err != nil {
return nil, err
}
// routeWithProbability groups a route with the probability of a
// payment of the given amount succeeding on that path.
type routeWithProbability struct {
route *route.Route
probability float64
}
// Iterate over all the candidate paths and determine the success
// probability of each path given the data we have about forwards
// between any two nodes on a path.
routes := make([]*routeWithProbability, 0, len(paths))
for _, path := range paths {
if len(path) < 1 {
return nil, fmt.Errorf("a blinded path must have at " +
"least one hop")
}
var (
introNode = path[0].vertex
prevNode = introNode
hops = make(
[]*route.Hop, 0, len(path)-1,
)
totalRouteProbability = float64(1)
)
// For each set of hops on the path, get the success probability
// of a forward between those two vertices and use that to
// update the overall route probability.
for j := 1; j < len(path); j++ {
probability := probabilitySrc(
prevNode, path[j].vertex, amt,
path[j-1].edgeCapacity,
)
totalRouteProbability *= probability
hops = append(hops, &route.Hop{
PubKeyBytes: path[j].vertex,
ChannelID: path[j-1].edgePolicy.ChannelID,
})
prevNode = path[j].vertex
}
// Don't bother adding a route if its success probability less
// minimum that can be assigned to any single pair.
if totalRouteProbability <= DefaultMinRouteProbability {
continue
}
routes = append(routes, &routeWithProbability{
route: &route.Route{
SourcePubKey: introNode,
Hops: hops,
},
probability: totalRouteProbability,
})
}
// Sort the routes based on probability.
sort.Slice(routes, func(i, j int) bool {
return routes[i].probability < routes[j].probability
})
// Now just choose the best paths up until the maximum number of allowed
// paths.
bestRoutes := make([]*route.Route, 0, restrictions.MaxNumPaths)
for _, route := range routes {
if len(bestRoutes) >= int(restrictions.MaxNumPaths) {
break
}
bestRoutes = append(bestRoutes, route.route)
}
return bestRoutes, nil
}
// generateNewSessionKey generates a new ephemeral private key to be used for a
// payment attempt.
func generateNewSessionKey() (*btcec.PrivateKey, error) {

View file

@ -7,6 +7,7 @@ import (
"math"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
@ -2589,3 +2590,221 @@ func createChannelEdge(bitcoinKey1, bitcoinKey2 []byte,
return fundingTx, &chanUtxo, chanID, nil
}
// TestFindBlindedPathsWithMC tests that the FindBlindedPaths method correctly
// selects a set of blinded paths by using mission control data to select the
// paths with the highest success probability.
func TestFindBlindedPathsWithMC(t *testing.T) {
t.Parallel()
rbFeatureBits := []lnwire.FeatureBit{
lnwire.RouteBlindingOptional,
}
// Create the following graph and let all the nodes advertise support
// for blinded paths.
//
// C
// / \
// / \
// E -- A -- F -- D
// \ /
// \ /
// B
//
featuresWithRouteBlinding := lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(rbFeatureBits...), lnwire.Features,
)
policyWithRouteBlinding := &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: 100000000,
Features: featuresWithRouteBlinding,
}
testChannels := []*testChannel{
symmetricTestChannel(
"eve", "alice", 100000, policyWithRouteBlinding, 1,
),
symmetricTestChannel(
"alice", "charlie", 100000, policyWithRouteBlinding, 2,
),
symmetricTestChannel(
"alice", "bob", 100000, policyWithRouteBlinding, 3,
),
symmetricTestChannel(
"charlie", "dave", 100000, policyWithRouteBlinding, 4,
),
symmetricTestChannel(
"bob", "dave", 100000, policyWithRouteBlinding, 5,
),
symmetricTestChannel(
"alice", "frank", 100000, policyWithRouteBlinding, 6,
),
symmetricTestChannel(
"frank", "dave", 100000, policyWithRouteBlinding, 7,
),
}
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "dave", rbFeatureBits...,
)
require.NoError(t, err)
ctx := createTestCtxFromGraphInstance(t, 101, testGraph)
var (
alice = ctx.aliases["alice"]
bob = ctx.aliases["bob"]
charlie = ctx.aliases["charlie"]
dave = ctx.aliases["dave"]
eve = ctx.aliases["eve"]
frank = ctx.aliases["frank"]
)
// Create a mission control store which initially sets the success
// probability of each node pair to 1.
missionControl := map[route.Vertex]map[route.Vertex]float64{
eve: {alice: 1},
alice: {
charlie: 1,
bob: 1,
frank: 1,
},
charlie: {dave: 1},
bob: {dave: 1},
frank: {dave: 1},
}
// probabilitySrc is a helper that returns the mission control success
// probability of a forward between two vertices.
probabilitySrc := func(from route.Vertex, to route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
return missionControl[from][to]
}
// All the probabilities are set to 1. So if we restrict the path length
// to 2 and allow a max of 3 routes, then we expect three paths here.
routes, err := ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
require.Len(t, routes, 3)
// assertPaths checks that the resulting set of paths is equal to the
// expected set and that the order of the paths is correct.
assertPaths := func(paths []*route.Route, expectedPaths []string) {
require.Len(t, paths, len(expectedPaths))
var actualPaths []string
for _, path := range paths {
label := getAliasFromPubKey(
path.SourcePubKey, ctx.aliases,
) + ","
for _, hop := range path.Hops {
label += getAliasFromPubKey(
hop.PubKeyBytes, ctx.aliases,
) + ","
}
actualPaths = append(
actualPaths, strings.TrimRight(label, ","),
)
}
for i, path := range expectedPaths {
require.Equal(t, expectedPaths[i], path)
}
}
// Now, let's lower the MC probability of the B-D to 0.5 and F-D link to
// 0.25. We will leave the MaxNumPaths as 3 and so all paths should
// still be returned but the order should be:
// 1) A -> C -> D
// 2) A -> B -> D
// 3) A -> F -> D
missionControl[bob][dave] = 0.5
missionControl[frank][dave] = 0.25
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,charlie,dave",
"alice,bob,dave",
"alice,frank,dave",
})
// Just to show that the above result was not a fluke, let's change
// the C->D link to be the weak one.
missionControl[charlie][dave] = 0.125
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
"alice,frank,dave",
"alice,charlie,dave",
})
// Change the MaxNumPaths to 1 to assert that only the best route is
// returned.
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 1,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
})
// Test the edge case where Dave, the recipient, is also the
// introduction node.
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 0,
NumHops: 0,
MaxNumPaths: 1,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"dave",
})
// Finally, we make one of the routes have a probability less than the
// minimum. This means we expect that route not to be chosen.
missionControl[charlie][dave] = DefaultMinRouteProbability
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
"alice,frank,dave",
})
}