itest: enhance testEstimateRouteFee with multi-LSP scenarios

This commit enhances the integration test to validate the LSP heuristic
end-to-end with real network topology and payment probing.

Network topology additions:
- Added Frank node as a private destination
- Created multi-LSP test scenario with Bob, Eve, and Dave as LSPs

New test cases:

1. "probe based estimate, public target with public hop hints"
   - Validates Rule 1: public invoice target routes directly
   - Even with public hop hints, direct routing is used
   - Expected: standard single-hop fees

2. "probe based estimate, multiple different public LSPs"
   - Validates multi-LSP worst-case selection
   - Frank has routes through Bob (low fee), Eve (HIGH fee), Dave (medium)
   - Expected: Eve's worst-case fees (most expensive)
   - Tests griefing protection (max 3 LSP probes)
This commit is contained in:
ziggie committed 2025-12-01 18:07:56 +01:00
1 parent 79fe67619b
commit 0e6bab1730
1 file changed
+104 -3
+104 -3
View File
@@ -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 {