Skip to content

Commit

Permalink
Merge pull request #7267 from carlaKC/7200-blindedpathfinding
Browse files Browse the repository at this point in the history
routing: Support Pathfinding to Blinded Routes
  • Loading branch information
yyforyongyu committed Sep 28, 2023
2 parents 4b69391 + e334fc0 commit bccd218
Show file tree
Hide file tree
Showing 25 changed files with 5,068 additions and 2,329 deletions.
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"
"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{
Name: "introduction_node",
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,
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,
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

0 comments on commit bccd218

Please sign in to comment.