Skip to content

[demo] blinded paths: set incoming channel list #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion cmd/commands/cmd_invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"fmt"
"strconv"
"strings"

"github.com/lightningnetwork/lnd/lnrpc"
"github.com/urfave/cli"
Expand Down Expand Up @@ -116,6 +117,12 @@ var AddInvoiceCommand = cli.Command{
"use on a blinded path. The flag may be " +
"specified multiple times.",
},
cli.StringFlag{
Name: "blinded_path_incoming_channel_list",
Usage: "The chained channels specified via channel " +
"id (separated by commas), starting from a " +
"channel which points to the self node.",
},
},
Action: actionDecorator(addInvoice),
}
Expand Down Expand Up @@ -202,7 +209,8 @@ func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
if ctx.IsSet("min_real_blinded_hops") ||
ctx.IsSet("num_blinded_hops") ||
ctx.IsSet("max_blinded_paths") ||
ctx.IsSet("blinded_path_omit_node") {
ctx.IsSet("blinded_path_omit_node") ||
ctx.IsSet("blinded_path_incoming_channel_list") {

return nil, fmt.Errorf("blinded path options are " +
"only used if the `--blind` options is set")
Expand Down Expand Up @@ -239,6 +247,21 @@ func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
)
}

if ctx.IsSet("blinded_path_incoming_channel_list") {
channels := strings.Split(
ctx.String("blinded_path_incoming_channel_list"), ",",
)
for _, channelID := range channels {
chanID, err := strconv.ParseUint(channelID, 10, 64)
if err != nil {
return nil, err
}
blindCfg.IncomingChannelList = append(
blindCfg.IncomingChannelList, chanID,
)
}
}

return &blindCfg, nil
}

Expand Down
5 changes: 5 additions & 0 deletions lncfg/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func (r *Routing) Validate() error {
"number of hops expected to be included in each path")
}

if r.BlindedPaths.MaxNumPaths == 0 {
return fmt.Errorf("the maximum number of blinded paths must " +
"be greater than 0")
}

if r.BlindedPaths.PolicyIncreaseMultiplier < 1 {
return fmt.Errorf("the blinded route policy increase " +
"multiplier must be greater than or equal to 1")
Expand Down
7 changes: 7 additions & 0 deletions lnrpc/invoicesrpc/invoices.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,13 @@
"format": "byte"
},
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
},
"incoming_channel_list": {
"type": "array",
"items": {
"type": "string",
"format": "uint64"
}
}
}
},
Expand Down
2,362 changes: 1,187 additions & 1,175 deletions lnrpc/lightning.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lnrpc/lightning.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4002,6 +4002,8 @@ message BlindedPathConfig {
blinded paths.
*/
repeated bytes node_omission_list = 4;

repeated uint64 incoming_channel_list = 5;
}

enum InvoiceHTLCState {
Expand Down
7 changes: 7 additions & 0 deletions lnrpc/lightning.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3688,6 +3688,13 @@
"format": "byte"
},
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
},
"incoming_channel_list": {
"type": "array",
"items": {
"type": "string",
"format": "uint64"
}
}
}
},
Expand Down
78 changes: 75 additions & 3 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,8 @@
// nodeOmissionSet holds a set of node IDs of nodes that we should
// ignore during blinded path selection.
nodeOmissionSet fn.Set[route.Vertex]

incomingChanSet []uint64
}

// blindedHop holds the information about a hop we have selected for a blinded
Expand Down Expand Up @@ -1221,11 +1223,67 @@
restrictions.minNumHops)
}

