core-lightning/plugins/renepay/main.c
Rusty Russell 46b0eb108b plugins: don't check for experimental-offers option: it's the default now.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2024-11-18 10:42:54 +01:00

426 lines
12 KiB
C

#include "config.h"
#include <ccan/array_size/array_size.h>
#include <ccan/cast/cast.h>
#include <ccan/htable/htable_type.h>
#include <ccan/tal/str/str.h>
#include <common/bolt11.h>
#include <common/bolt12_merkle.h>
#include <common/gossmap.h>
#include <common/gossmods_listpeerchannels.h>
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/memleak.h>
#include <common/pseudorand.h>
#include <common/utils.h>
#include <errno.h>
#include <plugins/renepay/json.h>
#include <plugins/renepay/mods.h>
#include <plugins/renepay/payplugin.h>
#include <plugins/renepay/routetracker.h>
#include <stdio.h>
// TODO(eduardo): notice that pending attempts performed with another
// pay plugin are not considered by the uncertainty network in renepay,
// it would be nice if listsendpay would give us the route of pending
// sendpays.
struct pay_plugin *pay_plugin;
static void memleak_mark(struct plugin *p, struct htable *memtable)
{
memleak_scan_obj(memtable, pay_plugin);
memleak_scan_htable(memtable,
&pay_plugin->uncertainty->chan_extra_map->raw);
memleak_scan_htable(memtable, &pay_plugin->payment_map->raw);
memleak_scan_htable(memtable, &pay_plugin->pending_routes->raw);
}
static const char *init(struct command *init_cmd,
const char *buf UNUSED, const jsmntok_t *config UNUSED)
{
struct plugin *p = init_cmd->plugin;
size_t num_channel_updates_rejected = 0;
tal_steal(p, pay_plugin);
pay_plugin->plugin = p;
pay_plugin->last_time = 0;
rpc_scan(init_cmd, "getinfo", take(json_out_obj(NULL, NULL, NULL)),
"{id:%}", JSON_SCAN(json_to_node_id, &pay_plugin->my_id));
/* BOLT #4:
* ## `max_htlc_cltv` Selection
*
* This ... value is defined as 2016 blocks, based on historical value
* deployed by Lightning implementations.
*/
/* FIXME: Typo in spec for CLTV in descripton! But it breaks our spelling check, so we omit it above */
pay_plugin->maxdelay_default = 2016;
/* max-locktime-blocks deprecated in v24.05, but still grab it! */
rpc_scan(init_cmd, "listconfigs",
take(json_out_obj(NULL, NULL, NULL)),
"{configs:"
"{max-locktime-blocks?:{value_int:%}}}",
JSON_SCAN(json_to_number, &pay_plugin->maxdelay_default)
);
pay_plugin->payment_map = tal(pay_plugin, struct payment_map);
payment_map_init(pay_plugin->payment_map);
pay_plugin->pending_routes = tal(pay_plugin, struct route_map);
route_map_init(pay_plugin->pending_routes);
pay_plugin->gossmap = gossmap_load(pay_plugin,
GOSSIP_STORE_FILENAME,
&num_channel_updates_rejected);
if (!pay_plugin->gossmap)
plugin_err(p, "Could not load gossmap %s: %s",
GOSSIP_STORE_FILENAME, strerror(errno));
if (num_channel_updates_rejected)
plugin_log(p, LOG_DBG,
"gossmap ignored %zu channel updates",
num_channel_updates_rejected);
pay_plugin->uncertainty = uncertainty_new(pay_plugin);
int skipped_count =
uncertainty_update(pay_plugin->uncertainty, pay_plugin->gossmap);
if (skipped_count)
plugin_log(pay_plugin->plugin, LOG_UNUSUAL,
"%s: uncertainty was updated but %d channels have "
"been ignored.",
__func__, skipped_count);
plugin_set_memleak_handler(p, memleak_mark);
return NULL;
}
static struct command_result *json_paystatus(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
const char *invstring;
struct json_stream *ret;
if (!param(cmd, buf, params,
p_opt("invstring", param_invstring, &invstring),
NULL))
return command_param_failed();
ret = jsonrpc_stream_success(cmd);
json_array_start(ret, "paystatus");
if(invstring)
{
/* select the payment that matches this invoice */
if (bolt12_has_prefix(invstring))
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"BOLT12 invoices are not yet supported.");
char *fail;
struct bolt11 *b11 =
bolt11_decode(tmpctx, invstring, plugin_feature_set(cmd->plugin),
NULL, chainparams, &fail);
if (b11 == NULL)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Invalid bolt11: %s", fail);
struct payment *payment =
payment_map_get(pay_plugin->payment_map, b11->payment_hash);
if(payment)
{
json_object_start(ret, NULL);
json_add_payment(ret, payment);
json_object_end(ret);
}
}else
{
/* show all payments */
struct payment_map_iter it;
for (struct payment *p =
payment_map_first(pay_plugin->payment_map, &it);
p; p = payment_map_next(pay_plugin->payment_map, &it)) {
json_object_start(ret, NULL);
json_add_payment(ret, p);
json_object_end(ret);
}
}
json_array_end(ret);
return command_finished(cmd, ret);
}
static struct command_result * payment_start(struct payment *p)
{
assert(p);
p->status = PAYMENT_PENDING;
plugin_log(pay_plugin->plugin, LOG_DBG, "Starting renepay");
p->exec_state = 0;
return payment_continue(p);
}
static struct command_result *json_pay(struct command *cmd, const char *buf,
const jsmntok_t *params)
{
/* === Parse command line arguments === */
// TODO check if we leak some of these temporary variables
const char *invstr;
struct amount_msat *msat;
struct amount_msat *maxfee;
u32 *maxdelay;
u32 *retryfor;
const char *description;
const char *label;
struct route_exclusion **exclusions;
// dev options
bool *use_shadow;
// MCF options
u64 *base_fee_penalty_millionths; // base fee to proportional fee
u64 *prob_cost_factor_millionths; // prob. cost to proportional fee
u64 *riskfactor_millionths; // delay to proportional proportional fee
u64 *min_prob_success_millionths; // target probability
/* base probability of success, probability for a randomly picked
* channel to be able to forward a payment request of amount greater
* than zero. */
u64 *base_prob_success_millionths;
if (!param(cmd, buf, params,
p_req("invstring", param_invstring, &invstr),
p_opt("amount_msat", param_msat, &msat),
p_opt("maxfee", param_msat, &maxfee),
p_opt_def("maxdelay", param_number, &maxdelay,
/* maxdelay has a configuration default value named
* "max-locktime-blocks", this is retrieved at
* init. */
pay_plugin->maxdelay_default),
p_opt_def("retry_for", param_number, &retryfor,
60), // 60 seconds
p_opt("description", param_string, &description),
p_opt("label", param_string, &label),
p_opt("exclude", param_route_exclusion_array, &exclusions),
// FIXME add support for offers
// p_opt("localofferid", param_sha256, &local_offer_id),
p_opt_dev("dev_use_shadow", param_bool, &use_shadow, true),
// MCF options
p_opt_dev("dev_base_fee_penalty", param_millionths,
&base_fee_penalty_millionths,
10000000), // default is 10.0
p_opt_dev("dev_prob_cost_factor", param_millionths,
&prob_cost_factor_millionths,
10000000), // default is 10.0
p_opt_dev("dev_riskfactor", param_millionths,
&riskfactor_millionths, 1), // default is 1e-6
p_opt_dev("dev_min_prob_success", param_millionths,
&min_prob_success_millionths,
800000), // default is 0.8
p_opt_dev("dev_base_prob_success", param_millionths,
&base_prob_success_millionths,
980000), // default is 0.98
NULL))
return command_param_failed();
if (*base_prob_success_millionths == 0 ||
*base_prob_success_millionths > 1000000)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Base probability should be a number "
"greater than zero and no greater than 1.");
/* === Parse invoice === */
// FIXME: add support for bolt12 invoices
if (bolt12_has_prefix(invstr))
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"BOLT12 invoices are not yet supported.");
char *fail;
struct bolt11 *b11 =
bolt11_decode(tmpctx, invstr, plugin_feature_set(cmd->plugin),
description, chainparams, &fail);
if (b11 == NULL)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Invalid bolt11: %s", fail);
/* Sanity check */
if (feature_offered(b11->features, OPT_VAR_ONION) &&
!b11->payment_secret)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Invalid bolt11:"
" sets feature var_onion with no secret");
if (b11->msat) {
// amount is written in the invoice
if (msat)
return command_fail(
cmd, JSONRPC2_INVALID_PARAMS,
"amount_msat parameter unnecessary");
msat = b11->msat;
} else {
// amount is not written in the invoice
if (!msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"amount_msat parameter required");
}
// Default max fee is 5 sats, or 0.5%, whichever is *higher*
if (!maxfee) {
struct amount_msat fee = amount_msat_div(*msat, 200);
if (amount_msat_less(fee, AMOUNT_MSAT(5000)))
fee = AMOUNT_MSAT(5000);
maxfee = tal_dup(tmpctx, struct amount_msat, &fee);
}
const u64 now_sec = time_now().ts.tv_sec;
if (now_sec > (b11->timestamp + b11->expiry))
return command_fail(cmd, PAY_INVOICE_EXPIRED,
"Invoice expired");
/* === Get payment === */
// one payment_hash one payment is not assumed, it is enforced
struct payment *payment =
payment_map_get(pay_plugin->payment_map, b11->payment_hash);
if(!payment)
{
payment = payment_new(
tmpctx,
&b11->payment_hash,
take(invstr),
take(label),
take(description),
b11->payment_secret,
b11->metadata,
cast_const2(const struct route_info**, b11->routes),
&b11->receiver_id,
*msat,
*maxfee,
*maxdelay,
*retryfor,
b11->min_final_cltv_expiry,
*base_fee_penalty_millionths,
*prob_cost_factor_millionths,
*riskfactor_millionths,
*min_prob_success_millionths,
*base_prob_success_millionths,
use_shadow,
cast_const2(const struct route_exclusion**, exclusions));
if (!payment)
return command_fail(cmd, PLUGIN_ERROR,
"failed to create a new payment");
if (!payment_register_command(payment, cmd))
return command_fail(cmd, PLUGIN_ERROR,
"failed to register command");
// good to go
payment = tal_steal(pay_plugin, payment);
payment_map_add(pay_plugin->payment_map, payment);
return payment_start(payment);
}
/* === Start or continue payment === */
if (payment->status == PAYMENT_SUCCESS) {
assert(payment_commands_empty(payment));
// this payment is already a success, we show the result
struct json_stream *result = jsonrpc_stream_success(cmd);
json_add_payment(result, payment);
return command_finished(cmd, result);
}
if (payment->status == PAYMENT_FAIL) {
// FIXME: should we refuse to pay if the invoices are different?
// or should we consider this a new payment?
if (!payment_update(payment,
*maxfee,
*maxdelay,
*retryfor,
b11->min_final_cltv_expiry,
*base_fee_penalty_millionths,
*prob_cost_factor_millionths,
*riskfactor_millionths,
*min_prob_success_millionths,
*base_prob_success_millionths,
use_shadow,
cast_const2(const struct route_exclusion**, exclusions)))
return command_fail(
cmd, PLUGIN_ERROR,
"failed to update the payment parameters");
// this payment already failed, we try again
assert(payment_commands_empty(payment));
if (!payment_register_command(payment, cmd))
return command_fail(cmd, PLUGIN_ERROR,
"failed to register command");
return payment_start(payment);
}
// else: this payment is pending we continue its execution, we merge all
// calling cmds into a single payment request
assert(payment->status == PAYMENT_PENDING);
if (!payment_register_command(payment, cmd))
return command_fail(cmd, PLUGIN_ERROR,
"failed to register command");
return command_still_pending(cmd);
}
static const struct plugin_command commands[] = {
{
"renepaystatus",
json_paystatus
},
{
"renepay",
json_pay
},
};
static const struct plugin_notification notifications[] = {
{
"sendpay_success",
notification_sendpay_success,
},
{
"sendpay_failure",
notification_sendpay_failure,
}
};
int main(int argc, char *argv[])
{
setup_locale();
/* Most gets initialized in init(), but set debug options here. */
pay_plugin = tal(NULL, struct pay_plugin);
pay_plugin->debug_mcf = pay_plugin->debug_payflow = false;
plugin_main(
argv,
init, NULL,
PLUGIN_RESTARTABLE,
/* init_rpc */ true,
/* features */ NULL,
commands, ARRAY_SIZE(commands),
notifications, ARRAY_SIZE(notifications),
/* hooks */ NULL, 0,
/* notification topics */ NULL, 0,
plugin_option("renepay-debug-mcf", "flag",
"Enable renepay MCF debug info.",
flag_option, NULL, &pay_plugin->debug_mcf),
plugin_option("renepay-debug-payflow", "flag",
"Enable renepay payment flows debug info.",
flag_option, NULL, &pay_plugin->debug_payflow),
NULL);
return 0;
}