mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 22:25:24 +01:00
itest: estimate route fee tests
This commit is contained in:
parent
ef069b658d
commit
e79b8b2986
2 changed files with 409 additions and 0 deletions
|
@ -257,6 +257,10 @@ var allTestCases = []*lntest.TestCase{
|
|||
Name: "multi-hop payments",
|
||||
TestFunc: testMultiHopPayments,
|
||||
},
|
||||
{
|
||||
Name: "estimate route fee",
|
||||
TestFunc: testEstimateRouteFee,
|
||||
},
|
||||
{
|
||||
Name: "anchors reserved value",
|
||||
TestFunc: testAnchorReservedValue,
|
||||
|
|
405
itest/lnd_estimate_route_fee_test.go
Normal file
405
itest/lnd_estimate_route_fee_test.go
Normal file
|
@ -0,0 +1,405 @@
|
|||
package itest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/node"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
probeInitiator *node.HarnessNode
|
||||
probeAmount = btcutil.Amount(100_000)
|
||||
probeAmt = int64(probeAmount) * 1_000
|
||||
|
||||
failureReasonNone = lnrpc.PaymentFailureReason_FAILURE_REASON_NONE
|
||||
failureReasonNoRoute = lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE //nolint:lll
|
||||
)
|
||||
|
||||
const (
|
||||
ErrNoRouteInGraph = "unable to find a path to destination"
|
||||
)
|
||||
|
||||
type estimateRouteFeeTestCase struct {
|
||||
// name is the name of the target test case.
|
||||
name string
|
||||
|
||||
// probing is a flag that indicates whether the test case estimates fees
|
||||
// using the graph or by probing.
|
||||
probing bool
|
||||
|
||||
// destination is the node that will receive the probe.
|
||||
destination *node.HarnessNode
|
||||
|
||||
// routeHints are the route hints that will be used for the probe.
|
||||
routeHints []*lnrpc.RouteHint
|
||||
|
||||
// expectedRoutingFeesMsat are the expected routing fees that will be
|
||||
// returned by the probe.
|
||||
expectedRoutingFeesMsat int64
|
||||
|
||||
// expectedCltvDelta is the expected cltv delta that will be returned by
|
||||
// the fee estimation.
|
||||
expectedCltvDelta int64
|
||||
|
||||
// expectedFailureReason is the expected payment failure reason that
|
||||
// will be returned by the probe.
|
||||
expectedFailureReason lnrpc.PaymentFailureReason
|
||||
|
||||
// expectedError are the expected error that will be returned if the
|
||||
// probing fails.
|
||||
expectedError string
|
||||
}
|
||||
|
||||
// testEstimateRouteFee tests the estimation of routing fees using either graph
|
||||
// data or sending out a probe payment.
|
||||
func testEstimateRouteFee(ht *lntest.HarnessTest) {
|
||||
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.
|
||||
// /-------------\
|
||||
// _ Eve _ (private) \
|
||||
// / \ \
|
||||
// Alice -- Carol ---- Bob --------- Paula
|
||||
// \ / (private)
|
||||
// \__ Dave ____/
|
||||
//
|
||||
req := &mppOpenChannelRequest{
|
||||
amtAliceCarol: 200_000,
|
||||
amtAliceDave: 200_000,
|
||||
amtCarolBob: 200_000,
|
||||
amtCarolEve: 200_000,
|
||||
amtDaveBob: 200_000,
|
||||
amtEveBob: 200_000,
|
||||
}
|
||||
mts.openChannels(req)
|
||||
chanPointDaveBob := mts.channelPoints[4]
|
||||
chanPointEveBob := mts.channelPoints[5]
|
||||
|
||||
// Alice will initiate all probe payments.
|
||||
probeInitiator = mts.alice
|
||||
|
||||
paula := ht.NewNode("Paula", 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
|
||||
// added to the invoice always have enough liquidity, but here we check
|
||||
// that the prober uses the more expensive route.
|
||||
ht.EnsureConnected(mts.bob, paula)
|
||||
channelPointBobPaula := ht.OpenChannel(
|
||||
mts.bob, paula, lntest.OpenChannelParams{
|
||||
Private: true,
|
||||
Amt: 90_000,
|
||||
PushAmt: 69_000,
|
||||
},
|
||||
)
|
||||
|
||||
ht.EnsureConnected(mts.eve, paula)
|
||||
channelPointEvePaula := ht.OpenChannel(
|
||||
mts.eve, paula, lntest.OpenChannelParams{
|
||||
Private: true,
|
||||
Amt: 1_000_000,
|
||||
},
|
||||
)
|
||||
|
||||
bobsPrivChannels := ht.Bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{
|
||||
PrivateOnly: true,
|
||||
})
|
||||
require.Len(ht, bobsPrivChannels.Channels, 1)
|
||||
bobPaulaChanID := bobsPrivChannels.Channels[0].ChanId
|
||||
|
||||
evesPrivChannels := mts.eve.RPC.ListChannels(&lnrpc.ListChannelsRequest{
|
||||
PrivateOnly: true,
|
||||
})
|
||||
require.Len(ht, evesPrivChannels.Channels, 1)
|
||||
evePaulaChanID := evesPrivChannels.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.
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: 200_000,
|
||||
FeeRateMilliMsat: int64(0.001 * 1_000_000),
|
||||
TimeLockDelta: 40,
|
||||
MinHtlc: 1000, // default value
|
||||
MaxHtlcMsat: 133_650_000,
|
||||
}
|
||||
mts.dave.UpdateGlobalPolicy(expectedPolicy)
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.dave, expectedPolicy, chanPointDaveBob, false,
|
||||
)
|
||||
expectedPolicy.FeeBaseMsat = 500_000
|
||||
mts.eve.UpdateGlobalPolicy(expectedPolicy)
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.eve, expectedPolicy, chanPointEveBob, false,
|
||||
)
|
||||
|
||||
var (
|
||||
bobHopHint = &lnrpc.HopHint{
|
||||
NodeId: mts.bob.PubKeyStr,
|
||||
FeeBaseMsat: 1_000,
|
||||
FeeProportionalMillionths: 1,
|
||||
CltvExpiryDelta: 100,
|
||||
ChanId: bobPaulaChanID,
|
||||
}
|
||||
|
||||
bobExpHint = &lnrpc.HopHint{
|
||||
NodeId: mts.bob.PubKeyStr,
|
||||
FeeBaseMsat: 2000,
|
||||
FeeProportionalMillionths: 2000,
|
||||
CltvExpiryDelta: 80,
|
||||
ChanId: bobPaulaChanID,
|
||||
}
|
||||
|
||||
eveHopHint = &lnrpc.HopHint{
|
||||
NodeId: mts.eve.PubKeyStr,
|
||||
FeeBaseMsat: 1_000_000,
|
||||
FeeProportionalMillionths: 1,
|
||||
CltvExpiryDelta: 200,
|
||||
ChanId: evePaulaChanID,
|
||||
}
|
||||
|
||||
singleRouteHint = []*lnrpc.RouteHint{
|
||||
{
|
||||
HopHints: []*lnrpc.HopHint{
|
||||
bobHopHint,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lspDifferentFeesHints = []*lnrpc.RouteHint{
|
||||
{
|
||||
HopHints: []*lnrpc.HopHint{
|
||||
bobHopHint,
|
||||
},
|
||||
},
|
||||
{
|
||||
HopHints: []*lnrpc.HopHint{
|
||||
bobExpHint,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
nonLspProbingRouteHints = []*lnrpc.RouteHint{
|
||||
{
|
||||
HopHints: []*lnrpc.HopHint{
|
||||
eveHopHint,
|
||||
},
|
||||
},
|
||||
{
|
||||
HopHints: []*lnrpc.HopHint{
|
||||
bobHopHint,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
defaultTimelock := int64(chainreg.DefaultBitcoinTimeLockDelta)
|
||||
|
||||
// Going A -> Carol -> Bob
|
||||
feeStandardSingleHop := int64(1_000) + probeAmt/1_000_000
|
||||
|
||||
// Going A -> Carol -> Bob -> Paula/no node
|
||||
feeBP := int64(bobHopHint.FeeBaseMsat) +
|
||||
int64(bobHopHint.FeeProportionalMillionths)*(probeAmt)/1_000_000
|
||||
deltaBP := int64(bobHopHint.CltvExpiryDelta)
|
||||
|
||||
feeCB := int64(1_000) + (probeAmt+feeBP)/1_000_000
|
||||
deltaCB := defaultTimelock
|
||||
|
||||
deltaACBP := deltaCB + deltaBP
|
||||
feeACBP := feeCB + feeBP
|
||||
|
||||
// The expensive alternative to Bob.
|
||||
expFeeBP := int64(bobExpHint.FeeBaseMsat) +
|
||||
int64(bobExpHint.FeeProportionalMillionths)*(probeAmt)/1_000_000
|
||||
expFeeCB := int64(1_000) + (probeAmt+expFeeBP)/1_000_000
|
||||
expensiveFeeACBP := expFeeCB + expFeeBP
|
||||
|
||||
// Going A -> Carol -> Eve -> Paula
|
||||
feeEP := int64(eveHopHint.FeeBaseMsat) +
|
||||
int64(eveHopHint.FeeProportionalMillionths)*(probeAmt)/1_000_000
|
||||
deltaEP := int64(eveHopHint.CltvExpiryDelta)
|
||||
|
||||
feeCE := int64(1_000) + (probeAmt+feeEP)/1_000_000
|
||||
deltaCE := defaultTimelock
|
||||
|
||||
feeACEP := feeEP + feeCE
|
||||
deltaACEP := deltaCE + deltaEP
|
||||
|
||||
initialBlockHeight := int64(mts.alice.RPC.GetInfo().BlockHeight)
|
||||
|
||||
// Locktime is always composed of the initial block height and the
|
||||
// time lock delta for the first hop. Additionally, there's a block
|
||||
// accounting for block height variance.
|
||||
locktime := initialBlockHeight + defaultTimelock +
|
||||
int64(routing.BlockPadding)
|
||||
|
||||
var testCases = []*estimateRouteFeeTestCase{
|
||||
// Single hop payment is free.
|
||||
{
|
||||
name: "graph based estimate, 0 fee",
|
||||
probing: false,
|
||||
destination: mts.dave,
|
||||
expectedRoutingFeesMsat: 0,
|
||||
expectedCltvDelta: locktime,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// 1000 msat base fee + 100 msat(1ppm*100_000sat)
|
||||
{
|
||||
name: "graph based estimate",
|
||||
probing: false,
|
||||
destination: mts.bob,
|
||||
expectedRoutingFeesMsat: feeStandardSingleHop,
|
||||
expectedCltvDelta: locktime + deltaCB,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// We expect the same result as the graph based estimate to Bob.
|
||||
{
|
||||
name: "probe based estimate, empty " +
|
||||
"route hint",
|
||||
probing: true,
|
||||
destination: mts.bob,
|
||||
routeHints: []*lnrpc.RouteHint{},
|
||||
expectedRoutingFeesMsat: feeStandardSingleHop,
|
||||
expectedCltvDelta: locktime + deltaCB,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// We expect the previous probing results adjusted by Paula's
|
||||
// hop data.
|
||||
{
|
||||
name: "probe based estimate, single" +
|
||||
" route hint",
|
||||
probing: true,
|
||||
destination: paula,
|
||||
routeHints: singleRouteHint,
|
||||
expectedRoutingFeesMsat: feeACBP,
|
||||
expectedCltvDelta: locktime + deltaACBP,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// With multiple route hints and lsp detected, we expect the
|
||||
// highest of fee settings to be used for estimation.
|
||||
{
|
||||
name: "probe based estimate, " +
|
||||
"multiple route hints, diff lsp fees",
|
||||
probing: true,
|
||||
destination: paula,
|
||||
routeHints: lspDifferentFeesHints,
|
||||
expectedRoutingFeesMsat: expensiveFeeACBP,
|
||||
expectedCltvDelta: locktime + deltaACBP,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// A destination without channels and an existing hop hint hop
|
||||
// should result in the same estimate as if the hidden node was
|
||||
// connected through a channel. This ensures that the probe is
|
||||
// actually just send to the LSP and not the destination.
|
||||
{
|
||||
name: "single hop hint, destination " +
|
||||
"without channels",
|
||||
probing: true,
|
||||
destination: ht.NewNode(
|
||||
"ImWithoutChannels", nil,
|
||||
),
|
||||
routeHints: singleRouteHint,
|
||||
expectedRoutingFeesMsat: feeACBP,
|
||||
expectedCltvDelta: locktime + deltaACBP,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// Test lnd native hop processing with non lsp probing. Paula is
|
||||
// lacking liqudity in the channel to Bob, so Eve is used, but
|
||||
// it has higher fees.
|
||||
{
|
||||
name: "probe based estimate, non " +
|
||||
"lsp",
|
||||
probing: true,
|
||||
destination: paula,
|
||||
routeHints: nonLspProbingRouteHints,
|
||||
expectedRoutingFeesMsat: feeACEP,
|
||||
expectedCltvDelta: locktime + deltaACEP,
|
||||
expectedFailureReason: failureReasonNone,
|
||||
},
|
||||
// We expect a NO_ROUTE error if route hints to paula aren't
|
||||
// provided while probing the graph.
|
||||
{
|
||||
name: "no route via graph",
|
||||
probing: false,
|
||||
destination: paula,
|
||||
expectedError: ErrNoRouteInGraph,
|
||||
},
|
||||
// We expect a NO_ROUTE error if route hints to paula aren't
|
||||
// provided while sending a probe payment.
|
||||
{
|
||||
name: "no route via probe",
|
||||
probing: true,
|
||||
destination: paula,
|
||||
expectedRoutingFeesMsat: 0,
|
||||
expectedCltvDelta: 0,
|
||||
expectedFailureReason: failureReasonNoRoute,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
success := ht.Run(
|
||||
testCase.name, func(tt *testing.T) {
|
||||
runFeeEstimationTestCase(ht, testCase)
|
||||
},
|
||||
)
|
||||
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mts.ht.CloseChannelAssertPending(mts.bob, channelPointBobPaula, false)
|
||||
mts.ht.CloseChannelAssertPending(mts.eve, channelPointEvePaula, false)
|
||||
mts.closeChannels()
|
||||
}
|
||||
|
||||
// runTestCase runs a single test case asserting that test conditions are met.
|
||||
func runFeeEstimationTestCase(ht *lntest.HarnessTest,
|
||||
tc *estimateRouteFeeTestCase) {
|
||||
|
||||
// Legacy graph based fee estimation.
|
||||
var feeReq *routerrpc.RouteFeeRequest
|
||||
if tc.probing {
|
||||
payReqs, _, _ := ht.CreatePayReqs(
|
||||
tc.destination, probeAmount, 1, tc.routeHints...,
|
||||
)
|
||||
feeReq = &routerrpc.RouteFeeRequest{
|
||||
PaymentRequest: payReqs[0],
|
||||
Timeout: 10,
|
||||
}
|
||||
} else {
|
||||
feeReq = &routerrpc.RouteFeeRequest{
|
||||
Dest: tc.destination.PubKey[:],
|
||||
AmtSat: int64(probeAmount),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := ht.Context()
|
||||
|
||||
// Kick off the parametrized fee estimation.
|
||||
resp, err := probeInitiator.RPC.Router.EstimateRouteFee(ctx, feeReq)
|
||||
if err != nil {
|
||||
require.ErrorContains(ht, err, tc.expectedError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(ht, tc.expectedFailureReason, resp.FailureReason)
|
||||
require.Equal(
|
||||
ht, tc.expectedRoutingFeesMsat, resp.RoutingFeeMsat,
|
||||
"routing fees",
|
||||
)
|
||||
require.Equal(
|
||||
ht, tc.expectedCltvDelta, resp.TimeLockDelay,
|
||||
"cltv delta",
|
||||
)
|
||||
}
|
Loading…
Add table
Reference in a new issue