lnd_test: add route fee cutoff test
This commit is contained in:
parent
b2585f33ad
commit
70a52c9eae
276
lnd_test.go
276
lnd_test.go
@ -9761,6 +9761,278 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testRouteFeeCutoff tests that we are able to prevent querying routes and
|
||||||
|
// sending payments that incur a fee higher than the fee limit.
|
||||||
|
func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
// For this test, we'll create the following topology:
|
||||||
|
//
|
||||||
|
// --- Bob ---
|
||||||
|
// / \
|
||||||
|
// Alice ---- ---- Dave
|
||||||
|
// \ /
|
||||||
|
// -- Carol --
|
||||||
|
//
|
||||||
|
// Alice will attempt to send payments to Dave that should not incur a
|
||||||
|
// fee greater than the fee limit expressed as a percentage of the
|
||||||
|
// amount and as a fixed amount of satoshis.
|
||||||
|
|
||||||
|
ctxb := context.Background()
|
||||||
|
timeout := time.Duration(time.Second * 15)
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, timeout)
|
||||||
|
chanPointAliceBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob, chanAmt, 0, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create Carol's node and open a channel between her and Alice with
|
||||||
|
// Alice being the funder.
|
||||||
|
carol, err := net.NewNode("Carol", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create carol's node: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
chanPointAliceCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol, chanAmt, 0, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create Dave's node and open a channel between him and Bob with Bob
|
||||||
|
// being the funder.
|
||||||
|
dave, err := net.NewNode("Dave", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create dave's node: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, dave, net.Bob); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to bob: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
chanPointBobDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, dave, chanAmt, 0, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel between Carol and Dave.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
chanPointCarolDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave, chanAmt, 0, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now that all the channels were set up, we'll wait for all the nodes
|
||||||
|
// to have seen all the channels.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
||||||
|
nodeNames := []string{"alice", "bob", "carol", "dave"}
|
||||||
|
networkChans := []*lnrpc.ChannelPoint{
|
||||||
|
chanPointAliceBob, chanPointAliceCarol, chanPointBobDave,
|
||||||
|
chanPointCarolDave,
|
||||||
|
}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txidHash, err := getChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
txid, e := chainhash.NewHash(txidHash)
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("unable to create sha hash: %v", e)
|
||||||
|
}
|
||||||
|
outpoint := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, timeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d) timed out waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, outpoint, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The payments should only be succesful across the route:
|
||||||
|
// Alice -> Bob -> Dave
|
||||||
|
// Therefore, we'll update the fee policy on Carol's side for the
|
||||||
|
// channel between her and Dave to invalidate the route:
|
||||||
|
// Alice -> Carol -> Dave
|
||||||
|
const feeBase = 1e+6
|
||||||
|
baseFee := int64(10000)
|
||||||
|
feeRate := int64(5)
|
||||||
|
timeLockDelta := uint32(144)
|
||||||
|
|
||||||
|
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||||
|
FeeBaseMsat: baseFee,
|
||||||
|
FeeRateMilliMsat: feeBase * feeRate,
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||||
|
BaseFeeMsat: baseFee,
|
||||||
|
FeeRate: float64(feeRate),
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||||
|
ChanPoint: chanPointCarolDave,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
if _, err := carol.UpdateChannelPolicy(ctxt, updateFeeReq); err != nil {
|
||||||
|
t.Fatalf("unable to update chan policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Alice to receive the channel update from Carol.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
aliceUpdates, aQuit := subscribeGraphNotifications(t, ctxt, net.Alice)
|
||||||
|
defer close(aQuit)
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, aliceUpdates, carol.PubKeyStr, expectedPolicy,
|
||||||
|
chanPointCarolDave,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We'll also need the channel IDs for Bob's channels in order to
|
||||||
|
// confirm the route of the payments.
|
||||||
|
listReq := &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
listResp, err := net.Bob.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve bob's channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliceBobChanID, bobDaveChanID uint64
|
||||||
|
for _, channel := range listResp.Channels {
|
||||||
|
switch channel.RemotePubkey {
|
||||||
|
case net.Alice.PubKeyStr:
|
||||||
|
aliceBobChanID = channel.ChanId
|
||||||
|
case dave.PubKeyStr:
|
||||||
|
bobDaveChanID = channel.ChanId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliceBobChanID == 0 {
|
||||||
|
t.Fatalf("channel between alice and bob not found")
|
||||||
|
}
|
||||||
|
if bobDaveChanID == 0 {
|
||||||
|
t.Fatalf("channel between bob and dave not found")
|
||||||
|
}
|
||||||
|
hopChanIDs := []uint64{aliceBobChanID, bobDaveChanID}
|
||||||
|
|
||||||
|
// checkRoute is a helper closure to ensure the route contains the
|
||||||
|
// correct intermediate hops.
|
||||||
|
checkRoute := func(route *lnrpc.Route) {
|
||||||
|
if len(route.Hops) != 2 {
|
||||||
|
t.Fatalf("expected two hops, got %d", len(route.Hops))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, hop := range route.Hops {
|
||||||
|
if hop.ChanId != hopChanIDs[i] {
|
||||||
|
t.Fatalf("expected chan id %d, got %d",
|
||||||
|
hop.ChanId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll be attempting to send two payments from Alice to Dave. One will
|
||||||
|
// have a fee cutoff expressed as a percentage of the amount and the
|
||||||
|
// other will have it expressed as a fixed amount of satoshis.
|
||||||
|
const paymentAmt = 100
|
||||||
|
carolFee := computeFee(lnwire.MilliSatoshi(baseFee), 1, paymentAmt)
|
||||||
|
|
||||||
|
// testFeeCutoff is a helper closure that will ensure the different
|
||||||
|
// types of fee limits work as intended when querying routes and sending
|
||||||
|
// payments.
|
||||||
|
testFeeCutoff := func(feeLimit *lnrpc.FeeLimit) {
|
||||||
|
queryRoutesReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: dave.PubKeyStr,
|
||||||
|
Amt: paymentAmt,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
|
NumRoutes: 2,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
routesResp, err := net.Alice.QueryRoutes(ctxt, queryRoutesReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get routes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(routesResp.Routes) != 1 {
|
||||||
|
t.Fatalf("expected one route, got %d",
|
||||||
|
len(routesResp.Routes))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRoute(routesResp.Routes[0])
|
||||||
|
|
||||||
|
invoice := &lnrpc.Invoice{Value: paymentAmt}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
invoiceResp, err := dave.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq := &lnrpc.SendRequest{
|
||||||
|
PaymentRequest: invoiceResp.PaymentRequest,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
paymentResp, err := net.Alice.SendPaymentSync(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
if paymentResp.PaymentError != "" {
|
||||||
|
t.Fatalf("unable to send payment: %v",
|
||||||
|
paymentResp.PaymentError)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRoute(paymentResp.PaymentRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll start off using percentages first. Since the fee along the
|
||||||
|
// route using Carol as an intermediate hop is 10% of the payment's
|
||||||
|
// amount, we'll use a lower percentage in order to invalid that route.
|
||||||
|
feeLimitPercent := &lnrpc.FeeLimit{
|
||||||
|
&lnrpc.FeeLimit_Percent{baseFee/1000 - 1},
|
||||||
|
}
|
||||||
|
testFeeCutoff(feeLimitPercent)
|
||||||
|
|
||||||
|
// Now we'll test using fixed fee limit amounts. Since we computed the
|
||||||
|
// fee for the route using Carol as an intermediate hop earlier, we can
|
||||||
|
// use a smaller value in order to invalidate that route.
|
||||||
|
feeLimitFixed := &lnrpc.FeeLimit{
|
||||||
|
&lnrpc.FeeLimit_Fixed{int64(carolFee.ToSatoshis()) - 1},
|
||||||
|
}
|
||||||
|
testFeeCutoff(feeLimitFixed)
|
||||||
|
|
||||||
|
// Once we're done, close the channels and shut down the nodes created
|
||||||
|
// throughout this test.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceBob, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceCarol, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobDave, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarolDave, false)
|
||||||
|
|
||||||
|
if err := net.ShutdownNode(carol); err != nil {
|
||||||
|
t.Fatalf("unable to shut down carol: %v", err)
|
||||||
|
}
|
||||||
|
if err := net.ShutdownNode(dave); err != nil {
|
||||||
|
t.Fatalf("unable to shut down dave: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
test func(net *lntest.NetworkHarness, t *harnessTest)
|
test func(net *lntest.NetworkHarness, t *harnessTest)
|
||||||
@ -9952,6 +10224,10 @@ var testsCases = []*testCase{
|
|||||||
name: "query routes",
|
name: "query routes",
|
||||||
test: testQueryRoutes,
|
test: testQueryRoutes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "route fee cutoff",
|
||||||
|
test: testRouteFeeCutoff,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||||
|
Loading…
Reference in New Issue
Block a user