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

routing: Support Pathfinding to Blinded Routes #7267

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sort"
"time"

"github.com/btcsuite/btcd/btcec/v2"
carlaKC marked this conversation as resolved.
Show resolved Hide resolved
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
Expand Down Expand Up @@ -1143,10 +1144,30 @@ func serializeHop(w io.Writer, h *route.Hop) error {
records = append(records, h.MPP.Record())
}

// Add blinding point and encrypted data if present.
if h.EncryptedData != nil {
records = append(records, record.NewEncryptedDataRecord(
&h.EncryptedData,
))
}

if h.BlindingPoint != nil {
records = append(records, record.NewBlindingPointRecord(
&h.BlindingPoint,
))
}

if h.Metadata != nil {
records = append(records, record.NewMetadataRecord(&h.Metadata))
}

if h.TotalAmtMsat != 0 {
totalMsatInt := uint64(h.TotalAmtMsat)
records = append(
records, record.NewTotalAmtMsatBlinded(&totalMsatInt),
)
}

// Final sanity check to absolutely rule out custom records that are not
// custom and write into the standard range.
if err := h.CustomRecords.Validate(); err != nil {
Expand Down Expand Up @@ -1261,13 +1282,54 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
h.MPP = mpp
}

// If encrypted data or blinding key are present, remove them from
// the TLV map and parse into proper types.
encryptedDataType := uint64(record.EncryptedDataOnionType)
if data, ok := tlvMap[encryptedDataType]; ok {
delete(tlvMap, encryptedDataType)
h.EncryptedData = data
}

blindingType := uint64(record.BlindingPointOnionType)
if blindingPoint, ok := tlvMap[blindingType]; ok {
delete(tlvMap, blindingType)

h.BlindingPoint, err = btcec.ParsePubKey(blindingPoint)
if err != nil {
return nil, fmt.Errorf("invalid blinding point: %w",
err)
}
}

// If the metatdata type is present, remove it from the tlv map and
// populate directly on the hop.
metadataType := uint64(record.MetadataOnionType)
if metadata, ok := tlvMap[metadataType]; ok {
delete(tlvMap, metadataType)

h.Metadata = metadata
}

totalAmtMsatType := uint64(record.TotalAmtMsatBlindedType)
if totalAmtMsat, ok := tlvMap[totalAmtMsatType]; ok {
delete(tlvMap, totalAmtMsatType)

var (
totalAmtMsatInt uint64
buf [8]byte
)
if err := tlv.DTUint64(
bytes.NewReader(totalAmtMsat),
&totalAmtMsatInt,
&buf,
uint64(len(totalAmtMsat)),
); err != nil {
return nil, err
}

h.TotalAmtMsat = lnwire.MilliSatoshi(totalAmtMsatInt)
}

h.CustomRecords = tlvMap

