123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- 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",
- )
- }
|