From 82afa8d38c6ef564490eaa666eea823f868e1b60 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 19 Jun 2024 17:49:08 +0200 Subject: [PATCH] pay: Add a pre-flight check for the spendable balance Changelog-Added: pay: The pay plugin now checks whether we have enough spendable capacity before computing a route, returning a clear error message if we don't --- common/jsonrpc_errors.h | 3 + plugins/libplugin-pay.c | 107 +++++++++++++++++++++++++++--- plugins/test/run-route-calc.c | 9 +++ plugins/test/run-route-overlong.c | 9 +++ 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index f2b7d975f..9a19d0f2d 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -48,6 +48,9 @@ enum jsonrpc_errcode { PAY_INVOICE_REQUEST_INVALID = 212, PAY_INVOICE_PREAPPROVAL_DECLINED = 213, PAY_KEYSEND_PREAPPROVAL_DECLINED = 214, + PAY_INSUFFICIENT_FUNDS = 215, + PAY_UNREACHABLE = 216, + PAY_USER_ERROR = 217, /* `fundchannel` or `withdraw` errors */ FUND_MAX_EXCEEDED = 300, diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index aa4cc675f..8bb94db8c 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -1002,16 +1002,105 @@ static struct command_result *payment_getroute(struct payment *p) return command_still_pending(p->cmd); } -static struct command_result * -payment_listpeerchannels_success(struct command *cmd, - const char *buffer, - const jsmntok_t *toks, - struct payment *p) +/** + * Compute the total sum of balances. Limits the maximum size we can + * pay as a preflight test. Returns `false` on errors, otherwise + * `sum` contains the sum of all channel balances.*/ +static bool payment_listpeerchannels_balance_sum(struct payment *p, + const char *buf, + const jsmntok_t *toks, + struct amount_msat *sum) { - p->mods = gossmods_from_listpeerchannels(p, p->local_id, - buffer, toks, true, - gossmod_add_localchan, - NULL); + *sum = AMOUNT_MSAT(0); + const jsmntok_t *channels, *channel; + struct amount_msat spendable; + bool connected; + size_t i; + const char *err; + + channels = json_get_member(buf, toks, "channels"); + + json_for_each_arr(i, channel, channels) + { + err = json_scan(tmpctx, buf, channel, + "{spendable_msat?:%,peer_connected:%}", + JSON_SCAN(json_to_msat, &spendable), + JSON_SCAN(json_to_bool, &connected)); + if (err) { + paymod_log(p, LOG_UNUSUAL, + "Bad listpeerchannels.channels %zu: %s", i, + err); + return false; + } + + if (!amount_msat_add(sum, *sum, spendable)) { + paymod_log( + p, LOG_BROKEN, + "Integer sum overflow summing spendable amounts."); + return false; + } + } + return true; +} + +static struct command_result * +payment_listpeerchannels_success(struct command *cmd, const char *buffer, + const jsmntok_t *toks, struct payment *p) +{ + /* The maximum amount we may end up trying to send. This + * includes the value and the full fee budget. If the + * available funds are below this, we emit a warning. */ + struct amount_msat maxrequired, spendable; + + if (!amount_msat_add(&maxrequired, p->getroute->amount, + p->constraints.fee_budget)) { + paymod_log(p, LOG_BROKEN, + "amount_msat overflow computing the fee budget"); + return payment_getroute(p); + } + + p->mods = gossmods_from_listpeerchannels( + p, p->local_id, buffer, toks, true, gossmod_add_localchan, NULL); + if (!payment_listpeerchannels_balance_sum(p, buffer, toks, + &spendable)) { + paymod_log(p, LOG_UNUSUAL, + "Unable to get total spendable amount from " + "listpeerchannels. Skipping affordability check."); + + /* Keep your fingers crossed, we may still succeed. */ + return payment_getroute(p); + } + + /* Pre-flight check: can we even afford the full amount of the + * payment? And if yes, can we afford the full amount with the + * full fee budget? If the former fails, we fail immediately, + * for the latter we log a warning, so we can root-cause this + * a bit better if we then run into routing issues. */ + if (amount_msat_greater(p->getroute->amount, spendable)) { + paymod_log(p, LOG_UNUSUAL, + "Insufficient funds to perform the payment: " + "spendable=%s < payment=%s", + fmt_amount_msat(tmpctx, spendable), + fmt_amount_msat(tmpctx, p->getroute->amount)); + payment_abort(p, PAY_INSUFFICIENT_FUNDS, + "Insufficient funds to perform the payment: " + "spendable=%s < payment=%s", + fmt_amount_msat(tmpctx, spendable), + fmt_amount_msat(tmpctx, p->getroute->amount)); + return command_still_pending(p->cmd); + } else if (amount_msat_greater(maxrequired, spendable)) { + char *msg = tal_fmt( + tmpctx, + "We do not have sufficient funds to pay for the specified " + "fee budget: spendable=%s < payment=%s + budget=%s. This " + "may cause a failed payment, but we'll try anyway.", + fmt_amount_msat(tmpctx, spendable), + fmt_amount_msat(tmpctx, p->getroute->amount), + fmt_amount_msat(tmpctx, p->constraints.fee_budget)); + + plugin_notify_message(p->cmd, LOG_INFORM, "%s", msg); + } + return payment_getroute(p); } diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index ee4a1ab74..b85ea5858 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -165,6 +165,9 @@ const char *json_scan(const tal_t *ctx UNNEEDED, /* Generated stub for json_strdup */ char *json_strdup(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) { fprintf(stderr, "json_strdup called!\n"); abort(); } +/* Generated stub for json_to_bool */ +bool json_to_bool(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool *b UNNEEDED) +{ fprintf(stderr, "json_to_bool called!\n"); abort(); } /* Generated stub for json_to_createonion_response */ struct createonion_response *json_to_createonion_response(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, @@ -264,6 +267,12 @@ void plugin_notification_end(struct plugin *plugin UNNEEDED, struct json_stream *plugin_notification_start(struct plugin *plugins UNNEEDED, const char *method UNNEEDED) { fprintf(stderr, "plugin_notification_start called!\n"); abort(); } +/* Generated stub for plugin_notify_message */ +void plugin_notify_message(struct command *cmd UNNEEDED, + enum log_level level UNNEEDED, + const char *fmt UNNEEDED, ...) + +{ fprintf(stderr, "plugin_notify_message called!\n"); abort(); } /* Generated stub for random_select */ bool random_select(double weight UNNEEDED, double *tot_weight UNNEEDED) { fprintf(stderr, "random_select called!\n"); abort(); } diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 8f9ce9bde..d2210ac71 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -162,6 +162,9 @@ const char *json_scan(const tal_t *ctx UNNEEDED, /* Generated stub for json_strdup */ char *json_strdup(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) { fprintf(stderr, "json_strdup called!\n"); abort(); } +/* Generated stub for json_to_bool */ +bool json_to_bool(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool *b UNNEEDED) +{ fprintf(stderr, "json_to_bool called!\n"); abort(); } /* Generated stub for json_to_createonion_response */ struct createonion_response *json_to_createonion_response(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, @@ -261,6 +264,12 @@ void plugin_notification_end(struct plugin *plugin UNNEEDED, struct json_stream *plugin_notification_start(struct plugin *plugins UNNEEDED, const char *method UNNEEDED) { fprintf(stderr, "plugin_notification_start called!\n"); abort(); } +/* Generated stub for plugin_notify_message */ +void plugin_notify_message(struct command *cmd UNNEEDED, + enum log_level level UNNEEDED, + const char *fmt UNNEEDED, ...) + +{ fprintf(stderr, "plugin_notify_message called!\n"); abort(); } /* Generated stub for random_select */ bool random_select(double weight UNNEEDED, double *tot_weight UNNEEDED) { fprintf(stderr, "random_select called!\n"); abort(); }