return h, nil
Expand Down
57 changes: 41 additions & 16 deletions channeldb/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import (
var (
priv, _ = btcec.NewPrivateKey()
pub = priv.PubKey()
vertex = route.NewVertex(pub)

testHop1 = &route.Hop{
PubKeyBytes: route.NewVertex(pub),
PubKeyBytes: vertex,
ChannelID: 12345,
OutgoingTimeLock: 111,
AmtToForward: 555,
Expand All @@ -36,7 +37,7 @@ var (
}

testHop2 = &route.Hop{
PubKeyBytes: route.NewVertex(pub),
PubKeyBytes: vertex,
ChannelID: 12345,
OutgoingTimeLock: 111,
AmtToForward: 555,
Expand All @@ -46,12 +47,39 @@ var (
testRoute = route.Route{
TotalTimeLock: 123,
TotalAmount: 1234567,
SourcePubKey: route.NewVertex(pub),
SourcePubKey: vertex,
Hops: []*route.Hop{
testHop2,
testHop1,
},
}

testBlindedRoute = route.Route{
TotalTimeLock: 150,
TotalAmount: 1000,
SourcePubKey: vertex,
Hops: []*route.Hop{
{
PubKeyBytes: vertex,
ChannelID: 9876,
OutgoingTimeLock: 120,
AmtToForward: 900,
EncryptedData: []byte{1, 3, 3},
BlindingPoint: pub,
},
{
PubKeyBytes: vertex,
EncryptedData: []byte{3, 2, 1},
},
{
PubKeyBytes: vertex,
Metadata: []byte{4, 5, 6},
AmtToForward: 500,
OutgoingTimeLock: 100,
TotalAmtMsat: 500,
},
},
}
)

func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
Expand Down Expand Up @@ -140,27 +168,24 @@ func assertRouteEqual(a, b *route.Route) error {
return nil
}

// TestRouteSerialization tests serialization of a regular and blinded route.
func TestRouteSerialization(t *testing.T) {
t.Parallel()

testSerializeRoute(t, testRoute)
testSerializeRoute(t, testBlindedRoute)
}

func testSerializeRoute(t *testing.T, route route.Route) {
var b bytes.Buffer
if err := SerializeRoute(&b, testRoute); err != nil {
t.Fatal(err)
}
err := SerializeRoute(&b, route)
require.NoError(t, err)

r := bytes.NewReader(b.Bytes())
route2, err := DeserializeRoute(r)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

// First we verify all the records match up porperly, as they aren't
// able to be properly compared using reflect.DeepEqual.
err = assertRouteEqual(&testRoute, &route2)
if err != nil {
t.Fatalf("routes not equal: \n%v vs \n%v",
spew.Sdump(testRoute), spew.Sdump(route2))
}
reflect.DeepEqual(route, route2)
}

// deletePayment removes a payment with paymentHash from the payments database.
Expand Down
153 changes: 144 additions & 9 deletions cmd/lncli/cmd_payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,47 @@ var (
Name: "time_pref",
Usage: "(optional) expresses time preference (range -1 to 1)",
}

introductionNodeFlag = cli.StringFlag{
carlaKC marked this conversation as resolved.
Show resolved Hide resolved
Name: "introduction_node",
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we are going this route, we'd need to mention this is optional, otherwise it can be confusing by reading the help menu,
Screen Shot 2023-08-25 at 3 37 26 PM

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How about (blinded paths) rather than optional? I think that could be a nice way to visually distinguish it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

def looks better,
Screen Shot 2023-08-28 at 9 53 47 PM

Tho I meant we need to let the user know it's optional to use a blinded path. I guess we can improve it later when there are actual user complains, so looks good now.

Usage: "(blinded paths) the hex encoded, cleartext node ID " +
"of the node to use for queries to a blinded route",
}

blindingPointFlag = cli.StringFlag{
Name: "blinding_point",
Usage: "(blinded paths) the hex encoded blinding point to " +
"use if querying a route to a blinded path, this " +
"value *must* be set for queries to a blinded path",
}

blindedHopsFlag = cli.StringSliceFlag{
Name: "blinded_hops",
Usage: "(blinded paths) the blinded hops to include in the " +
"query, formatted as <blinded_node_id>:" +
"<hex_encrypted_data>. These hops must be provided " +
"*in order* starting with the introduction point and " +
"ending with the receiving node",
}

blindedBaseFlag = cli.Uint64Flag{
Name: "blinded_base_fee",
Usage: "(blinded paths) the aggregate base fee for the " +
"blinded portion of the route, expressed in msat",
}

blindedPPMFlag = cli.Uint64Flag{
Name: "blinded_ppm_fee",
Usage: "(blinded paths) the aggregate proportional fee for " +
"the blinded portion of the route, expressed in " +
"parts per million",
}

blindedCLTVFlag = cli.Uint64Flag{
Name: "blinded_cltv",
Usage: "(blinded paths) the total cltv delay for the " +
"blinded portion of the route",
}
)

// paymentFlags returns common flags for sendpayment and payinvoice.
Expand Down Expand Up @@ -1056,6 +1097,12 @@ var queryRoutesCommand = cli.Command{
},
timePrefFlag,
cltvLimitFlag,
introductionNodeFlag,
Copy link
Collaborator

Choose a reason for hiding this comment

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

how about creating a new cli, sth like queryrouteswithblindedpath?

blindingPointFlag,
blindedHopsFlag,
blindedBaseFlag,
blindedPPMFlag,
blindedCLTVFlag,
},
Action: actionDecorator(queryRoutes),
}
Expand All @@ -1076,9 +1123,15 @@ func queryRoutes(ctx *cli.Context) error {
switch {
case ctx.IsSet("dest"):
dest = ctx.String("dest")

case args.Present():
dest = args.First()
args = args.Tail()

// If we have a blinded path set, we don't have to specify a
// destination.
case ctx.IsSet(introductionNodeFlag.Name):

default:
return fmt.Errorf("dest argument missing")
}
Expand Down Expand Up @@ -1125,16 +1178,22 @@ func queryRoutes(ctx *cli.Context) error {
}
}

blindedRoutes, err := parseBlindedPaymentParameters(ctx)
if err != nil {
return err
}

req := &lnrpc.QueryRoutesRequest{
PubKey: dest,
Amt: amt,
FeeLimit: feeLimit,
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
UseMissionControl: ctx.Bool("use_mc"),
CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)),
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
TimePref: ctx.Float64(timePrefFlag.Name),
IgnoredPairs: ignoredPairs,
PubKey: dest,
Amt: amt,
FeeLimit: feeLimit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we check this is not set for blinded routes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This logic should apply as before for blinded paths?

