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:
12 files changed
+1524
-771
No files matched your search
+143
-64
@@ -1617,6 +1617,16 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Name: "amt, a",
|
Name: "amt, a",
|
||||||
Usage: "number of satoshis to send",
|
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{
|
cli.StringFlag{
|
||||||
Name: "payment_hash, r",
|
Name: "payment_hash, r",
|
||||||
Usage: "the hash to use within the payment's HTLC",
|
Usage: "the hash to use within the payment's HTLC",
|
||||||
@@ -1637,6 +1647,32 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Action: sendPayment,
|
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 {
|
func sendPayment(ctx *cli.Context) error {
|
||||||
// Show command help if no arguments provided
|
// Show command help if no arguments provided
|
||||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||||
@@ -1644,87 +1680,99 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
return nil
|
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") {
|
if ctx.IsSet("pay_req") {
|
||||||
req = &lnrpc.SendRequest{
|
req := &lnrpc.SendRequest{
|
||||||
PaymentRequest: ctx.String("pay_req"),
|
PaymentRequest: ctx.String("pay_req"),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
|
FeeLimit: feeLimit,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
args := ctx.Args()
|
|
||||||
|
|
||||||
var (
|
return sendPaymentRequest(ctx, req)
|
||||||
destNode []byte
|
}
|
||||||
err error
|
|
||||||
amount int64
|
var (
|
||||||
)
|
destNode []byte
|
||||||
|
amount int64
|
||||||
|
)
|
||||||
|
|
||||||
|
args := ctx.Args()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ctx.IsSet("dest"):
|
||||||
|
destNode, err = hex.DecodeString(ctx.String("dest"))
|
||||||
|
case args.Present():
|
||||||
|
destNode, err = hex.DecodeString(args.First())
|
||||||
|
args = args.Tail()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("destination txid argument missing")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(destNode) != 33 {
|
||||||
|
return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
|
||||||
|
"instead: %v", len(destNode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsSet("amt") {
|
||||||
|
amount = ctx.Int64("amt")
|
||||||
|
} else if args.Present() {
|
||||||
|
amount, err = strconv.ParseInt(args.First(), 10, 64)
|
||||||
|
args = args.Tail()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode payment amount: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &lnrpc.SendRequest{
|
||||||
|
Dest: destNode,
|
||||||
|
Amt: amount,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Bool("debug_send") && (ctx.IsSet("payment_hash") || args.Present()) {
|
||||||
|
return fmt.Errorf("do not provide a payment hash with debug send")
|
||||||
|
} else if !ctx.Bool("debug_send") {
|
||||||
|
var rHash []byte
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ctx.IsSet("dest"):
|
case ctx.IsSet("payment_hash"):
|
||||||
destNode, err = hex.DecodeString(ctx.String("dest"))
|
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
||||||
case args.Present():
|
case args.Present():
|
||||||
destNode, err = hex.DecodeString(args.First())
|
rHash, err = hex.DecodeString(args.First())
|
||||||
args = args.Tail()
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("destination txid argument missing")
|
return fmt.Errorf("payment hash argument missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(rHash) != 32 {
|
||||||
if len(destNode) != 33 {
|
return fmt.Errorf("payment hash must be exactly 32 "+
|
||||||
return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
|
"bytes, is instead %v", len(rHash))
|
||||||
"instead: %v", len(destNode))
|
|
||||||
}
|
}
|
||||||
|
req.PaymentHash = rHash
|
||||||
|
|
||||||
if ctx.IsSet("amt") {
|
switch {
|
||||||
amount = ctx.Int64("amt")
|
case ctx.IsSet("final_cltv_delta"):
|
||||||
} else if args.Present() {
|
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
|
||||||
amount, err = strconv.ParseInt(args.First(), 10, 64)
|
case args.Present():
|
||||||
args = args.Tail()
|
delta, err := strconv.ParseInt(args.First(), 10, 64)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to decode payment amount: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req = &lnrpc.SendRequest{
|
|
||||||
Dest: destNode,
|
|
||||||
Amt: amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Bool("debug_send") && (ctx.IsSet("payment_hash") || args.Present()) {
|
|
||||||
return fmt.Errorf("do not provide a payment hash with debug send")
|
|
||||||
} else if !ctx.Bool("debug_send") {
|
|
||||||
var rHash []byte
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case ctx.IsSet("payment_hash"):
|
|
||||||
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
|
||||||
case args.Present():
|
|
||||||
rHash, err = hex.DecodeString(args.First())
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("payment hash argument missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(rHash) != 32 {
|
req.FinalCltvDelta = int32(delta)
|
||||||
return fmt.Errorf("payment hash must be exactly 32 "+
|
|
||||||
"bytes, is instead %v", len(rHash))
|
|
||||||
}
|
|
||||||
req.PaymentHash = rHash
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case ctx.IsSet("final_cltv_delta"):
|
|
||||||
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
|
|
||||||
case args.Present():
|
|
||||||
delta, err := strconv.ParseInt(args.First(), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.FinalCltvDelta = int32(delta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1779,6 +1827,16 @@ var payInvoiceCommand = cli.Command{
|
|||||||
Usage: "(optional) number of satoshis to fulfill the " +
|
Usage: "(optional) number of satoshis to fulfill the " +
|
||||||
"invoice",
|
"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),
|
Action: actionDecorator(payInvoice),
|
||||||
}
|
}
|
||||||
@@ -1787,7 +1845,6 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
args := ctx.Args()
|
args := ctx.Args()
|
||||||
|
|
||||||
var payReq string
|
var payReq string
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ctx.IsSet("pay_req"):
|
case ctx.IsSet("pay_req"):
|
||||||
payReq = ctx.String("pay_req")
|
payReq = ctx.String("pay_req")
|
||||||
@@ -1797,9 +1854,15 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
return fmt.Errorf("pay_req argument missing")
|
return fmt.Errorf("pay_req argument missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feeLimit, err := retrieveFeeLimit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
req := &lnrpc.SendRequest{
|
req := &lnrpc.SendRequest{
|
||||||
PaymentRequest: payReq,
|
PaymentRequest: payReq,
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
|
FeeLimit: feeLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return sendPaymentRequest(ctx, req)
|
||||||
@@ -2499,6 +2562,16 @@ var queryRoutesCommand = cli.Command{
|
|||||||
Name: "amt",
|
Name: "amt",
|
||||||
Usage: "the amount to send expressed in satoshis",
|
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{
|
cli.Int64Flag{
|
||||||
Name: "num_max_routes",
|
Name: "num_max_routes",
|
||||||
Usage: "the max number of routes to be returned (default: 10)",
|
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")
|
return fmt.Errorf("amt argument missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feeLimit, err := retrieveFeeLimit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
req := &lnrpc.QueryRoutesRequest{
|
req := &lnrpc.QueryRoutesRequest{
|
||||||
PubKey: dest,
|
PubKey: dest,
|
||||||
Amt: amt,
|
Amt: amt,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
NumRoutes: int32(ctx.Int("num_max_routes")),
|
NumRoutes: int32(ctx.Int("num_max_routes")),
|
||||||
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
|
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
|
||||||
}
|
}
|
||||||
|
|||||||
+455
-154
@@ -674,6 +674,135 @@ func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false)
|
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
|
// testUpdateChannelPolicy tests that policy updates made to a channel
|
||||||
// gets propagated to other nodes in the network.
|
// gets propagated to other nodes in the network.
|
||||||
func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
@@ -746,162 +875,46 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
feeRate := int64(12)
|
feeRate := int64(12)
|
||||||
timeLockDelta := uint32(66)
|
timeLockDelta := uint32(66)
|
||||||
|
|
||||||
|
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||||
|
FeeBaseMsat: baseFee,
|
||||||
|
FeeRateMilliMsat: feeBase * feeRate,
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
}
|
||||||
|
|
||||||
req := &lnrpc.PolicyUpdateRequest{
|
req := &lnrpc.PolicyUpdateRequest{
|
||||||
BaseFeeMsat: baseFee,
|
BaseFeeMsat: baseFee,
|
||||||
FeeRate: float64(feeRate),
|
FeeRate: float64(feeRate),
|
||||||
TimeLockDelta: timeLockDelta,
|
TimeLockDelta: timeLockDelta,
|
||||||
}
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||||
req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
|
ChanPoint: chanPoint,
|
||||||
ChanPoint: chanPoint,
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = net.Bob.UpdateChannelPolicy(ctxb, req)
|
if _, err := net.Bob.UpdateChannelPolicy(ctxb, req); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get alice's balance: %v", err)
|
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.
|
// Wait for all nodes to have seen the policy update done by Bob.
|
||||||
waitForChannelUpdate(aliceUpdates, net.Bob.PubKeyStr, chanPoint)
|
waitForChannelUpdate(
|
||||||
waitForChannelUpdate(bobUpdates, net.Bob.PubKeyStr, chanPoint)
|
t, aliceUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
waitForChannelUpdate(carolUpdates, net.Bob.PubKeyStr, chanPoint)
|
)
|
||||||
|
waitForChannelUpdate(
|
||||||
// assertChannelPolicy asserts that the passed node's known channel
|
t, bobUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
// policy for the passed chanPoint is consistent with Bob's current
|
)
|
||||||
// expected policy values.
|
waitForChannelUpdate(
|
||||||
assertChannelPolicy := func(node *lntest.HarnessNode,
|
t, carolUpdates, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all nodes now know about Bob's updated policy.
|
// Check that all nodes now know about Bob's updated policy.
|
||||||
assertChannelPolicy(net.Alice, net.Bob.PubKeyStr, chanPoint)
|
assertChannelPolicy(
|
||||||
assertChannelPolicy(net.Bob, net.Bob.PubKeyStr, chanPoint)
|
t, net.Alice, net.Bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
assertChannelPolicy(carol, net.Bob.PubKeyStr, 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
|
// 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
|
// 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)
|
feeRate = int64(123)
|
||||||
timeLockDelta = uint32(22)
|
timeLockDelta = uint32(22)
|
||||||
|
|
||||||
|
expectedPolicy.FeeBaseMsat = baseFee
|
||||||
|
expectedPolicy.FeeRateMilliMsat = feeBase * feeRate
|
||||||
|
expectedPolicy.TimeLockDelta = timeLockDelta
|
||||||
|
|
||||||
req = &lnrpc.PolicyUpdateRequest{
|
req = &lnrpc.PolicyUpdateRequest{
|
||||||
BaseFeeMsat: baseFee,
|
BaseFeeMsat: baseFee,
|
||||||
FeeRate: float64(feeRate),
|
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
|
// Wait for all nodes to have seen the policy updates for both of
|
||||||
// Alice's channels.
|
// Alice's channels.
|
||||||
waitForChannelUpdate(
|
waitForChannelUpdate(
|
||||||
aliceUpdates, net.Alice.PubKeyStr, chanPoint, chanPoint3,
|
t, aliceUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
|
chanPoint3,
|
||||||
)
|
)
|
||||||
waitForChannelUpdate(
|
waitForChannelUpdate(
|
||||||
bobUpdates, net.Alice.PubKeyStr, chanPoint, chanPoint3,
|
t, bobUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
|
chanPoint3,
|
||||||
)
|
)
|
||||||
waitForChannelUpdate(
|
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
|
// And finally check that all nodes remembers the policy update they
|
||||||
// received.
|
// received.
|
||||||
assertChannelPolicy(net.Alice, net.Alice.PubKeyStr, chanPoint)
|
assertChannelPolicy(
|
||||||
assertChannelPolicy(net.Bob, net.Alice.PubKeyStr, chanPoint)
|
t, net.Alice, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
assertChannelPolicy(carol, net.Alice.PubKeyStr, chanPoint)
|
chanPoint3,
|
||||||
|
)
|
||||||
assertChannelPolicy(net.Alice, net.Alice.PubKeyStr, chanPoint3)
|
assertChannelPolicy(
|
||||||
assertChannelPolicy(net.Bob, net.Alice.PubKeyStr, chanPoint3)
|
t, net.Bob, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
assertChannelPolicy(carol, net.Alice.PubKeyStr, chanPoint3)
|
chanPoint3,
|
||||||
|
)
|
||||||
|
assertChannelPolicy(
|
||||||
|
t, carol, net.Alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||||
|
chanPoint3,
|
||||||
|
)
|
||||||
|
|
||||||
// Close the channels.
|
// Close the channels.
|
||||||
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
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 {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
test func(net *lntest.NetworkHarness, t *harnessTest)
|
test func(net *lntest.NetworkHarness, t *harnessTest)
|
||||||
@@ -9927,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
|
||||||
|
|||||||
+608
-472
File diff suppressed because it is too large.
Load diff
+31
-2
@@ -646,6 +646,16 @@ message TransactionDetails {
|
|||||||
repeated Transaction transactions = 1 [json_name = "transactions"];
|
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 {
|
message SendRequest {
|
||||||
/// The identity pubkey of the payment recipient
|
/// The identity pubkey of the payment recipient
|
||||||
bytes dest = 1;
|
bytes dest = 1;
|
||||||
@@ -669,8 +679,19 @@ message SendRequest {
|
|||||||
*/
|
*/
|
||||||
string payment_request = 6;
|
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;
|
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 {
|
message SendResponse {
|
||||||
string payment_error = 1 [json_name = "payment_error"];
|
string payment_error = 1 [json_name = "payment_error"];
|
||||||
@@ -1230,9 +1251,17 @@ message QueryRoutesRequest {
|
|||||||
|
|
||||||
/// An optional CLTV delta from the current height that should be used for the timelock of the final hop
|
/// An optional CLTV delta from the current height that should be used for the timelock of the final hop
|
||||||
int32 final_cltv_delta = 4;
|
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 {
|
message QueryRoutesResponse {
|
||||||
repeated Route routes = 1 [ json_name = "routes"];
|
repeated Route routes = 1 [json_name = "routes"];
|
||||||
}
|
}
|
||||||
|
|
||||||
message Hop {
|
message Hop {
|
||||||
|
|||||||
+36
-1
@@ -474,6 +474,22 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"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": [
|
"tags": [
|
||||||
@@ -1332,6 +1348,21 @@
|
|||||||
"lnrpcDisconnectPeerResponse": {
|
"lnrpcDisconnectPeerResponse": {
|
||||||
"type": "object"
|
"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": {
|
"lnrpcFeeReportResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2380,7 +2411,11 @@
|
|||||||
"final_cltv_delta": {
|
"final_cltv_delta": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32",
|
"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
|
// ErrPaymentAttemptTimeout is an error that indicates that a payment
|
||||||
// attempt timed out before we were able to successfully route an HTLC.
|
// attempt timed out before we were able to successfully route an HTLC.
|
||||||
ErrPaymentAttemptTimeout
|
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,
|
// 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.
|
// a route by applying the time-lock and fee requirements.
|
||||||
sourceVertex := Vertex(p.mc.selfNode.PubKeyBytes)
|
sourceVertex := Vertex(p.mc.selfNode.PubKeyBytes)
|
||||||
route, err := newRoute(
|
route, err := newRoute(
|
||||||
payment.Amount, sourceVertex, path, height, finalCltvDelta,
|
payment.Amount, payment.FeeLimit, sourceVertex, path, height,
|
||||||
|
finalCltvDelta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(roasbeef): return which edge/vertex didn't work
|
// TODO(roasbeef): return which edge/vertex didn't work
|
||||||
|
|||||||
+8
-4
@@ -248,7 +248,7 @@ func (r *Route) ToHopPayloads() []sphinx.HopData {
|
|||||||
//
|
//
|
||||||
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from
|
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from
|
||||||
// the source to the target node of the path finding attempt.
|
// 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,
|
pathEdges []*ChannelHop, currentHeight uint32,
|
||||||
finalCLTVDelta uint16) (*Route, error) {
|
finalCLTVDelta uint16) (*Route, error) {
|
||||||
|
|
||||||
@@ -265,9 +265,6 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
|
|||||||
prevHopMap: make(map[Vertex]*ChannelHop),
|
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
|
// We'll populate the next hop map for the _source_ node with the
|
||||||
// information for the first hop so the mapping is sound.
|
// information for the first hop so the mapping is sound.
|
||||||
route.nextHopMap[sourceVertex] = pathEdges[0]
|
route.nextHopMap[sourceVertex] = pathEdges[0]
|
||||||
@@ -342,6 +339,13 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
|
|||||||
|
|
||||||
route.TotalFees += nextHop.Fee
|
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
|
// As a sanity check, we ensure that the selected channel has
|
||||||
// enough capacity to forward the required amount which
|
// enough capacity to forward the required amount which
|
||||||
// includes the fee dictated at each hop.
|
// includes the fee dictated at each hop.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -40,6 +41,11 @@ const (
|
|||||||
// implementations will use in order to ensure that they're calculating
|
// implementations will use in order to ensure that they're calculating
|
||||||
// the payload for each hop in path properly.
|
// the payload for each hop in path properly.
|
||||||
specExampleFilePath = "testdata/spec_example.json"
|
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 (
|
var (
|
||||||
@@ -318,8 +324,11 @@ func TestBasicGraphPathFinding(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unable to create path: %v", err)
|
t.Fatalf("unable to create path: %v", err)
|
||||||
}
|
}
|
||||||
@@ -460,8 +469,11 @@ func TestBasicGraphPathFinding(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find route: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unable to create path: %v", err)
|
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
|
// TestRouteFailMinHTLC tests that if we attempt to route an HTLC which is
|
||||||
// smaller than the advertised minHTLC of an edge, then path finding fails.
|
// smaller than the advertised minHTLC of an edge, then path finding fails.
|
||||||
func TestRouteFailMinHTLC(t *testing.T) {
|
func TestRouteFailMinHTLC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -760,6 +774,8 @@ func TestRouteFailMinHTLC(t *testing.T) {
|
|||||||
// that's disabled, then that edge is disqualified, and the routing attempt
|
// that's disabled, then that edge is disqualified, and the routing attempt
|
||||||
// will fail.
|
// will fail.
|
||||||
func TestRouteFailDisabledEdge(t *testing.T) {
|
func TestRouteFailDisabledEdge(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
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) {
|
func TestPathInsufficientCapacityWithFee(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -852,7 +910,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
// Query for a route of 4,999,999 mSAT to carol.
|
// Query for a route of 4,999,999 mSAT to carol.
|
||||||
carol := ctx.aliases["C"]
|
carol := ctx.aliases["C"]
|
||||||
const amt lnwire.MilliSatoshi = 4999999
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unable to find route: %v", err)
|
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.
|
// We'll now request a route from A -> B -> C.
|
||||||
ctx.router.routeCache = make(map[routeTuple][]*Route)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unable to find routes: %v", err)
|
t.Fatalf("unable to find routes: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-5
@@ -1258,8 +1258,9 @@ func pruneChannelFromRoutes(routes []*Route, skipChan uint64) []*Route {
|
|||||||
// fee information attached. The set of routes returned may be less than the
|
// 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
|
// 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.
|
// total payment flow after fees are calculated.
|
||||||
func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop, finalCLTVDelta uint16,
|
func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop,
|
||||||
amt lnwire.MilliSatoshi, currentHeight uint32) ([]*Route, error) {
|
finalCLTVDelta uint16, amt, feeLimit lnwire.MilliSatoshi,
|
||||||
|
currentHeight uint32) ([]*Route, error) {
|
||||||
|
|
||||||
validRoutes := make([]*Route, 0, len(paths))
|
validRoutes := make([]*Route, 0, len(paths))
|
||||||
for _, path := range 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
|
// hop in the path as it contains a "self-hop" that is inserted
|
||||||
// by our KSP algorithm.
|
// by our KSP algorithm.
|
||||||
route, err := newRoute(
|
route, err := newRoute(
|
||||||
amt, source, path[1:], currentHeight, finalCLTVDelta,
|
amt, feeLimit, source, path[1:], currentHeight,
|
||||||
|
finalCLTVDelta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(roasbeef): report straw breaking edge?
|
// 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
|
// route that will be ranked the highest is the one with the lowest cumulative
|
||||||
// fee along the route.
|
// fee along the route.
|
||||||
func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey,
|
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
|
var finalCLTVDelta uint16
|
||||||
if len(finalExpiry) == 0 {
|
if len(finalExpiry) == 0 {
|
||||||
@@ -1402,7 +1405,7 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey,
|
|||||||
// factored in.
|
// factored in.
|
||||||
sourceVertex := Vertex(r.selfNode.PubKeyBytes)
|
sourceVertex := Vertex(r.selfNode.PubKeyBytes)
|
||||||
validRoutes, err := pathsToFeeSortedRoutes(
|
validRoutes, err := pathsToFeeSortedRoutes(
|
||||||
sourceVertex, shortestPaths, finalCLTVDelta, amt,
|
sourceVertex, shortestPaths, finalCLTVDelta, amt, feeLimit,
|
||||||
uint32(currentHeight),
|
uint32(currentHeight),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1502,6 +1505,11 @@ type LightningPayment struct {
|
|||||||
// milli-satoshis.
|
// milli-satoshis.
|
||||||
Amount lnwire.MilliSatoshi
|
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
|
// PaymentHash is the r-hash value to use within the HTLC extended to
|
||||||
// the first hop.
|
// the first hop.
|
||||||
PaymentHash [32]byte
|
PaymentHash [32]byte
|
||||||
|
|||||||
+79
-12
@@ -175,8 +175,10 @@ func TestFindRoutesFeeSorting(t *testing.T) {
|
|||||||
// Execute a query for all possible routes between roasbeef and luo ji.
|
// Execute a query for all possible routes between roasbeef and luo ji.
|
||||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||||
target := ctx.aliases["luoji"]
|
target := ctx.aliases["luoji"]
|
||||||
routes, err := ctx.router.FindRoutes(target, paymentAmt,
|
routes, err := ctx.router.FindRoutes(
|
||||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
target, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||||
|
DefaultFinalCLTVDelta,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find any routes: %v", err)
|
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
|
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
|
||||||
// one of the target routes is seen as unavailable, then the next route in the
|
// 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
|
// 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
|
// 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
|
var payHash [32]byte
|
||||||
|
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
payment := LightningPayment{
|
payment := LightningPayment{
|
||||||
Target: ctx.aliases["luoji"],
|
Target: ctx.aliases["luoji"],
|
||||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
Amount: paymentAmt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
PaymentHash: payHash,
|
PaymentHash: payHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +356,11 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
// to luo ji for 100 satoshis.
|
// to luo ji for 100 satoshis.
|
||||||
var payHash [32]byte
|
var payHash [32]byte
|
||||||
|
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
payment := LightningPayment{
|
payment := LightningPayment{
|
||||||
Target: ctx.aliases["sophon"],
|
Target: ctx.aliases["sophon"],
|
||||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
Amount: amt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
PaymentHash: payHash,
|
PaymentHash: payHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,9 +456,11 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
// to sophon for 1k satoshis.
|
// to sophon for 1k satoshis.
|
||||||
var payHash [32]byte
|
var payHash [32]byte
|
||||||
|
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
payment := LightningPayment{
|
payment := LightningPayment{
|
||||||
Target: ctx.aliases["sophon"],
|
Target: ctx.aliases["sophon"],
|
||||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
Amount: amt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
PaymentHash: payHash,
|
PaymentHash: payHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,11 +585,13 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// 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
|
var payHash [32]byte
|
||||||
|
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
payment := LightningPayment{
|
payment := LightningPayment{
|
||||||
Target: ctx.aliases["luoji"],
|
Target: ctx.aliases["luoji"],
|
||||||
Amount: lnwire.NewMSatFromSatoshis(1000),
|
Amount: paymentAmt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
PaymentHash: payHash,
|
PaymentHash: payHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,8 +1029,10 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
// We should now be able to find two routes to node 2.
|
// We should now be able to find two routes to node 2.
|
||||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||||
targetNode := priv2.PubKey()
|
targetNode := priv2.PubKey()
|
||||||
routes, err := ctx.router.FindRoutes(targetNode, paymentAmt,
|
routes, err := ctx.router.FindRoutes(
|
||||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
targetNode, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||||
|
DefaultFinalCLTVDelta,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find any routes: %v", err)
|
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
|
// Should still be able to find the routes, and the info should be
|
||||||
// updated.
|
// updated.
|
||||||
routes, err = ctx.router.FindRoutes(targetNode, paymentAmt,
|
routes, err = ctx.router.FindRoutes(
|
||||||
defaultNumRoutes, DefaultFinalCLTVDelta)
|
targetNode, paymentAmt, noFeeLimit, defaultNumRoutes,
|
||||||
|
DefaultFinalCLTVDelta,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find any routes: %v", err)
|
t.Fatalf("unable to find any routes: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-50
@@ -1765,6 +1765,27 @@ type rpcPaymentRequest struct {
|
|||||||
routes []*routing.Route
|
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
|
// SendPayment dispatches a bi-directional streaming RPC for sending payments
|
||||||
// through the Lightning Network. A single RPC invocation creates a persistent
|
// through the Lightning Network. A single RPC invocation creates a persistent
|
||||||
// bi-directional stream allowing clients to rapidly send payments through the
|
// 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.
|
// directly to the channel router for dispatching.
|
||||||
type rpcPaymentIntent struct {
|
type rpcPaymentIntent struct {
|
||||||
msat lnwire.MilliSatoshi
|
msat lnwire.MilliSatoshi
|
||||||
|
feeLimit lnwire.MilliSatoshi
|
||||||
dest *btcec.PublicKey
|
dest *btcec.PublicKey
|
||||||
rHash [32]byte
|
rHash [32]byte
|
||||||
cltvDelta uint16
|
cltvDelta uint16
|
||||||
@@ -1897,57 +1919,68 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
|
|||||||
payIntent.msat = *payReq.MilliSat
|
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[:])
|
copy(payIntent.rHash[:], payReq.PaymentHash[:])
|
||||||
payIntent.dest = payReq.Destination
|
payIntent.dest = payReq.Destination
|
||||||
payIntent.cltvDelta = uint16(payReq.MinFinalCLTVExpiry())
|
payIntent.cltvDelta = uint16(payReq.MinFinalCLTVExpiry())
|
||||||
payIntent.routeHints = payReq.RouteHints
|
payIntent.routeHints = payReq.RouteHints
|
||||||
|
|
||||||
return payIntent, nil
|
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.
|
|
||||||
if len(rpcPayReq.Dest) != 0 {
|
|
||||||
payIntent.dest, err = btcec.ParsePubKey(
|
|
||||||
rpcPayReq.Dest, btcec.S256(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return payIntent, err
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
// At this point, a destination MUST be specified, so we'll convert it
|
||||||
pubBytes, err := hex.DecodeString(rpcPayReq.DestString)
|
// into the proper representation now. The destination will either be
|
||||||
if err != nil {
|
// encoded as raw bytes, or via a hex string.
|
||||||
return payIntent, err
|
if len(rpcPayReq.Dest) != 0 {
|
||||||
}
|
payIntent.dest, err = btcec.ParsePubKey(
|
||||||
payIntent.dest, err = btcec.ParsePubKey(pubBytes, btcec.S256())
|
rpcPayReq.Dest, btcec.S256(),
|
||||||
if err != nil {
|
|
||||||
return payIntent, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, If the payment request field was not specified
|
|
||||||
// (and a custom route wasn't specified), construct the payment
|
|
||||||
// from the other fields.
|
|
||||||
payIntent.msat = lnwire.NewMSatFromSatoshis(
|
|
||||||
btcutil.Amount(rpcPayReq.Amt),
|
|
||||||
)
|
)
|
||||||
payIntent.cltvDelta = uint16(rpcPayReq.FinalCltvDelta)
|
if err != nil {
|
||||||
|
return payIntent, err
|
||||||
// If the user is manually specifying payment details, then the
|
|
||||||
// payment hash may be encoded as a string.
|
|
||||||
if rpcPayReq.PaymentHashString != "" {
|
|
||||||
paymentHash, err := hex.DecodeString(
|
|
||||||
rpcPayReq.PaymentHashString,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return payIntent, err
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(payIntent.rHash[:], paymentHash)
|
|
||||||
} else {
|
|
||||||
copy(payIntent.rHash[:], rpcPayReq.PaymentHash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
pubBytes, err := hex.DecodeString(rpcPayReq.DestString)
|
||||||
|
if err != nil {
|
||||||
|
return payIntent, err
|
||||||
|
}
|
||||||
|
payIntent.dest, err = btcec.ParsePubKey(pubBytes, btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
return payIntent, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, If the payment request field was not specified
|
||||||
|
// (and a custom route wasn't specified), construct the payment
|
||||||
|
// from the other fields.
|
||||||
|
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
|
||||||
|
// payment hash may be encoded as a string.
|
||||||
|
if rpcPayReq.PaymentHashString != "" {
|
||||||
|
paymentHash, err := hex.DecodeString(
|
||||||
|
rpcPayReq.PaymentHashString,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return payIntent, err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(payIntent.rHash[:], paymentHash)
|
||||||
|
} else {
|
||||||
|
copy(payIntent.rHash[:], rpcPayReq.PaymentHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in debug HTLC mode, then all outgoing HTLCs will pay to the
|
// If we're in debug HTLC mode, then all outgoing HTLCs will pay to the
|
||||||
@@ -1993,6 +2026,7 @@ func (r *rpcServer) dispatchPaymentIntent(payIntent *rpcPaymentIntent) (*routing
|
|||||||
payment := &routing.LightningPayment{
|
payment := &routing.LightningPayment{
|
||||||
Target: payIntent.dest,
|
Target: payIntent.dest,
|
||||||
Amount: payIntent.msat,
|
Amount: payIntent.msat,
|
||||||
|
FeeLimit: payIntent.feeLimit,
|
||||||
PaymentHash: payIntent.rHash,
|
PaymentHash: payIntent.rHash,
|
||||||
RouteHints: payIntent.routeHints,
|
RouteHints: payIntent.routeHints,
|
||||||
}
|
}
|
||||||
@@ -2052,9 +2086,6 @@ func (r *rpcServer) sendPayment(stream *paymentStream) error {
|
|||||||
payChan := make(chan *rpcPaymentIntent)
|
payChan := make(chan *rpcPaymentIntent)
|
||||||
errChan := make(chan error, 1)
|
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
|
// 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"
|
// syncing as we may be trying to sent a payment over a "stale"
|
||||||
// channel.
|
// channel.
|
||||||
@@ -2241,9 +2272,6 @@ func (r *rpcServer) SendToRouteSync(ctx context.Context,
|
|||||||
func (r *rpcServer) sendPaymentSync(ctx context.Context,
|
func (r *rpcServer) sendPaymentSync(ctx context.Context,
|
||||||
nextPayment *rpcPaymentRequest) (*lnrpc.SendResponse, error) {
|
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
|
// 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"
|
// syncing as we may be trying to sent a payment over a "stale"
|
||||||
// channel.
|
// channel.
|
||||||
@@ -3053,6 +3081,8 @@ func (r *rpcServer) QueryRoutes(ctx context.Context,
|
|||||||
"allowed is %v", amt, maxPaymentMSat.ToSatoshis())
|
"allowed is %v", amt, maxPaymentMSat.ToSatoshis())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feeLimit := calculateFeeLimit(in.FeeLimit, amtMSat)
|
||||||
|
|
||||||
// Query the channel router for a possible path to the destination that
|
// Query the channel router for a possible path to the destination that
|
||||||
// can carry `in.Amt` satoshis _including_ the total fee required on
|
// can carry `in.Amt` satoshis _including_ the total fee required on
|
||||||
// the route.
|
// the route.
|
||||||
@@ -3062,11 +3092,12 @@ func (r *rpcServer) QueryRoutes(ctx context.Context,
|
|||||||
)
|
)
|
||||||
if in.FinalCltvDelta == 0 {
|
if in.FinalCltvDelta == 0 {
|
||||||
routes, findErr = r.server.chanRouter.FindRoutes(
|
routes, findErr = r.server.chanRouter.FindRoutes(
|
||||||
pubKey, amtMSat, uint32(in.NumRoutes),
|
pubKey, amtMSat, feeLimit, uint32(in.NumRoutes),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
routes, findErr = r.server.chanRouter.FindRoutes(
|
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 {
|
if findErr != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user