lnrpc: add blinded payment to QueryRoutes request

This commit is contained in:
Carla Kirk-Cohen authored and Olaoluwa Osuntokun committed 2023-10-06 16:38:33 -07:00
1 parent 63b4ac8057
commit cae22dc862
4 files changed
+2849 -2189

No files matched your search

+2479 -2149
View File
File diff suppressed because it is too large. Load diff
+67
View File
@@ -2932,6 +2932,9 @@ message QueryRoutesRequest {
not add any additional block padding on top of final_ctlv_delta. This
padding of a few blocks needs to be added manually or otherwise failures may
happen when a block comes in while the payment is in flight.
Note: must not be set if making a payment to a blinded path (delta is
set by the aggregate parameters provided by blinded_payment_paths)
*/
int32 final_cltv_delta = 4;
@@ -3005,12 +3008,21 @@ message QueryRoutesRequest {
*/
repeated lnrpc.RouteHint route_hints = 16;
/*
An optional blinded path(s) to reach the destination. Note that the
introduction node must be provided as the first hop in the route.
*/
repeated BlindedPaymentPath blinded_payment_paths = 19;
/*
Features assumed to be supported by the final node. All transitive feature
dependencies must also be set properly. For a given feature bit pair, either
optional or remote may be set, but not both. If this field is nil or empty,
the router will try to load destination features from the graph as a
fallback.
Note: must not be set if making a payment to a blinded route (features
are provided in blinded_payment_paths).
*/
repeated lnrpc.FeatureBit dest_features = 17;
@@ -3487,6 +3499,61 @@ message RouteHint {
repeated HopHint hop_hints = 1;
}
message BlindedPaymentPath {
// The blinded path to send the payment to.
BlindedPath blinded_path = 1;
// The base fee for the blinded path provided, expressed in msat.
uint64 base_fee_msat = 2;
// The proportional fee for the blinded path provided, expressed in msat.
uint64 proportional_fee_msat = 3;
/*
The total CLTV delta for the blinded path provided, including the
final CLTV delta for the receiving node.
*/
uint32 total_cltv_delta = 4;
/*
The minimum hltc size that may be sent over the blinded path, expressed
in msat.
*/
uint64 htlc_min_msat = 5;
/*
The maximum htlc size that may be sent over the blinded path, expressed
in msat.
*/
uint64 htlc_max_msat = 6;
// The feature bits for the route.
repeated FeatureBit features = 7;
}
message BlindedPath {
// The unblinded pubkey of the introduction node for the route.
bytes introduction_node = 1;
// The ephemeral pubkey used by nodes in the blinded route.
bytes blinding_point = 2;
/*
A set of blinded node keys and data blobs for the blinded portion of the
route. Note that the first hop is expected to be the introduction node,
so the route is always expected to have at least one hop.
*/
repeated BlindedHop blinded_hops = 3;
}
message BlindedHop {
// The blinded public key of the node.
bytes blinded_node = 1;
// An encrypted blob of data provided to the blinded node.
bytes encrypted_data = 2;
}
message AMPInvoiceState {
// The state the HTLCs associated with this setID are in.
InvoiceHTLCState state = 1;
+89 -4
View File
@@ -1311,7 +1311,7 @@
},
{
"name": "final_cltv_delta",
"description": "An optional CLTV delta from the current height that should be used for the\ntimelock of the final hop. Note that unlike SendPayment, QueryRoutes does\nnot add any additional block padding on top of final_ctlv_delta. This\npadding of a few blocks needs to be added manually or otherwise failures may\nhappen when a block comes in while the payment is in flight.",
"description": "An optional CLTV delta from the current height that should be used for the\ntimelock of the final hop. Note that unlike SendPayment, QueryRoutes does\nnot add any additional block padding on top of final_ctlv_delta. This\npadding of a few blocks needs to be added manually or otherwise failures may\nhappen when a block comes in while the payment is in flight.\n\nNote: must not be set if making a payment to a blinded path (delta is\nset by the aggregate parameters provided by blinded_payment_paths)",
"in": "query",
"required": false,
"type": "integer",
@@ -1393,7 +1393,7 @@
},
{
"name": "dest_features",
"description": "Features assumed to be supported by the final node. All transitive feature\ndependencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback.",
"description": "Features assumed to be supported by the final node. All transitive feature\ndependencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback.\n\nNote: must not be set if making a payment to a blinded route (features\nare provided in blinded_payment_paths).",
"in": "query",
"required": false,
"type": "array",
@@ -1491,7 +1491,7 @@
"final_cltv_delta": {
"type": "integer",
"format": "int32",
"description": "An optional CLTV delta from the current height that should be used for the\ntimelock of the final hop. Note that unlike SendPayment, QueryRoutes does\nnot add any additional block padding on top of final_ctlv_delta. This\npadding of a few blocks needs to be added manually or otherwise failures may\nhappen when a block comes in while the payment is in flight."
"description": "An optional CLTV delta from the current height that should be used for the\ntimelock of the final hop. Note that unlike SendPayment, QueryRoutes does\nnot add any additional block padding on top of final_ctlv_delta. This\npadding of a few blocks needs to be added manually or otherwise failures may\nhappen when a block comes in while the payment is in flight.\n\nNote: must not be set if making a payment to a blinded path (delta is\nset by the aggregate parameters provided by blinded_payment_paths)"
},
"fee_limit": {
"$ref": "#/definitions/lnrpcFeeLimit",
@@ -1557,12 +1557,19 @@
},
"description": "Optional route hints to reach the destination through private channels."
},
"blinded_payment_paths": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcBlindedPaymentPath"
},
"description": "An optional blinded path(s) to reach the destination. Note that the\nintroduction node must be provided as the first hop in the route."
},
"dest_features": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcFeatureBit"
},
"description": "Features assumed to be supported by the final node. All transitive feature\ndependencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback."
"description": "Features assumed to be supported by the final node. All transitive feature\ndependencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback.\n\nNote: must not be set if making a payment to a blinded route (features\nare provided in blinded_payment_paths)."
},
"time_pref": {
"type": "number",
@@ -3415,6 +3422,84 @@
}
}
},
"lnrpcBlindedHop": {
"type": "object",
"properties": {
"blinded_node": {
"type": "string",
"format": "byte",
"description": "The blinded public key of the node."
},
"encrypted_data": {
"type": "string",
"format": "byte",
"description": "An encrypted blob of data provided to the blinded node."
}
}
},
"lnrpcBlindedPath": {
"type": "object",
"properties": {
"introduction_node": {
"type": "string",
"format": "byte",
"description": "The unblinded pubkey of the introduction node for the route."
},
"blinding_point": {
"type": "string",
"format": "byte",
"description": "The ephemeral pubkey used by nodes in the blinded route."
},
"blinded_hops": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcBlindedHop"
},
"description": "A set of blinded node keys and data blobs for the blinded portion of the\nroute. Note that the first hop is expected to be the introduction node,\nso the route is always expected to have at least one hop."
}
}
},
"lnrpcBlindedPaymentPath": {
"type": "object",
"properties": {
"blinded_path": {
"$ref": "#/definitions/lnrpcBlindedPath",
"description": "The blinded path to send the payment to."
},
"base_fee_msat": {
"type": "string",
"format": "uint64",
"description": "The base fee for the blinded path provided, expressed in msat."
},
"proportional_fee_msat": {
"type": "string",
"format": "uint64",
"description": "The proportional fee for the blinded path provided, expressed in msat."
},
"total_cltv_delta": {
"type": "integer",
"format": "int64",
"description": "The total CLTV delta for the blinded path provided, including the\nfinal CLTV delta for the receiving node."
},
"htlc_min_msat": {
"type": "string",
"format": "uint64",
"description": "The minimum hltc size that may be sent over the blinded path, expressed\nin msat."
},
"htlc_max_msat": {
"type": "string",
"format": "uint64",
"description": "The maximum htlc size that may be sent over the blinded path, expressed\nin msat."
},
"features": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcFeatureBit"
},
"description": "The feature bits for the route."
}
}
},
"lnrpcChain": {
"type": "object",
"properties": {
+214 -36
View File
@@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/htlcswitch"
@@ -229,12 +230,8 @@ func (r *RouterBackend) parseIgnored(in *lnrpc.QueryRoutesRequest) (
func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
*routing.RouteRequest, error) {
// Parse the hex-encoded source and target public keys into full public
// key objects we can properly manipulate.
targetPubKey, err := parsePubKey(in.PubKey)
if err != nil {
return nil, err
}
// Parse the hex-encoded source public key into a full public key that
// we can properly manipulate.
var sourcePubKey route.Vertex
if in.SourcePubKey != "" {
@@ -265,34 +262,93 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
if sourcePubKey != r.SelfNode {
maxTotalTimelock = math.MaxUint32
}
cltvLimit, err := ValidateCLTVLimit(in.CltvLimit, maxTotalTimelock)
if err != nil {
return nil, err
}
// If we have a blinded path set, we'll get a few of our fields from
// inside of the path rather than the request's fields.
var (
targetPubKey *route.Vertex
routeHintEdges map[route.Vertex][]*channeldb.CachedEdgePolicy
blindedPmt *routing.BlindedPayment
// finalCLTVDelta varies depending on whether we're sending to
// a blinded route or an unblinded node. For blinded paths,
// our final cltv is already baked into the path so we restrict
// this value to zero on the API. Bolt11 invoices have a
// default, so we'll fill that in for the non-blinded case.
finalCLTVDelta uint16
// destinationFeatures is the set of features for the
// destination node.
destinationFeatures *lnwire.FeatureVector
)
// Validate that the fields provided in the request are sane depending
// on whether it is using a blinded path or not.
if len(in.BlindedPaymentPaths) > 0 {
blindedPmt, err = parseBlindedPayment(in)
if err != nil {
return nil, err
}
if blindedPmt.Features != nil {
destinationFeatures = blindedPmt.Features.Clone()
}
} else {
// If we do not have a blinded path, a target pubkey must be
// set.
pk, err := parsePubKey(in.PubKey)
if err != nil {
return nil, err
}
targetPubKey = &pk
// Convert route hints to an edge map.
routeHints, err := unmarshallRouteHints(in.RouteHints)
if err != nil {
return nil, err
}
routeHintEdges, err = routing.RouteHintsToEdges(
routeHints, *targetPubKey,
)
if err != nil {
return nil, err
}
// Set a non-zero final CLTV delta for payments that are not
// to blinded paths, as bolt11 has a default final cltv delta
// value that is used in the absence of a value.
finalCLTVDelta = r.DefaultFinalCltvDelta
if in.FinalCltvDelta != 0 {
finalCLTVDelta = uint16(in.FinalCltvDelta)
}
// Do bounds checking without block padding so we don't give
// routes that will leave the router in a zombie payment state.
err = routing.ValidateCLTVLimit(
cltvLimit, finalCLTVDelta, false,
)
if err != nil {
return nil, err
}
// Parse destination feature bits.
destinationFeatures, err = UnmarshalFeatures(in.DestFeatures)
if err != nil {
return nil, err
}
}
// We need to subtract the final delta before passing it into path
// finding. The optimal path is independent of the final cltv delta and
// the path finding algorithm is unaware of this value.
finalCLTVDelta := r.DefaultFinalCltvDelta
if in.FinalCltvDelta != 0 {
finalCLTVDelta = uint16(in.FinalCltvDelta)
}
// Do bounds checking without block padding so we don't give routes
// that will leave the router in a zombie payment state.
err = routing.ValidateCLTVLimit(cltvLimit, finalCLTVDelta, false)
if err != nil {
return nil, err
}
cltvLimit -= uint32(finalCLTVDelta)
// Parse destination feature bits.
features, err := UnmarshalFeatures(in.DestFeatures)
if err != nil {
return nil, err
}
ignoredNodes, ignoredPairs, err := r.parseIgnored(in)
if err != nil {
return nil, err
@@ -326,7 +382,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
},
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
CltvLimit: cltvLimit,
DestFeatures: features,
DestFeatures: destinationFeatures,
}
// Pass along an outgoing channel restriction if specified.
@@ -353,22 +409,144 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
return nil, err
}
// Convert route hints to an edge map.
routeHints, err := unmarshallRouteHints(in.RouteHints)
if err != nil {
return nil, err
}
routeHintEdges, err := routing.RouteHintsToEdges(
routeHints, targetPubKey,
return routing.NewRouteRequest(
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
customRecords, routeHintEdges, blindedPmt, finalCLTVDelta,
)
}
func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
*routing.BlindedPayment, error) {
if len(in.PubKey) != 0 {
return nil, fmt.Errorf("target pubkey: %x should not be set "+
"when blinded path is provided", in.PubKey)
}
if len(in.BlindedPaymentPaths) != 1 {
return nil, errors.New("query routes only supports a single " +
"blinded path")
}
blindedPath := in.BlindedPaymentPaths[0]
if len(in.RouteHints) > 0 {
return nil, errors.New("route hints and blinded path can't " +
"both be set")
}
blindedPmt, err := unmarshalBlindedPayment(blindedPath)
if err != nil {
return nil, fmt.Errorf("parse blinded payment: %w", err)
}
if err := blindedPmt.Validate(); err != nil {
return nil, fmt.Errorf("invalid blinded path: %w", err)
}
if in.FinalCltvDelta != 0 {
return nil, errors.New("final cltv delta should be " +
"zero for blinded paths")
}
// For blinded paths, we get one set of features for the relaying
// intermediate nodes and the final destination. We don't allow the
// destination feature bit field for regular payments to be set, as
// this could lead to ambiguity.
if len(in.DestFeatures) > 0 {
return nil, errors.New("destination features should " +
"be populated in blinded path")
}
return blindedPmt, nil
}
func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
*routing.BlindedPayment, error) {
if rpcPayment == nil {
return nil, errors.New("nil blinded payment")
}
path, err := unmarshalBlindedPaymentPaths(rpcPayment.BlindedPath)
if err != nil {
return nil, err
}
return routing.NewRouteRequest(
sourcePubKey, &targetPubKey, amt, in.TimePref, restrictions,
customRecords, routeHintEdges, nil, finalCLTVDelta,
)
features, err := UnmarshalFeatures(rpcPayment.Features)
if err != nil {
return nil, err
}
return &routing.BlindedPayment{
BlindedPath: path,
CltvExpiryDelta: uint16(rpcPayment.TotalCltvDelta),
BaseFee: uint32(rpcPayment.BaseFeeMsat),
ProportionalFee: uint32(
rpcPayment.ProportionalFeeMsat,
),
HtlcMinimum: rpcPayment.HtlcMinMsat,
HtlcMaximum: rpcPayment.HtlcMaxMsat,
Features: features,
}, nil
}
func unmarshalBlindedPaymentPaths(rpcPath *lnrpc.BlindedPath) (
*sphinx.BlindedPath, error) {
if rpcPath == nil {
return nil, errors.New("blinded path required when blinded " +
"route is provided")
}
introduction, err := btcec.ParsePubKey(rpcPath.IntroductionNode)
if err != nil {
return nil, err
}
blinding, err := btcec.ParsePubKey(rpcPath.BlindingPoint)
if err != nil {
return nil, err
}
if len(rpcPath.BlindedHops) < 1 {
return nil, errors.New("at least 1 blinded hops required")
}
path := &sphinx.BlindedPath{
IntroductionPoint: introduction,
BlindingPoint: blinding,
BlindedHops: make(
[]*sphinx.BlindedHopInfo, len(rpcPath.BlindedHops),
),
}
for i, hop := range rpcPath.BlindedHops {
path.BlindedHops[i], err = unmarshalBlindedHop(hop)
if err != nil {
return nil, err
}
}
return path, nil
}
func unmarshalBlindedHop(rpcHop *lnrpc.BlindedHop) (*sphinx.BlindedHopInfo,
error) {
pubkey, err := btcec.ParsePubKey(rpcHop.BlindedNode)
if err != nil {
return nil, err
}
if len(rpcHop.EncryptedData) == 0 {
return nil, errors.New("empty encrypted data not allowed")
}
return &sphinx.BlindedHopInfo{
BlindedNodePub: pubkey,
CipherText: rpcHop.EncryptedData,
}, nil
}
// rpcEdgeToPair looks up the provided channel and returns the channel endpoints