Copy link
Collaborator

Choose a reason for hiding this comment

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

exactly this feelimit still needs to be in place for blinded routes as well.

FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
UseMissionControl: ctx.Bool("use_mc"),
CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)),
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
TimePref: ctx.Float64(timePrefFlag.Name),
IgnoredPairs: ignoredPairs,
BlindedPaymentPaths: blindedRoutes,
}

route, err := client.QueryRoutes(ctxc, req)
Expand All @@ -1146,6 +1205,82 @@ func queryRoutes(ctx *cli.Context) error {
return nil
}

func parseBlindedPaymentParameters(ctx *cli.Context) (
[]*lnrpc.BlindedPaymentPath, error) {

// Return nil if we don't have a blinding set, as we don't have a
// blinded path.
if !ctx.IsSet(blindingPointFlag.Name) {
return nil, nil
}

// If any one of our blinding related flags is set, we expect the
// full set to be set and we'll error out accordingly.
introNode, err := route.NewVertexFromStr(
ctx.String(introductionNodeFlag.Name),
)
if err != nil {
return nil, fmt.Errorf("decode introduction node: %w", err)
}

blindingPoint, err := route.NewVertexFromStr(ctx.String(
blindingPointFlag.Name,
))
if err != nil {
return nil, fmt.Errorf("decode blinding point: %w", err)
}

blindedHops := ctx.StringSlice(blindedHopsFlag.Name)

pmt := &lnrpc.BlindedPaymentPath{
BlindedPath: &lnrpc.BlindedPath{
IntroductionNode: introNode[:],
BlindingPoint: blindingPoint[:],
BlindedHops: make(
[]*lnrpc.BlindedHop, len(blindedHops),
),
},
BaseFeeMsat: ctx.Uint64(
blindedBaseFlag.Name,
),
ProportionalFeeMsat: ctx.Uint64(
blindedPPMFlag.Name,
),
TotalCltvDelta: uint32(ctx.Uint64(
blindedCLTVFlag.Name,
)),
}

for i, hop := range blindedHops {
parts := strings.Split(hop, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("blinded hops should be "+
"expressed as "+
"blinded_node_id:hex_encrypted_data, got: %v",
hop)
}

hop, err := route.NewVertexFromStr(parts[0])
if err != nil {
return nil, fmt.Errorf("hop: %v node: %w", i, err)
}

data, err := hex.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("hop: %v data: %w", i, err)
}

pmt.BlindedPath.BlindedHops[i] = &lnrpc.BlindedHop{
BlindedNode: hop[:],
EncryptedData: data,
}
}

return []*lnrpc.BlindedPaymentPath{
pmt,
}, nil
}

// retrieveFeeLimitLegacy retrieves the fee limit based on the different fee
// limit flags passed. This function will eventually disappear in favor of
// retrieveFeeLimit and the new payment rpc.
Expand Down