mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 14:22:37 +01:00
Merge pull request #1113 from wpaulino/fee-cutoff
rpc+routing: add support for fee limits when finding routes for payments
This commit is contained in:
commit
26636ce994
12 changed files with 1525 additions and 772 deletions
|
@ -1617,6 +1617,16 @@ var sendPaymentCommand = cli.Command{
|
|||
Name: "amt, a",
|
||||
Usage: "number of satoshis to send",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit",
|
||||
Usage: "maximum fee allowed in satoshis when sending" +
|
||||
"the payment",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit_percent",
|
||||
Usage: "percentage of the payment's amount used as the" +
|
||||
"maximum fee allowed when sending the payment",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "payment_hash, r",
|
||||
Usage: "the hash to use within the payment's HTLC",
|
||||
|
@ -1637,6 +1647,32 @@ var sendPaymentCommand = cli.Command{
|
|||
Action: sendPayment,
|
||||
}
|
||||
|
||||
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
||||
// flags passed.
|
||||
func retrieveFeeLimit(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
|
||||
switch {
|
||||
case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
|
||||
return nil, fmt.Errorf("either fee_limit or fee_limit_percent " +
|
||||
"can be set, but not both")
|
||||
case ctx.IsSet("fee_limit"):
|
||||
return &lnrpc.FeeLimit{
|
||||
Limit: &lnrpc.FeeLimit_Fixed{
|
||||
Fixed: ctx.Int64("fee_limit"),
|
||||
},
|
||||
}, nil
|
||||
case ctx.IsSet("fee_limit_percent"):
|
||||
return &lnrpc.FeeLimit{
|
||||
Limit: &lnrpc.FeeLimit_Percent{
|
||||
Percent: ctx.Int64("fee_limit_percent"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Since the fee limit flags aren't required, we don't return an error
|
||||
// if they're not set.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func sendPayment(ctx *cli.Context) error {
|
||||
// Show command help if no arguments provided
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
|
@ -1644,21 +1680,33 @@ func sendPayment(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var req *lnrpc.SendRequest
|
||||
// First, we'll retrieve the fee limit value passed since it can apply
|
||||
// to both ways of sending payments (with the payment request or
|
||||
// providing the details manually).
|
||||
feeLimit, err := retrieveFeeLimit(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a payment request was provided, we can exit early since all of the
|
||||
// details of the payment are encoded within the request.
|
||||
if ctx.IsSet("pay_req") {
|
||||
req = &lnrpc.SendRequest{
|
||||
req := &lnrpc.SendRequest{
|
||||
PaymentRequest: ctx.String("pay_req"),
|
||||
Amt: ctx.Int64("amt"),
|
||||
FeeLimit: feeLimit,
|
||||
}
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
}
|
||||
} else {
|
||||
args := ctx.Args()
|
||||
|
||||
var (
|
||||
destNode []byte
|
||||
err error
|
||||
amount int64
|
||||
)
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("dest"):
|
||||
destNode, err = hex.DecodeString(ctx.String("dest"))
|
||||
|
@ -1687,9 +1735,10 @@ func sendPayment(ctx *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
req = &lnrpc.SendRequest{
|
||||
req := &lnrpc.SendRequest{
|
||||
Dest: destNode,
|
||||
Amt: amount,
|
||||
FeeLimit: feeLimit,
|
||||
}
|
||||
|
||||
if ctx.Bool("debug_send") && (ctx.IsSet("payment_hash") || args.Present()) {
|
||||
|
@ -1726,7 +1775,6 @@ func sendPayment(ctx *cli.Context) error {
|
|||
req.FinalCltvDelta = int32(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
}
|
||||
|
@ -1779,6 +1827,16 @@ var payInvoiceCommand = cli.Command{
|
|||
Usage: "(optional) number of satoshis to fulfill the " +
|
||||
"invoice",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit",
|
||||
Usage: "maximum fee allowed in satoshis when sending" +
|
||||
"the payment",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit_percent",
|
||||
Usage: "percentage of the payment's amount used as the" +
|
||||
"maximum fee allowed when sending the payment",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(payInvoice),
|
||||
}
|
||||
|
@ -1787,7 +1845,6 @@ func payInvoice(ctx *cli.Context) error {
|
|||
args := ctx.Args()
|
||||
|
||||
var payReq string
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("pay_req"):
|
||||
payReq = ctx.String("pay_req")
|
||||
|
@ -1797,9 +1854,15 @@ func payInvoice(ctx *cli.Context) error {
|
|||
return fmt.Errorf("pay_req argument missing")
|
||||
}
|
||||
|
||||
feeLimit, err := retrieveFeeLimit(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &lnrpc.SendRequest{
|
||||
PaymentRequest: payReq,
|
||||
Amt: ctx.Int64("amt"),
|
||||
FeeLimit: feeLimit,
|
||||
}
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
|
@ -2499,6 +2562,16 @@ var queryRoutesCommand = cli.Command{
|
|||
Name: "amt",
|
||||
Usage: "the amount to send expressed in satoshis",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit",
|
||||
Usage: "maximum fee allowed in satoshis when sending" +
|
||||
"the payment",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "fee_limit_percent",
|
||||
Usage: "percentage of the payment's amount used as the" +
|
||||
"maximum fee allowed when sending the payment",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "num_max_routes",
|
||||
Usage: "the max number of routes to be returned (default: 10)",
|
||||
|
@ -2548,9 +2621,15 @@ func queryRoutes(ctx *cli.Context) error {
|
|||
return fmt.Errorf("amt argument missing")
|
||||
}
|
||||
|
||||
feeLimit, err := retrieveFeeLimit(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: dest,
|
||||
Amt: amt,
|
||||
FeeLimit: feeLimit,
|
||||
NumRoutes: int32(ctx.Int("num_max_routes")),
|
||||
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
|
||||
}
|
||||
|
|
607
lnd_test.go
607
lnd_test.go
|
@ -674,6 +674,135 @@ func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false)
|
||||
}
|
||||
|
||||
// txStr returns the string representation of the channel's funding transaction.
|
||||
func txStr(chanPoint *lnrpc.ChannelPoint) string {
|
||||
txidHash, err := getChanPointFundingTxid(chanPoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fundingTxID, err := chainhash.NewHash(txidHash)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
cp := wire.OutPoint{
|
||||
Hash: *fundingTxID,
|
||||
Index: chanPoint.OutputIndex,
|
||||
}
|
||||
return cp.String()
|
||||
}
|
||||
|
||||
// waitForChannelUpdate waits for a node to receive updates from the advertising
|
||||
// node for the specified channels.
|
||||
func waitForChannelUpdate(t *harnessTest, graphUpdates chan *lnrpc.GraphTopologyUpdate,
|
||||
advertisingNode string, expectedPolicy *lnrpc.RoutingPolicy,
|
||||
chanPoints ...*lnrpc.ChannelPoint) {
|
||||
|
||||
// Create a set containing all the channel points we are awaiting
|
||||
// updates for.
|
||||
cps := make(map[string]struct{})
|
||||
for _, chanPoint := range chanPoints {
|
||||
cps[txStr(chanPoint)] = struct{}{}
|
||||
}
|
||||
out:
|
||||
for {
|
||||
select {
|
||||
case graphUpdate := <-graphUpdates:
|
||||
for _, update := range graphUpdate.ChannelUpdates {
|
||||
fundingTxStr := txStr(update.ChanPoint)
|
||||
if _, ok := cps[fundingTxStr]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if update.AdvertisingNode != advertisingNode {
|
||||
continue
|
||||
}
|
||||
|
||||
err := checkChannelPolicy(
|
||||
update.RoutingPolicy, expectedPolicy,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// We got a policy update that matched the
|
||||
// values and channel point of what we
|
||||
// expected, delete it from the map.
|
||||
delete(cps, fundingTxStr)
|
||||
|
||||
// If we have no more channel points we are
|
||||
// waiting for, break out of the loop.
|
||||
if len(cps) == 0 {
|
||||
break out
|
||||
}
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("did not receive channel update")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assertChannelPolicy asserts that the passed node's known channel policy for
|
||||
// the passed chanPoint is consistent with the expected policy values.
|
||||
func assertChannelPolicy(t *harnessTest, node *lntest.HarnessNode,
|
||||
advertisingNode string, expectedPolicy *lnrpc.RoutingPolicy,
|
||||
chanPoints ...*lnrpc.ChannelPoint) {
|
||||
|
||||
descReq := &lnrpc.ChannelGraphRequest{}
|
||||
chanGraph, err := node.DescribeGraph(context.Background(), descReq)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for alice's graph: %v", err)
|
||||
}
|
||||
|
||||
out:
|
||||
for _, chanPoint := range chanPoints {
|
||||
for _, e := range chanGraph.Edges {
|
||||
if e.ChanPoint != txStr(chanPoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if e.Node1Pub == advertisingNode {
|
||||
err = checkChannelPolicy(
|
||||
e.Node1Policy, expectedPolicy,
|
||||
)
|
||||
} else {
|
||||
err = checkChannelPolicy(
|
||||
e.Node2Policy, expectedPolicy,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
continue out
|
||||
}
|
||||
|
||||
// If we've iterated over all the known edges and we weren't
|
||||
// able to find this specific one, then we'll fail.
|
||||
t.Fatalf("did not find edge %v", txStr(chanPoint))
|
||||
}
|
||||
}
|
||||
|
||||
// checkChannelPolicy checks that the policy matches the expected one.
|
||||
func checkChannelPolicy(policy, expectedPolicy *lnrpc.RoutingPolicy) error {
|
||||
if policy.FeeBaseMsat != expectedPolicy.FeeBaseMsat {
|
||||
return fmt.Errorf("expected base fee %v, got %v",
|
||||
expectedPolicy.FeeBaseMsat, policy.FeeBaseMsat)
|
||||
}
|
||||
if policy.FeeRateMilliMsat != expectedPolicy.FeeRateMilliMsat {
|
||||
return fmt.Errorf("expected fee rate %v, got %v",
|
||||
expectedPolicy.FeeRateMilliMsat,
|
||||
policy.FeeRateMilliMsat)
|
||||
}
|
||||
if policy.TimeLockDelta != expectedPolicy.TimeLockDelta {
|
||||
return fmt.Errorf("expected time lock delta %v, got %v",
|
||||
expectedPolicy.TimeLockDelta,
|
||||
policy.TimeLockDelta)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// testUpdateChannelPolicy tests that policy updates made to a channel
|
||||
// gets propagated to other nodes in the network.
|
||||
func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
@ -746,162 +875,46 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
feeRate := int64(12)
|
||||
timeLockDelta := uint32(66)
|
||||
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: baseFee,
|
||||
FeeRateMilliMsat: feeBase * feeRate,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
}
|
||||
|
||||
req := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: float64(feeRate),
|
||||
TimeLockDelta: timeLockDelta,
|
||||
}
|
||||
req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
ChanPoint: chanPoint,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = net.Bob.UpdateChannelPolicy(ctxb, req)
|
||||
if err != nil {
|
||||
if _, err := net.Bob.UpdateChannelPolicy(ctxb, req); err != nil {
|
||||
t.Fatalf("unable to get alice's balance: %v", err)
|
||||
}
|
||||
|
||||
// txStr returns the string representation of the channel's
|
||||
// funding tx.
|
||||
txStr := func(chanPoint *lnrpc.ChannelPoint) string {
|
||||
txidHash, err := getChanPointFundingTxid(chanPoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fundingTxID, err := chainhash.NewHash(txidHash)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
cp := wire.OutPoint{
|
||||
Hash: *fundingTxID,
|
||||
Index: chanPoint.OutputIndex,
|
||||
}
|
||||
return cp.String()
|
||||
}
|
||||
|
||||
// A closure that is used to wait for a channel updates that matches
|
||||
// the channel policy update done by Alice.
|
||||
waitForChannelUpdate := func(graphUpdates chan *lnrpc.GraphTopologyUpdate,
|
||||
advertisingNode string, chanPoints ...*lnrpc.ChannelPoint) {
|
||||
|
||||
// Create a map containing all the channel points we are
|
||||
// waiting for updates for.
|
||||
cps := make(map[string]bool)
|
||||
for _, chanPoint := range chanPoints {
|
||||
cps[txStr(chanPoint)] = true
|
||||
}
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case graphUpdate := <-graphUpdates:
|
||||
for _, update := range graphUpdate.ChannelUpdates {
|
||||
fundingTxStr := txStr(update.ChanPoint)
|
||||
if _, ok := cps[fundingTxStr]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if update.AdvertisingNode != advertisingNode {
|
||||
continue
|
||||
}
|
||||
|
||||
policy := update.RoutingPolicy
|
||||
if policy.FeeBaseMsat != baseFee {
|
||||
continue
|
||||
}
|
||||
if policy.FeeRateMilliMsat != feeRate*feeBase {
|
||||
continue
|
||||
}
|
||||
if policy.TimeLockDelta != timeLockDelta {
|
||||
continue
|
||||
}
|
||||
|
||||
// We got a policy update that matched the
|
||||
// values and channel point of what we
|
||||
// expected, delete it from the map.
|
||||
delete(cps, fundingTxStr)
|
||||
|
||||
// If we have no more channel points we are
|
||||
// waiting for, break out of the loop.
|
||||
if len(cps) == 0 {
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("did not receive channel update")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all nodes to have seen the policy update done by Bob.
|
||||
waitForChannelUpdate(aliceUpdates, net.Bob.PubKeyStr, chanPoint)
|
||||
waitForChannelUpdate(bobUpdates, net.Bob.PubKeyStr, chanPoint)
|
||||
waitForChannelUpdate(carolUpdates, net.Bob.PubKeyStr, chanPoint)
|
||||
|
||||
// assertChannelPolicy asserts that the passed node's known channel
|
||||
// policy for the passed chanPoint is consistent with Bob's current
|
||||
// expected policy values.
|
||||
assertChannelPolicy := func(node *lntest.HarnessNode,
|
||||
advertisingNode string, chanPoint *lnrpc.ChannelPoint) {
|
||||
|
||||
// Get a DescribeGraph from the node.
|
||||
descReq := &lnrpc.ChannelGraphRequest{}
|
||||
chanGraph, err := node.DescribeGraph(ctxb, descReq)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for alice's routing table: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
edgeFound := false
|
||||
for _, e := range chanGraph.Edges {
|
||||
if e.ChanPoint == txStr(chanPoint) {
|
||||
edgeFound = true
|
||||
if e.Node1Pub == advertisingNode {
|
||||
if e.Node1Policy.FeeBaseMsat != baseFee {
|
||||
t.Fatalf("expected base fee "+
|
||||
"%v, got %v", baseFee,
|
||||
e.Node1Policy.FeeBaseMsat)
|
||||
}
|
||||
if e.Node1Policy.FeeRateMilliMsat != feeRate*feeBase {
|
||||
t.Fatalf("expected fee rate "+
|
||||
"%v, got %v", feeRate*feeBase,
|
||||
e.Node1Policy.FeeRateMilliMsat)
|
||||
}
|
||||
if e.Node1Policy.TimeLockDelta != timeLockDelta {
|
||||
t.Fatalf("expected time lock "+
|
||||
"delta %v, got %v",
|
||||
timeLockDelta,
|
||||
e.Node1Policy.TimeLockDelta)
|
||||
}
|
||||
} else {
|
||||
if e.Node2Policy.FeeBaseMsat != baseFee {
|
||||
t.Fatalf("expected base fee "+
|
||||
"%v, got %v", baseFee,
|
||||
e.Node2Policy.FeeBaseMsat)
|
||||
}
|
||||
if e.Node2Policy.FeeRateMilliMsat != feeRate*feeBase {
|
||||
t.Fatalf("expected fee rate "+
|
||||
"%v, got %v", feeRate*feeBase,
|
||||
e.Node2Policy.FeeRateMilliMsat)
|
||||
}
|
||||
if e.Node2Policy.TimeLockDelta != timeLockDelta {
|
||||
t.Fatalf("expected time lock "+
|
||||
"delta %v, got %v",
|
||||
timeLockDelta,
|
||||
e.Node2Policy.TimeLockDelta)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !edgeFound {
|
||||
t.Fatalf("did not find edge")
|
||||
}
|
||||
|
||||
}
|
||||
waitForChannelUpdate(
|
||||
t, aliceUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
waitForChannelUpdate(
|
||||
t, bobUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
waitForChannelUpdate(
|
||||
t, carolUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
|
||||
// Check that all nodes now know about Bob's updated policy.
|
||||
assertChannelPolicy(net.Alice, net.Bob.PubKeyStr, chanPoint)
|
||||
assertChannelPolicy(net.Bob, net.Bob.PubKeyStr, chanPoint)
|
||||
assertChannelPolicy(carol, net.Bob.PubKeyStr, chanPoint)
|
||||
assertChannelPolicy(
|
||||
t, net.Alice, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
assertChannelPolicy(
|
||||
t, net.Bob, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
assertChannelPolicy(
|
||||
t, carol, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
|
||||
// Now that all nodes have received the new channel update, we'll try
|
||||
// to send a payment from Alice to Carol to ensure that Alice has
|
||||
|
@ -951,6 +964,10 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
feeRate = int64(123)
|
||||
timeLockDelta = uint32(22)
|
||||
|
||||
expectedPolicy.FeeBaseMsat = baseFee
|
||||
expectedPolicy.FeeRateMilliMsat = feeBase * feeRate
|
||||
expectedPolicy.TimeLockDelta = timeLockDelta
|
||||
|
||||
req = &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: float64(feeRate),
|
||||
|
@ -966,24 +983,32 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
// Wait for all nodes to have seen the policy updates for both of
|
||||
// Alice's channels.
|
||||
waitForChannelUpdate(
|
||||
aliceUpdates, net.Alice.PubKeyStr, chanPoint, chanPoint3,
|
||||
t, aliceUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
waitForChannelUpdate(
|
||||
bobUpdates, net.Alice.PubKeyStr, chanPoint, chanPoint3,
|
||||
t, bobUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
waitForChannelUpdate(
|
||||
carolUpdates, net.Alice.PubKeyStr, chanPoint, chanPoint3,
|
||||
t, carolUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
|
||||
// And finally check that all nodes remembers the policy update they
|
||||
// received.
|
||||
assertChannelPolicy(net.Alice, net.Alice.PubKeyStr, chanPoint)
|
||||
assertChannelPolicy(net.Bob, net.Alice.PubKeyStr, chanPoint)
|
||||
assertChannelPolicy(carol, net.Alice.PubKeyStr, chanPoint)
|
||||
|
||||
assertChannelPolicy(net.Alice, net.Alice.PubKeyStr, chanPoint3)
|
||||
assertChannelPolicy(net.Bob, net.Alice.PubKeyStr, chanPoint3)
|
||||
assertChannelPolicy(carol, net.Alice.PubKeyStr, chanPoint3)
|
||||
assertChannelPolicy(
|
||||
t, net.Alice, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
assertChannelPolicy(
|
||||
t, net.Bob, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
assertChannelPolicy(
|
||||
t, carol, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
chanPoint3,
|
||||
)
|
||||
|
||||
// Close the channels.
|
||||
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||
|
@ -9736,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 {
|
||||
name string
|
||||
test func(net *lntest.NetworkHarness, t *harnessTest)
|
||||
|
@ -9927,6 +10224,10 @@ var testsCases = []*testCase{
|
|||
name: "query routes",
|
||||
test: testQueryRoutes,
|
||||
},
|
||||
{
|
||||
name: "route fee cutoff",
|
||||
test: testRouteFeeCutoff,
|
||||
},
|
||||
}
|
||||
|
||||
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||
|
|
1080
lnrpc/rpc.pb.go
1080
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -646,6 +646,16 @@ message TransactionDetails {
|
|||
repeated Transaction transactions = 1 [json_name = "transactions"];
|
||||
}
|
||||
|
||||
message FeeLimit {
|
||||
oneof limit {
|
||||
/// The fee limit expressed as a fixed amount of satoshis.
|
||||
int64 fixed = 1;
|
||||
|
||||
/// The fee limit expressed as a percentage of the payment amount.
|
||||
int64 percent = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SendRequest {
|
||||
/// The identity pubkey of the payment recipient
|
||||
bytes dest = 1;
|
||||
|
@ -669,8 +679,19 @@ message SendRequest {
|
|||
*/
|
||||
string payment_request = 6;
|
||||
|
||||
/// The CLTV delta from the current height that should be used to set the timelock for the final hop.
|
||||
/**
|
||||
The CLTV delta from the current height that should be used to set the
|
||||
timelock for the final hop.
|
||||
*/
|
||||
int32 final_cltv_delta = 7;
|
||||
|
||||
/**
|
||||
The maximum number of satoshis that will be paid as a fee of the payment.
|
||||
This value can be represented either as a percentage of the amount being
|
||||
sent, or as a fixed amount of the maximum fee the user is willing the pay to
|
||||
send the payment.
|
||||
*/
|
||||
FeeLimit fee_limit = 8;
|
||||
}
|
||||
message SendResponse {
|
||||
string payment_error = 1 [json_name = "payment_error"];
|
||||
|
@ -1230,6 +1251,14 @@ message QueryRoutesRequest {
|
|||
|
||||
/// An optional CLTV delta from the current height that should be used for the timelock of the final hop
|
||||
int32 final_cltv_delta = 4;
|
||||
|
||||
/**
|
||||
The maximum number of satoshis that will be paid as a fee of the payment.
|
||||
This value can be represented either as a percentage of the amount being
|
||||
sent, or as a fixed amount of the maximum fee the user is willing the pay to
|
||||
send the payment.
|
||||
*/
|
||||
FeeLimit fee_limit = 5;
|
||||
}
|
||||
message QueryRoutesResponse {
|
||||
repeated Route routes = 1 [json_name = "routes"];
|
||||
|
|
|
@ -474,6 +474,22 @@
|
|||
"required": false,
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
{
|
||||
"name": "fee_limit.fixed",
|
||||
"description": "/ The fee limit expressed as a fixed amount of satoshis.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"name": "fee_limit.percent",
|
||||
"description": "/ The fee limit expressed as a percentage of the payment amount.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
|
@ -1332,6 +1348,21 @@
|
|||
"lnrpcDisconnectPeerResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"lnrpcFeeLimit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fixed": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "/ The fee limit expressed as a fixed amount of satoshis."
|
||||
},
|
||||
"percent": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "/ The fee limit expressed as a percentage of the payment amount."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcFeeReportResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2380,7 +2411,11 @@
|
|||
"final_cltv_delta": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "/ The CLTV delta from the current height that should be used to set the timelock for the final hop."
|
||||
"description": "*\nThe CLTV delta from the current height that should be used to set the\ntimelock for the final hop."
|
||||
},
|
||||
"fee_limit": {
|
||||
"$ref": "#/definitions/lnrpcFeeLimit",
|
||||
"description": "*\nThe maximum number of satoshis that will be paid as a fee of the payment.\nThis value can be represented either as a percentage of the amount being\nsent, or as a fixed amount of the maximum fee the user is willing the pay to\nsend the payment."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -42,6 +42,10 @@ const (
|
|||
// ErrPaymentAttemptTimeout is an error that indicates that a payment
|
||||
// attempt timed out before we were able to successfully route an HTLC.
|
||||
ErrPaymentAttemptTimeout
|
||||
|
||||
// ErrFeeLimitExceeded is returned when the total fees of a route exceed
|
||||
// the user-specified fee limit.
|
||||
ErrFeeLimitExceeded
|
||||
)
|
||||
|
||||
// routerError is a structure that represent the error inside the routing package,
|
||||
|
|
|
@ -384,7 +384,8 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
|
|||
// a route by applying the time-lock and fee requirements.
|
||||
sourceVertex := Vertex(p.mc.selfNode.PubKeyBytes)
|
||||
route, err := newRoute(
|
||||
payment.Amount, sourceVertex, path, height, finalCltvDelta,
|
||||
payment.Amount, payment.FeeLimit, sourceVertex, path, height,
|
||||
finalCltvDelta,
|
||||
)
|
||||
if err != nil {
|
||||
// TODO(roasbeef): return which edge/vertex didn't work
|
||||
|
|
|
@ -248,7 +248,7 @@ func (r *Route) ToHopPayloads() []sphinx.HopData {
|
|||
//
|
||||
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from
|
||||
// the source to the target node of the path finding attempt.
|
||||
func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
|
||||
func newRoute(amtToSend, feeLimit lnwire.MilliSatoshi, sourceVertex Vertex,
|
||||
pathEdges []*ChannelHop, currentHeight uint32,
|
||||
finalCLTVDelta uint16) (*Route, error) {
|
||||
|
||||
|
@ -265,9 +265,6 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
|
|||
prevHopMap: make(map[Vertex]*ChannelHop),
|
||||
}
|
||||
|
||||
// TODO(roasbeef): need to do sanity check to ensure we don't make a
|
||||
// "dust" payment: over x% of money sending to fees
|
||||
|
||||
// We'll populate the next hop map for the _source_ node with the
|
||||
// information for the first hop so the mapping is sound.
|
||||
route.nextHopMap[sourceVertex] = pathEdges[0]
|
||||
|
@ -342,6 +339,13 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
|
|||
|
||||
route.TotalFees += nextHop.Fee
|
||||
|
||||
// Invalidate this route if its total fees exceed our fee limit.
|
||||
if route.TotalFees > feeLimit {
|
||||
err := fmt.Sprintf("total route fees exceeded fee "+
|
||||
"limit of %v", feeLimit)
|
||||
return nil, newErrf(ErrFeeLimitExceeded, err)
|
||||
}
|
||||
|
||||
// As a sanity check, we ensure that the selected channel has
|
||||
// enough capacity to forward the required amount which
|
||||
// includes the fee dictated at each hop.
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
|
@ -40,6 +41,11 @@ const (
|
|||
// implementations will use in order to ensure that they're calculating
|
||||
// the payload for each hop in path properly.
|
||||
specExampleFilePath = "testdata/spec_example.json"
|
||||
|
||||
// noFeeLimit is the maximum value of a payment through Lightning. We
|
||||
// can use this value to signal there is no fee limit since payments
|
||||
// should never be larger than this.
|
||||
noFeeLimit = lnwire.MilliSatoshi(math.MaxUint32)
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -318,8 +324,11 @@ func TestBasicGraphPathFinding(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unable to find path: %v", err)
|
||||
}
|
||||
route, err := newRoute(paymentAmt, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV)
|
||||
|
||||
route, err := newRoute(
|
||||
paymentAmt, noFeeLimit, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
}
|
||||
|
@ -460,8 +469,11 @@ func TestBasicGraphPathFinding(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unable to find route: %v", err)
|
||||
}
|
||||
route, err = newRoute(paymentAmt, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV)
|
||||
|
||||
route, err = newRoute(
|
||||
paymentAmt, noFeeLimit, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
}
|
||||
|
@ -729,6 +741,8 @@ func TestPathInsufficientCapacity(t *testing.T) {
|
|||
// TestRouteFailMinHTLC tests that if we attempt to route an HTLC which is
|
||||
// smaller than the advertised minHTLC of an edge, then path finding fails.
|
||||
func TestRouteFailMinHTLC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
|
@ -760,6 +774,8 @@ func TestRouteFailMinHTLC(t *testing.T) {
|
|||
// that's disabled, then that edge is disqualified, and the routing attempt
|
||||
// will fail.
|
||||
func TestRouteFailDisabledEdge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
|
@ -807,6 +823,48 @@ func TestRouteFailDisabledEdge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRouteExceededFeeLimit tests that routes respect the fee limit imposed.
|
||||
func TestRouteExceededFeeLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create graph: %v", err)
|
||||
}
|
||||
|
||||
sourceNode, err := graph.SourceNode()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to fetch source node: %v", err)
|
||||
}
|
||||
sourceVertex := Vertex(sourceNode.PubKeyBytes)
|
||||
|
||||
ignoredVertices := make(map[Vertex]struct{})
|
||||
ignoredEdges := make(map[uint64]struct{})
|
||||
|
||||
// Find a path to send 100 satoshis from roasbeef to sophon.
|
||||
target := aliases["sophon"]
|
||||
amt := lnwire.NewMSatFromSatoshis(100)
|
||||
path, err := findPath(
|
||||
nil, graph, nil, sourceNode, target, ignoredVertices,
|
||||
ignoredEdges, amt, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find path from roasbeef to phamnuwen for "+
|
||||
"100 satoshis: %v", err)
|
||||
}
|
||||
|
||||
// We'll now purposefully set a fee limit of 0 to trigger the exceeded
|
||||
// fee limit error. This should work since the path retrieved spans
|
||||
// multiple hops incurring a fee.
|
||||
feeLimit := lnwire.NewMSatFromSatoshis(0)
|
||||
|
||||
_, err = newRoute(amt, feeLimit, sourceVertex, path, 100, 1)
|
||||
if !IsError(err, ErrFeeLimitExceeded) {
|
||||
t.Fatalf("route should've exceeded fee limit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathInsufficientCapacityWithFee(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -852,7 +910,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||
// Query for a route of 4,999,999 mSAT to carol.
|
||||
carol := ctx.aliases["C"]
|
||||
const amt lnwire.MilliSatoshi = 4999999
|
||||
routes, err := ctx.router.FindRoutes(carol, amt, 100)
|
||||
routes, err := ctx.router.FindRoutes(carol, amt, noFeeLimit, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find route: %v", err)
|
||||
}
|
||||
|
@ -912,7 +970,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||
|
||||
// We'll now request a route from A -> B -> C.
|
||||
ctx.router.routeCache = make(map[routeTuple][]*Route)
|
||||
routes, err = ctx.router.FindRoutes(carol, amt, 100)
|
||||
routes, err = ctx.router.FindRoutes(carol, amt, noFeeLimit, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find routes: %v", err)
|
||||
}
|
||||
|
|
|
@ -1258,8 +1258,9 @@ func pruneChannelFromRoutes(routes []*Route, skipChan uint64) []*Route {
|
|||
// fee information attached. The set of routes returned may be less than the
|
||||
// initial set of paths as it's possible we drop a route if it can't handle the
|
||||
// total payment flow after fees are calculated.
|
||||
func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop, finalCLTVDelta uint16,
|
||||
amt lnwire.MilliSatoshi, currentHeight uint32) ([]*Route, error) {
|
||||
func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop,
|
||||
finalCLTVDelta uint16, amt, feeLimit lnwire.MilliSatoshi,
|
||||
currentHeight uint32) ([]*Route, error) {
|
||||
|
||||
validRoutes := make([]*Route, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
|
@ -1267,7 +1268,8 @@ func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop, finalCLTVDelta
|
|||
// hop in the path as it contains a "self-hop" that is inserted
|
||||
// by our KSP algorithm.
|
||||
route, err := newRoute(
|
||||
amt, source, path[1:], currentHeight, finalCLTVDelta,
|
||||
amt, feeLimit, source, path[1:], currentHeight,
|
||||
finalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
// TODO(roasbeef): report straw breaking edge?
|
||||
|
@ -1316,7 +1318,8 @@ func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop, finalCLTVDelta
|
|||
// route that will be ranked the highest is the one with the lowest cumulative
|
||||
// fee along the route.
|
||||
func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey,
|
||||
amt lnwire.MilliSatoshi, numPaths uint32, finalExpiry ...uint16) ([]*Route, error) {
|
||||
amt, feeLimit lnwire.MilliSatoshi, numPaths uint32,
|
||||
finalExpiry ...uint16) ([]*Route, error) {
|
||||
|
||||
var finalCLTVDelta uint16
|
||||
if len(finalExpiry) == 0 {
|
||||
|
@ -1402,7 +1405,7 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey,
|
|||
// factored in.
|
||||
sourceVertex := Vertex(r.selfNode.PubKeyBytes)
|
||||
validRoutes, err := pathsToFeeSortedRoutes(
|
||||
sourceVertex, shortestPaths, finalCLTVDelta, amt,
|
||||
sourceVertex, shortestPaths, finalCLTVDelta, amt, feeLimit,
|
||||
uint32(currentHeight),
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -1502,6 +1505,11 @@ type LightningPayment struct {
|
|||
// milli-satoshis.
|
||||
Amount lnwire.MilliSatoshi
|
||||
|
||||
// FeeLimit is the maximum fee in millisatoshis that the payment should
|
||||
// accept when sending it through the network. The payment will fail
|
||||
// if there isn't a route with lower fees than this limit.
|
||||
FeeLimit lnwire.MilliSatoshi
|
||||
|
||||
// PaymentHash is the r-hash value to use within the HTLC extended to
|
||||
// the first hop.
|
||||
PaymentHash [32]byte
|
||||
|
|
|
@ -175,8 +175,10 @@ func TestFindRoutesFeeSorting(t *testing.T) {
|
|||
// Execute a query for all possible routes between roasbeef and luo ji.
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
target := ctx.aliases["luoji"]
|
||||
routes, err := ctx.router.FindRoutes(target, paymentAmt,
|
||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
target, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
@ -206,6 +208,59 @@ func TestFindRoutesFeeSorting(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
|
||||
// within the channel router contain a total fee less than or equal to the fee
|
||||
// limit.
|
||||
func TestFindRoutesWithFeeLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtx(
|
||||
startingBlockHeight, basicGraphFilePath,
|
||||
)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
|
||||
// This test will attempt to find routes from roasbeef to sophon for 100
|
||||
// satoshis with a fee limit of 10 satoshis. There are two routes from
|
||||
// roasbeef to sophon:
|
||||
// 1. roasbeef -> songoku -> sophon
|
||||
// 2. roasbeef -> phamnuwen -> sophon
|
||||
// The second route violates our fee limit, so we should only expect to
|
||||
// see the first route.
|
||||
target := ctx.aliases["sophon"]
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
feeLimit := lnwire.NewMSatFromSatoshis(10)
|
||||
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
target, paymentAmt, feeLimit, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("expected 1 route, got %d", len(routes))
|
||||
}
|
||||
|
||||
if routes[0].TotalFees > feeLimit {
|
||||
t.Fatalf("route exceeded fee limit: %v", spew.Sdump(routes[0]))
|
||||
}
|
||||
|
||||
hops := routes[0].Hops
|
||||
if len(hops) != 2 {
|
||||
t.Fatalf("expected 2 hops, got %d", len(hops))
|
||||
}
|
||||
|
||||
if hops[0].Channel.Node.Alias != "songoku" {
|
||||
t.Fatalf("expected first hop through songoku, got %s",
|
||||
hops[0].Channel.Node.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
|
||||
// one of the target routes is seen as unavailable, then the next route in the
|
||||
// queue is used instead. This process should continue until either a payment
|
||||
|
@ -221,11 +276,13 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
|
|||
}
|
||||
|
||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||
// to luo ji for 100 satoshis.
|
||||
// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
|
||||
var payHash [32]byte
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: ctx.aliases["luoji"],
|
||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
||||
Amount: paymentAmt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
|
@ -299,9 +356,11 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||
// to luo ji for 100 satoshis.
|
||||
var payHash [32]byte
|
||||
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: ctx.aliases["sophon"],
|
||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
||||
Amount: amt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
|
@ -397,9 +456,11 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||
// to sophon for 1k satoshis.
|
||||
var payHash [32]byte
|
||||
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: ctx.aliases["sophon"],
|
||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
||||
Amount: amt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
|
@ -524,11 +585,13 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||
}
|
||||
|
||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||
// to luo ji for 100 satoshis.
|
||||
// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
|
||||
var payHash [32]byte
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: ctx.aliases["luoji"],
|
||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
||||
Amount: paymentAmt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
|
@ -966,8 +1029,10 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||
// We should now be able to find two routes to node 2.
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
targetNode := priv2.PubKey()
|
||||
routes, err := ctx.router.FindRoutes(targetNode, paymentAmt,
|
||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
targetNode, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
@ -1009,8 +1074,10 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||
|
||||
// Should still be able to find the routes, and the info should be
|
||||
// updated.
|
||||
routes, err = ctx.router.FindRoutes(targetNode, paymentAmt,
|
||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
||||
routes, err = ctx.router.FindRoutes(
|
||||
targetNode, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
|
51
rpcserver.go
51
rpcserver.go
|
@ -1765,6 +1765,27 @@ type rpcPaymentRequest struct {
|
|||
routes []*routing.Route
|
||||
}
|
||||
|
||||
// calculateFeeLimit returns the fee limit in millisatoshis. If a percentage
|
||||
// based fee limit has been requested, we'll factor in the ratio provided with
|
||||
// the amount of the payment.
|
||||
func calculateFeeLimit(feeLimit *lnrpc.FeeLimit,
|
||||
amount lnwire.MilliSatoshi) lnwire.MilliSatoshi {
|
||||
|
||||
switch feeLimit.GetLimit().(type) {
|
||||
case *lnrpc.FeeLimit_Fixed:
|
||||
return lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(feeLimit.GetFixed()),
|
||||
)
|
||||
case *lnrpc.FeeLimit_Percent:
|
||||
return amount * lnwire.MilliSatoshi(feeLimit.GetPercent()) / 100
|
||||
default:
|
||||
// If a fee limit was not specified, we'll use the payment's
|
||||
// amount as an upper bound in order to avoid payment attempts
|
||||
// from incurring fees higher than the payment amount itself.
|
||||
return amount
|
||||
}
|
||||
}
|
||||
|
||||
// SendPayment dispatches a bi-directional streaming RPC for sending payments
|
||||
// through the Lightning Network. A single RPC invocation creates a persistent
|
||||
// bi-directional stream allowing clients to rapidly send payments through the
|
||||
|
@ -1831,6 +1852,7 @@ func (r *rpcServer) SendToRoute(stream lnrpc.Lightning_SendToRouteServer) error
|
|||
// directly to the channel router for dispatching.
|
||||
type rpcPaymentIntent struct {
|
||||
msat lnwire.MilliSatoshi
|
||||
feeLimit lnwire.MilliSatoshi
|
||||
dest *btcec.PublicKey
|
||||
rHash [32]byte
|
||||
cltvDelta uint16
|
||||
|
@ -1897,13 +1919,19 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
|
|||
payIntent.msat = *payReq.MilliSat
|
||||
}
|
||||
|
||||
// Calculate the fee limit that should be used for this payment.
|
||||
payIntent.feeLimit = calculateFeeLimit(
|
||||
rpcPayReq.FeeLimit, payIntent.msat,
|
||||
)
|
||||
|
||||
copy(payIntent.rHash[:], payReq.PaymentHash[:])
|
||||
payIntent.dest = payReq.Destination
|
||||
payIntent.cltvDelta = uint16(payReq.MinFinalCLTVExpiry())
|
||||
payIntent.routeHints = payReq.RouteHints
|
||||
|
||||
return payIntent, nil
|
||||
} else {
|
||||
}
|
||||
|
||||
// At this point, a destination MUST be specified, so we'll convert it
|
||||
// into the proper representation now. The destination will either be
|
||||
// encoded as raw bytes, or via a hex string.
|
||||
|
@ -1932,6 +1960,12 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
|
|||
payIntent.msat = lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(rpcPayReq.Amt),
|
||||
)
|
||||
|
||||
// Calculate the fee limit that should be used for this payment.
|
||||
payIntent.feeLimit = calculateFeeLimit(
|
||||
rpcPayReq.FeeLimit, payIntent.msat,
|
||||
)
|
||||
|
||||
payIntent.cltvDelta = uint16(rpcPayReq.FinalCltvDelta)
|
||||
|
||||
// If the user is manually specifying payment details, then the
|
||||
|
@ -1948,7 +1982,6 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
|
|||
} else {
|
||||
copy(payIntent.rHash[:], rpcPayReq.PaymentHash)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in debug HTLC mode, then all outgoing HTLCs will pay to the
|
||||
// same debug rHash. Otherwise, we pay to the rHash specified within
|
||||
|
@ -1993,6 +2026,7 @@ func (r *rpcServer) dispatchPaymentIntent(payIntent *rpcPaymentIntent) (*routing
|
|||
payment := &routing.LightningPayment{
|
||||
Target: payIntent.dest,
|
||||
Amount: payIntent.msat,
|
||||
FeeLimit: payIntent.feeLimit,
|
||||
PaymentHash: payIntent.rHash,
|
||||
RouteHints: payIntent.routeHints,
|
||||
}
|
||||
|
@ -2052,9 +2086,6 @@ func (r *rpcServer) sendPayment(stream *paymentStream) error {
|
|||
payChan := make(chan *rpcPaymentIntent)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// TODO(roasbeef): enforce fee limits, pass into router, ditch if exceed limit
|
||||
// * limit either a %, or absolute, or iff more than sending
|
||||
|
||||
// We don't allow payments to be sent while the daemon itself is still
|
||||
// syncing as we may be trying to sent a payment over a "stale"
|
||||
// channel.
|
||||
|
@ -2241,9 +2272,6 @@ func (r *rpcServer) SendToRouteSync(ctx context.Context,
|
|||
func (r *rpcServer) sendPaymentSync(ctx context.Context,
|
||||
nextPayment *rpcPaymentRequest) (*lnrpc.SendResponse, error) {
|
||||
|
||||
// TODO(roasbeef): enforce fee limits, pass into router, ditch if exceed limit
|
||||
// * limit either a %, or absolute, or iff more than sending
|
||||
|
||||
// We don't allow payments to be sent while the daemon itself is still
|
||||
// syncing as we may be trying to sent a payment over a "stale"
|
||||
// channel.
|
||||
|
@ -3053,6 +3081,8 @@ func (r *rpcServer) QueryRoutes(ctx context.Context,
|
|||
"allowed is %v", amt, maxPaymentMSat.ToSatoshis())
|
||||
}
|
||||
|
||||
feeLimit := calculateFeeLimit(in.FeeLimit, amtMSat)
|
||||
|
||||
// Query the channel router for a possible path to the destination that
|
||||
// can carry `in.Amt` satoshis _including_ the total fee required on
|
||||
// the route.
|
||||
|
@ -3062,11 +3092,12 @@ func (r *rpcServer) QueryRoutes(ctx context.Context,
|
|||
)
|
||||
if in.FinalCltvDelta == 0 {
|
||||
routes, findErr = r.server.chanRouter.FindRoutes(
|
||||
pubKey, amtMSat, uint32(in.NumRoutes),
|
||||
pubKey, amtMSat, feeLimit, uint32(in.NumRoutes),
|
||||
)
|
||||
} else {
|
||||
routes, findErr = r.server.chanRouter.FindRoutes(
|
||||
pubKey, amtMSat, uint32(in.NumRoutes), uint16(in.FinalCltvDelta),
|
||||
pubKey, amtMSat, feeLimit, uint32(in.NumRoutes),
|
||||
uint16(in.FinalCltvDelta),
|
||||
)
|
||||
}
|
||||
if findErr != nil {
|
||||
|
|
Loading…
Add table
Reference in a new issue