mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-11-28 15:13:04 +01:00
multi: add blinded route to route requests expressed as hints
Add the option to include a blinded route in a route request (exclusive to including hop hints, because it's incongruous to include both), and express the route as a chain of hop hints. Using a chain of hints over a single hint to represent the whole path allows us to re-use our route construction to fill in a lot of the path on our behalf.
This commit is contained in:
committed by
Olaoluwa Osuntokun
parent
48e36d93d4
commit
c9609b8214
@@ -327,7 +327,7 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context,
|
|||||||
// the route.
|
// the route.
|
||||||
routeReq, err := routing.NewRouteRequest(
|
routeReq, err := routing.NewRouteRequest(
|
||||||
sourcePubKey, &targetPubKey, amt, in.TimePref, restrictions,
|
sourcePubKey, &targetPubKey, amt, in.TimePref, restrictions,
|
||||||
customRecords, routeHintEdges, finalCLTVDelta,
|
customRecords, routeHintEdges, nil, finalCLTVDelta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
|
|||||||
FeeLimit: feeLimit,
|
FeeLimit: feeLimit,
|
||||||
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
|
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
|
||||||
ProbabilitySource: mc.GetProbability,
|
ProbabilitySource: mc.GetProbability,
|
||||||
}, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
|
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -77,3 +79,94 @@ func (b *BlindedPayment) Validate() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toRouteHints produces a set of chained route hints that represent a blinded
|
||||||
|
// path. In the case of a single hop blinded route (which is paying directly
|
||||||
|
// to the introduction point), no hints will be returned. In this case callers
|
||||||
|
// *must* account for the blinded route's CLTV delta elsewhere (as this is
|
||||||
|
// effectively the final_cltv_delta for the receiving introduction node). In
|
||||||
|
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
|
||||||
|
// hints (both for intermediate hops and the final_cltv_delta for the receiving
|
||||||
|
// node).
|
||||||
|
func (b *BlindedPayment) toRouteHints() RouteHints {
|
||||||
|
// If we just have a single hop in our blinded route, it just contains
|
||||||
|
// an introduction node (this is a valid path according to the spec).
|
||||||
|
// Since we have the un-blinded node ID for the introduction node, we
|
||||||
|
// don't need to add any route hints.
|
||||||
|
if len(b.BlindedPath.BlindedHops) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hintCount := len(b.BlindedPath.BlindedHops) - 1
|
||||||
|
hints := make(
|
||||||
|
map[route.Vertex][]*channeldb.CachedEdgePolicy, hintCount,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start at the unblinded introduction node, because our pathfinding
|
||||||
|
// will be able to locate this point in the graph.
|
||||||
|
fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
|
||||||
|
|
||||||
|
features := lnwire.EmptyFeatureVector()
|
||||||
|
if b.Features != nil {
|
||||||
|
features = b.Features.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the total aggregate relay parameters for the entire blinded
|
||||||
|
// route as the policy for the hint from our introduction node. This
|
||||||
|
// will ensure that pathfinding provides sufficient fees/delay for the
|
||||||
|
// blinded portion to the introduction node.
|
||||||
|
firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
|
||||||
|
hints[fromNode] = []*channeldb.CachedEdgePolicy{
|
||||||
|
{
|
||||||
|
TimeLockDelta: b.CltvExpiryDelta,
|
||||||
|
MinHTLC: lnwire.MilliSatoshi(b.HtlcMinimum),
|
||||||
|
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
|
||||||
|
FeeBaseMSat: lnwire.MilliSatoshi(b.BaseFee),
|
||||||
|
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||||
|
b.ProportionalFee,
|
||||||
|
),
|
||||||
|
ToNodePubKey: func() route.Vertex {
|
||||||
|
return route.NewVertex(
|
||||||
|
// The first node in this slice is
|
||||||
|
// the introduction node, so we start
|
||||||
|
// at index 1 to get the first blinded
|
||||||
|
// relaying node.
|
||||||
|
firstBlindedHop,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
ToNodeFeatures: features,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start at an offset of 1 because the first node in our blinded hops
|
||||||
|
// is the introduction node and terminate at the second-last node
|
||||||
|
// because we're dealing with hops as pairs.
|
||||||
|
for i := 1; i < hintCount; i++ {
|
||||||
|
// Set our origin node to the current
|
||||||
|
fromNode = route.NewVertex(
|
||||||
|
b.BlindedPath.BlindedHops[i].BlindedNodePub,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a hint which has no fee or cltv delta. We
|
||||||
|
// specifically want zero values here because our relay
|
||||||
|
// parameters are expressed in encrypted blobs rather than the
|
||||||
|
// route itself for blinded routes.
|
||||||
|
nextHopIdx := i + 1
|
||||||
|
nextNode := route.NewVertex(
|
||||||
|
b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
|
||||||
|
)
|
||||||
|
|
||||||
|
hint := &channeldb.CachedEdgePolicy{
|
||||||
|
ToNodePubKey: func() route.Vertex {
|
||||||
|
return nextNode
|
||||||
|
},
|
||||||
|
ToNodeFeatures: features,
|
||||||
|
}
|
||||||
|
|
||||||
|
hints[fromNode] = []*channeldb.CachedEdgePolicy{
|
||||||
|
hint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hints
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package routing
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,3 +71,113 @@ func TestBlindedPathValidation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBlindedPaymentToHints tests conversion of a blinded path to a chain of
|
||||||
|
// route hints. As our function assumes that the blinded payment has already
|
||||||
|
// been validated.
|
||||||
|
func TestBlindedPaymentToHints(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
_, pk1 = btcec.PrivKeyFromBytes([]byte{1})
|
||||||
|
_, pkb1 = btcec.PrivKeyFromBytes([]byte{2})
|
||||||
|
_, pkb2 = btcec.PrivKeyFromBytes([]byte{3})
|
||||||
|
_, pkb3 = btcec.PrivKeyFromBytes([]byte{4})
|
||||||
|
|
||||||
|
v1 = route.NewVertex(pk1)
|
||||||
|
vb2 = route.NewVertex(pkb2)
|
||||||
|
vb3 = route.NewVertex(pkb3)
|
||||||
|
|
||||||
|
baseFee uint32 = 1000
|
||||||
|
ppmFee uint32 = 500
|
||||||
|
cltvDelta uint16 = 140
|
||||||
|
htlcMin uint64 = 100
|
||||||
|
htlcMax uint64 = 100_000_000
|
||||||
|
|
||||||
|
rawFeatures = lnwire.NewRawFeatureVector(
|
||||||
|
lnwire.AMPOptional,
|
||||||
|
)
|
||||||
|
|
||||||
|
features = lnwire.NewFeatureVector(
|
||||||
|
rawFeatures, lnwire.Features,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a blinded payment that's just to the introduction node and
|
||||||
|
// assert that we get nil hints.
|
||||||
|
blindedPayment := &BlindedPayment{
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: pk1,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BaseFee: baseFee,
|
||||||
|
ProportionalFee: ppmFee,
|
||||||
|
CltvExpiryDelta: cltvDelta,
|
||||||
|
HtlcMinimum: htlcMin,
|
||||||
|
HtlcMaximum: htlcMax,
|
||||||
|
Features: features,
|
||||||
|
}
|
||||||
|
require.Nil(t, blindedPayment.toRouteHints())
|
||||||
|
|
||||||
|
// Populate the blinded payment with hops.
|
||||||
|
blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := RouteHints{
|
||||||
|
v1: {
|
||||||
|
{
|
||||||
|
TimeLockDelta: cltvDelta,
|
||||||
|
MinHTLC: lnwire.MilliSatoshi(htlcMin),
|
||||||
|
MaxHTLC: lnwire.MilliSatoshi(htlcMax),
|
||||||
|
FeeBaseMSat: lnwire.MilliSatoshi(baseFee),
|
||||||
|
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||||
|
ppmFee,
|
||||||
|
),
|
||||||
|
ToNodePubKey: func() route.Vertex {
|
||||||
|
return vb2
|
||||||
|
},
|
||||||
|
ToNodeFeatures: features,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vb2: {
|
||||||
|
{
|
||||||
|
ToNodePubKey: func() route.Vertex {
|
||||||
|
return vb3
|
||||||
|
},
|
||||||
|
ToNodeFeatures: features,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
actual := blindedPayment.toRouteHints()
|
||||||
|
|
||||||
|
require.Equal(t, len(expected), len(actual))
|
||||||
|
for vertex, expectedHint := range expected {
|
||||||
|
actualHint, ok := actual[vertex]
|
||||||
|
require.True(t, ok, "node not found: %v", vertex)
|
||||||
|
|
||||||
|
require.Len(t, expectedHint, 1)
|
||||||
|
require.Len(t, actualHint, 1)
|
||||||
|
|
||||||
|
// We can't assert that our functions are equal, so we check
|
||||||
|
// their output and then mark as nil so that we can use
|
||||||
|
// require.Equal for all our other fields.
|
||||||
|
require.Equal(t, expectedHint[0].ToNodePubKey(),
|
||||||
|
actualHint[0].ToNodePubKey())
|
||||||
|
|
||||||
|
actualHint[0].ToNodePubKey = nil
|
||||||
|
expectedHint[0].ToNodePubKey = nil
|
||||||
|
|
||||||
|
require.Equal(t, expectedHint[0], actualHint[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2293,7 +2293,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
const amt lnwire.MilliSatoshi = 4999999
|
const amt lnwire.MilliSatoshi = 4999999
|
||||||
req, err := NewRouteRequest(
|
req, err := NewRouteRequest(
|
||||||
bobNode.PubKeyBytes, &carol, amt, 0, noRestrictions, nil, nil,
|
bobNode.PubKeyBytes, &carol, amt, 0, noRestrictions, nil, nil,
|
||||||
MinCLTVDelta,
|
nil, MinCLTVDelta,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "invalid route request")
|
require.NoError(t, err, "invalid route request")
|
||||||
|
|
||||||
@@ -2346,7 +2346,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
// We'll now request a route from A -> B -> C.
|
// We'll now request a route from A -> B -> C.
|
||||||
req, err = NewRouteRequest(
|
req, err = NewRouteRequest(
|
||||||
source.PubKeyBytes, &carol, amt, 0, noRestrictions, nil, nil,
|
source.PubKeyBytes, &carol, amt, 0, noRestrictions, nil, nil,
|
||||||
MinCLTVDelta,
|
nil, MinCLTVDelta,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "invalid route request")
|
require.NoError(t, err, "invalid route request")
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,34 @@ var (
|
|||||||
// ErrRouterShuttingDown is returned if the router is in the process of
|
// ErrRouterShuttingDown is returned if the router is in the process of
|
||||||
// shutting down.
|
// shutting down.
|
||||||
ErrRouterShuttingDown = fmt.Errorf("router shutting down")
|
ErrRouterShuttingDown = fmt.Errorf("router shutting down")
|
||||||
|
|
||||||
|
// ErrSelfIntro is a failure returned when the source node of a
|
||||||
|
// route request is also the introduction node. This is not yet
|
||||||
|
// supported because LND does not support blinded forwardingg.
|
||||||
|
ErrSelfIntro = errors.New("introduction point as own node not " +
|
||||||
|
"supported")
|
||||||
|
|
||||||
|
// ErrHintsAndBlinded is returned if a route request has both
|
||||||
|
// bolt 11 route hints and a blinded path set.
|
||||||
|
ErrHintsAndBlinded = errors.New("bolt 11 route hints and blinded " +
|
||||||
|
"paths are mutually exclusive")
|
||||||
|
|
||||||
|
// ErrExpiryAndBlinded is returned if a final cltv and a blinded path
|
||||||
|
// are provided, as the cltv should be provided within the blinded
|
||||||
|
// path.
|
||||||
|
ErrExpiryAndBlinded = errors.New("final cltv delta and blinded " +
|
||||||
|
"paths are mutually exclusive")
|
||||||
|
|
||||||
|
// ErrTargetAndBlinded is returned is a target destination and a
|
||||||
|
// blinded path are both set (as the target is inferred from the
|
||||||
|
// blinded path).
|
||||||
|
ErrTargetAndBlinded = errors.New("target node and blinded paths " +
|
||||||
|
"are mutually exclusive")
|
||||||
|
|
||||||
|
// ErrNoTarget is returned when the target node for a route is not
|
||||||
|
// provided by either a blinded route or a cleartext pubkey.
|
||||||
|
ErrNoTarget = errors.New("destination not set in target or blinded " +
|
||||||
|
"path")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChannelGraphSource represents the source of information about the topology
|
// ChannelGraphSource represents the source of information about the topology
|
||||||
@@ -1809,12 +1837,16 @@ type routingMsg struct {
|
|||||||
err chan error
|
err chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteRequest contains the parameters for a pathfinding request.
|
// RouteRequest contains the parameters for a pathfinding request. It may
|
||||||
|
// describe a request to make a regular payment or one to a blinded path
|
||||||
|
// (incdicated by a non-nil BlindedPayment field).
|
||||||
type RouteRequest struct {
|
type RouteRequest struct {
|
||||||
// Source is the node that the path originates from.
|
// Source is the node that the path originates from.
|
||||||
Source route.Vertex
|
Source route.Vertex
|
||||||
|
|
||||||
// Target is the node that the path terminates at.
|
// Target is the node that the path terminates at. If the route
|
||||||
|
// includes a blinded path, target will be the blinded node id of the
|
||||||
|
// final hop in the blinded route.
|
||||||
Target route.Vertex
|
Target route.Vertex
|
||||||
|
|
||||||
// Amount is the Amount in millisatoshis to be delivered to the target
|
// Amount is the Amount in millisatoshis to be delivered to the target
|
||||||
@@ -1834,39 +1866,131 @@ type RouteRequest struct {
|
|||||||
CustomRecords record.CustomSet
|
CustomRecords record.CustomSet
|
||||||
|
|
||||||
// RouteHints contains an additional set of edges to include in our
|
// RouteHints contains an additional set of edges to include in our
|
||||||
// view of the graph.
|
// view of the graph. This may either be a set of hints for private
|
||||||
|
// channels or a "virtual" hop hint that represents a blinded route.
|
||||||
RouteHints RouteHints
|
RouteHints RouteHints
|
||||||
|
|
||||||
// FinalExpiry is the cltv delta for the final hop.
|
// FinalExpiry is the cltv delta for the final hop. If paying to a
|
||||||
|
// blinded path, this value is a duplicate of the delta provided
|
||||||
|
// in blinded payment.
|
||||||
FinalExpiry uint16
|
FinalExpiry uint16
|
||||||
|
|
||||||
|
// BlindedPayment contains an optional blinded path and parameters
|
||||||
|
// used to reach a target node via a blinded path. This field is
|
||||||
|
// mutually exclusive with the Target field.
|
||||||
|
BlindedPayment *BlindedPayment
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteHints is an alias type for a set of route hints, with the source node
|
// RouteHints is an alias type for a set of route hints, with the source node
|
||||||
// as the map's key and the details of the hint(s) in the edge policy.
|
// as the map's key and the details of the hint(s) in the edge policy.
|
||||||
type RouteHints map[route.Vertex][]*channeldb.CachedEdgePolicy
|
type RouteHints map[route.Vertex][]*channeldb.CachedEdgePolicy
|
||||||
|
|
||||||
// NewRouteRequest produces a new route request.
|
// NewRouteRequest produces a new route request for a regular payment or one
|
||||||
|
// to a blinded route, validating that the target, routeHints and finalExpiry
|
||||||
|
// parameters are mutually exclusive with the blindedPayment parameter (which
|
||||||
|
// contains these values for blinded payments).
|
||||||
func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
||||||
amount lnwire.MilliSatoshi, timePref float64,
|
amount lnwire.MilliSatoshi, timePref float64,
|
||||||
restrictions *RestrictParams, customRecords record.CustomSet,
|
restrictions *RestrictParams, customRecords record.CustomSet,
|
||||||
routeHints RouteHints, finalExpiry uint16) (*RouteRequest, error) {
|
routeHints RouteHints, blindedPayment *BlindedPayment,
|
||||||
|
finalExpiry uint16) (*RouteRequest, error) {
|
||||||
|
|
||||||
if target == nil {
|
var (
|
||||||
return nil, errors.New("target node required")
|
// Assume that we're starting off with a regular payment.
|
||||||
|
requestHints = routeHints
|
||||||
|
requestExpiry = finalExpiry
|
||||||
|
)
|
||||||
|
|
||||||
|
if blindedPayment != nil {
|
||||||
|
if err := blindedPayment.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid blinded payment: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
introVertex := route.NewVertex(
|
||||||
|
blindedPayment.BlindedPath.IntroductionPoint,
|
||||||
|
)
|
||||||
|
if source == introVertex {
|
||||||
|
return nil, ErrSelfIntro
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the values for a clear path have not been set,
|
||||||
|
// as this is an ambiguous signal from the caller.
|
||||||
|
if routeHints != nil {
|
||||||
|
return nil, ErrHintsAndBlinded
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalExpiry != 0 {
|
||||||
|
return nil, ErrExpiryAndBlinded
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a blinded path with 1 hop, the cltv expiry
|
||||||
|
// will not be included in any hop hints (since we're just
|
||||||
|
// sending to the introduction node and need no blinded hints).
|
||||||
|
// In this case, we include it to make sure that the final
|
||||||
|
// cltv delta is accounted for (since it's part of the blinded
|
||||||
|
// delta). In the case of a multi-hop route, we set our final
|
||||||
|
// cltv to zero, since it's going to be accounted for in the
|
||||||
|
// delta for our hints.
|
||||||
|
if len(blindedPayment.BlindedPath.BlindedHops) == 1 {
|
||||||
|
requestExpiry = blindedPayment.CltvExpiryDelta
|
||||||
|
}
|
||||||
|
|
||||||
|
requestHints = blindedPayment.toRouteHints()
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTarget, err := getTargetNode(target, blindedPayment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RouteRequest{
|
return &RouteRequest{
|
||||||
Source: source,
|
Source: source,
|
||||||
Target: *target,
|
Target: requestTarget,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
TimePreference: timePref,
|
TimePreference: timePref,
|
||||||
Restrictions: restrictions,
|
Restrictions: restrictions,
|
||||||
CustomRecords: customRecords,
|
CustomRecords: customRecords,
|
||||||
RouteHints: routeHints,
|
RouteHints: requestHints,
|
||||||
FinalExpiry: finalExpiry,
|
FinalExpiry: requestExpiry,
|
||||||
|
BlindedPayment: blindedPayment,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
|
||||||
|
route.Vertex, error) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
blinded = blindedPayment != nil
|
||||||
|
targetSet = target != nil
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blinded && targetSet:
|
||||||
|
return route.Vertex{}, ErrTargetAndBlinded
|
||||||
|
|
||||||
|
case blinded:
|
||||||
|
// If we're dealing with an edge-case blinded path that just
|
||||||
|
// has an introduction node (first hop expected to be the intro
|
||||||
|
// hop), then we return the unblinded introduction node as our
|
||||||
|
// target.
|
||||||
|
hops := blindedPayment.BlindedPath.BlindedHops
|
||||||
|
if len(hops) == 1 {
|
||||||
|
return route.NewVertex(
|
||||||
|
blindedPayment.BlindedPath.IntroductionPoint,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return route.NewVertex(hops[len(hops)-1].BlindedNodePub), nil
|
||||||
|
|
||||||
|
case targetSet:
|
||||||
|
return *target, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return route.Vertex{}, ErrNoTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FindRoute attempts to query the ChannelRouter for the optimum path to a
|
// FindRoute attempts to query the ChannelRouter for the optimum path to a
|
||||||
// particular target destination to which it is able to send `amt` after
|
// particular target destination to which it is able to send `amt` after
|
||||||
// factoring in channel capacities and cumulative fees along the route.
|
// factoring in channel capacities and cumulative fees along the route.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/clock"
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
@@ -273,7 +274,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
|||||||
|
|
||||||
req, err := NewRouteRequest(
|
req, err := NewRouteRequest(
|
||||||
ctx.router.selfNode.PubKeyBytes, &target, paymentAmt, 0,
|
ctx.router.selfNode.PubKeyBytes, &target, paymentAmt, 0,
|
||||||
restrictions, nil, nil, MinCLTVDelta,
|
restrictions, nil, nil, nil, MinCLTVDelta,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "invalid route request")
|
require.NoError(t, err, "invalid route request")
|
||||||
|
|
||||||
@@ -1563,7 +1564,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
|
|
||||||
req, err := NewRouteRequest(
|
req, err := NewRouteRequest(
|
||||||
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
|
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
|
||||||
paymentAmt, 0, noRestrictions, nil, nil, MinCLTVDelta,
|
paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "invalid route request")
|
require.NoError(t, err, "invalid route request")
|
||||||
_, _, err = ctx.router.FindRoute(req)
|
_, _, err = ctx.router.FindRoute(req)
|
||||||
@@ -1605,7 +1606,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
// updated.
|
// updated.
|
||||||
req, err = NewRouteRequest(
|
req, err = NewRouteRequest(
|
||||||
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
|
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
|
||||||
paymentAmt, 0, noRestrictions, nil, nil, MinCLTVDelta,
|
paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "invalid route request")
|
require.NoError(t, err, "invalid route request")
|
||||||
|
|
||||||
@@ -4612,3 +4613,155 @@ func TestSendToRouteTempFailure(t *testing.T) {
|
|||||||
payer.AssertExpectations(t)
|
payer.AssertExpectations(t)
|
||||||
missionControl.AssertExpectations(t)
|
missionControl.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewRouteRequest tests creation of route requests for blinded and
|
||||||
|
// unblinded routes.
|
||||||
|
func TestNewRouteRequest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
source, err := route.NewVertexFromStr("0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6")
|
||||||
|
require.NoError(t, err)
|
||||||
|
sourcePubkey, err := btcec.ParsePubKey(source[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
v1, err := route.NewVertexFromStr("026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318")
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubkey1, err := btcec.ParsePubKey(v1[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
v2, err := route.NewVertexFromStr("03c19f0027ffbb0ae0e14a4d958788793f9d74e107462473ec0c3891e4feb12e99")
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubkey2, err := btcec.ParsePubKey(v2[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unblindedCltv uint16 = 500
|
||||||
|
blindedCltv uint16 = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
blindedSelfIntro := &BlindedPayment{
|
||||||
|
CltvExpiryDelta: blindedCltv,
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: sourcePubkey,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
blindedOtherIntro := &BlindedPayment{
|
||||||
|
CltvExpiryDelta: blindedCltv,
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: pubkey1,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
blindedMultiHop := &BlindedPayment{
|
||||||
|
CltvExpiryDelta: blindedCltv,
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: pubkey1,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pubkey2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
target *route.Vertex
|
||||||
|
routeHints RouteHints
|
||||||
|
blindedPayment *BlindedPayment
|
||||||
|
finalExpiry uint16
|
||||||
|
|
||||||
|
expectedTarget route.Vertex
|
||||||
|
expectedCltv uint16
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "blinded and target",
|
||||||
|
target: &v1,
|
||||||
|
blindedPayment: blindedOtherIntro,
|
||||||
|
err: ErrTargetAndBlinded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// For single-hop blinded we have a final cltv.
|
||||||
|
name: "blinded intro node only",
|
||||||
|
blindedPayment: blindedOtherIntro,
|
||||||
|
expectedTarget: v1,
|
||||||
|
expectedCltv: blindedCltv,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// For multi-hop blinded, we have no final cltv.
|
||||||
|
name: "blinded multi-hop",
|
||||||
|
blindedPayment: blindedMultiHop,
|
||||||
|
expectedTarget: v2,
|
||||||
|
expectedCltv: 0,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unblinded",
|
||||||
|
target: &v2,
|
||||||
|
finalExpiry: unblindedCltv,
|
||||||
|
expectedTarget: v2,
|
||||||
|
expectedCltv: unblindedCltv,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "source node intro",
|
||||||
|
blindedPayment: blindedSelfIntro,
|
||||||
|
err: ErrSelfIntro,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hints and blinded",
|
||||||
|
blindedPayment: blindedMultiHop,
|
||||||
|
routeHints: make(
|
||||||
|
map[route.Vertex][]*channeldb.CachedEdgePolicy,
|
||||||
|
),
|
||||||
|
err: ErrHintsAndBlinded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expiry and blinded",
|
||||||
|
blindedPayment: blindedMultiHop,
|
||||||
|
finalExpiry: unblindedCltv,
|
||||||
|
err: ErrExpiryAndBlinded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid blinded payment",
|
||||||
|
blindedPayment: &BlindedPayment{},
|
||||||
|
err: ErrNoBlindedPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req, err := NewRouteRequest(
|
||||||
|
source, testCase.target, 1000, 0, nil, nil,
|
||||||
|
testCase.routeHints, testCase.blindedPayment,
|
||||||
|
testCase.finalExpiry,
|
||||||
|
)
|
||||||
|
require.ErrorIs(t, err, testCase.err)
|
||||||
|
|
||||||
|
// Skip request validation if we got a non-nil error.
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, req.Target, testCase.expectedTarget)
|
||||||
|
require.Equal(
|
||||||
|
t, req.FinalExpiry, testCase.expectedCltv,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user