mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-12 10:30:29 +01:00
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
This commit is contained in:
parent
c2a698069e
commit
82afa8d38c
4 changed files with 119 additions and 9 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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(); }
|
||||
|
|
Loading…
Add table
Reference in a new issue