mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-10-09 21:13:28 +02:00
Merge pull request #9925 from ellemouton/rbIncomingFollowup
routing: clean-up & fix blinded path incoming chained channel logic
This commit is contained in:
@@ -37,7 +37,8 @@ circuit. The indices are only available for forwarding events saved after v0.20.
|
|||||||
|
|
||||||
|
|
||||||
* The `lncli addinvoice --blind` command now has the option to include a
|
* The `lncli addinvoice --blind` command now has the option to include a
|
||||||
[chained channels](https://github.com/lightningnetwork/lnd/pull/9127)
|
chained channels [1](https://github.com/lightningnetwork/lnd/pull/9127)
|
||||||
|
[2](https://github.com/lightningnetwork/lnd/pull/9925)
|
||||||
incoming list `--blinded_path_incoming_channel_list` which gives users the
|
incoming list `--blinded_path_incoming_channel_list` which gives users the
|
||||||
control of specifying the channels they prefer to receive the payment on. With
|
control of specifying the channels they prefer to receive the payment on. With
|
||||||
the option to specify multiple channels this control can be extended to
|
the option to specify multiple channels this control can be extended to
|
||||||
|
@@ -1428,25 +1428,55 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) {
|
|||||||
// pre-specified.
|
// pre-specified.
|
||||||
func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) {
|
func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) {
|
||||||
// Create a six hop network:
|
// Create a six hop network:
|
||||||
// Alice -> Bob -> Carol -> Dave -> Eve -> Frank.
|
// Alice -> Bob -> Carol -> Dave -> Eve -> Frank.
|
||||||
chanAmt := btcutil.Amount(100000)
|
// Carol will be the node generating invoices containing blinded paths.
|
||||||
cfgs := [][]string{nil, nil, nil, nil, nil, nil}
|
|
||||||
chanPoints, nodes := ht.CreateSimpleNetwork(
|
chanPoints, nodes := ht.CreateSimpleNetwork(
|
||||||
cfgs, lntest.OpenChannelParams{Amt: chanAmt},
|
[][]string{nil, nil, nil, nil, nil, nil},
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: chanAmt / 2,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
alice, bob, carol, dave, eve := nodes[0], nodes[1], nodes[2], nodes[3],
|
var (
|
||||||
nodes[4]
|
alice = nodes[0]
|
||||||
|
bob = nodes[1]
|
||||||
chanPointAliceBob, chanPointBobCarol, chanPointCarolDave :=
|
carol = nodes[2]
|
||||||
chanPoints[0], chanPoints[1], chanPoints[2]
|
dave = nodes[3]
|
||||||
|
eve = nodes[4]
|
||||||
|
frank = nodes[5]
|
||||||
|
)
|
||||||
|
|
||||||
// Lookup full channel info so that we have channel ids for our route.
|
// Lookup full channel info so that we have channel ids for our route.
|
||||||
aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob)
|
aliceBobChan := ht.GetChannelByChanPoint(alice, chanPoints[0])
|
||||||
bobCarolChan := ht.GetChannelByChanPoint(bob, chanPointBobCarol)
|
bobCarolChan := ht.GetChannelByChanPoint(bob, chanPoints[1])
|
||||||
carolDaveChan := ht.GetChannelByChanPoint(carol, chanPointCarolDave)
|
carolDaveChan := ht.GetChannelByChanPoint(carol, chanPoints[2])
|
||||||
|
|
||||||
// Let carol set no incoming channel restrictions.
|
// assertPathDetails is a helper function that asserts the details of
|
||||||
|
// the blinded paths returned in the invoice.
|
||||||
|
assertPathDetails := func(invoice *lnrpc.AddInvoiceResponse,
|
||||||
|
expPathLengths int, expIntroNodes ...[]byte) {
|
||||||
|
|
||||||
|
payReq := carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||||
|
paths := payReq.BlindedPaths
|
||||||
|
|
||||||
|
introNodes := make([][]byte, 0, len(paths))
|
||||||
|
for _, p := range paths {
|
||||||
|
require.Len(
|
||||||
|
ht, p.BlindedPath.BlindedHops, expPathLengths,
|
||||||
|
)
|
||||||
|
|
||||||
|
introNodes = append(
|
||||||
|
introNodes, p.BlindedPath.IntroductionNode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
require.ElementsMatch(ht, introNodes, expIntroNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let carol set no incoming channel restrictions. With a min of 1 hop,
|
||||||
|
// we expect the following blinded paths to be included:
|
||||||
|
// 1) Bob->Carol
|
||||||
|
// 2) Dave->Carol
|
||||||
var (
|
var (
|
||||||
minNumRealHops uint32 = 1
|
minNumRealHops uint32 = 1
|
||||||
numHops uint32 = 1
|
numHops uint32 = 1
|
||||||
@@ -1460,122 +1490,94 @@ func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) {
|
|||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
assertPathDetails(invoice, 2, bob.PubKey[:], dave.PubKey[:])
|
||||||
|
|
||||||
introNodesFound := make([][]byte, 0)
|
// Let carol choose an invalid incoming channel list for the blinded
|
||||||
introNodesExpected := [][]byte{bob.PubKey[:], dave.PubKey[:]}
|
// path. It is invalid since it does not connect to her node.
|
||||||
|
// Assert that the expected error is returned.
|
||||||
// Assert that it contains two blinded path with only 2 hops each one.
|
|
||||||
payReq := carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
|
||||||
require.Len(ht, payReq.BlindedPaths, 2)
|
|
||||||
path := payReq.BlindedPaths[0].BlindedPath
|
|
||||||
require.Len(ht, path.BlindedHops, 2)
|
|
||||||
introNodesFound = append(introNodesFound, path.IntroductionNode)
|
|
||||||
path = payReq.BlindedPaths[1].BlindedPath
|
|
||||||
require.Len(ht, path.BlindedHops, 2)
|
|
||||||
introNodesFound = append(introNodesFound, path.IntroductionNode)
|
|
||||||
|
|
||||||
// Assert the introduction nodes without caring about the routes order.
|
|
||||||
require.ElementsMatch(ht, introNodesExpected, introNodesFound)
|
|
||||||
|
|
||||||
// Let carol choose the wrong incoming channel.
|
|
||||||
var (
|
|
||||||
incomingChannelList = []uint64{aliceBobChan.ChanId}
|
|
||||||
)
|
|
||||||
|
|
||||||
err := fmt.Sprintf("incoming channel %v is not seen as owned by node",
|
|
||||||
aliceBobChan.ChanId)
|
|
||||||
|
|
||||||
carol.RPC.AddInvoiceAssertErr(
|
carol.RPC.AddInvoiceAssertErr(
|
||||||
&lnrpc.Invoice{
|
&lnrpc.Invoice{
|
||||||
Memo: "test",
|
Memo: "test",
|
||||||
ValueMsat: 10_000_000,
|
ValueMsat: 10_000_000,
|
||||||
IsBlinded: true,
|
IsBlinded: true,
|
||||||
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
MinNumRealHops: &minNumRealHops,
|
MinNumRealHops: &minNumRealHops,
|
||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
IncomingChannelList: incomingChannelList,
|
IncomingChannelList: []uint64{
|
||||||
|
aliceBobChan.ChanId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err,
|
fmt.Sprintf("incoming channel %v is not seen as owned by node",
|
||||||
|
aliceBobChan.ChanId),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Let Carol set the incoming channel list greater than minimum number
|
// Let Carol set the incoming channel list greater than minimum number
|
||||||
// of real hops.
|
// of real hops.
|
||||||
incomingChannelList = []uint64{aliceBobChan.ChanId, bobCarolChan.ChanId}
|
// Assert that the expected error is returned.
|
||||||
err = fmt.Sprintf("minimum number of blinded path hops (%d) must be "+
|
|
||||||
"greater than or equal to the number of hops specified on "+
|
|
||||||
"the chained channels (%d)", minNumRealHops,
|
|
||||||
len(incomingChannelList),
|
|
||||||
)
|
|
||||||
|
|
||||||
carol.RPC.AddInvoiceAssertErr(
|
carol.RPC.AddInvoiceAssertErr(
|
||||||
&lnrpc.Invoice{
|
&lnrpc.Invoice{
|
||||||
Memo: "test",
|
Memo: "test",
|
||||||
ValueMsat: 10_000_000,
|
ValueMsat: 10_000_000,
|
||||||
IsBlinded: true,
|
IsBlinded: true,
|
||||||
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
MinNumRealHops: &minNumRealHops,
|
MinNumRealHops: &minNumRealHops,
|
||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
IncomingChannelList: incomingChannelList,
|
IncomingChannelList: []uint64{
|
||||||
|
aliceBobChan.ChanId,
|
||||||
|
bobCarolChan.ChanId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err,
|
"must be greater than or equal to the number of hops "+
|
||||||
|
"specified on the chained channels",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Let Carol choose an incoming channel that points to a node in the
|
// Let Carol choose an incoming channel that points to a node in the
|
||||||
// omission set.
|
// omission set.
|
||||||
incomingChannelList = []uint64{bobCarolChan.ChanId}
|
|
||||||
var nodeOmissionList [][]byte
|
|
||||||
nodeOmissionList = append(nodeOmissionList, bob.PubKey[:])
|
|
||||||
|
|
||||||
err = fmt.Sprintf("cannot simultaneously be included in the omission " +
|
|
||||||
"set and in the partially specified path",
|
|
||||||
)
|
|
||||||
|
|
||||||
carol.RPC.AddInvoiceAssertErr(
|
carol.RPC.AddInvoiceAssertErr(
|
||||||
&lnrpc.Invoice{
|
&lnrpc.Invoice{
|
||||||
Memo: "test",
|
Memo: "test",
|
||||||
ValueMsat: 10_000_000,
|
ValueMsat: 10_000_000,
|
||||||
IsBlinded: true,
|
IsBlinded: true,
|
||||||
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
MinNumRealHops: &minNumRealHops,
|
MinNumRealHops: &minNumRealHops,
|
||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
IncomingChannelList: incomingChannelList,
|
IncomingChannelList: []uint64{
|
||||||
NodeOmissionList: nodeOmissionList,
|
bobCarolChan.ChanId,
|
||||||
|
},
|
||||||
|
NodeOmissionList: [][]byte{
|
||||||
|
bob.PubKey[:],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err,
|
"cannot simultaneously be included in the omission set and "+
|
||||||
|
"in the partially specified path",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Let carol restrict bob as incoming channel.
|
// Let carol restrict bob as incoming channel.
|
||||||
incomingChannelList = []uint64{bobCarolChan.ChanId}
|
|
||||||
|
|
||||||
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||||
Memo: "test",
|
Memo: "test",
|
||||||
ValueMsat: 10_000_000,
|
ValueMsat: 10_000_000,
|
||||||
IsBlinded: true,
|
IsBlinded: true,
|
||||||
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
MinNumRealHops: &minNumRealHops,
|
MinNumRealHops: &minNumRealHops,
|
||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
IncomingChannelList: incomingChannelList,
|
IncomingChannelList: []uint64{
|
||||||
|
bobCarolChan.ChanId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Assert that it contains a single blinded path with only
|
// Assert that it contains a single blinded path with only
|
||||||
// 2 hops, with bob as the introduction node.
|
// 2 hops, with bob as the introduction node.
|
||||||
payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
assertPathDetails(invoice, 2, bob.PubKey[:])
|
||||||
require.Len(ht, payReq.BlindedPaths, 1)
|
|
||||||
path = payReq.BlindedPaths[0].BlindedPath
|
|
||||||
require.Len(ht, path.BlindedHops, 2)
|
|
||||||
require.EqualValues(ht, path.IntroductionNode, bob.PubKey[:])
|
|
||||||
|
|
||||||
// Now let alice pay the invoice.
|
// Check that Alice can pay the invoice.
|
||||||
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
|
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
|
||||||
|
|
||||||
// Let carol restrict dave as incoming channel and max Hops as 2
|
// Let carol restrict dave as incoming channel and max hops as 2.
|
||||||
numHops = 2
|
numHops = 2
|
||||||
incomingChannelList = []uint64{carolDaveChan.ChanId}
|
|
||||||
|
|
||||||
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||||
Memo: "test",
|
Memo: "test",
|
||||||
ValueMsat: 10_000_000,
|
ValueMsat: 10_000_000,
|
||||||
@@ -1583,16 +1585,15 @@ func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) {
|
|||||||
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
MinNumRealHops: &minNumRealHops,
|
MinNumRealHops: &minNumRealHops,
|
||||||
NumHops: &numHops,
|
NumHops: &numHops,
|
||||||
IncomingChannelList: incomingChannelList,
|
IncomingChannelList: []uint64{carolDaveChan.ChanId},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Assert that it contains one path with 3 hops, with dave as the
|
// Assert that it contains two paths: one 3 hop one starting at Eve and
|
||||||
// introduction node. The path alice -> bob -> carol is discarded
|
// a 3 hop one starting at Dave (this one will be padded with a dummy
|
||||||
// because alice is a dead-end.
|
// hop) in order to keep all the paths equidistant.
|
||||||
payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
assertPathDetails(invoice, 3, eve.PubKey[:], dave.PubKey[:])
|
||||||
require.Len(ht, payReq.BlindedPaths, 1)
|
|
||||||
path = payReq.BlindedPaths[0].BlindedPath
|
// Check that Frank can pay the invoice.
|
||||||
require.Len(ht, path.BlindedHops, 3)
|
ht.CompletePaymentRequests(frank, []string{invoice.PaymentRequest})
|
||||||
require.EqualValues(ht, path.IntroductionNode, eve.PubKey[:])
|
|
||||||
}
|
}
|
||||||
|
@@ -1227,24 +1227,46 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// The target node is always the last hop in the path.
|
// The target node is always the last hop in the path, and so
|
||||||
incomingPath = []blindedHop{{vertex: target}}
|
// we add it to the incoming path from the get-go. Any additions
|
||||||
whiteListedNodes = map[route.Vertex]bool{target: true}
|
// to the slice should be prepended.
|
||||||
|
incomingPath = []blindedHop{{
|
||||||
|
vertex: target,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// supportsRouteBlinding is a list of nodes that we can assume
|
||||||
|
// support route blinding without needing to rely on the feature
|
||||||
|
// bits announced in their node announcement. Since we are
|
||||||
|
// finding a path to the target node, we can assume it supports
|
||||||
|
// route blinding.
|
||||||
|
supportsRouteBlinding = map[route.Vertex]bool{
|
||||||
|
target: true,
|
||||||
|
}
|
||||||
|
|
||||||
visited = make(map[route.Vertex]bool)
|
visited = make(map[route.Vertex]bool)
|
||||||
errChanFound = errors.New("found incoming channel")
|
|
||||||
nextTarget = target
|
nextTarget = target
|
||||||
|
haveIncomingPath = len(restrictions.incomingChainedChannels) > 0
|
||||||
|
|
||||||
|
// errChanFound is an error variable we return from the DB
|
||||||
|
// iteration call below when we have found the channel we are
|
||||||
|
// looking for. This lets us exit the iteration early.
|
||||||
|
errChanFound = errors.New("found incoming channel")
|
||||||
)
|
)
|
||||||
for _, chanID := range restrictions.incomingChainedChannels {
|
for _, chanID := range restrictions.incomingChainedChannels {
|
||||||
|
// Mark that we have visited this node so that we don't revisit
|
||||||
|
// it later on when we call "processNodeForBlindedPath".
|
||||||
visited[nextTarget] = true
|
visited[nextTarget] = true
|
||||||
|
|
||||||
err := g.ForEachNodeDirectedChannel(nextTarget,
|
err := g.ForEachNodeDirectedChannel(nextTarget,
|
||||||
func(channel *graphdb.DirectedChannel) error {
|
func(channel *graphdb.DirectedChannel) error {
|
||||||
// Not the right channel, continue to the node's
|
// This is not the right channel, continue to
|
||||||
// other channels.
|
// the node's other channels.
|
||||||
if channel.ChannelID != chanID {
|
if channel.ChannelID != chanID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We found the channel in question. Prepend it
|
||||||
|
// to the incoming path.
|
||||||
incomingPath = append([]blindedHop{
|
incomingPath = append([]blindedHop{
|
||||||
{
|
{
|
||||||
vertex: channel.OtherNode,
|
vertex: channel.OtherNode,
|
||||||
@@ -1259,39 +1281,37 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
return errChanFound
|
return errChanFound
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err == nil {
|
// We expect errChanFound to be returned if the channel in
|
||||||
|
// question was found.
|
||||||
|
if !errors.Is(err, errChanFound) && err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err == nil {
|
||||||
return nil, fmt.Errorf("incoming channel %d is not "+
|
return nil, fmt.Errorf("incoming channel %d is not "+
|
||||||
"seen as owned by node %v", chanID, nextTarget)
|
"seen as owned by node %v", chanID, nextTarget)
|
||||||
}
|
}
|
||||||
if !errors.Is(err, errChanFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the user didn't accidentally add a channel that
|
// Check that the user didn't accidentally add a channel that
|
||||||
// is owned by a node in the node omission set
|
// is owned by a node in the node omission set.
|
||||||
if restrictions.nodeOmissionSet.Contains(nextTarget) {
|
if restrictions.nodeOmissionSet.Contains(nextTarget) {
|
||||||
return nil, fmt.Errorf("node %v cannot simultaneously "+
|
return nil, fmt.Errorf("node %v cannot simultaneously "+
|
||||||
"be included in the omission set and in the "+
|
"be included in the omission set and in the "+
|
||||||
"partially specified path", nextTarget)
|
"partially specified path", nextTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
if whiteListedNodes[nextTarget] {
|
// Check that we have not already visited the next target node
|
||||||
|
// since this would mean a circular incoming path.
|
||||||
|
if visited[nextTarget] {
|
||||||
return nil, fmt.Errorf("a circular route cannot be " +
|
return nil, fmt.Errorf("a circular route cannot be " +
|
||||||
"specified for the incoming blinded path")
|
"specified for the incoming blinded path")
|
||||||
}
|
}
|
||||||
whiteListedNodes[nextTarget] = true
|
|
||||||
|
supportsRouteBlinding[nextTarget] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the node is not the destination node, then it is required that the
|
// A helper closure which checks if the node in question has advertised
|
||||||
// node advertise the route blinding feature-bit in order for it to be
|
// that it supports route blinding.
|
||||||
// chosen as a node on the blinded path.
|
nodeSupportsRouteBlinding := func(node route.Vertex) (bool, error) {
|
||||||
// We skip checking the target node, as accepting blinded payments
|
if supportsRouteBlinding[node] {
|
||||||
// (via invoice) doesn't imply support for routing them (via node
|
|
||||||
// announcement).
|
|
||||||
// We skip checking incomingChainedChannels nodes, as we might not yet
|
|
||||||
// have an updated node announcement for them.
|
|
||||||
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
|
|
||||||
if whiteListedNodes[node] {
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,39 +1328,46 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
// conditions such as: The maxHops number being reached or reaching
|
// conditions such as: The maxHops number being reached or reaching
|
||||||
// a node that doesn't have any other edges - in that final case, the
|
// a node that doesn't have any other edges - in that final case, the
|
||||||
// whole path should be ignored.
|
// whole path should be ignored.
|
||||||
|
//
|
||||||
|
// NOTE: any paths returned will end at the "nextTarget" node meaning
|
||||||
|
// that if we have a fixed list of incoming chained channels, then this
|
||||||
|
// fixed list must be appended to any of the returned paths.
|
||||||
paths, _, err := processNodeForBlindedPath(
|
paths, _, err := processNodeForBlindedPath(
|
||||||
g, nextTarget, supportsRouteBlinding, visited, restrictions,
|
g, nextTarget, nodeSupportsRouteBlinding, visited, restrictions,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
orderedPaths := make([][]blindedHop, len(paths))
|
// Reverse each path so that the order is correct (from introduction
|
||||||
|
// node to last hop node) and then append the incoming path (if any was
|
||||||
|
// specified) to the end of the path.
|
||||||
|
orderedPaths := make([][]blindedHop, 0, len(paths))
|
||||||
|
for _, path := range paths {
|
||||||
|
sort.Slice(path, func(i, j int) bool {
|
||||||
|
return j < i
|
||||||
|
})
|
||||||
|
|
||||||
// When there is no path to add, but incomingChainedChannels can be
|
orderedPaths = append(
|
||||||
// used.
|
orderedPaths, append(path, incomingPath...),
|
||||||
lenChainedChannels := int8(len(restrictions.incomingChainedChannels))
|
)
|
||||||
if len(paths) == 0 && lenChainedChannels > 0 {
|
}
|
||||||
orderedPaths = [][]blindedHop{incomingPath}
|
|
||||||
} else {
|
|
||||||
// Reverse each path so that the order is correct (from
|
|
||||||
// introduction node to last hop node) and then append this node
|
|
||||||
// on as the destination of each path.
|
|
||||||
for i, path := range paths {
|
|
||||||
sort.Slice(path, func(i, j int) bool {
|
|
||||||
return j < i
|
|
||||||
})
|
|
||||||
|
|
||||||
orderedPaths[i] = append(path, incomingPath...)
|
// There is a chance we have an incoming path that by itself satisfies
|
||||||
}
|
// the minimum hop restriction. In that case, we add it as its own path.
|
||||||
|
if haveIncomingPath &&
|
||||||
|
len(incomingPath) > int(restrictions.minNumHops) {
|
||||||
|
|
||||||
|
orderedPaths = append(orderedPaths, incomingPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the special case that allows a blinded path with the
|
// Handle the special case that allows a blinded path with the
|
||||||
// introduction node as the destination node.
|
// introduction node as the destination node. This only applies if no
|
||||||
if restrictions.minNumHops == 0 && lenChainedChannels == 0 {
|
// incoming path was specified since in that case, by definition, the
|
||||||
|
// caller wants a non-zero length blinded path.
|
||||||
|
if restrictions.minNumHops == 0 && !haveIncomingPath {
|
||||||
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
||||||
|
|
||||||
//nolint:makezero
|
|
||||||
orderedPaths = append(
|
orderedPaths = append(
|
||||||
orderedPaths, singleHopPath...,
|
orderedPaths, singleHopPath...,
|
||||||
)
|
)
|
||||||
|
@@ -538,6 +538,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
|
|
||||||
aliasMap := make(map[string]route.Vertex)
|
aliasMap := make(map[string]route.Vertex)
|
||||||
privKeyMap := make(map[string]*btcec.PrivateKey)
|
privKeyMap := make(map[string]*btcec.PrivateKey)
|
||||||
|
channelIDs := make(map[route.Vertex]map[route.Vertex]uint64)
|
||||||
|
|
||||||
nodeIndex := byte(0)
|
nodeIndex := byte(0)
|
||||||
addNodeWithAlias := func(alias string, features *lnwire.FeatureVector) (
|
addNodeWithAlias := func(alias string, features *lnwire.FeatureVector) (
|
||||||
@@ -652,6 +653,16 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
node1Vertex, node2Vertex = node2Vertex, node1Vertex
|
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
|
// We first insert the existence of the edge between the two
|
||||||
// nodes.
|
// nodes.
|
||||||
edgeInfo := models.ChannelEdgeInfo{
|
edgeInfo := models.ChannelEdgeInfo{
|
||||||
@@ -765,6 +776,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
|
|||||||
mcBackend: graphBackend,
|
mcBackend: graphBackend,
|
||||||
aliasMap: aliasMap,
|
aliasMap: aliasMap,
|
||||||
privKeyMap: privKeyMap,
|
privKeyMap: privKeyMap,
|
||||||
|
channelIDs: channelIDs,
|
||||||
links: links,
|
links: links,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -3192,6 +3204,16 @@ func newPathFindingTestContext(t *testing.T, useCache bool,
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *pathFindingTestContext) nodePairChannel(alias1, alias2 string) uint64 {
|
||||||
|
node1 := c.keyFromAlias(alias1)
|
||||||
|
node2 := c.keyFromAlias(alias2)
|
||||||
|
|
||||||
|
channel, ok := c.testGraphInstance.channelIDs[node1][node2]
|
||||||
|
require.True(c.t, ok)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
func (c *pathFindingTestContext) keyFromAlias(alias string) route.Vertex {
|
func (c *pathFindingTestContext) keyFromAlias(alias string) route.Vertex {
|
||||||
return c.testGraphInstance.aliasMap[alias]
|
return c.testGraphInstance.aliasMap[alias]
|
||||||
}
|
}
|
||||||
@@ -3876,7 +3898,7 @@ func TestFindBlindedPaths(t *testing.T) {
|
|||||||
"eve,bob,dave",
|
"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.
|
// is also the recipient.
|
||||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
minNumHops: 0,
|
minNumHops: 0,
|
||||||
@@ -3888,93 +3910,138 @@ func TestFindBlindedPaths(t *testing.T) {
|
|||||||
"dave",
|
"dave",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 6) Restrict the min & max path length such that we only include paths
|
// 6) Now, we will test some cases where the user manually specifies
|
||||||
// with one being only the intro-node and the others with one hop other
|
// the first few incoming channels of a route.
|
||||||
// than the destination hop.
|
//
|
||||||
|
// 6.1) Let the user specify the B-D channel as the last hop with a
|
||||||
|
// max of 1 hop.
|
||||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
minNumHops: 0,
|
minNumHops: 1,
|
||||||
maxNumHops: 1,
|
maxNumHops: 1,
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("bob", "dave"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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) Extend the search to include 2 hops along with the B-D channel
|
||||||
|
// restriction.
|
||||||
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 1,
|
||||||
|
maxNumHops: 2,
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("bob", "dave"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// We expect the following paths:
|
// We expect the following paths:
|
||||||
// - D
|
|
||||||
// - B, D
|
// - B, D
|
||||||
// - C, D
|
|
||||||
// The (A, D) path is not chosen since A does not advertise the route
|
|
||||||
// blinding feature bit. The (G, D) path is not chosen since G does not
|
|
||||||
// have any other known channels.
|
|
||||||
assertPaths(paths, []string{
|
|
||||||
"dave",
|
|
||||||
"bob,dave",
|
|
||||||
"charlie,dave",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 7) Restrict the min & max path length such that we only include paths
|
|
||||||
// with one hop other than the destination hop. Now with bob-dave as the
|
|
||||||
// incoming channel.
|
|
||||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
||||||
minNumHops: 1,
|
|
||||||
maxNumHops: 1,
|
|
||||||
incomingChainedChannels: []uint64{2},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Expect only B->D path.
|
|
||||||
assertPaths(paths, []string{
|
|
||||||
"bob,dave",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 8) Extend the search to include 2 hops other than the destination,
|
|
||||||
// with bob-dave as the incoming channel.
|
|
||||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
||||||
minNumHops: 1,
|
|
||||||
maxNumHops: 2,
|
|
||||||
incomingChainedChannels: []uint64{2},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// We expect the following paths:
|
|
||||||
// - F, B, D
|
// - F, B, D
|
||||||
// - E, B, D
|
// - E, B, D
|
||||||
assertPaths(paths, []string{
|
assertPaths(paths, []string{
|
||||||
|
"bob,dave",
|
||||||
"frank,bob,dave",
|
"frank,bob,dave",
|
||||||
"eve,bob,dave",
|
"eve,bob,dave",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 9) Extend the search even further and also increase the minimum path
|
// 6.3) Repeat the above test but instruct the function to never use
|
||||||
// length, but this time with charlie-dave as the incoming channel.
|
// bob. This should fail since bob owns one of the channels in the
|
||||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
// partially specified path.
|
||||||
minNumHops: 2,
|
|
||||||
maxNumHops: 3,
|
|
||||||
incomingChainedChannels: []uint64{3},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// We expect the following paths:
|
|
||||||
// - E, C, D
|
|
||||||
// - B, E, C, D
|
|
||||||
assertPaths(paths, []string{
|
|
||||||
"eve,charlie,dave",
|
|
||||||
"bob,eve,charlie,dave",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 10) Repeat the above test but instruct the function to never use
|
|
||||||
// charlie.
|
|
||||||
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
minNumHops: 2,
|
minNumHops: 1,
|
||||||
maxNumHops: 3,
|
maxNumHops: 2,
|
||||||
nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("charlie")),
|
nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("bob")),
|
||||||
incomingChainedChannels: []uint64{3},
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("bob", "dave"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
require.ErrorContains(t, err, "cannot simultaneously be included in "+
|
require.ErrorContains(t, err, "cannot simultaneously be included in "+
|
||||||
"the omission set and in the partially specified path")
|
"the omission set and in the partially specified path")
|
||||||
|
|
||||||
// 11) Test the circular route error.
|
// 6.4) Repeat it again but this time omit frank and demonstrate that
|
||||||
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
// the resulting set contains all the results from 6.2 except for the
|
||||||
minNumHops: 2,
|
// frank path.
|
||||||
maxNumHops: 3,
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
incomingChainedChannels: []uint64{2, 7, 6, 3},
|
minNumHops: 1,
|
||||||
|
maxNumHops: 2,
|
||||||
|
nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("frank")),
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("bob", "dave"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// We expect the following paths:
|
||||||
|
// - B, D
|
||||||
|
// - E, B, D
|
||||||
|
assertPaths(paths, []string{
|
||||||
|
"bob,dave",
|
||||||
|
"eve,bob,dave",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 6.5) 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,
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.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.6) Assert that an error is returned if a user accidentally tries
|
||||||
|
// to force a circular path.
|
||||||
|
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 2,
|
||||||
|
maxNumHops: 3,
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("dave", "alice"),
|
||||||
|
ctx.nodePairChannel("alice", "frank"),
|
||||||
|
ctx.nodePairChannel("frank", "bob"),
|
||||||
|
ctx.nodePairChannel("bob", "dave"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "circular route")
|
||||||
|
|
||||||
|
// 6.7) Test specifying a chain of incoming channels. We specify
|
||||||
|
// the following incoming list: [A->D, F->A].
|
||||||
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 1,
|
||||||
|
maxNumHops: 4,
|
||||||
|
incomingChainedChannels: []uint64{
|
||||||
|
ctx.nodePairChannel("dave", "alice"),
|
||||||
|
ctx.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",
|
||||||
})
|
})
|
||||||
require.ErrorContains(t, err, "a circular route cannot be specified")
|
|
||||||
}
|
}
|
||||||
|
@@ -699,8 +699,8 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||||||
"path since it resulted in an low "+
|
"path since it resulted in an low "+
|
||||||
"probability path(%.3f)",
|
"probability path(%.3f)",
|
||||||
route.ChanIDString(routeWithProbability.route),
|
route.ChanIDString(routeWithProbability.route),
|
||||||
routeWithProbability.probability,
|
routeWithProbability.probability)
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user