Merge pull request #10396 from ziggie1984/enhance-lsp-heuritic
Enhance Lsp Heuristic when probing a payment
This commit is contained in:
8 files changed
+955
-377
No files matched your search
+37
-35
@@ -150,52 +150,58 @@ probing.
|
|||||||
|
|
||||||
The heuristic examines the structure of route hints provided in the invoice to
|
The heuristic examines the structure of route hints provided in the invoice to
|
||||||
identify characteristic LSP patterns. The detection operates on the principle
|
identify characteristic LSP patterns. The detection operates on the principle
|
||||||
that LSPs typically maintain private channels to their users and appear as the
|
that LSPs typically maintain private channels to their users and appear as
|
||||||
penultimate hop in payment routing.
|
public nodes in the network, while the final destination is private.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
Start([Route Hints Received]) --> Empty{Empty Hints?}
|
Start([Route Hints Received]) --> Empty{Empty Hints?}
|
||||||
Empty -->|Yes| NotLSP([Not LSP])
|
Empty -->|Yes| NotLSP([Not LSP])
|
||||||
Empty -->|No| GetFirst[Get First Hint's Last Hop]
|
Empty -->|No| CheckTarget{Invoice Target<br/>in Graph?}
|
||||||
|
|
||||||
GetFirst --> CheckPub1{Is Channel<br/>Public?}
|
CheckTarget -->|Yes| NotLSP
|
||||||
CheckPub1 -->|Yes| NotLSP
|
CheckTarget -->|No| GetFirstDest[Get First Hint's<br/>Destination Hop]
|
||||||
CheckPub1 -->|No| SaveNode[Save Node ID]
|
|
||||||
|
GetFirstDest --> CheckPub1{Destination Node<br/>in Graph?}
|
||||||
SaveNode --> MoreHints{More Hints?}
|
CheckPub1 -->|Yes| IsLSP([Detected as LSP])
|
||||||
MoreHints -->|No| IsLSP([Detected as LSP])
|
CheckPub1 -->|No| MoreHints{More Hints?}
|
||||||
|
|
||||||
|
MoreHints -->|No| NotLSP
|
||||||
MoreHints -->|Yes| NextHint[Check Next Hint]
|
MoreHints -->|Yes| NextHint[Check Next Hint]
|
||||||
|
|
||||||
NextHint --> GetLast[Get Last Hop]
|
NextHint --> GetNextDest[Get Destination Hop]
|
||||||
GetLast --> CheckPub2{Is Channel<br/>Public?}
|
GetNextDest --> CheckPub2{Destination Node<br/>in Graph?}
|
||||||
CheckPub2 -->|Yes| NotLSP
|
CheckPub2 -->|Yes| IsLSP
|
||||||
CheckPub2 -->|No| SameNode{Same Node ID<br/>as First?}
|
CheckPub2 -->|No| MoreHints
|
||||||
|
|
||||||
SameNode -->|No| NotLSP
|
|
||||||
SameNode -->|Yes| MoreHints
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The detection criteria are:
|
The detection follows three simple rules applied sequentially:
|
||||||
|
|
||||||
- **All route hints must terminate at the same node ID** - This indicates a
|
**Rule 1: Public Invoice Target → NOT an LSP**
|
||||||
single destination behind potentially multiple LSP entry points
|
- If the invoice target (destination) is a public node that exists in the
|
||||||
|
channel graph, the payment can be routed directly to it
|
||||||
|
- This means it's not an LSP setup, regardless of what route hints are provided
|
||||||
|
- Example: A well-connected merchant node with route hints for liquidity
|
||||||
|
signaling
|
||||||
|
|
||||||
- **Final hop channels must be private** - The channels in the last hop of
|
**Rule 2: Public Destination Hop → IS an LSP**
|
||||||
each route hint must not exist in the public channel graph
|
- If at least one route hint has a destination hop (last hop in the route hint)
|
||||||
|
that is a public node in the graph, LSP detection is triggered
|
||||||
|
- This indicates the destination hop is an LSP serving a private client
|
||||||
|
- The private client is reached through the LSP's private channel
|
||||||
|
|
||||||
- **No public channels in final hops** - If any route hint contains a public
|
**Rule 3: All Private Destination Hops → NOT an LSP**
|
||||||
channel in its final hop, LSP detection is disabled entirely
|
- If all destination hops in all route hints are private nodes (not in the
|
||||||
|
public graph), this is not treated as an LSP setup
|
||||||
- **Multiple route hints strengthen detection** - While not required,
|
- The payment will be routed directly to the invoice destination using the
|
||||||
multiple hints converging on the same destination strongly suggest an LSP
|
route hints as additional path information
|
||||||
configuration
|
- This is the standard case for private channel payments
|
||||||
|
|
||||||
This pattern effectively distinguishes LSP configurations from other routing
|
This pattern effectively distinguishes LSP configurations from other routing
|
||||||
scenarios. For instance, some Lightning implementations like CLN include route
|
scenarios. For instance, some Lightning implementations like CLN include route
|
||||||
hints even for public nodes to signal liquidity availability or preferred
|
hints even for public nodes to signal liquidity availability or preferred
|
||||||
routing paths. The heuristic correctly identifies these as non-LSP scenarios by
|
routing paths. The heuristic correctly identifies these as non-LSP scenarios
|
||||||
detecting the presence of public channels.
|
by Rule 1 (detecting that the invoice target itself is public).
|
||||||
|
|
||||||
### How Probing Differs When an LSP is Detected
|
### How Probing Differs When an LSP is Detected
|
||||||
|
|
||||||
@@ -432,10 +438,6 @@ appropriately.
|
|||||||
The `EstimateRouteFee` implementation continues to evolve based on real-world
|
The `EstimateRouteFee` implementation continues to evolve based on real-world
|
||||||
usage patterns. Ongoing discussions in the LND community focus on:
|
usage patterns. Ongoing discussions in the LND community focus on:
|
||||||
|
|
||||||
**Improved LSP Detection**: Developing more sophisticated heuristics that
|
|
||||||
accurately identify LSP configurations while avoiding false positives for
|
|
||||||
regular private channels.
|
|
||||||
|
|
||||||
**Multi-Path Payment Support**: Extending fee estimation to support MPP
|
**Multi-Path Payment Support**: Extending fee estimation to support MPP
|
||||||
scenarios where payments split across multiple routes.
|
scenarios where payments split across multiple routes.
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,14 @@
|
|||||||
|
|
||||||
## RPC Updates
|
## RPC Updates
|
||||||
|
|
||||||
|
* The `EstimateRouteFee` RPC now implements an [LSP detection
|
||||||
|
heuristic](https://github.com/lightningnetwork/lnd/pull/10396) that probes up
|
||||||
|
to 3 unique Lightning Service Providers when route hints indicate an LSP
|
||||||
|
setup. The implementation returns worst-case (most expensive) fee estimates
|
||||||
|
for conservative budgeting and includes griefing protection by limiting the
|
||||||
|
number of probed LSPs. It enhances the previous LSP design by being more
|
||||||
|
generic and more flexible.
|
||||||
|
|
||||||
## lncli Updates
|
## lncli Updates
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|||||||
@@ -3431,11 +3431,10 @@ func (c *KVStore) fetchLightningNode(tx kvdb.RTx,
|
|||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasLightningNode determines if the graph has a vertex identified by the
|
// HasNode determines if the graph has a vertex identified by the target node
|
||||||
// target node identity public key. If the node exists in the database, a
|
// identity public key. If the node exists in the database, a timestamp of when
|
||||||
// timestamp of when the data for the node was lasted updated is returned along
|
// the data for the node was lasted updated is returned along with a true
|
||||||
// with a true boolean. Otherwise, an empty time.Time is returned with a false
|
// boolean. Otherwise, an empty time.Time is returned with a false boolean.
|
||||||
// boolean.
|
|
||||||
func (c *KVStore) HasNode(_ context.Context,
|
func (c *KVStore) HasNode(_ context.Context,
|
||||||
nodePub [33]byte) (time.Time, bool, error) {
|
nodePub [33]byte) (time.Time, bool, error) {
|
||||||
|
|
||||||
|
|||||||
@@ -59,18 +59,38 @@ type estimateRouteFeeTestCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testEstimateRouteFee tests the estimation of routing fees using either graph
|
// testEstimateRouteFee tests the estimation of routing fees using either graph
|
||||||
// data or sending out a probe payment.
|
// data or sending out a probe payment. This test validates graph-based fee
|
||||||
|
// estimation, probe-based fee estimation with single LSP, probe-based fee
|
||||||
|
// estimation with multiple route hints to same LSP (worst-case fee selection),
|
||||||
|
// probe-based fee estimation with multiple different public LSPs (worst-case
|
||||||
|
// fee selection across LSPs, up to MaxLspsToProbe), and non-LSP probing (all
|
||||||
|
// private destination hops).
|
||||||
|
//
|
||||||
|
// Note: We test with exactly MaxLspsToProbe (3) LSPs. Testing with more LSPs
|
||||||
|
// is not feasible because the LSP selection uses map iteration, which has
|
||||||
|
// non-deterministic order in Go, making it impossible to predict which LSPs
|
||||||
|
// will be probed.
|
||||||
func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
||||||
|
// Ensure MaxLspsToProbe is set to 3 as expected by this test. The test
|
||||||
|
// uses exactly 3 LSPs in the multi-LSP test case. If MaxLspsToProbe
|
||||||
|
// changes, this assertion will fail as a reminder to update the test.
|
||||||
|
require.Equal(ht, 3, routerrpc.MaxLspsToProbe,
|
||||||
|
"MaxLspsToProbe should be 3")
|
||||||
|
|
||||||
mts := newMppTestScenario(ht)
|
mts := newMppTestScenario(ht)
|
||||||
|
|
||||||
// We extend the regular mpp test scenario with a new node Paula. Paula
|
// We extend the regular mpp test scenario with two new nodes:
|
||||||
// is connected to Bob and Eve through private channels.
|
// - Paula: connected to Bob and Eve through private channels
|
||||||
|
// - Frank: connected to Dave through a private channel
|
||||||
|
//
|
||||||
// /-------------\
|
// /-------------\
|
||||||
// _ Eve _ (private) \
|
// _ Eve _ (private) \
|
||||||
// / \ \
|
// / \ \
|
||||||
// Alice -- Carol ---- Bob --------- Paula
|
// Alice -- Carol ---- Bob --------- Paula
|
||||||
// \ / (private)
|
// \ / (private)
|
||||||
// \__ Dave ____/
|
// \__ Dave ____/
|
||||||
|
// \
|
||||||
|
// \__ Frank (private)
|
||||||
//
|
//
|
||||||
req := &mppOpenChannelRequest{
|
req := &mppOpenChannelRequest{
|
||||||
amtAliceCarol: 200_000,
|
amtAliceCarol: 200_000,
|
||||||
@@ -88,6 +108,7 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
probeInitiator = mts.alice
|
probeInitiator = mts.alice
|
||||||
|
|
||||||
paula := ht.NewNode("Paula", nil)
|
paula := ht.NewNode("Paula", nil)
|
||||||
|
frank := ht.NewNode("Frank", nil)
|
||||||
|
|
||||||
// The channel from Bob to Paula actually doesn't have enough liquidity
|
// The channel from Bob to Paula actually doesn't have enough liquidity
|
||||||
// to carry out the probe. We assume in normal operation that hop hints
|
// to carry out the probe. We assume in normal operation that hop hints
|
||||||
@@ -106,6 +127,13 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
Amt: 1_000_000,
|
Amt: 1_000_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Frank is a private node connected to Dave (public LSP).
|
||||||
|
ht.EnsureConnected(mts.dave, frank)
|
||||||
|
ht.OpenChannel(mts.dave, frank, lntest.OpenChannelParams{
|
||||||
|
Private: true,
|
||||||
|
Amt: 1_000_000,
|
||||||
|
})
|
||||||
|
|
||||||
bobsPrivChannels := mts.bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{
|
bobsPrivChannels := mts.bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{
|
||||||
PrivateOnly: true,
|
PrivateOnly: true,
|
||||||
})
|
})
|
||||||
@@ -118,6 +146,14 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
require.Len(ht, evesPrivChannels.Channels, 1)
|
require.Len(ht, evesPrivChannels.Channels, 1)
|
||||||
evePaulaChanID := evesPrivChannels.Channels[0].ChanId
|
evePaulaChanID := evesPrivChannels.Channels[0].ChanId
|
||||||
|
|
||||||
|
davesPrivChannels := mts.dave.RPC.ListChannels(
|
||||||
|
&lnrpc.ListChannelsRequest{
|
||||||
|
PrivateOnly: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.Len(ht, davesPrivChannels.Channels, 1)
|
||||||
|
daveFrankChanID := davesPrivChannels.Channels[0].ChanId
|
||||||
|
|
||||||
// Let's disable the paths from Alice to Bob through Dave and Eve with
|
// Let's disable the paths from Alice to Bob through Dave and Eve with
|
||||||
// high fees. This ensures that the path estimates are based on Carol's
|
// high fees. This ensures that the path estimates are based on Carol's
|
||||||
// channel to Bob for the first set of tests.
|
// channel to Bob for the first set of tests.
|
||||||
@@ -196,6 +232,33 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
daveHopHint = &lnrpc.HopHint{
|
||||||
|
NodeId: mts.dave.PubKeyStr,
|
||||||
|
FeeBaseMsat: 3_000,
|
||||||
|
FeeProportionalMillionths: 3_000,
|
||||||
|
CltvExpiryDelta: 120,
|
||||||
|
ChanId: daveFrankChanID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple different public LSPs (Bob, Eve, Dave).
|
||||||
|
multipleLspsRouteHints = []*lnrpc.RouteHint{
|
||||||
|
{
|
||||||
|
HopHints: []*lnrpc.HopHint{
|
||||||
|
bobHopHint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HopHints: []*lnrpc.HopHint{
|
||||||
|
eveHopHint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HopHints: []*lnrpc.HopHint{
|
||||||
|
daveHopHint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
defaultTimelock := int64(chainreg.DefaultBitcoinTimeLockDelta)
|
defaultTimelock := int64(chainreg.DefaultBitcoinTimeLockDelta)
|
||||||
@@ -231,6 +294,14 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
feeACEP := feeEP + feeCE
|
feeACEP := feeEP + feeCE
|
||||||
deltaACEP := deltaCE + deltaEP
|
deltaACEP := deltaCE + deltaEP
|
||||||
|
|
||||||
|
// For multiple LSPs test, the route with the highest fee should be
|
||||||
|
// selected (Eve). Note that we return both fee and CLTV delta from
|
||||||
|
// the same route (the highest-fee route), not the max fee and max
|
||||||
|
// delta independently. This ensures the returned values represent an
|
||||||
|
// actual viable route.
|
||||||
|
highestFeeRouteFee := feeACEP
|
||||||
|
highestFeeRouteDelta := deltaACEP
|
||||||
|
|
||||||
initialBlockHeight := int64(mts.alice.RPC.GetInfo().BlockHeight)
|
initialBlockHeight := int64(mts.alice.RPC.GetInfo().BlockHeight)
|
||||||
|
|
||||||
// Locktime is always composed of the initial block height and the
|
// Locktime is always composed of the initial block height and the
|
||||||
@@ -271,6 +342,19 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
expectedCltvDelta: locktime + deltaCB,
|
expectedCltvDelta: locktime + deltaCB,
|
||||||
expectedFailureReason: failureReasonNone,
|
expectedFailureReason: failureReasonNone,
|
||||||
},
|
},
|
||||||
|
// Rule 1: Invoice target is public (Bob), even with public
|
||||||
|
// destination hop hints. Should route directly to Bob, NOT
|
||||||
|
// treat as LSP.
|
||||||
|
{
|
||||||
|
name: "probe based estimate, public " +
|
||||||
|
"target with public hop hints",
|
||||||
|
probing: true,
|
||||||
|
destination: mts.bob,
|
||||||
|
routeHints: singleRouteHint,
|
||||||
|
expectedRoutingFeesMsat: feeStandardSingleHop,
|
||||||
|
expectedCltvDelta: locktime + deltaCB,
|
||||||
|
expectedFailureReason: failureReasonNone,
|
||||||
|
},
|
||||||
// We expect the previous probing results adjusted by Paula's
|
// We expect the previous probing results adjusted by Paula's
|
||||||
// hop data.
|
// hop data.
|
||||||
{
|
{
|
||||||
@@ -340,6 +424,23 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
|||||||
expectedCltvDelta: 0,
|
expectedCltvDelta: 0,
|
||||||
expectedFailureReason: failureReasonNoRoute,
|
expectedFailureReason: failureReasonNoRoute,
|
||||||
},
|
},
|
||||||
|
// Test multiple different public LSPs. The worst-case (most
|
||||||
|
// expensive) route should be returned. Eve has the highest
|
||||||
|
// fees among the 3 LSPs tested. Note: We don't test with more
|
||||||
|
// than MaxLspsToProbe LSPs because map iteration order in Go
|
||||||
|
// is non-deterministic, making it impossible to predict which
|
||||||
|
// LSPs will be selected for probing.
|
||||||
|
{
|
||||||
|
name: "probe based estimate, " +
|
||||||
|
"multiple different public LSPs",
|
||||||
|
probing: true,
|
||||||
|
destination: frank,
|
||||||
|
routeHints: multipleLspsRouteHints,
|
||||||
|
expectedRoutingFeesMsat: highestFeeRouteFee,
|
||||||
|
expectedCltvDelta: locktime +
|
||||||
|
highestFeeRouteDelta,
|
||||||
|
expectedFailureReason: failureReasonNone,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ type RouterBackend struct {
|
|||||||
FetchChannelEndpoints func(chanID uint64) (route.Vertex,
|
FetchChannelEndpoints func(chanID uint64) (route.Vertex,
|
||||||
route.Vertex, error)
|
route.Vertex, error)
|
||||||
|
|
||||||
|
// HasNode returns true if the node exists in the graph (i.e., has
|
||||||
|
// public channels), false otherwise. This means the node is a public
|
||||||
|
// node and should be reachable.
|
||||||
|
HasNode func(nodePub route.Vertex) (bool, error)
|
||||||
|
|
||||||
// FindRoute is a closure that abstracts away how we locate/query for
|
// FindRoute is a closure that abstracts away how we locate/query for
|
||||||
// routes.
|
// routes.
|
||||||
FindRoute func(*routing.RouteRequest) (*route.Route, float64, error)
|
FindRoute func(*routing.RouteRequest) (*route.Route, float64, error)
|
||||||
|
|||||||
+304
-157
@@ -1,7 +1,6 @@
|
|||||||
package routerrpc
|
package routerrpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -44,6 +43,12 @@ const (
|
|||||||
// DefaultPaymentTimeout is the default value of time we should spend
|
// DefaultPaymentTimeout is the default value of time we should spend
|
||||||
// when attempting to fulfill the payment.
|
// when attempting to fulfill the payment.
|
||||||
DefaultPaymentTimeout int32 = 60
|
DefaultPaymentTimeout int32 = 60
|
||||||
|
|
||||||
|
// MaxLspsToProbe is the maximum number of LSPs to probe when
|
||||||
|
// estimating fees for worst-case fee estimation. This is a
|
||||||
|
// precautionary measure to prevent the estimation from taking too
|
||||||
|
// long, and it is also a griefing protection.
|
||||||
|
MaxLspsToProbe = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -171,10 +176,9 @@ var (
|
|||||||
DefaultRouterMacFilename = "router.macaroon"
|
DefaultRouterMacFilename = "router.macaroon"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FetchChannelEndpoints returns the pubkeys of both endpoints of the
|
// HasNode returns true if the node exists in the graph (i.e., has public
|
||||||
// given channel id if it exists in the graph.
|
// channels), false otherwise.
|
||||||
type FetchChannelEndpoints func(chanID uint64) (route.Vertex, route.Vertex,
|
type HasNode func(nodePub route.Vertex) (bool, error)
|
||||||
error)
|
|
||||||
|
|
||||||
// ServerShell is a shell struct holding a reference to the actual sub-server.
|
// ServerShell is a shell struct holding a reference to the actual sub-server.
|
||||||
// It is used to register the gRPC sub-server with the root server before we
|
// It is used to register the gRPC sub-server with the root server before we
|
||||||
@@ -561,7 +565,8 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
|
|||||||
// If the hints don't indicate an LSP then chances are that our probe
|
// If the hints don't indicate an LSP then chances are that our probe
|
||||||
// payment won't be blocked along the route to the destination. We send
|
// payment won't be blocked along the route to the destination. We send
|
||||||
// a probe payment with unmodified route hints.
|
// a probe payment with unmodified route hints.
|
||||||
if !isLSP(hints, s.cfg.RouterBackend.FetchChannelEndpoints) {
|
invoiceTargetCompressed := payReq.Destination.SerializeCompressed()
|
||||||
|
if !isLSP(hints, invoiceTargetCompressed, s.cfg.RouterBackend.HasNode) {
|
||||||
log.Infof("No LSP detected, probing destination %x",
|
log.Infof("No LSP detected, probing destination %x",
|
||||||
probeRequest.Dest)
|
probeRequest.Dest)
|
||||||
|
|
||||||
@@ -569,200 +574,342 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
|
|||||||
return s.sendProbePayment(ctx, probeRequest)
|
return s.sendProbePayment(ctx, probeRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the heuristic indicates an LSP we modify the route hints to allow
|
// If the heuristic indicates an LSP, we filter and group route hints by
|
||||||
// probing the LSP.
|
// public LSP nodes, then probe each unique LSP separately and return
|
||||||
lspAdjustedRouteHints, lspHint, err := prepareLspRouteHints(
|
// the cheapest route.
|
||||||
hints, *payReq.MilliSat,
|
lspGroups, err := prepareLspRouteHints(
|
||||||
|
hints, *payReq.MilliSat, s.cfg.RouterBackend.HasNode,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the destination to the LSP node ID.
|
log.Infof("LSP detected, found %d unique public LSP node(s) to probe",
|
||||||
lspDest := lspHint.NodeID.SerializeCompressed()
|
len(lspGroups))
|
||||||
probeRequest.Dest = lspDest
|
|
||||||
|
|
||||||
log.Infof("LSP detected, probing LSP with destination: %x", lspDest)
|
// Probe up to MaxLspsToProbe LSPs and track the most expensive route
|
||||||
|
// for worst-case fee estimation.
|
||||||
// The adjusted route hints serve the payment probe to find the last
|
if len(lspGroups) > MaxLspsToProbe {
|
||||||
// public hop to the LSP on the route.
|
log.Debugf("Limiting LSP probes from %d to %d for worst-case "+
|
||||||
if len(lspAdjustedRouteHints) > 0 {
|
"fee estimation", len(lspGroups), MaxLspsToProbe)
|
||||||
probeRequest.RouteHints = invoicesrpc.CreateRPCRouteHints(
|
|
||||||
lspAdjustedRouteHints,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
var (
|
||||||
|
worstCaseResp *RouteFeeResponse
|
||||||
|
worstCaseLspDest route.Vertex
|
||||||
|
probeCount int
|
||||||
|
)
|
||||||
|
|
||||||
// The payment probe will be able to calculate the fee up until the LSP
|
for lspKey, group := range lspGroups {
|
||||||
// node. The fee of the last hop has to be calculated manually. Since
|
if probeCount >= MaxLspsToProbe {
|
||||||
// the last hop's fee amount has to be sent across the payment path we
|
break
|
||||||
// have to add it to the original payment amount. Only then will the
|
}
|
||||||
// payment probe be able to determine the correct fee to the last hop
|
probeCount++
|
||||||
// prior to the private destination. For example, if the user wants to
|
|
||||||
// send 1000 sats to a private destination and the last hop's fee is 10
|
|
||||||
// sats, then 1010 sats will have to arrive at the last hop. This means
|
|
||||||
// that the probe has to be dispatched with 1010 sats to correctly
|
|
||||||
// calculate the routing fee.
|
|
||||||
//
|
|
||||||
// Calculate the hop fee for the last hop manually.
|
|
||||||
hopFee := lspHint.HopFee(*payReq.MilliSat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the last hop's fee to the requested payment amount that we want
|
lspHint := group.LspHopHint
|
||||||
// to get an estimate for.
|
|
||||||
probeRequest.AmtMsat += int64(hopFee)
|
|
||||||
|
|
||||||
// Use the hop hint's cltv delta as the payment request's final cltv
|
log.Infof("Probing LSP with destination: %v", lspKey)
|
||||||
// delta. The actual final cltv delta of the invoice will be added to
|
|
||||||
// the payment probe's cltv delta.
|
|
||||||
probeRequest.FinalCltvDelta = int32(lspHint.CLTVExpiryDelta)
|
|
||||||
|
|
||||||
// Dispatch the payment probe with adjusted fee amount.
|
// Create a new probe request for this LSP.
|
||||||
resp, err := s.sendProbePayment(ctx, probeRequest)
|
lspProbeRequest := &SendPaymentRequest{
|
||||||
if err != nil {
|
TimeoutSeconds: probeRequest.TimeoutSeconds,
|
||||||
return nil, fmt.Errorf("failed to send probe payment to "+
|
Dest: lspKey[:],
|
||||||
"LSP with destination %x: %w", lspDest, err)
|
MaxParts: probeRequest.MaxParts,
|
||||||
}
|
AllowSelfPayment: probeRequest.AllowSelfPayment,
|
||||||
|
AmtMsat: amtMsat,
|
||||||
|
PaymentHash: probeRequest.PaymentHash,
|
||||||
|
FeeLimitSat: probeRequest.FeeLimitSat,
|
||||||
|
FinalCltvDelta: int32(lspHint.CLTVExpiryDelta),
|
||||||
|
DestFeatures: probeRequest.DestFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
// If the payment probe failed we only return the failure reason and
|
// Copy the payment address if present.
|
||||||
// leave the probe result params unaltered.
|
if len(probeRequest.PaymentAddr) > 0 {
|
||||||
if resp.FailureReason != lnrpc.PaymentFailureReason_FAILURE_REASON_NONE { //nolint:ll
|
copy(
|
||||||
return resp, nil
|
lspProbeRequest.PaymentAddr,
|
||||||
}
|
probeRequest.PaymentAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// The probe succeeded, so we can add the last hop's fee to fee the
|
// Set the adjusted route hints for this LSP.
|
||||||
// payment probe returned.
|
if len(group.AdjustedRouteHints) > 0 {
|
||||||
resp.RoutingFeeMsat += int64(hopFee)
|
lspProbeRequest.RouteHints = invoicesrpc.
|
||||||
|
CreateRPCRouteHints(group.AdjustedRouteHints)
|
||||||
|
}
|
||||||
|
|
||||||
// Add the final cltv delta of the invoice to the payment probe's total
|
// Calculate the hop fee for the last hop manually.
|
||||||
// cltv delta. This is the cltv delta for the hop behind the LSP.
|
hopFee := lspHint.HopFee(*payReq.MilliSat)
|
||||||
resp.TimeLockDelay += int64(payReq.MinFinalCLTVExpiry())
|
|
||||||
|
|
||||||
return resp, nil
|
// Add the last hop's fee to the probe amount.
|
||||||
}
|
lspProbeRequest.AmtMsat += int64(hopFee)
|
||||||
|
|
||||||
// isLSP checks if the route hints indicate an LSP. An LSP is indicated with
|
// Dispatch the payment probe for this LSP.
|
||||||
// true if the destination hop hint in each route hint has the same node id,
|
resp, err := s.sendProbePayment(ctx, lspProbeRequest)
|
||||||
// false otherwise. If the destination hop hint of any route hint contains a
|
if err != nil {
|
||||||
// public channel, the function returns false because we can directly send a
|
log.Warnf("Failed to probe LSP %v: %v", lspKey, err)
|
||||||
// probe to the final destination.
|
|
||||||
func isLSP(routeHints [][]zpay32.HopHint,
|
|
||||||
fetchChannelEndpoints FetchChannelEndpoints) bool {
|
|
||||||
|
|
||||||
if len(routeHints) == 0 || len(routeHints[0]) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
destHopHint := routeHints[0][len(routeHints[0])-1]
|
|
||||||
|
|
||||||
// If the destination hop hint of the first route hint contains a public
|
|
||||||
// channel we can send a probe to it directly, hence we don't signal an
|
|
||||||
// LSP.
|
|
||||||
_, _, err := fetchChannelEndpoints(destHopHint.ChannelID)
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; i < len(routeHints); i++ {
|
|
||||||
// Skip empty route hints.
|
|
||||||
if len(routeHints[i]) == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lastHop := routeHints[i][len(routeHints[i])-1]
|
// If the probe failed, skip this LSP.
|
||||||
|
if resp.FailureReason !=
|
||||||
|
lnrpc.PaymentFailureReason_FAILURE_REASON_NONE {
|
||||||
|
|
||||||
// If the last hop hint of any route hint contains a public
|
log.Debugf("Probe to LSP %v failed with reason: %v",
|
||||||
// channel we can send a probe to it directly, hence we don't
|
lspKey, resp.FailureReason)
|
||||||
// signal an LSP.
|
|
||||||
_, _, err = fetchChannelEndpoints(lastHop.ChannelID)
|
continue
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matchesDestNode := bytes.Equal(
|
// The probe succeeded, add the last hop's fee.
|
||||||
lastHop.NodeID.SerializeCompressed(),
|
resp.RoutingFeeMsat += int64(hopFee)
|
||||||
destHopHint.NodeID.SerializeCompressed(),
|
|
||||||
)
|
// Add the final cltv delta of the invoice.
|
||||||
if !matchesDestNode {
|
resp.TimeLockDelay += int64(payReq.MinFinalCLTVExpiry())
|
||||||
|
|
||||||
|
log.Infof("Probe to LSP %v succeeded with fee: %d msat",
|
||||||
|
lspKey, resp.RoutingFeeMsat)
|
||||||
|
|
||||||
|
// Track the most expensive route for worst-case estimation.
|
||||||
|
// We solely consider the routing fee for the worst-case
|
||||||
|
// estimation.
|
||||||
|
if worstCaseResp == nil ||
|
||||||
|
resp.RoutingFeeMsat > worstCaseResp.RoutingFeeMsat {
|
||||||
|
|
||||||
|
if worstCaseResp != nil {
|
||||||
|
log.Debugf("LSP %v has higher fee "+
|
||||||
|
"(%d msat) than current worst-case "+
|
||||||
|
"%v (%d msat), updating worst-case "+
|
||||||
|
"estimate", lspKey,
|
||||||
|
resp.RoutingFeeMsat, worstCaseLspDest,
|
||||||
|
worstCaseResp.RoutingFeeMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
worstCaseResp = resp
|
||||||
|
worstCaseLspDest = lspKey
|
||||||
|
} else {
|
||||||
|
log.Debugf("LSP %v fee (%d msat) is lower than "+
|
||||||
|
"current worst-case %v (%d msat), keeping "+
|
||||||
|
"worst-case estimate", lspKey,
|
||||||
|
resp.RoutingFeeMsat, worstCaseLspDest,
|
||||||
|
worstCaseResp.RoutingFeeMsat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no LSP probe succeeded, return an error.
|
||||||
|
if worstCaseResp == nil {
|
||||||
|
return nil, fmt.Errorf("all LSP probe payments failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Returning worst-case route via LSP %v with fee: %d msat, "+
|
||||||
|
"timelock: %d", worstCaseLspDest, worstCaseResp.RoutingFeeMsat,
|
||||||
|
worstCaseResp.TimeLockDelay)
|
||||||
|
|
||||||
|
return worstCaseResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLSP checks if the route hints indicate an LSP setup. An LSP setup is
|
||||||
|
// identified when the invoice destination is private but the final hop in the
|
||||||
|
// route hints is a public node (the LSP). This function implements three rules:
|
||||||
|
//
|
||||||
|
// 1. If the invoice target is a public node (exists in graph) => isLsp = false
|
||||||
|
// We can route directly to the target, so no LSP is involved.
|
||||||
|
//
|
||||||
|
// 2. If at least one destination hop hint (last hop in route hint) is public
|
||||||
|
// => isLsp = true. The public destination hop is the LSP, and the actual
|
||||||
|
// invoice target is a private node behind it.
|
||||||
|
//
|
||||||
|
// 3. If all destination hop hints are private nodes => isLsp = false.
|
||||||
|
// We assume this is NOT an LSP setup. Instead, we expect the route hints
|
||||||
|
// contain public nodes earlier in the path (not the final hop) that our
|
||||||
|
// pathfinder can route to. For example:
|
||||||
|
// The pathfinder will route to PublicNode and use the hints from there.
|
||||||
|
// Note: If no public nodes exist anywhere in the route hints, the
|
||||||
|
// destination would be unreachable (malformed invoice), but we don't
|
||||||
|
// validate that here.
|
||||||
|
func isLSP(routeHints [][]zpay32.HopHint, invoiceTarget []byte,
|
||||||
|
hasNode HasNode) bool {
|
||||||
|
|
||||||
|
if len(routeHints) == 0 || len(routeHints[0]) == 0 {
|
||||||
|
log.Debugf("No route hints provided, this is not an LSP setup")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 1: If the invoice target is a public node (exists in the graph),
|
||||||
|
// we can route directly to it, so it's not an LSP setup.
|
||||||
|
if len(invoiceTarget) > 0 {
|
||||||
|
var targetVertex route.Vertex
|
||||||
|
copy(targetVertex[:], invoiceTarget)
|
||||||
|
|
||||||
|
isPublic, err := hasNode(targetVertex)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to check if invoice target %x is "+
|
||||||
|
"public: %v", invoiceTarget, err)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isPublic {
|
||||||
|
log.Infof("Invoice target %x is a public node in the "+
|
||||||
|
"graph, this is NOT an LSP setup",
|
||||||
|
invoiceTarget)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We ensured that the destination hop hint doesn't contain a public
|
for _, hopHints := range routeHints {
|
||||||
// channel, and that all destination hop hints of all route hints match,
|
// Skip empty route hints.
|
||||||
// so we signal an LSP.
|
if len(hopHints) == 0 {
|
||||||
return true
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHop := hopHints[len(hopHints)-1]
|
||||||
|
lastHopNodeCompressed := lastHop.NodeID.SerializeCompressed()
|
||||||
|
|
||||||
|
// Check if this destination hop hint node is public.
|
||||||
|
// Rule 2: If we find a public node, we can exit early.
|
||||||
|
var lastHopVertex route.Vertex
|
||||||
|
copy(lastHopVertex[:], lastHopNodeCompressed)
|
||||||
|
|
||||||
|
isPublic, err := hasNode(lastHopVertex)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to check if destination hop "+
|
||||||
|
"hint %x is public: %v", lastHopNodeCompressed,
|
||||||
|
err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isPublic {
|
||||||
|
log.Infof("Destination hop hint %x is a public node, "+
|
||||||
|
"this is an LSP setup", lastHopNodeCompressed)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 3: If all destination hop hints are private nodes (not in the
|
||||||
|
// graph), this is NOT an LSP setup. We assume the route hints contain
|
||||||
|
// public nodes earlier in the path that we can route through using
|
||||||
|
// standard pathfinding with the hints.
|
||||||
|
log.Infof("All destination hop hints are private, this is NOT an " +
|
||||||
|
"LSP setup")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// LspRouteGroup represents a group of route hints that share the same public
|
||||||
|
// LSP destination node. This is needed when probing LSPs separately to find
|
||||||
|
// the cheapest route.
|
||||||
|
type LspRouteGroup struct {
|
||||||
|
// LspHopHint is the hop hint for the LSP node with worst-case fees and
|
||||||
|
// CLTV delta.
|
||||||
|
LspHopHint *zpay32.HopHint
|
||||||
|
|
||||||
|
// AdjustedRouteHints are the route hints with the LSP hop stripped off.
|
||||||
|
AdjustedRouteHints [][]zpay32.HopHint
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareLspRouteHints assumes that the isLsp heuristic returned true for the
|
// prepareLspRouteHints assumes that the isLsp heuristic returned true for the
|
||||||
// route hints passed in here. It constructs a modified list of route hints that
|
// route hints passed in here. It filters route hints to only include those with
|
||||||
// allows the caller to probe the LSP, which itself is returned as a separate
|
// public destination nodes, groups them by unique LSP node, and returns a map
|
||||||
// hop hint.
|
// of LSP groups keyed by the LSP node's compressed public key.
|
||||||
func prepareLspRouteHints(routeHints [][]zpay32.HopHint,
|
func prepareLspRouteHints(routeHints [][]zpay32.HopHint,
|
||||||
amt lnwire.MilliSatoshi) ([][]zpay32.HopHint, *zpay32.HopHint, error) {
|
amt lnwire.MilliSatoshi,
|
||||||
|
hasNode HasNode) (map[route.Vertex]*LspRouteGroup, error) {
|
||||||
|
|
||||||
|
// This should never happen, but we check for it for completeness.
|
||||||
|
// Because the isLSP heuristic already checked that the route hints are
|
||||||
|
// not empty.
|
||||||
if len(routeHints) == 0 {
|
if len(routeHints) == 0 {
|
||||||
return nil, nil, fmt.Errorf("no route hints provided")
|
return nil, fmt.Errorf("no route hints provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the LSP hop hint. We are probing for the worst case fee and
|
// Map to group route hints by LSP node pubkey.
|
||||||
// cltv delta. So we look for the max values amongst all LSP hop hints.
|
lspGroups := make(map[route.Vertex]*LspRouteGroup)
|
||||||
refHint := routeHints[0][len(routeHints[0])-1]
|
|
||||||
refHint.CLTVExpiryDelta = maxLspCltvDelta(routeHints)
|
|
||||||
refHint.FeeBaseMSat, refHint.FeeProportionalMillionths = maxLspFee(
|
|
||||||
routeHints, amt,
|
|
||||||
)
|
|
||||||
|
|
||||||
// We construct a modified list of route hints that allows the caller to
|
for _, routeHint := range routeHints {
|
||||||
// probe the LSP.
|
// Skip empty route hints.
|
||||||
adjustedHints := make([][]zpay32.HopHint, 0, len(routeHints))
|
if len(routeHint) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Strip off the LSP hop hint from all route hints.
|
// Get the destination hop hint (last hop in the route).
|
||||||
for i := 0; i < len(routeHints); i++ {
|
destHop := routeHint[len(routeHint)-1]
|
||||||
hint := routeHints[i]
|
destNodeCompressed := destHop.NodeID.SerializeCompressed()
|
||||||
if len(hint) > 1 {
|
|
||||||
adjustedHints = append(
|
// Check if this destination node is public.
|
||||||
adjustedHints, hint[:len(hint)-1],
|
var destVertex route.Vertex
|
||||||
|
copy(destVertex[:], destNodeCompressed)
|
||||||
|
|
||||||
|
isPublic, err := hasNode(destVertex)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to check if dest hop hint %x is "+
|
||||||
|
"public: %v", destNodeCompressed, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip private destination nodes - we only probe public LSPs.
|
||||||
|
if !isPublic {
|
||||||
|
log.Debugf("Skipping route hint with private dest "+
|
||||||
|
"node %x", destNodeCompressed)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the compressed pubkey as the map key.
|
||||||
|
var lspKey route.Vertex
|
||||||
|
copy(lspKey[:], destNodeCompressed)
|
||||||
|
|
||||||
|
// Get or create the LSP group for this node.
|
||||||
|
group, exists := lspGroups[lspKey]
|
||||||
|
if !exists {
|
||||||
|
//nolint:ll
|
||||||
|
lspHop := zpay32.HopHint{
|
||||||
|
NodeID: destHop.NodeID,
|
||||||
|
ChannelID: destHop.ChannelID,
|
||||||
|
FeeBaseMSat: destHop.FeeBaseMSat,
|
||||||
|
FeeProportionalMillionths: destHop.FeeProportionalMillionths,
|
||||||
|
CLTVExpiryDelta: destHop.CLTVExpiryDelta,
|
||||||
|
}
|
||||||
|
group = &LspRouteGroup{
|
||||||
|
LspHopHint: &lspHop,
|
||||||
|
AdjustedRouteHints: make([][]zpay32.HopHint, 0),
|
||||||
|
}
|
||||||
|
lspGroups[lspKey] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the LSP hop hint with worst-case (max) fees and CLTV.
|
||||||
|
hopFee := destHop.HopFee(amt)
|
||||||
|
currentMaxFee := group.LspHopHint.HopFee(amt)
|
||||||
|
if hopFee > currentMaxFee {
|
||||||
|
group.LspHopHint.FeeBaseMSat = destHop.FeeBaseMSat
|
||||||
|
group.LspHopHint.FeeProportionalMillionths = destHop.
|
||||||
|
FeeProportionalMillionths
|
||||||
|
}
|
||||||
|
|
||||||
|
if destHop.CLTVExpiryDelta > group.LspHopHint.CLTVExpiryDelta {
|
||||||
|
group.LspHopHint.CLTVExpiryDelta = destHop.
|
||||||
|
CLTVExpiryDelta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the route hint with the LSP hop stripped off (if there
|
||||||
|
// are hops before the LSP).
|
||||||
|
if len(routeHint) > 1 {
|
||||||
|
group.AdjustedRouteHints = append(
|
||||||
|
group.AdjustedRouteHints,
|
||||||
|
routeHint[:len(routeHint)-1],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return adjustedHints, &refHint, nil
|
if len(lspGroups) == 0 {
|
||||||
}
|
return nil, fmt.Errorf("no public LSP nodes found in " +
|
||||||
|
"route hints")
|
||||||
// maxLspFee returns base fee and fee rate amongst all LSP route hints that
|
|
||||||
// results in the overall highest fee for the given amount.
|
|
||||||
func maxLspFee(routeHints [][]zpay32.HopHint, amt lnwire.MilliSatoshi) (uint32,
|
|
||||||
uint32) {
|
|
||||||
|
|
||||||
var maxFeePpm uint32
|
|
||||||
var maxBaseFee uint32
|
|
||||||
var maxTotalFee lnwire.MilliSatoshi
|
|
||||||
for _, rh := range routeHints {
|
|
||||||
lastHop := rh[len(rh)-1]
|
|
||||||
lastHopFee := lastHop.HopFee(amt)
|
|
||||||
if lastHopFee > maxTotalFee {
|
|
||||||
maxTotalFee = lastHopFee
|
|
||||||
maxBaseFee = lastHop.FeeBaseMSat
|
|
||||||
maxFeePpm = lastHop.FeeProportionalMillionths
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxBaseFee, maxFeePpm
|
log.Infof("Found %d unique public LSP node(s) in route hints",
|
||||||
}
|
len(lspGroups))
|
||||||
|
|
||||||
// maxLspCltvDelta returns the maximum cltv delta amongst all LSP route hints.
|
return lspGroups, nil
|
||||||
func maxLspCltvDelta(routeHints [][]zpay32.HopHint) uint16 {
|
|
||||||
var maxCltvDelta uint16
|
|
||||||
for _, rh := range routeHints {
|
|
||||||
rhLastHop := rh[len(rh)-1]
|
|
||||||
if rhLastHop.CLTVExpiryDelta > maxCltvDelta {
|
|
||||||
maxCltvDelta = rhLastHop.CLTVExpiryDelta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxCltvDelta
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// probePaymentStream is a custom implementation of the grpc.ServerStream
|
// probePaymentStream is a custom implementation of the grpc.ServerStream
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package routerrpc
|
package routerrpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
graphdb "github.com/lightningnetwork/lnd/graph/db"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
|
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
|
||||||
@@ -220,12 +220,18 @@ func TestTrackPaymentsNoInflightUpdates(t *testing.T) {
|
|||||||
require.Equal(t, lnrpc.Payment_SUCCEEDED, payment.Status)
|
require.Equal(t, lnrpc.Payment_SUCCEEDED, payment.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIsLsp tests the isLSP heuristic. Combinations of different route hints
|
// TestIsLsp tests the isLSP heuristic. It validates all three LSP detection
|
||||||
// with different fees and cltv deltas are tested to ensure that the heuristic
|
// rules:
|
||||||
// correctly identifies whether a route leads to an LSP or not.
|
// Rule 1: Invoice target is public => not LSP.
|
||||||
|
// Rule 2: All destination hop hints are private => not LSP (Boltz case).
|
||||||
|
// Rule 3: At least one destination hop hint is public => LSP (Muun case).
|
||||||
func TestIsLsp(t *testing.T) {
|
func TestIsLsp(t *testing.T) {
|
||||||
probeAmtMsat := lnwire.MilliSatoshi(1_000_000)
|
// Setup test nodes:
|
||||||
|
// - Alice: public node (in graph)
|
||||||
|
// - Bob: private node
|
||||||
|
// - Carol: private node
|
||||||
|
// - Dave: public node (in graph)
|
||||||
|
// - Eve: private node
|
||||||
alicePrivKey, err := btcec.NewPrivateKey()
|
alicePrivKey, err := btcec.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
alicePubKey := alicePrivKey.PubKey()
|
alicePubKey := alicePrivKey.PubKey()
|
||||||
@@ -242,216 +248,519 @@ func TestIsLsp(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
davePubKey := davePrivKey.PubKey()
|
davePubKey := davePrivKey.PubKey()
|
||||||
|
|
||||||
var (
|
evePrivKey, err := btcec.NewPrivateKey()
|
||||||
aliceHopHint = zpay32.HopHint{
|
require.NoError(t, err)
|
||||||
NodeID: alicePubKey,
|
evePubKey := evePrivKey.PubKey()
|
||||||
FeeBaseMSat: 100,
|
|
||||||
FeeProportionalMillionths: 1_000,
|
|
||||||
ChannelID: 421337,
|
|
||||||
}
|
|
||||||
|
|
||||||
bobHopHint = zpay32.HopHint{
|
// Create hop hints for each node.
|
||||||
NodeID: bobPubKey,
|
aliceHopHint := zpay32.HopHint{
|
||||||
FeeBaseMSat: 2_000,
|
NodeID: alicePubKey,
|
||||||
FeeProportionalMillionths: 2_000,
|
FeeBaseMSat: 100,
|
||||||
CLTVExpiryDelta: 288,
|
FeeProportionalMillionths: 1_000,
|
||||||
ChannelID: 815,
|
CLTVExpiryDelta: 40,
|
||||||
}
|
ChannelID: 1,
|
||||||
|
}
|
||||||
|
|
||||||
carolHopHint = zpay32.HopHint{
|
bobHopHint := zpay32.HopHint{
|
||||||
NodeID: carolPubKey,
|
NodeID: bobPubKey,
|
||||||
FeeBaseMSat: 2_000,
|
FeeBaseMSat: 2_000,
|
||||||
FeeProportionalMillionths: 2_000,
|
FeeProportionalMillionths: 2_000,
|
||||||
ChannelID: 815,
|
CLTVExpiryDelta: 144,
|
||||||
}
|
ChannelID: 2,
|
||||||
|
}
|
||||||
|
|
||||||
daveHopHint = zpay32.HopHint{
|
carolHopHint := zpay32.HopHint{
|
||||||
NodeID: davePubKey,
|
NodeID: carolPubKey,
|
||||||
FeeBaseMSat: 2_000,
|
FeeBaseMSat: 1_500,
|
||||||
FeeProportionalMillionths: 2_000,
|
FeeProportionalMillionths: 1_500,
|
||||||
ChannelID: 815,
|
CLTVExpiryDelta: 144,
|
||||||
}
|
ChannelID: 3,
|
||||||
|
}
|
||||||
|
|
||||||
publicChannelID = uint64(42)
|
daveHopHint := zpay32.HopHint{
|
||||||
daveHopHintPublicChan = zpay32.HopHint{
|
NodeID: davePubKey,
|
||||||
NodeID: davePubKey,
|
FeeBaseMSat: 3_000,
|
||||||
FeeBaseMSat: 2_000,
|
FeeProportionalMillionths: 3_000,
|
||||||
FeeProportionalMillionths: 2_000,
|
CLTVExpiryDelta: 288,
|
||||||
ChannelID: publicChannelID,
|
ChannelID: 4,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
bobExpensiveCopy := bobHopHint.Copy()
|
eveHopHint := zpay32.HopHint{
|
||||||
bobExpensiveCopy.FeeBaseMSat = 1_000_000
|
NodeID: evePubKey,
|
||||||
bobExpensiveCopy.FeeProportionalMillionths = 1_000_000
|
FeeBaseMSat: 500,
|
||||||
bobExpensiveCopy.CLTVExpiryDelta = bobHopHint.CLTVExpiryDelta - 1
|
FeeProportionalMillionths: 500,
|
||||||
|
CLTVExpiryDelta: 40,
|
||||||
|
ChannelID: 5,
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:ll
|
// Mock hasNode: returns true only for alice and dave.
|
||||||
lspTestCases := []struct {
|
hasNode := func(nodePub route.Vertex) (bool, error) {
|
||||||
name string
|
aliceVertex := route.NewVertex(alicePubKey)
|
||||||
routeHints [][]zpay32.HopHint
|
daveVertex := route.NewVertex(davePubKey)
|
||||||
probeAmtMsat lnwire.MilliSatoshi
|
return bytes.Equal(nodePub[:], aliceVertex[:]) ||
|
||||||
isLsp bool
|
bytes.Equal(nodePub[:], daveVertex[:]), nil
|
||||||
expectedHints [][]zpay32.HopHint
|
}
|
||||||
expectedLspHop *zpay32.HopHint
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
routeHints [][]zpay32.HopHint
|
||||||
|
invoiceTarget []byte
|
||||||
|
expectLSP bool
|
||||||
}{
|
}{
|
||||||
|
// Edge cases.
|
||||||
{
|
{
|
||||||
name: "empty route hints",
|
name: "no route hints",
|
||||||
routeHints: [][]zpay32.HopHint{{}},
|
routeHints: [][]zpay32.HopHint{},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: nil,
|
||||||
isLsp: false,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{},
|
|
||||||
expectedLspHop: nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single route hint",
|
name: "empty route hint array",
|
||||||
routeHints: [][]zpay32.HopHint{{daveHopHint}},
|
routeHints: [][]zpay32.HopHint{{}},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: nil,
|
||||||
isLsp: true,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{},
|
|
||||||
expectedLspHop: &daveHopHint,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Rule 1: Invoice target is public => NOT an LSP.
|
||||||
|
// Rationale: Can route directly to public target.
|
||||||
{
|
{
|
||||||
name: "single route, multiple hints",
|
name: "invoice target is public (alice)",
|
||||||
routeHints: [][]zpay32.HopHint{{
|
|
||||||
aliceHopHint, bobHopHint,
|
|
||||||
}},
|
|
||||||
probeAmtMsat: probeAmtMsat,
|
|
||||||
isLsp: true,
|
|
||||||
expectedHints: [][]zpay32.HopHint{{aliceHopHint}},
|
|
||||||
expectedLspHop: &bobHopHint,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple routes, multiple hints",
|
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{bobHopHint, carolHopHint},
|
||||||
aliceHopHint, bobHopHint,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carolHopHint, bobHopHint,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: alicePubKey.SerializeCompressed(),
|
||||||
isLsp: true,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{
|
|
||||||
{aliceHopHint}, {carolHopHint},
|
|
||||||
},
|
|
||||||
expectedLspHop: &bobHopHint,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple routes, multiple hints with min length",
|
name: "invoice target is public with public dest hop",
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{bobHopHint, daveHopHint},
|
||||||
bobHopHint,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carolHopHint, bobHopHint,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: davePubKey.SerializeCompressed(),
|
||||||
isLsp: true,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{
|
|
||||||
{carolHopHint},
|
|
||||||
},
|
|
||||||
expectedLspHop: &bobHopHint,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple routes, multiple hints, diff fees+cltv",
|
name: "invoice target is public with multiple routes",
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{bobHopHint, carolHopHint},
|
||||||
bobHopHint,
|
{aliceHopHint, daveHopHint},
|
||||||
},
|
|
||||||
{
|
|
||||||
carolHopHint, bobExpensiveCopy,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: alicePubKey.SerializeCompressed(),
|
||||||
isLsp: true,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{
|
},
|
||||||
{carolHopHint},
|
|
||||||
},
|
// Rule 2: All destination hop hints are private => NOT an LSP.
|
||||||
expectedLspHop: &zpay32.HopHint{
|
// Rationale: The destination hop hint is private so it cannot
|
||||||
NodeID: bobHopHint.NodeID,
|
// be probed so we default to NOT an LSP.
|
||||||
ChannelID: bobHopHint.ChannelID,
|
{
|
||||||
FeeBaseMSat: bobExpensiveCopy.FeeBaseMSat,
|
name: "single route to private dest",
|
||||||
FeeProportionalMillionths: bobExpensiveCopy.FeeProportionalMillionths,
|
routeHints: [][]zpay32.HopHint{
|
||||||
CLTVExpiryDelta: bobHopHint.CLTVExpiryDelta,
|
{aliceHopHint, bobHopHint},
|
||||||
},
|
},
|
||||||
|
invoiceTarget: bobPubKey.SerializeCompressed(),
|
||||||
|
expectLSP: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple routes, different final hops",
|
name: "multiple routes, all to private dests",
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{aliceHopHint, bobHopHint},
|
||||||
aliceHopHint, bobHopHint,
|
{daveHopHint, carolHopHint},
|
||||||
},
|
|
||||||
{
|
|
||||||
carolHopHint, daveHopHint,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: nil,
|
||||||
isLsp: false,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{},
|
|
||||||
expectedLspHop: nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple routes, same public hops",
|
name: "single hop to private node",
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{eveHopHint},
|
||||||
aliceHopHint, daveHopHintPublicChan,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carolHopHint, daveHopHintPublicChan,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: evePubKey.SerializeCompressed(),
|
||||||
isLsp: false,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{},
|
|
||||||
expectedLspHop: nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple routes, same public hops",
|
name: "all routes to same private node",
|
||||||
routeHints: [][]zpay32.HopHint{
|
routeHints: [][]zpay32.HopHint{
|
||||||
{
|
{aliceHopHint, bobHopHint},
|
||||||
aliceHopHint, daveHopHint,
|
{daveHopHint, bobHopHint},
|
||||||
},
|
{carolHopHint, bobHopHint},
|
||||||
{
|
|
||||||
carolHopHint, daveHopHintPublicChan,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aliceHopHint, daveHopHintPublicChan,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
probeAmtMsat: probeAmtMsat,
|
invoiceTarget: nil,
|
||||||
isLsp: false,
|
expectLSP: false,
|
||||||
expectedHints: [][]zpay32.HopHint{},
|
},
|
||||||
expectedLspHop: nil,
|
|
||||||
|
// Rule 3: At least one destination hop is public => IS an LSP.
|
||||||
|
// Rationale: As long as there is at least one public
|
||||||
|
// destination route hint, it is an LSP setup and can be probed.
|
||||||
|
{
|
||||||
|
name: "single route to public dest (dave)",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, daveHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: evePubKey.SerializeCompressed(),
|
||||||
|
expectLSP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct hop to public LSP (alice)",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{aliceHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: bobPubKey.SerializeCompressed(),
|
||||||
|
expectLSP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routes to same public LSP (dave)",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, daveHopHint},
|
||||||
|
{carolHopHint, daveHopHint},
|
||||||
|
{eveHopHint, daveHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: nil,
|
||||||
|
expectLSP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routes to different public LSPs",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint},
|
||||||
|
{carolHopHint, daveHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: nil,
|
||||||
|
expectLSP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed public and private dest hops",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{aliceHopHint, bobHopHint},
|
||||||
|
{carolHopHint, daveHopHint},
|
||||||
|
{bobHopHint, eveHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: nil,
|
||||||
|
expectLSP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first route has public dest, rest private",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint},
|
||||||
|
{carolHopHint, eveHopHint},
|
||||||
|
},
|
||||||
|
invoiceTarget: nil,
|
||||||
|
expectLSP: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns ErrEdgeNotFound for private channels.
|
for _, tt := range tests {
|
||||||
fetchChannelEndpoints := func(chanID uint64) (route.Vertex,
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
route.Vertex, error) {
|
result := isLSP(
|
||||||
|
tt.routeHints, tt.invoiceTarget, hasNode,
|
||||||
if chanID == publicChannelID {
|
|
||||||
return route.Vertex{}, route.Vertex{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return route.Vertex{}, route.Vertex{}, graphdb.ErrEdgeNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range lspTestCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
isLsp := isLSP(tc.routeHints, fetchChannelEndpoints)
|
|
||||||
require.Equal(t, tc.isLsp, isLsp)
|
|
||||||
if !tc.isLsp {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedHints, lspHint, _ := prepareLspRouteHints(
|
|
||||||
tc.routeHints, tc.probeAmtMsat,
|
|
||||||
)
|
)
|
||||||
require.Equal(t, tc.expectedHints, adjustedHints)
|
require.Equal(t, tt.expectLSP, result)
|
||||||
require.Equal(t, tc.expectedLspHop, lspHint)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPrepareLspRouteHints tests the prepareLspRouteHints function to ensure
|
||||||
|
// it correctly filters, groups, and calculates worst-case fees for LSP routes.
|
||||||
|
func TestPrepareLspRouteHints(t *testing.T) {
|
||||||
|
// Setup test nodes:
|
||||||
|
// - Alice: public LSP node (in graph)
|
||||||
|
// - Bob: private node
|
||||||
|
// - Carol: private node
|
||||||
|
// - Dave: public LSP node (in graph)
|
||||||
|
// - Eve: private node
|
||||||
|
alicePrivKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
alicePubKey := alicePrivKey.PubKey()
|
||||||
|
|
||||||
|
bobPrivKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
bobPubKey := bobPrivKey.PubKey()
|
||||||
|
|
||||||
|
carolPrivKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
carolPubKey := carolPrivKey.PubKey()
|
||||||
|
|
||||||
|
davePrivKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
davePubKey := davePrivKey.PubKey()
|
||||||
|
|
||||||
|
evePrivKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
evePubKey := evePrivKey.PubKey()
|
||||||
|
|
||||||
|
// Create hop hints with varying fees and CLTV deltas.
|
||||||
|
aliceHopHint1 := zpay32.HopHint{
|
||||||
|
NodeID: alicePubKey,
|
||||||
|
FeeBaseMSat: 100,
|
||||||
|
FeeProportionalMillionths: 1_000,
|
||||||
|
CLTVExpiryDelta: 40,
|
||||||
|
ChannelID: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceHopHint2 := zpay32.HopHint{
|
||||||
|
NodeID: alicePubKey,
|
||||||
|
FeeBaseMSat: 200,
|
||||||
|
FeeProportionalMillionths: 2_000,
|
||||||
|
CLTVExpiryDelta: 80,
|
||||||
|
ChannelID: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
bobHopHint := zpay32.HopHint{
|
||||||
|
NodeID: bobPubKey,
|
||||||
|
FeeBaseMSat: 500,
|
||||||
|
FeeProportionalMillionths: 500,
|
||||||
|
CLTVExpiryDelta: 144,
|
||||||
|
ChannelID: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
carolHopHint := zpay32.HopHint{
|
||||||
|
NodeID: carolPubKey,
|
||||||
|
FeeBaseMSat: 300,
|
||||||
|
FeeProportionalMillionths: 300,
|
||||||
|
CLTVExpiryDelta: 40,
|
||||||
|
ChannelID: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
daveHopHint1 := zpay32.HopHint{
|
||||||
|
NodeID: davePubKey,
|
||||||
|
FeeBaseMSat: 1_000,
|
||||||
|
FeeProportionalMillionths: 1_000,
|
||||||
|
CLTVExpiryDelta: 144,
|
||||||
|
ChannelID: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
daveHopHint2 := zpay32.HopHint{
|
||||||
|
NodeID: davePubKey,
|
||||||
|
FeeBaseMSat: 2_000,
|
||||||
|
FeeProportionalMillionths: 500,
|
||||||
|
CLTVExpiryDelta: 288,
|
||||||
|
ChannelID: 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
eveHopHint := zpay32.HopHint{
|
||||||
|
NodeID: evePubKey,
|
||||||
|
FeeBaseMSat: 100,
|
||||||
|
FeeProportionalMillionths: 100,
|
||||||
|
CLTVExpiryDelta: 40,
|
||||||
|
ChannelID: 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock hasNode: returns true only for alice and dave.
|
||||||
|
hasNode := func(nodePub route.Vertex) (bool, error) {
|
||||||
|
aliceVertex := route.NewVertex(alicePubKey)
|
||||||
|
daveVertex := route.NewVertex(davePubKey)
|
||||||
|
return bytes.Equal(nodePub[:], aliceVertex[:]) ||
|
||||||
|
bytes.Equal(nodePub[:], daveVertex[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
amt := lnwire.MilliSatoshi(1_000_000)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
routeHints [][]zpay32.HopHint
|
||||||
|
expectedGrps int
|
||||||
|
validateFunc func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single public LSP with one route",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint1},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
require.Len(t, groups, 1)
|
||||||
|
|
||||||
|
// Find alice's group.
|
||||||
|
aliceKey := route.NewVertex(alicePubKey)
|
||||||
|
group, ok := groups[aliceKey]
|
||||||
|
require.True(t, ok, "alice group not found")
|
||||||
|
|
||||||
|
// Verify LSP hop hint.
|
||||||
|
require.Equal(t, aliceHopHint1.FeeBaseMSat,
|
||||||
|
group.LspHopHint.FeeBaseMSat)
|
||||||
|
require.Equal(t, aliceHopHint1.CLTVExpiryDelta,
|
||||||
|
group.LspHopHint.CLTVExpiryDelta)
|
||||||
|
|
||||||
|
// Verify adjusted route hints.
|
||||||
|
require.Len(t, group.AdjustedRouteHints, 1)
|
||||||
|
require.Len(t, group.AdjustedRouteHints[0], 1)
|
||||||
|
require.Equal(t, bobHopHint.NodeID,
|
||||||
|
group.AdjustedRouteHints[0][0].NodeID)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single LSP with multiple routes, same fees",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint1},
|
||||||
|
{carolHopHint, aliceHopHint1},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
aliceKey := route.NewVertex(alicePubKey)
|
||||||
|
group, ok := groups[aliceKey]
|
||||||
|
require.True(t, ok, "alice group not found")
|
||||||
|
|
||||||
|
// Should have 2 adjusted route hints.
|
||||||
|
require.Len(t, group.AdjustedRouteHints, 2)
|
||||||
|
|
||||||
|
// Fees should match the single hop hint.
|
||||||
|
require.Equal(t, aliceHopHint1.FeeBaseMSat,
|
||||||
|
group.LspHopHint.FeeBaseMSat)
|
||||||
|
require.Equal(t, aliceHopHint1.CLTVExpiryDelta,
|
||||||
|
group.LspHopHint.CLTVExpiryDelta)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single LSP with different fees, uses worst case",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint1},
|
||||||
|
{carolHopHint, aliceHopHint2},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
aliceKey := route.NewVertex(alicePubKey)
|
||||||
|
group, ok := groups[aliceKey]
|
||||||
|
require.True(t, ok, "alice group not found")
|
||||||
|
|
||||||
|
// Should use worst-case (higher) fees.
|
||||||
|
fee1 := aliceHopHint1.HopFee(amt)
|
||||||
|
fee2 := aliceHopHint2.HopFee(amt)
|
||||||
|
require.Greater(t, fee2, fee1,
|
||||||
|
"hint2 should have higher fees")
|
||||||
|
|
||||||
|
// Group should have hint2's fees.
|
||||||
|
require.Equal(t, aliceHopHint2.FeeBaseMSat,
|
||||||
|
group.LspHopHint.FeeBaseMSat)
|
||||||
|
|
||||||
|
//nolint:ll
|
||||||
|
require.Equal(t,
|
||||||
|
aliceHopHint2.FeeProportionalMillionths,
|
||||||
|
group.LspHopHint.FeeProportionalMillionths)
|
||||||
|
|
||||||
|
// Should use worst-case CLTV delta.
|
||||||
|
require.Equal(t, aliceHopHint2.CLTVExpiryDelta,
|
||||||
|
group.LspHopHint.CLTVExpiryDelta)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple public LSPs",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, aliceHopHint1},
|
||||||
|
{carolHopHint, daveHopHint1},
|
||||||
|
},
|
||||||
|
expectedGrps: 2,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
require.Len(t, groups, 2)
|
||||||
|
|
||||||
|
aliceKey := route.NewVertex(alicePubKey)
|
||||||
|
daveKey := route.NewVertex(davePubKey)
|
||||||
|
|
||||||
|
_, hasAlice := groups[aliceKey]
|
||||||
|
_, hasDave := groups[daveKey]
|
||||||
|
require.True(t, hasAlice, "alice group missing")
|
||||||
|
require.True(t, hasDave, "dave group missing")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filters out private dest hops",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{aliceHopHint1, bobHopHint},
|
||||||
|
{carolHopHint, daveHopHint1},
|
||||||
|
{bobHopHint, eveHopHint},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
require.Len(t, groups, 1)
|
||||||
|
|
||||||
|
daveKey := route.NewVertex(davePubKey)
|
||||||
|
group, ok := groups[daveKey]
|
||||||
|
require.True(t, ok, "dave group not found")
|
||||||
|
|
||||||
|
// Only one route hint should remain
|
||||||
|
require.Len(t, group.AdjustedRouteHints, 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routes to same LSP with varying CLTV",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{bobHopHint, daveHopHint1},
|
||||||
|
{carolHopHint, daveHopHint2},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
daveKey := route.NewVertex(davePubKey)
|
||||||
|
group, ok := groups[daveKey]
|
||||||
|
require.True(t, ok, "dave group not found")
|
||||||
|
|
||||||
|
// Should use maximum CLTV delta.
|
||||||
|
require.Equal(t, daveHopHint2.CLTVExpiryDelta,
|
||||||
|
group.LspHopHint.CLTVExpiryDelta)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single hop to public LSP",
|
||||||
|
routeHints: [][]zpay32.HopHint{
|
||||||
|
{aliceHopHint1},
|
||||||
|
},
|
||||||
|
expectedGrps: 1,
|
||||||
|
validateFunc: func(t *testing.T,
|
||||||
|
groups map[route.Vertex]*LspRouteGroup) {
|
||||||
|
|
||||||
|
aliceKey := route.NewVertex(alicePubKey)
|
||||||
|
group, ok := groups[aliceKey]
|
||||||
|
require.True(t, ok, "alice group not found")
|
||||||
|
|
||||||
|
// No adjusted hints since it's a direct hop
|
||||||
|
require.Len(t, group.AdjustedRouteHints, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
groups, err := prepareLspRouteHints(
|
||||||
|
tt.routeHints, amt, hasNode,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, groups, tt.expectedGrps)
|
||||||
|
|
||||||
|
// Run custom validation if provided.
|
||||||
|
if tt.validateFunc != nil {
|
||||||
|
tt.validateFunc(t, groups)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error cases which in operation should never happen because we always
|
||||||
|
// call isLSP first to check if the route hints are an LSP setup.
|
||||||
|
t.Run("error: no route hints", func(t *testing.T) {
|
||||||
|
_, err := prepareLspRouteHints(
|
||||||
|
[][]zpay32.HopHint{}, amt, hasNode,
|
||||||
|
)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "no route hints")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error: no public LSP nodes found", func(t *testing.T) {
|
||||||
|
// All private destination hops. If all destination hops are
|
||||||
|
// private we cannot probe any LSPs so we return an error.
|
||||||
|
routeHints := [][]zpay32.HopHint{
|
||||||
|
{aliceHopHint1, bobHopHint},
|
||||||
|
{daveHopHint1, carolHopHint},
|
||||||
|
}
|
||||||
|
_, err := prepareLspRouteHints(routeHints, amt, hasNode)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "no public LSP nodes found")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -695,6 +695,8 @@ func newRPCServer(cfg *Config, interceptorChain *rpcperms.InterceptorChain,
|
|||||||
// addDeps populates all dependencies needed by the RPC server, and any
|
// addDeps populates all dependencies needed by the RPC server, and any
|
||||||
// of the sub-servers that it maintains. When this is done, the RPC server can
|
// of the sub-servers that it maintains. When this is done, the RPC server can
|
||||||
// be started, and start accepting RPC calls.
|
// be started, and start accepting RPC calls.
|
||||||
|
//
|
||||||
|
//nolint:funlen
|
||||||
func (r *rpcServer) addDeps(ctx context.Context, s *server,
|
func (r *rpcServer) addDeps(ctx context.Context, s *server,
|
||||||
macService *macaroons.Service,
|
macService *macaroons.Service,
|
||||||
subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager,
|
subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager,
|
||||||
@@ -744,6 +746,11 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server,
|
|||||||
|
|
||||||
return info.NodeKey1Bytes, info.NodeKey2Bytes, nil
|
return info.NodeKey1Bytes, info.NodeKey2Bytes, nil
|
||||||
},
|
},
|
||||||
|
HasNode: func(nodePub route.Vertex) (bool, error) {
|
||||||
|
_, exists, err := graph.HasNode(ctx, nodePub)
|
||||||
|
|
||||||
|
return exists, err
|
||||||
|
},
|
||||||
FindRoute: s.chanRouter.FindRoute,
|
FindRoute: s.chanRouter.FindRoute,
|
||||||
MissionControl: s.defaultMC,
|
MissionControl: s.defaultMC,
|
||||||
ActiveNetParams: r.cfg.ActiveNetParams.Params,
|
ActiveNetParams: r.cfg.ActiveNetParams.Params,
|
||||||
|
|||||||
Reference in new issue
Block a user