var (
// The target node is always the last hop in the path.
incomingPath = []blindedHop{{vertex: target}}
whiteListedNodes = map[route.Vertex]bool{target: true}
visited = make(map[route.Vertex]bool)
errChanFound = errors.New("found incoming channel")
nextTarget = target
)
for _, chanID := range restrictions.incomingChanSet {
visited[nextTarget] = true

err := g.ForEachNodeDirectedChannel(
nextTarget, func(channel *graphdb.DirectedChannel) error {

Check failure on line 1238 in routing/pathfind.go

View workflow job for this annotation

GitHub Actions / lint code

the line is 82 characters long, which exceeds the maximum of 80 characters. (ll)
// Not the right channel, continue to the node's
// other channels.
if channel.ChannelID != chanID {
return nil
}

incomingPath = append([]blindedHop{
{
vertex: channel.OtherNode,
channelID: channel.ChannelID,
edgeCapacity: channel.Capacity,
},
}, incomingPath...)

// Update the target node.
nextTarget = channel.OtherNode

return errChanFound
},
)
if !errors.Is(err, errChanFound) && err != nil {
return nil, err
} else if err == nil {
return nil, fmt.Errorf("incoming channel %d is "+
"not seen as owned by node %v", chanID,
nextTarget)
}

// Check that the user didn't accidentally add a channel that
// is owned by a node in the node omission set
if restrictions.nodeOmissionSet.Contains(nextTarget) {
return nil, fmt.Errorf("node %v cannot simultaneously "+
"be included in the omission set and in the "+
"partially specified path", nextTarget)
}

if whiteListedNodes[nextTarget] {
return nil, fmt.Errorf("a circular route cannot be " +
"specified for the incoming blinded path")
}
whiteListedNodes[nextTarget] = true
}

// If the node is not the destination node, then it is required that the
// node advertise the route blinding feature-bit in order for it to be
// chosen as a node on the blinded path.
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
if node == target {
if whiteListedNodes[node] {
return true, nil
}

Expand All @@ -1243,7 +1301,7 @@
// a node that doesn't have any other edges - in that final case, the
// whole path should be ignored.
paths, _, err := processNodeForBlindedPath(
g, target, supportsRouteBlinding, nil, restrictions,
g, nextTarget, supportsRouteBlinding, visited, restrictions,
)
if err != nil {
return nil, err
Expand All @@ -1258,7 +1316,21 @@
return j < i
})

orderedPaths[i] = append(path, blindedHop{vertex: target})
// Then append the remainder of the path.
orderedPaths[i] = append(path, incomingPath...)
}

// If no paths were returned, we still want to include the entire
// initial path that was passed in.
// var singleHopPathIncluded bool
if len(incomingPath) > 1 &&
len(incomingPath) >= int(restrictions.minNumHops) {

orderedPaths = append(orderedPaths, incomingPath)

Check failure on line 1329 in routing/pathfind.go

View workflow job for this annotation

GitHub Actions / lint code

append to slice `orderedPaths` with non-zero initialized length (makezero)

//if len(incomingPath) == 1 {

Check failure on line 1331 in routing/pathfind.go

View workflow job for this annotation

GitHub Actions / lint code

commentFormatting: put a space between `//` and comment text (gocritic)
// singleHopPathIncluded = true
//}
}

// Handle the special case that allows a blinded path with the
Expand Down
131 changes: 126 additions & 5 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,

aliasMap := make(map[string]route.Vertex)
privKeyMap := make(map[string]*btcec.PrivateKey)
channelIDs := make(map[route.Vertex]map[route.Vertex]uint64)

nodeIndex := byte(0)
addNodeWithAlias := func(alias string, features *lnwire.FeatureVector) (
Expand Down Expand Up @@ -652,6 +653,16 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
node1Vertex, node2Vertex = node2Vertex, node1Vertex
}

if _, ok := channelIDs[node1Vertex]; !ok {
channelIDs[node1Vertex] = map[route.Vertex]uint64{}
}
channelIDs[node1Vertex][node2Vertex] = channelID

if _, ok := channelIDs[node2Vertex]; !ok {
channelIDs[node2Vertex] = map[route.Vertex]uint64{}
}
channelIDs[node2Vertex][node1Vertex] = channelID

