Skip to content

Add the option on path creator to specify the incoming channel on blinded path #9127

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 10 commits into
base: master
Choose a base branch
from
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
9 changes: 9 additions & 0 deletions docs/release-notes/release-notes-0.20.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@

## 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

# Improvements
Expand Down Expand Up @@ -72,3 +79,5 @@
## Tooling and Documentation

# Contributors (Alphabetical Order)

Pins
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,10 @@ var allTestCases = []*lntest.TestCase{
Name: "bump fee low budget",
TestFunc: testBumpFeeLowBudget,
},
{
Name: "partially specified route blinded invoice",
TestFunc: testPartiallySpecifiedBlindedPath,
},
}

// appendPrefixed is used to add a prefix to each test name in the subtests
Expand Down
176 changes: 176 additions & 0 deletions itest/lnd_route_blinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1420,3 +1420,179 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) {
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[:])
}
4 changes: 4 additions & 0 deletions lncfg/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func (r *Routing) Validate() error {
"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 {
return fmt.Errorf("the blinded route policy increase " +
"multiplier must be greater than or equal to 1")
Expand Down
8 changes: 8 additions & 0 deletions lnrpc/invoicesrpc/invoices.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,14 @@
"format": "byte"
},
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commit msg:

Micro Nit: in the descriptive part:

Add an option to the addinvoice rpc which allows to specify the blinded path via a list of chained channels.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MPins - you resolved this comment but it hasnt been addressed?

"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."
}
}
},
Expand Down
Loading
Loading