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:
1 file changed
+104
-3
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user