Skip to content
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

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
71 changes: 69 additions & 2 deletions cmd/commands/cmd_invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
MPins marked this conversation as resolved.
Show resolved Hide resolved
"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.StringSliceFlag{
Name: "blinded_path_incoming_channel_list",
Usage: "The chained channels (specified via channel " +
"id) starting from the receiver node which " +
"shall be used for the blinded path.",
},
},
Action: actionDecorator(addInvoice),
}
Expand Down Expand Up @@ -168,7 +175,7 @@ func addInvoice(ctx *cli.Context) error {

blindedPathCfg, err := parseBlindedPathCfg(ctx)
if err != nil {
return fmt.Errorf("could not parse blinded path config: %w",
return fmt.Errorf("could not parse blinded path config: %v",
err)
}

Expand Down Expand Up @@ -197,12 +204,57 @@ func addInvoice(ctx *cli.Context) error {
return nil
}

// parseChanIDFromString parses the channelId from string accepting both formats
// <blockheight>x<txindex>x<txposition> and plain number, returning the channel
// ID as a uint64.
func parseChanIDFromString(channelID string) (uint64, error) {
// First try parsing as a plain number
chanID, err := strconv.ParseUint(channelID, 10, 64)
if err == nil {
return chanID, nil
} else {
// Try the blockHeight x txIndex x outputIndex format.
parts := strings.Split(channelID, "x")

if len(parts) != 3 {
return 0, fmt.Errorf("error on channel ID %v",
channelID)
}

// Parse block height (limited to 3 bytes = 24 bits)
bh, err := strconv.ParseUint(parts[0], 10, 24)
if err != nil {
return 0, fmt.Errorf("error on channel ID %v",
channelID)
}

// Parse transaction index (limited to 3 bytes = 24 bits)
ti, err := strconv.ParseUint(parts[1], 10, 24)
if err != nil {
return 0, fmt.Errorf("error on channel ID %v",
channelID)
}

// Parse transaction position (limited to 2 bytes = 16 bits)
tp, err := strconv.ParseUint(parts[2], 10, 16)
if err != nil {
return 0, fmt.Errorf("error on channel ID %v",
channelID)
}

chanID = bh<<40 | ti<<16 | tp

return chanID, nil
}
}

func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
if !ctx.Bool("blind") {
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 +291,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 := parseChanIDFromString(channelID)
if err != nil {
return nil, err
MPins marked this conversation as resolved.
Show resolved Hide resolved
}
blindCfg.IncomingChannelList = append(
blindCfg.IncomingChannelList, chanID,
)
}
}

return &blindCfg, nil
}

Expand Down
7 changes: 7 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@
the misnomer of `chan_id` which was describing the short channel
id to `scid` to represent what it really is.

* [The `lncli addinvoice --blind` command now has the option to include a chained
channels 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.](https://github.com/lightningnetwork/lnd/pull/9127)

# Improvements
## Functional Updates

Expand Down
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,10 @@ var allTestCases = []*lntest.TestCase{
Name: "quiescence",
TestFunc: testQuiescence,
},
{
Name: "restricted route in blinding invoice",
TestFunc: testRestrictedBlindedRouteInvoices,
},
}

// appendPrefixed is used to add a prefix to each test name in the subtests
Expand Down
102 changes: 102 additions & 0 deletions itest/lnd_route_blinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1461,3 +1461,105 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) {
require.Fail(ht, "timeout waiting for sending payment")
}
}

// testGivenBlindedRouteInvoices tests lnd's ability to:
MPins marked this conversation as resolved.
Show resolved Hide resolved
// - Assert the error when attempting to create a blinded payment with an
// invalid path.
// - Create a blinded payment path when the blinded path is partially
// pre-specified.
func testRestrictedBlindedRouteInvoices(ht *lntest.HarnessTest) {
// Create a five hop network: Alice -> Bob -> Carol -> Dave -> Erin.
chanAmt := btcutil.Amount(100000)
cfgs := [][]string{nil, nil, nil, nil, nil}
chanPoints, nodes := ht.CreateSimpleNetwork(
cfgs, lntest.OpenChannelParams{Amt: chanAmt},
)
alice, bob, carol, dave, erin := nodes[0], nodes[1], nodes[2], nodes[3],
nodes[4]
chanPointAliceBob, chanPointBobCarol, chanPointCarolDave,
chanPointDaveErin := chanPoints[0], chanPoints[1],
chanPoints[2], chanPoints[3]

// Lookup full channel info so that we have channel ids for our route.
aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob)
bobCarolChan := ht.GetChannelByChanPoint(bob, chanPointBobCarol)

// Wait for all nodes to have seen all channels.
for _, chanPoint := range chanPoints {
for _, node := range nodes {
ht.AssertChannelInGraph(node, chanPoint)
}
}

// 1) Let carol choose the wrong incoming channel.
var (
minNumRealHops uint32 = 1
MPins marked this conversation as resolved.
Show resolved Hide resolved
numHops uint32 = 1
incomingChannelList = []uint64{aliceBobChan.ChanId}
)

err := carol.RPC.AddInvoiceAssertErr(&lnrpc.Invoice{
Memo: "test",
ValueMsat: 10_000_000,
IsBlinded: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
IncomingChannelList: incomingChannelList,
},
})

errChan := fmt.Errorf("rpc error: code = Unknown desc = required"+
" channel %v at position %d not found",
aliceBobChan.ChanId, 1)
require.Equal(ht, err.Error(), errChan.Error())

// 2) Let carol set no income channel restrictions.
invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{
Memo: "test",
ValueMsat: 10_000_000,
IsBlinded: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
})

// 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)
path = payReq.BlindedPaths[1].BlindedPath
require.Len(ht, path.BlindedHops, 2)

// 3) 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})

ht.CloseChannel(alice, chanPointAliceBob)
MPins marked this conversation as resolved.
Show resolved Hide resolved
ht.CloseChannel(bob, chanPointBobCarol)
ht.CloseChannel(dave, chanPointCarolDave)
ht.CloseChannel(erin, chanPointDaveErin)
}
8 changes: 8 additions & 0 deletions lnrpc/invoicesrpc/invoices.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,14 @@
"format": "byte"
},
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
},
MPins marked this conversation as resolved.
Show resolved Hide resolved
"incoming_channel_list": {
"type": "array",
"items": {
"type": "string",
"format": "uint64"
},
"description": "A list of chained channels that should be used in any of our\ngenerated blinded paths as incoming hops, first channel the closest to us."
}
}
},
Expand Down
Loading
Loading