2024-02-09 15:21:51 +01:00
|
|
|
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)
|
2024-05-20 17:06:20 +02:00
|
|
|
ht.MineBlocksAndAssertNumTxes(1, 2)
|
|
|
|
|
2024-02-09 15:21:51 +01:00
|
|
|
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",
|
|
|
|
)
|
|
|
|
}
|