itest: enhance testEstimateRouteFee with multi-LSP scenarios
What changed, and why it matters
This commit only adds and improves an integration test for the EstimateRouteFee feature in LND. It does not change production code, so it cannot introduce a security vulnerability or fix one directly. The test checks that route-fee estimates correctly handle public targets, multiple LSPs, and worst-case fee selection.
No security action needed. Review as normal test-quality change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies itest/lnd_estimate_route_fee_test.go to extend the testEstimateRouteFee integration test. It adds a new private node Frank, a private channel Dave→Frank, and two new test cases: (1) a public invoice target with public hop hints should route directly, and (2) multiple different public LSPs should return the worst-case fee route. The commit also asserts that routerrpc.MaxLspsToProbe equals 3. No production logic is changed.
Changed components
itest/lnd_estimate_route_fee_test.goInspect captured patch +104 / −3
diff --git a/itest/lnd_estimate_route_fee_test.go b/itest/lnd_estimate_route_fee_test.go
index 713cfe1..07329d9 100644
--- a/itest/lnd_estimate_route_fee_test.go
+++ b/itest/lnd_estimate_route_fee_test.go
@@ -59,18 +59,38 @@ type estimateRouteFeeTestCase struct {
}
// 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) {
+ // 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)
- // We extend the regular mpp test scenario with a new node Paula. Paula
- // is connected to Bob and Eve through private channels.
+ // We extend the regular mpp test scenario with two new nodes:
+ // - Paula: connected to Bob and Eve through private channels
+ // - Frank: connected to Dave through a private channel
+ //
// /-------------\
// _ Eve _ (private) \
// / \ \
// Alice -- Carol ---- Bob --------- Paula
// \ / (private)
// \__ Dave ____/
+ // \
+ // \__ Frank (private)
//
req := &mppOpenChannelRequest{
amtAliceCarol: 200_000,
@@ -88,6 +108,7 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
probeInitiator = mts.alice
paula := ht.NewNode("Paula", nil)
+ frank := ht.NewNode("Frank", nil)
// 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
@@ -106,6 +127,13 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
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{
PrivateOnly: true,
})
@@ -118,6 +146,14 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
require.Len(ht, evesPrivChannels.Channels, 1)
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
// high fees. This ensures that the path estimates are based on Carol's
// 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)
@@ -231,6 +294,14 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
feeACEP := feeEP + feeCE
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)
// Locktime is always composed of the initial block height and the
@@ -271,6 +342,19 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
expectedCltvDelta: locktime + deltaCB,
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
// hop data.
{
@@ -340,6 +424,23 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
expectedCltvDelta: 0,
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 {
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.