diff --git a/plugins/pay.c b/plugins/pay.c index 8599c9ac3..1f27096f5 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -19,6 +19,11 @@ struct pay_command { u64 msatoshi; double riskfactor; + /* Limits on what routes we'll accept. */ + double maxfeepercent; + unsigned int maxdelay; + u64 exemptfee; + /* Payment hash, as text. */ const char *payment_hash; @@ -121,10 +126,40 @@ static struct command_result *getroute_done(struct command *cmd, struct pay_attempt attempt; const jsmntok_t *t = json_get_member(buf, result, "route"); char *json_desc; + u64 fee; + u32 delay; + double feepercent; + if (!t) plugin_err("getroute gave no 'route'? '%.*s'", result->end - result->start, buf); + if (!json_to_u64(buf, json_delve(buf, t, "[0].msatoshi"), &fee)) + plugin_err("getroute with invalid msatoshi? '%.*s'", + result->end - result->start, buf); + fee -= pc->msatoshi; + + if (!json_to_number(buf, json_delve(buf, t, "[0].delay"), &delay)) + plugin_err("getroute with invalid delay? '%.*s'", + result->end - result->start, buf); + + /* Casting u64 to double will lose some precision. The loss of precision + * in feepercent will be like 3.0000..(some dots)..1 % - 3.0 %. + * That loss will not be representable in double. So, it's Okay to + * cast u64 to double for feepercent calculation. */ + feepercent = ((double)fee) * 100.0 / ((double) pc->msatoshi); + + if (fee > pc->exemptfee && feepercent > pc->maxfeepercent) { + return command_fail(cmd, PAY_ROUTE_TOO_EXPENSIVE, + "Route wanted fee of %"PRIu64" msatoshis", + fee); + } + + if (delay > pc->maxdelay) { + return command_fail(cmd, PAY_ROUTE_TOO_EXPENSIVE, + "Route wanted delay of %u blocks", delay); + } + attempt.route = json_strdup(pc->attempts, buf, result); tal_arr_expand(&pc->attempts, attempt); @@ -230,11 +265,9 @@ static struct command_result *handle_pay(struct command *cmd, double *riskfactor; unsigned int *retryfor; struct pay_command *pc = tal(cmd, struct pay_command); - - /* FIXME! */ double *maxfeepercent; unsigned int *maxdelay; - unsigned int *exemptfee; + u64 *exemptfee; setup_locale(); @@ -248,7 +281,7 @@ static struct command_result *handle_pay(struct command *cmd, p_opt_def("maxdelay", param_number, &maxdelay, /* FIXME! */ 14 * 24 * 6), - p_opt_def("exemptfee", param_number, &exemptfee, 5000), + p_opt_def("exemptfee", param_u64, &exemptfee, 5000), NULL)) return NULL; @@ -276,6 +309,9 @@ static struct command_result *handle_pay(struct command *cmd, pc->msatoshi = *msatoshi; } + pc->maxfeepercent = *maxfeepercent; + pc->maxdelay = *maxdelay; + pc->exemptfee = *exemptfee; pc->riskfactor = *riskfactor; pc->dest = type_to_string(cmd, struct pubkey, &b11->receiver_id); pc->payment_hash = type_to_string(pc, struct sha256, diff --git a/tests/test_pay.py b/tests/test_pay.py index 028015a56..3893e7228 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -59,6 +59,31 @@ def test_pay(node_factory): assert len(payments) == 1 and payments[0]['payment_preimage'] == preimage +def test_pay_limits(node_factory): + """Test that we enforce fee max percentage and max delay""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + # FIXME: pylightning should define these! + PAY_ROUTE_TOO_EXPENSIVE = 206 + + inv = l3.rpc.invoice("any", "any", 'description') + + # Fee too high. + with pytest.raises(RpcError, match=r'Route wanted fee of .* msatoshis') as err: + l1.rpc.call('pay2', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxfeepercent': 0.0001, 'exemptfee': 0}) + + assert err.value.error['code'] == PAY_ROUTE_TOO_EXPENSIVE + + # Delay too high. + with pytest.raises(RpcError, match=r'Route wanted delay of .* blocks') as err: + l1.rpc.call('pay2', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxdelay': 0}) + + assert err.value.error['code'] == PAY_ROUTE_TOO_EXPENSIVE + + # This works, because fee is less than exemptfee. + l1.rpc.call('pay2', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxfeepercent': 0.0001, 'exemptfee': 2000}) + + def test_pay0(node_factory): """Test paying 0 amount """