From afc0147d6372c4c0e5c328dd21d9acd3c88e40b5 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 3 Jun 2020 17:58:20 +0200 Subject: [PATCH] paymod: Exclude most expensive/slowest chan if limits are exceeded --- plugins/libplugin-pay.c | 78 +++++++++++++++++++++++++++++++++++++++++ plugins/libplugin-pay.h | 7 ++++ plugins/pay.c | 6 ++++ tests/test_pay.py | 38 ++++++++++++++++++-- 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index a1b2b00df..b61544b4b 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -216,6 +216,48 @@ tal_route_from_json(const tal_t *ctx, const char *buffer, const jsmntok_t *toks) return hops; } +static void payment_exclude_most_expensive(struct payment *p) +{ + struct payment *root = payment_root(p); + struct route_hop *e = &p->route[0]; + struct amount_msat fee, worst = AMOUNT_MSAT(0); + struct channel_hint hint; + + for (size_t i = 0; i < tal_count(p->route)-1; i++) { + if (!amount_msat_sub(&fee, p->route[i].amount, p->route[i+1].amount)) + plugin_err(p->plugin, "Negative fee in a route."); + + if (amount_msat_greater_eq(fee, worst)) { + e = &p->route[i]; + worst = fee; + } + } + hint.scid.scid = e->channel_id; + hint.scid.dir = e->direction; + hint.enabled = false; + tal_arr_expand(&root->channel_hints, hint); +} + +static void payment_exclude_longest_delay(struct payment *p) +{ + struct payment *root = payment_root(p); + struct route_hop *e = &p->route[0]; + u32 delay, worst = 0; + struct channel_hint hint; + + for (size_t i = 0; i < tal_count(p->route)-1; i++) { + delay = p->route[i].delay - p->route[i+1].delay; + if (delay >= worst) { + e = &p->route[i]; + worst = delay; + } + } + hint.scid.scid = e->channel_id; + hint.scid.dir = e->direction; + hint.enabled = false; + tal_arr_expand(&root->channel_hints, hint); +} + static struct command_result *payment_getroute_result(struct command *cmd, const char *buffer, const jsmntok_t *toks, @@ -246,6 +288,7 @@ static struct command_result *payment_getroute_result(struct command *cmd, "Fee exceeds our fee budget: %s > %s, discarding route", type_to_string(tmpctx, struct amount_msat, &fee), type_to_string(tmpctx, struct amount_msat, &p->fee_budget)); + payment_exclude_most_expensive(p); payment_fail(p); return command_still_pending(cmd); } @@ -254,6 +297,7 @@ static struct command_result *payment_getroute_result(struct command *cmd, plugin_log(p->plugin, LOG_INFORM, "CLTV delay exceeds our CLTV budget: %d > %d", p->route[0].delay, p->cltv_budget); + payment_exclude_longest_delay(p); payment_fail(p); return command_still_pending(cmd); } @@ -1493,3 +1537,37 @@ static struct routehints_data *routehint_data_init(struct payment *p) REGISTER_PAYMENT_MODIFIER(routehints, struct routehints_data *, routehint_data_init, routehint_step_cb); + +/* For tiny payments the fees incurred due to the fixed base_fee may dominate + * the overall cost of the payment. Since these payments are often used as a + * way to signal, rather than actually transfer the amount, we add an + * exemption that allows tiny payments to exceed the fee allowance. This is + * implemented by setting a larger allowance than we would normally do if the + * payment is below the threshold. */ + +static struct exemptfee_data *exemptfee_data_init(struct payment *p) +{ + struct exemptfee_data *d = tal(p, struct exemptfee_data); + d->amount = AMOUNT_MSAT(5000); + return d; +} + +static void exemptfee_cb(struct exemptfee_data *d, struct payment *p) +{ + if (p->step != PAYMENT_STEP_INITIALIZED) + return payment_continue(p); + + if (amount_msat_greater_eq(d->amount, p->amount)) { + p->fee_budget = d->amount; + plugin_log( + p->plugin, LOG_INFORM, + "Payment amount is below exemption threshold, " + "allowing a maximum fee of %s", + type_to_string(tmpctx, struct amount_msat, &p->fee_budget)); + } + return payment_continue(p); +} + +REGISTER_PAYMENT_MODIFIER(exemptfee, struct exemptfee_data *, + exemptfee_data_init, exemptfee_cb); + diff --git a/plugins/libplugin-pay.h b/plugins/libplugin-pay.h index fca8cd748..b8b0b44db 100644 --- a/plugins/libplugin-pay.h +++ b/plugins/libplugin-pay.h @@ -293,9 +293,16 @@ struct routehints_data { u32 final_cltv; }; +struct exemptfee_data { + /* Amounts below this amount will get their fee limit raised to + * exemptfee, i.e., we're willing to pay twice exemptfee to get this + * payment through. */ + struct amount_msat amount; +}; /* List of globally available payment modifiers. */ REGISTER_PAYMENT_MODIFIER_HEADER(retry, struct retry_mod_data); REGISTER_PAYMENT_MODIFIER_HEADER(routehints, struct routehints_data); +REGISTER_PAYMENT_MODIFIER_HEADER(exemptfee, struct exemptfee_data); /* For the root payment we can seed the channel_hints with the result from * `listpeers`, hence avoid channels that we know have insufficient capacity diff --git a/plugins/pay.c b/plugins/pay.c index aec557bd3..f63e99cff 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -1834,6 +1834,7 @@ static void init(struct plugin *p, } struct payment_modifier *paymod_mods[] = { + &exemptfee_pay_mod, &routehints_pay_mod, &local_channel_hints_pay_mod, &retry_pay_mod, @@ -1853,6 +1854,7 @@ static struct command_result *json_paymod(struct command *cmd, char *fail; u64 *maxfee_pct_millionths; u32 *maxdelay; + struct amount_msat *exemptfee; p = payment_new(NULL, cmd, NULL /* No parent */, paymod_mods); @@ -1860,6 +1862,7 @@ static struct command_result *json_paymod(struct command *cmd, * would add them to the `param()` call below, and have them be * initialized directly that way. */ if (!param(cmd, buf, params, p_req("bolt11", param_string, &b11str), + p_opt_def("exemptfee", param_msat, &exemptfee, AMOUNT_MSAT(5000)), p_opt_def("maxdelay", param_number, &maxdelay, maxdelay_default), p_opt_def("maxfeepercent", param_millionths, @@ -1913,6 +1916,9 @@ static struct command_result *json_paymod(struct command *cmd, cmd, JSONRPC2_INVALID_PARAMS, "Overflow when computing fee budget, fee rate too high."); } + + payment_mod_exemptfee_get_data(p)->amount = *exemptfee; + payment_start(p); list_add_tail(&payments, &p->list); diff --git a/tests/test_pay.py b/tests/test_pay.py index 10aff37b2..3ffbb936d 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -3077,7 +3077,7 @@ def test_pay_modifiers(node_factory): # Make sure that the dummy param is in the help (and therefore assigned to # the modifier data). hlp = l1.rpc.help("paymod")['help'][0] - assert(hlp['command'] == 'paymod bolt11 [maxdelay] [maxfeepercent]') + assert(hlp['command'] == 'paymod bolt11 [exemptfee] [maxdelay] [maxfeepercent]') inv = l2.rpc.invoice(123, 'lbl', 'desc')['bolt11'] r = l1.rpc.paymod(inv) @@ -3085,7 +3085,39 @@ def test_pay_modifiers(node_factory): assert(sha256(unhexlify(r['payment_preimage'])).hexdigest() == r['payment_hash']) -def test_pay_exemptfee(node_factory): +@unittest.skipIf(not DEVELOPER, "Requires use_shadow") +def test_pay_exemptfee(node_factory, compat): """Tiny payment, huge fee + + l1 -> l2 -> l3 + + Create a tiny invoice for 1 msat, it'll be dominated by the base_fee on + the l2->l3 channel. So it'll get rejected on the first attempt if we set + the exemptfee way too low. The default fee exemption threshold is + 5000msat, so 5001msat is not exempted by default and a 5001msat fee on + l2->l3 should trigger this. + """ - l1, l2, l3 = node_factory.line_graph(3) + l1, l2, l3 = node_factory.line_graph( + 3, + opts=[{}, {'fee-base': 5001, 'fee-per-satoshi': 0}, {}], + wait_for_announce=True + ) + + err = r'Route wanted fee of 5001msat' if compat('090') else r'Ran out of routes to try' + + with pytest.raises(RpcError, match=err): + l1.rpc.dev_pay(l3.rpc.invoice(1, "lbl1", "desc")['bolt11'], use_shadow=False) + + # If we tell our node that 5001msat is ok this should work + l1.rpc.dev_pay(l3.rpc.invoice(1, "lbl2", "desc")['bolt11'], use_shadow=False, exemptfee=5001) + + # Given the above network this is the smallest amount that passes without + # the fee-exemption (notice that we let it through on equality). + threshold = int(5001 / 0.05) + + # This should be just below the fee-exemption and is the first value that is allowed through + with pytest.raises(RpcError, match=err): + l1.rpc.dev_pay(l3.rpc.invoice(threshold - 1, "lbl3", "desc")['bolt11'], use_shadow=False) + + l1.rpc.pay(inv, exemptfee=5001)