// We first insert the existence of the edge between the two
// nodes.
edgeInfo := models.ChannelEdgeInfo{
Expand Down Expand Up @@ -751,6 +762,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
graphBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
channelIDs: channelIDs,
links: links,
}, nil
}
Expand Down Expand Up @@ -3752,10 +3764,7 @@ func TestFindBlindedPaths(t *testing.T) {
// assertPaths checks that the set of selected paths contains all the
// expected paths.
assertPaths := func(paths [][]blindedHop, expectedPaths []string) {
require.Len(t, paths, len(expectedPaths))

actualPaths := make(map[string]bool)

for _, path := range paths {
var label string
for _, hop := range path {
Expand All @@ -3764,12 +3773,25 @@ func TestFindBlindedPaths(t *testing.T) {

actualPaths[strings.TrimRight(label, ",")] = true
}
require.Len(t, paths, len(expectedPaths), "expected %v got %v",
expectedPaths, actualPaths)

for _, path := range expectedPaths {
require.True(t, actualPaths[path])
require.True(t, actualPaths[path], "expected %s in %v",
path, actualPaths)
}
}

nodePairChannel := func(alias1, alias2 string) uint64 {
node1 := ctx.keyFromAlias(alias1)
node2 := ctx.keyFromAlias(alias2)

channel, ok := ctx.testGraphInstance.channelIDs[node1][node2]
require.True(t, ok)

return channel
}

// 1) Restrict the min & max path length such that we only include paths
// with one hop other than the destination hop.
paths, err := ctx.findBlindedPaths(&blindedPathRestrictions{
Expand Down Expand Up @@ -3849,7 +3871,7 @@ func TestFindBlindedPaths(t *testing.T) {
"eve,bob,dave",
})

// 5) Finally, we will test the special case where the destination node
// 5) We will also test the special case where the destination node
// is also the recipient.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 0,
Expand All @@ -3860,4 +3882,103 @@ func TestFindBlindedPaths(t *testing.T) {
assertPaths(paths, []string{
"dave",
})

// 6) Now, we will test some cases where the user manually specifies
// the first few incoming channels of a route.
//
// 6.1) Let the user specify the B-D channel as the last hop with a
// max of 1 hops.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 1,
incomingChanSet: []uint64{
nodePairChannel("dave", "bob"),
},
})
require.NoError(t, err)

// If the max number of hops is 1, then only the B->D path is chosen
assertPaths(paths, []string{
"bob,dave",
})

// 6.2) Let the user specify the B-D channel as the last hop with a
// max of 2 hops.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 2,
incomingChanSet: []uint64{
nodePairChannel("dave", "bob"),
},
})
require.NoError(t, err)

// If the max number of hops is 2, then the following is expected:
// - B, D
// - F, B, D
// - E, B, D
assertPaths(paths, []string{
"bob,dave",
"frank,bob,dave",
"eve,bob,dave",
})

// 6.3) Users may specify channels to nodes that do not signal route
// blinding (like A). So if we specify the A-D channel, we should get
// valid paths.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 4,
incomingChanSet: []uint64{
nodePairChannel("dave", "alice"),
},
})
require.NoError(t, err)

// We expect the following paths:
// - A, D
// - F, A, D
// - B, F, A, D
// - E, B, F, A, D
assertPaths(paths, []string{
"alice,dave",
"frank,alice,dave",
"bob,frank,alice,dave",
"eve,bob,frank,alice,dave",
})

// 6.4) Assert that an error is returned if a user accidentally tries
// to force a circular path.
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 10,
incomingChanSet: []uint64{
nodePairChannel("dave", "alice"),
nodePairChannel("alice", "frank"),
nodePairChannel("frank", "bob"),
nodePairChannel("bob", "dave"),
},
})
require.ErrorContains(t, err, "circular route")

// 6.5) Test specifying a chain of incoming channels.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 4,
incomingChanSet: []uint64{
nodePairChannel("dave", "alice"),
nodePairChannel("alice", "frank"),
},
})
require.NoError(t, err)

// We expect the following paths:
// - F, A, D
// - B, F, A, D
// - E, B, F, A, D
assertPaths(paths, []string{
"frank,alice,dave",
"bob,frank,alice,dave",
"eve,bob,frank,alice,dave",
})
}
Loading
Loading