Merge pull request #9127 from MPins/issue-8993
Add the option on path creator to specify the incoming channel on blinded path
This commit is contained in:
15 files changed
+1694
-1197
No files matched your search
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
@@ -116,6 +117,12 @@ var AddInvoiceCommand = cli.Command{
|
|||||||
"use on a blinded path. The flag may be " +
|
"use on a blinded path. The flag may be " +
|
||||||
"specified multiple times.",
|
"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),
|
Action: actionDecorator(addInvoice),
|
||||||
}
|
}
|
||||||
@@ -202,7 +209,8 @@ func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
|
|||||||
if ctx.IsSet("min_real_blinded_hops") ||
|
if ctx.IsSet("min_real_blinded_hops") ||
|
||||||
ctx.IsSet("num_blinded_hops") ||
|
ctx.IsSet("num_blinded_hops") ||
|
||||||
ctx.IsSet("max_blinded_paths") ||
|
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 " +
|
return nil, fmt.Errorf("blinded path options are " +
|
||||||
"only used if the `--blind` options is set")
|
"only used if the `--blind` options is set")
|
||||||
@@ -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
|
return &blindCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
|
|
||||||
## RPC Additions
|
## RPC Additions
|
||||||
|
|
||||||
|
* The `lncli addinvoice --blind` command now has the option to include a
|
||||||
|
[chained channels](https://github.com/lightningnetwork/lnd/pull/9127)
|
||||||
|
incoming list `--blinded_path_incoming_channel_list` which gives users the
|
||||||
|
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
|
||||||
|
multiple hops leading to the node.
|
||||||
|
|
||||||
## lncli Additions
|
## lncli Additions
|
||||||
|
|
||||||
* [`lncli sendpayment` and `lncli queryroutes` now support the
|
* [`lncli sendpayment` and `lncli queryroutes` now support the
|
||||||
@@ -86,3 +93,5 @@
|
|||||||
## Tooling and Documentation
|
## Tooling and Documentation
|
||||||
|
|
||||||
# Contributors (Alphabetical Order)
|
# Contributors (Alphabetical Order)
|
||||||
|
|
||||||
|
Pins
|
||||||
@@ -707,6 +707,10 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "bump fee low budget",
|
Name: "bump fee low budget",
|
||||||
TestFunc: testBumpFeeLowBudget,
|
TestFunc: testBumpFeeLowBudget,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "partially specified route blinded invoice",
|
||||||
|
TestFunc: testPartiallySpecifiedBlindedPath,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendPrefixed is used to add a prefix to each test name in the subtests
|
// appendPrefixed is used to add a prefix to each test name in the subtests
|
||||||
|
|||||||
@@ -1420,3 +1420,179 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) {
|
|||||||
require.Fail(ht, "timeout waiting for sending payment")
|
require.Fail(ht, "timeout waiting for sending payment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testPartiallySpecifiedBlindedPath tests lnd's ability to:
|
||||||
|
// - Assert the error when attempting to create a blinded payment with an
|
||||||
|
// invalid partially specified path.
|
||||||
|
// - Create a blinded payment path when the blinded path is partially
|
||||||
|
// pre-specified.
|
||||||
|
func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) {
|
||||||
|
// Create a six hop network:
|
||||||
|
// Alice -> Bob -> Carol -> Dave -> Eve -> Frank.
|
||||||
|
chanAmt := btcutil.Amount(100000)
|
||||||
|
cfgs := [][]string{nil, nil, nil, nil, nil, nil}
|
||||||
|
chanPoints, nodes := ht.CreateSimpleNetwork(
|
||||||
|
cfgs, lntest.OpenChannelParams{Amt: chanAmt},
|
||||||
|
)
|
||||||
|
|
||||||
|
alice, bob, carol, dave, eve := nodes[0], nodes[1], nodes[2], nodes[3],
|
||||||
|
nodes[4]
|
||||||
|
|
||||||
|
chanPointAliceBob, chanPointBobCarol, chanPointCarolDave :=
|
||||||
|
chanPoints[0], chanPoints[1], chanPoints[2]
|
||||||
|
|
||||||
|
// Lookup full channel info so that we have channel ids for our route.
|
||||||
|
aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob)
|
||||||
|
bobCarolChan := ht.GetChannelByChanPoint(bob, chanPointBobCarol)
|
||||||
|
carolDaveChan := ht.GetChannelByChanPoint(carol, chanPointCarolDave)
|
||||||
|
|
||||||
|
// Let carol set no incoming channel restrictions.
|
||||||
|
var (
|
||||||
|
minNumRealHops uint32 = 1
|
||||||
|
numHops uint32 = 1
|
||||||
|
)
|
||||||
|
invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
introNodesFound := make([][]byte, 0)
|
||||||
|
introNodesExpected := [][]byte{bob.PubKey[:], dave.PubKey[:]}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
IncomingChannelList: incomingChannelList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Let Carol set the incoming channel list greater than minimum number
|
||||||
|
// of real hops.
|
||||||
|
incomingChannelList = []uint64{aliceBobChan.ChanId, bobCarolChan.ChanId}
|
||||||
|
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(
|
||||||
|
&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
IncomingChannelList: incomingChannelList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Let Carol choose an incoming channel that points to a node in the
|
||||||
|
// 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(
|
||||||
|
&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
IncomingChannelList: incomingChannelList,
|
||||||
|
NodeOmissionList: nodeOmissionList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Let carol restrict bob as incoming channel.
|
||||||
|
incomingChannelList = []uint64{bobCarolChan.ChanId}
|
||||||
|
|
||||||
|
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
IncomingChannelList: incomingChannelList,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assert that it contains a single blinded path with only
|
||||||
|
// 2 hops, with bob as the introduction node.
|
||||||
|
payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||||
|
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.
|
||||||
|
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
|
||||||
|
|
||||||
|
// Let carol restrict dave as incoming channel and max Hops as 2
|
||||||
|
numHops = 2
|
||||||
|
incomingChannelList = []uint64{carolDaveChan.ChanId}
|
||||||
|
|
||||||
|
invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{
|
||||||
|
Memo: "test",
|
||||||
|
ValueMsat: 10_000_000,
|
||||||
|
IsBlinded: true,
|
||||||
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
||||||
|
MinNumRealHops: &minNumRealHops,
|
||||||
|
NumHops: &numHops,
|
||||||
|
IncomingChannelList: incomingChannelList,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assert that it contains one path with 3 hops, with dave as the
|
||||||
|
// introduction node. The path alice -> bob -> carol is discarded
|
||||||
|
// because alice is a dead-end.
|
||||||
|
payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||||
|
require.Len(ht, payReq.BlindedPaths, 1)
|
||||||
|
path = payReq.BlindedPaths[0].BlindedPath
|
||||||
|
require.Len(ht, path.BlindedHops, 3)
|
||||||
|
require.EqualValues(ht, path.IntroductionNode, eve.PubKey[:])
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@ func (r *Routing) Validate() error {
|
|||||||
"number of hops expected to be included in each path")
|
"number of hops expected to be included in each path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.BlindedPaths.MaxNumPaths == 0 {
|
||||||
|
return fmt.Errorf("blinded max num paths cannot be 0")
|
||||||
|
}
|
||||||
|
|
||||||
if r.BlindedPaths.PolicyIncreaseMultiplier < 1 {
|
if r.BlindedPaths.PolicyIncreaseMultiplier < 1 {
|
||||||
return fmt.Errorf("the blinded route policy increase " +
|
return fmt.Errorf("the blinded route policy increase " +
|
||||||
"multiplier must be greater than or equal to 1")
|
"multiplier must be greater than or equal to 1")
|
||||||
|
|||||||
@@ -534,6 +534,14 @@
|
|||||||
"format": "byte"
|
"format": "byte"
|
||||||
},
|
},
|
||||||
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
|
"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"
|
||||||
|
},
|
||||||
|
"description": "The chained channels list specified via channel id (separated by commas),\nstarting from a channel owned by the receiver node."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1188
-1174
File diff suppressed because it is too large.
Load diff
@@ -4002,6 +4002,12 @@ message BlindedPathConfig {
|
|||||||
blinded paths.
|
blinded paths.
|
||||||
*/
|
*/
|
||||||
repeated bytes node_omission_list = 4;
|
repeated bytes node_omission_list = 4;
|
||||||
|
|
||||||
|
/*
|
||||||
|
The chained channels list specified via channel id (separated by commas),
|
||||||
|
starting from a channel owned by the receiver node.
|
||||||
|
*/
|
||||||
|
repeated uint64 incoming_channel_list = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InvoiceHTLCState {
|
enum InvoiceHTLCState {
|
||||||
|
|||||||
@@ -3688,6 +3688,14 @@
|
|||||||
"format": "byte"
|
"format": "byte"
|
||||||
},
|
},
|
||||||
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
|
"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"
|
||||||
|
},
|
||||||
|
"description": "The chained channels list specified via channel id (separated by commas),\nstarting from a channel owned by the receiver node."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,6 +293,16 @@ func (h *HarnessRPC) AddInvoice(req *lnrpc.Invoice) *lnrpc.AddInvoiceResponse {
|
|||||||
return invoice
|
return invoice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddInvoiceAssertErr makes a RPC call to AddInvoice and asserts an error
|
||||||
|
// has returned with a specific error message.
|
||||||
|
func (h *HarnessRPC) AddInvoiceAssertErr(req *lnrpc.Invoice, errStr string) {
|
||||||
|
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := h.LN.AddInvoice(ctxt, req)
|
||||||
|
require.ErrorContains(h, err, errStr)
|
||||||
|
}
|
||||||
|
|
||||||
// AbandonChannel makes a RPC call to AbandonChannel and asserts.
|
// AbandonChannel makes a RPC call to AbandonChannel and asserts.
|
||||||
func (h *HarnessRPC) AbandonChannel(
|
func (h *HarnessRPC) AbandonChannel(
|
||||||
req *lnrpc.AbandonChannelRequest) *lnrpc.AbandonChannelResponse {
|
req *lnrpc.AbandonChannelRequest) *lnrpc.AbandonChannelResponse {
|
||||||
|
|||||||
+85
-11
@@ -1192,6 +1192,11 @@ type blindedPathRestrictions struct {
|
|||||||
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
||||||
// ignore during blinded path selection.
|
// ignore during blinded path selection.
|
||||||
nodeOmissionSet fn.Set[route.Vertex]
|
nodeOmissionSet fn.Set[route.Vertex]
|
||||||
|
|
||||||
|
// incomingChainedChannels holds the chained channels list specified
|
||||||
|
// via scid (short channel id) starting from a channel which points to
|
||||||
|
// the receiver node.
|
||||||
|
incomingChainedChannels []uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// blindedHop holds the information about a hop we have selected for a blinded
|
// blindedHop holds the information about a hop we have selected for a blinded
|
||||||
@@ -1221,11 +1226,72 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
restrictions.minNumHops)
|
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.incomingChainedChannels {
|
||||||
|
visited[nextTarget] = true
|
||||||
|
|
||||||
|
err := g.ForEachNodeDirectedChannel(nextTarget,
|
||||||
|
func(channel *graphdb.DirectedChannel) error {
|
||||||
|
// 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 err == nil {
|
||||||
|
return nil, fmt.Errorf("incoming channel %d is not "+
|
||||||
|
"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
|
||||||
|
// 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
|
// 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
|
// node advertise the route blinding feature-bit in order for it to be
|
||||||
// chosen as a node on the blinded path.
|
// chosen as a node on the blinded path.
|
||||||
|
// We skip checking the target node, as accepting blinded payments
|
||||||
|
// (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) {
|
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
|
||||||
if node == target {
|
if whiteListedNodes[node] {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1243,27 +1309,35 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
// 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.
|
||||||
paths, _, err := processNodeForBlindedPath(
|
paths, _, err := processNodeForBlindedPath(
|
||||||
g, target, supportsRouteBlinding, nil, restrictions,
|
g, nextTarget, supportsRouteBlinding, visited, restrictions,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
|
||||||
orderedPaths := make([][]blindedHop, len(paths))
|
orderedPaths := make([][]blindedHop, len(paths))
|
||||||
for i, path := range paths {
|
|
||||||
sort.Slice(path, func(i, j int) bool {
|
|
||||||
return j < i
|
|
||||||
})
|
|
||||||
|
|
||||||
orderedPaths[i] = append(path, blindedHop{vertex: target})
|
// When there is no path to add, but incomingChainedChannels can be
|
||||||
|
// used.
|
||||||
|
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...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
if restrictions.minNumHops == 0 {
|
if restrictions.minNumHops == 0 && lenChainedChannels == 0 {
|
||||||
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
||||||
|
|
||||||
//nolint:makezero
|
//nolint:makezero
|
||||||
|
|||||||
@@ -3855,4 +3855,94 @@ func TestFindBlindedPaths(t *testing.T) {
|
|||||||
assertPaths(paths, []string{
|
assertPaths(paths, []string{
|
||||||
"dave",
|
"dave",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 6) Restrict the min & max path length such that we only include paths
|
||||||
|
// with one being only the intro-node and the others with one hop other
|
||||||
|
// than the destination hop.
|
||||||
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 0,
|
||||||
|
maxNumHops: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// We expect the following paths:
|
||||||
|
// - 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
|
||||||
|
// - E, B, D
|
||||||
|
assertPaths(paths, []string{
|
||||||
|
"frank,bob,dave",
|
||||||
|
"eve,bob,dave",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 9) Extend the search even further and also increase the minimum path
|
||||||
|
// length, but this time with charlie-dave as the incoming channel.
|
||||||
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
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{
|
||||||
|
minNumHops: 2,
|
||||||
|
maxNumHops: 3,
|
||||||
|
nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("charlie")),
|
||||||
|
incomingChainedChannels: []uint64{3},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "cannot simultaneously be included in "+
|
||||||
|
"the omission set and in the partially specified path")
|
||||||
|
|
||||||
|
// 11) Test the circular route error.
|
||||||
|
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 2,
|
||||||
|
maxNumHops: 3,
|
||||||
|
incomingChainedChannels: []uint64{2, 7, 6, 3},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "a circular route cannot be specified")
|
||||||
}
|
}
|
||||||
@@ -773,3 +773,19 @@ func (r *Route) String() string {
|
|||||||
b.String(), r.TotalTimeLock,
|
b.String(), r.TotalTimeLock,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChanIDString returns the route's channel IDs as a formatted string.
|
||||||
|
func ChanIDString(r *Route) string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
for i, hop := range r.Hops {
|
||||||
|
b.WriteString(fmt.Sprintf("%v",
|
||||||
|
strconv.FormatUint(hop.ChannelID, 10),
|
||||||
|
))
|
||||||
|
if i != len(r.Hops)-1 {
|
||||||
|
b.WriteString(" -> ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
+27
-11
@@ -610,6 +610,11 @@ type BlindedPathRestrictions struct {
|
|||||||
// NodeOmissionSet is a set of nodes that should not be used within any
|
// NodeOmissionSet is a set of nodes that should not be used within any
|
||||||
// of the blinded paths that we generate.
|
// of the blinded paths that we generate.
|
||||||
NodeOmissionSet fn.Set[route.Vertex]
|
NodeOmissionSet fn.Set[route.Vertex]
|
||||||
|
|
||||||
|
// IncomingChainedChannels holds the chained channels list (specified
|
||||||
|
// via channel id) starting from a channel which points to the receiver
|
||||||
|
// node.
|
||||||
|
IncomingChainedChannels []uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindBlindedPaths finds a selection of paths to the destination node that can
|
// FindBlindedPaths finds a selection of paths to the destination node that can
|
||||||
@@ -620,11 +625,14 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||||||
|
|
||||||
// First, find a set of candidate paths given the destination node and
|
// First, find a set of candidate paths given the destination node and
|
||||||
// path length restrictions.
|
// path length restrictions.
|
||||||
|
incomingChainedChannels := restrictions.IncomingChainedChannels
|
||||||
|
minDistanceFromIntroNode := restrictions.MinDistanceFromIntroNode
|
||||||
paths, err := findBlindedPaths(
|
paths, err := findBlindedPaths(
|
||||||
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
|
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
|
||||||
minNumHops: restrictions.MinDistanceFromIntroNode,
|
minNumHops: minDistanceFromIntroNode,
|
||||||
maxNumHops: restrictions.NumHops,
|
maxNumHops: restrictions.NumHops,
|
||||||
nodeOmissionSet: restrictions.NodeOmissionSet,
|
nodeOmissionSet: restrictions.NodeOmissionSet,
|
||||||
|
incomingChainedChannels: incomingChainedChannels,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -676,19 +684,27 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||||||
prevNode = path[j].vertex
|
prevNode = path[j].vertex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't bother adding a route if its success probability less
|
routeWithProbability := &routeWithProbability{
|
||||||
// minimum that can be assigned to any single pair.
|
|
||||||
if totalRouteProbability <= DefaultMinRouteProbability {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
routes = append(routes, &routeWithProbability{
|
|
||||||
route: &route.Route{
|
route: &route.Route{
|
||||||
SourcePubKey: introNode,
|
SourcePubKey: introNode,
|
||||||
Hops: hops,
|
Hops: hops,
|
||||||
},
|
},
|
||||||
probability: totalRouteProbability,
|
probability: totalRouteProbability,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Don't bother adding a route if its success probability less
|
||||||
|
// minimum that can be assigned to any single pair.
|
||||||
|
if totalRouteProbability <= DefaultMinRouteProbability {
|
||||||
|
log.Debugf("Not using route (%v) as a blinded "+
|
||||||
|
"path since it resulted in an low "+
|
||||||
|
"probability path(%.3f)",
|
||||||
|
route.ChanIDString(routeWithProbability.route),
|
||||||
|
routeWithProbability.probability,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
routes = append(routes, routeWithProbability)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the routes based on probability.
|
// Sort the routes based on probability.
|
||||||
|
|||||||
@@ -6199,6 +6199,10 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||||||
blindingRestrictions.NumHops = uint8(*blindCfg.NumHops)
|
blindingRestrictions.NumHops = uint8(*blindCfg.NumHops)
|
||||||
}
|
}
|
||||||
if blindCfg.MaxNumPaths != nil {
|
if blindCfg.MaxNumPaths != nil {
|
||||||
|
if *blindCfg.MaxNumPaths == 0 {
|
||||||
|
return nil, fmt.Errorf("blinded max num " +
|
||||||
|
"paths cannot be 0")
|
||||||
|
}
|
||||||
blindingRestrictions.MaxNumPaths =
|
blindingRestrictions.MaxNumPaths =
|
||||||
uint8(*blindCfg.MaxNumPaths)
|
uint8(*blindCfg.MaxNumPaths)
|
||||||
}
|
}
|
||||||
@@ -6211,6 +6215,41 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||||||
|
|
||||||
blindingRestrictions.NodeOmissionSet.Add(vertex)
|
blindingRestrictions.NodeOmissionSet.Add(vertex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blindingRestrictions.IncomingChainedChannels = append(
|
||||||
|
blindingRestrictions.IncomingChainedChannels,
|
||||||
|
blindCfg.IncomingChannelList...,
|
||||||
|
)
|
||||||
|
|
||||||
|
numChainedChannels :=
|
||||||
|
uint8(len(blindingRestrictions.IncomingChainedChannels))
|
||||||
|
|
||||||
|
// When selecting the blinded incoming channel list parameter
|
||||||
|
// the maximum number of hops is implictitly set.
|
||||||
|
if numChainedChannels > blindingRestrictions.NumHops {
|
||||||
|
rpcsLog.Warnf("Changing the num_blinded_hops "+
|
||||||
|
"from (%d) to (%d)",
|
||||||
|
blindingRestrictions.NumHops,
|
||||||
|
numChainedChannels)
|
||||||
|
|
||||||
|
blindingRestrictions.NumHops =
|
||||||
|
numChainedChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MinDistanceFromIntroNode must be greater than or equal to
|
||||||
|
// the number of hops specified on the chained channels.
|
||||||
|
minNumHops := blindingRestrictions.MinDistanceFromIntroNode
|
||||||
|
if minNumHops < numChainedChannels {
|
||||||
|
// Ensure MinimumPath is at least the size of the
|
||||||
|
// chained path to avoid shorter routes being returned
|
||||||
|
// by the pathfinder.
|
||||||
|
return nil, fmt.Errorf("minimum number of blinded "+
|
||||||
|
"path hops (%d) must be greater than or equal "+
|
||||||
|
"to the number of hops specified on the "+
|
||||||
|
"chained channels (%d)", minNumHops,
|
||||||
|
numChainedChannels)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if blindingRestrictions.MinDistanceFromIntroNode >
|
if blindingRestrictions.MinDistanceFromIntroNode >
|
||||||
|
|||||||
Reference in new issue
Block a user