diff --git a/itest/lnd_estimate_route_fee_test.go b/itest/lnd_estimate_route_fee_test.go index 713cfe1ed..07329d969 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 {