From 7c2407ef48be29207f45e97aaf8fd7976b09e9fa Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sun, 17 Nov 2024 16:11:06 +1030 Subject: [PATCH] xpay: new plugin which uses askrene, injectpaymentonion. Changelog-Added: Plugins: cln-xpay, with associated `xpay` command for payments. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 123 +++ doc/Makefile | 3 +- doc/index.rst | 1 + doc/schemas/lightning-xpay.json | 123 +++ plugins/Makefile | 2 + plugins/xpay/Makefile | 15 + plugins/xpay/xpay.c | 1533 +++++++++++++++++++++++++++++ tests/test_askrene.py | 4 +- tests/test_xpay.py | 62 ++ 9 files changed, 1864 insertions(+), 2 deletions(-) create mode 100644 doc/schemas/lightning-xpay.json create mode 100644 plugins/xpay/Makefile create mode 100644 plugins/xpay/xpay.c diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 7cab9563d..332fae3ee 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -36882,6 +36882,129 @@ } } ] + }, + "lightning-xpay.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "xpay", + "title": "Command for sending a payment for an invoice", + "description": [ + "The **xpay** RPC command attempts to find routes to the given destination, and send the funds it asks for.", + "", + "This plugin is simpler and more sophisticated than the older 'pay' plugin, but does not have all the same features." + ], + "request": { + "required": [ + "invstring" + ], + "properties": { + "invstring": { + "type": "string", + "description": [ + "bolt11 or bolt12 invoice" + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Only possible for a bolt11 invoice which does not have an amount (in which case, it's compulsory). *amount_msat* is in millisatoshi precision; it can be a whole number, or a whole number with suffix *msat* or *sat*, or a three decimal point number with suffix *sat*, or an 1 to 11 decimal point number suffixed by *btc*." + ] + }, + "maxfee": { + "type": "msat", + "description": [ + "*maxfee* creates an absolute limit on what fee we will pay." + ], + "default": "5000msat, or 1% (whatever is greater)" + }, + "layers": { + "type": "array", + "description": [ + "These are askrene layers to apply in addition to xpay's own: these can alter the topology or provide additional information on the lightning network. See askrene-create-layer." + ], + "items": { + "type": "string", + "description": [ + "name of an existing layer" + ] + } + }, + "retry_for": { + "type": "u32", + "description": [ + "Until *retry_for* seconds passes, the command will keep finding routes and retrying the payment." + ], + "default": "60 seconds" + }, + "partial_msat": { + "type": "msat", + "description": [ + "Explicitly state that you are only paying some part of the invoice. Presumably someone else is paying the rest (otherwise the payment will time out at the recipient)." + ] + } + } + }, + "response": { + "required": [ + "payment_preimage", + "failed_parts", + "successful_parts", + "amount_msat", + "amount_sent_msat" + ], + "properties": { + "payment_preimage": { + "type": "secret", + "description": [ + "The proof of payment: SHA256 of this **payment_hash**." + ] + }, + "failed_parts": { + "type": "u64", + "description": [ + "How many separate payment parts failed." + ] + }, + "successful_parts": { + "type": "u64", + "description": [ + "How many separate payment parts succeeded (or are anticipated to succeed). This will be at least one." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Amount the recipient received." + ] + }, + "amount_sent_msat": { + "type": "msat", + "description": [ + "Total amount we sent (including fees)." + ] + } + } + }, + "errors": [ + "The following error codes may occur:", + "", + "- -1: Catchall nonspecific error.", + "- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)", + "- 205: Couldn't find, or find a way to, the destination.", + "- 219: Invoice has already been paid.", + "- 209: Other payment error." + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "see_also": [ + "lightning-pay(7)", + "lightning-decodepay(7)" + ], + "resources": [ + "Main web site: " + ] } }, "notifications": { diff --git a/doc/Makefile b/doc/Makefile index b53fdedc7..9ccee55e5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -142,7 +142,8 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-waitinvoice.7 \ doc/lightning-wait.7 \ doc/lightning-waitsendpay.7 \ - doc/lightning-withdraw.7 + doc/lightning-withdraw.7 \ + doc/lightning-xpay.7 ifeq ($(HAVE_SQLITE3),1) GENERATE_MARKDOWN += doc/lightning-listsqlschemas.7 \ diff --git a/doc/index.rst b/doc/index.rst index f64b6d161..55faa5df9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -155,6 +155,7 @@ Core Lightning Documentation lightning-waitinvoice lightning-waitsendpay lightning-withdraw + lightning-xpay lightningd-config lightningd-rpc lightningd diff --git a/doc/schemas/lightning-xpay.json b/doc/schemas/lightning-xpay.json new file mode 100644 index 000000000..b9084b0a7 --- /dev/null +++ b/doc/schemas/lightning-xpay.json @@ -0,0 +1,123 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "xpay", + "title": "Command for sending a payment for an invoice", + "description": [ + "The **xpay** RPC command attempts to find routes to the given destination, and send the funds it asks for.", + "", + "This plugin is simpler and more sophisticated than the older 'pay' plugin, but does not have all the same features." + ], + "request": { + "required": [ + "invstring" + ], + "properties": { + "invstring": { + "type": "string", + "description": [ + "bolt11 or bolt12 invoice" + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Only possible for a bolt11 invoice which does not have an amount (in which case, it's compulsory). *amount_msat* is in millisatoshi precision; it can be a whole number, or a whole number with suffix *msat* or *sat*, or a three decimal point number with suffix *sat*, or an 1 to 11 decimal point number suffixed by *btc*." + ] + }, + "maxfee": { + "type": "msat", + "description": [ + "*maxfee* creates an absolute limit on what fee we will pay." + ], + "default": "5000msat, or 1% (whatever is greater)" + }, + "layers": { + "type": "array", + "description": [ + "These are askrene layers to apply in addition to xpay's own: these can alter the topology or provide additional information on the lightning network. See askrene-create-layer." + ], + "items": { + "type": "string", + "description": [ + "name of an existing layer" + ] + } + }, + "retry_for": { + "type": "u32", + "description": [ + "Until *retry_for* seconds passes, the command will keep finding routes and retrying the payment." + ], + "default": "60 seconds" + }, + "partial_msat": { + "type": "msat", + "description": [ + "Explicitly state that you are only paying some part of the invoice. Presumably someone else is paying the rest (otherwise the payment will time out at the recipient)." + ] + } + } + }, + "response": { + "required": [ + "payment_preimage", + "failed_parts", + "successful_parts", + "amount_msat", + "amount_sent_msat" + ], + "properties": { + "payment_preimage": { + "type": "secret", + "description": [ + "The proof of payment: SHA256 of this **payment_hash**." + ] + }, + "failed_parts": { + "type": "u64", + "description": [ + "How many separate payment parts failed." + ] + }, + "successful_parts": { + "type": "u64", + "description": [ + "How many separate payment parts succeeded (or are anticipated to succeed). This will be at least one." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Amount the recipient received." + ] + }, + "amount_sent_msat": { + "type": "msat", + "description": [ + "Total amount we sent (including fees)." + ] + } + } + }, + "errors": [ + "The following error codes may occur:", + "", + "- -1: Catchall nonspecific error.", + "- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)", + "- 205: Couldn't find, or find a way to, the destination.", + "- 219: Invoice has already been paid.", + "- 209: Other payment error." + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "see_also": [ + "lightning-pay(7)", + "lightning-decodepay(7)" + ], + "resources": [ + "Main web site: " + ] +} diff --git a/plugins/Makefile b/plugins/Makefile index e2b2c8e25..924c6ccb8 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -119,6 +119,7 @@ C_PLUGINS := \ plugins/recover \ plugins/txprepare \ plugins/cln-renepay \ + plugins/cln-xpay \ plugins/spenderp \ plugins/cln-askrene @@ -202,6 +203,7 @@ PLUGIN_COMMON_OBJS := \ include plugins/askrene/Makefile include plugins/bkpr/Makefile include plugins/renepay/Makefile +include plugins/xpay/Makefile # Make sure these depend on everything. ALL_C_SOURCES += $(PLUGIN_ALL_SRC) diff --git a/plugins/xpay/Makefile b/plugins/xpay/Makefile new file mode 100644 index 000000000..56f53f445 --- /dev/null +++ b/plugins/xpay/Makefile @@ -0,0 +1,15 @@ +PLUGIN_XPAY_SRC := \ + plugins/xpay/xpay.c + +PLUGIN_XPAY_HDRS := + +PLUGIN_XPAY_OBJS := $(PLUGIN_XPAY_SRC:.c=.o) + +# Make sure these depend on everything. +ALL_C_SOURCES += $(PLUGIN_XPAY_SRC) +ALL_C_HEADERS += $(PLUGIN_XPAY_HDRS) + +# Make all plugins depend on all plugin headers, for simplicity. +$(PLUGIN_XPAY_OBJS): $(PLUGIN_XPAY_HDRS) + +plugins/cln-xpay: $(PLUGIN_XPAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o common/sciddir_or_pubkey.o wire/bolt12_wiregen.o wire/onion_wiregen.o common/onionreply.o common/onion_encode.o common/sphinx.o common/hmac.o diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c new file mode 100644 index 000000000..463ac066b --- /dev/null +++ b/plugins/xpay/xpay.c @@ -0,0 +1,1533 @@ +/* FIXME: Timeout! */ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* For the whole plugin */ +struct xpay { + struct pubkey local_id; + /* Access via get_gossmap() */ + struct gossmap *global_gossmap; + /* Creates unique layer names */ + size_t counter; + /* Can-never-exist fake key for blinded paths */ + struct pubkey fakenode; + /* We need to know current block height */ + u32 blockheight; +}; + +static struct xpay *xpay_of(struct plugin *plugin) +{ + return plugin_get_data(plugin, struct xpay); +} + +/* This refreshes the gossmap. */ +static struct gossmap *get_gossmap(struct xpay *xpay) +{ + gossmap_refresh(xpay->global_gossmap, NULL); + return xpay->global_gossmap; +} + +/* The unifies bolt11 and bolt12 handling */ +struct payment { + struct plugin *plugin; + /* This is the command which is expecting the success/fail. When + * it's NULL, that means we're just cleaning up */ + struct command *cmd; + /* Unique id */ + u64 unique_id; + /* For logging, and for sendpays */ + const char *invstring; + /* Explicit layers they told us to include */ + const char **layers; + /* Where we're trying to pay */ + struct pubkey destination; + /* Hash we want the preimage for */ + struct sha256 payment_hash; + /* Amount we're trying to pay */ + struct amount_msat amount; + /* Maximum fee we're prepare to pay */ + struct amount_msat maxfee; + /* BOLT11 payment secret (NULL for BOLT12, it uses blinded paths) */ + const struct secret *payment_secret; + /* BOLT11 payment metadata (NULL for BOLT12, it uses blinded paths) */ + const u8 *payment_metadata; + /* Final CLTV value */ + u32 final_cltv; + /* Group id for this payment */ + uint64_t group_id; + /* Counter for partids (also, total attempts) */ + uint64_t total_num_attempts; + /* How many parts failed? */ + uint64_t num_failures; + + /* Name of our temporary additional layer */ + const char *private_layer; + + /* For bolt11 we have route hints */ + struct route_info **route_hints; + + /* For bolt12 we have blinded paths */ + struct blinded_path **paths; + struct blinded_payinfo **payinfos; + + /* Current attempts, waiting for injectpaymentonion. */ + struct list_head current_attempts; + + /* We keep these around, since they may still be cleaning up. */ + struct list_head past_attempts; + + /* Amount we just asked getroutes for (0 means no getroutes + * call outstanding). */ + struct amount_msat amount_being_routed; + + /* Useful information from prior attempts if any. */ + char *prior_results; + + /* Requests currently outstanding */ + struct out_req **requests; +}; + +/* One step in a path. */ +struct hop { + /* Node this hop leads to. */ + struct pubkey next_node; + /* Via this channel */ + struct short_channel_id_dir scidd; + /* This is amount the node needs (including fees) */ + struct amount_msat amount_in; + /* ... to send this amount */ + struct amount_msat amount_out; + /* This is the delay, including delay across node */ + u32 cltv_value_in; + /* This is the delay, out from node. */ + u32 cltv_value_out; +}; + +/* Each actual payment attempt */ +struct attempt { + /* Inside payment->attempts */ + struct list_node list; + u64 partid; + + struct payment *payment; + struct amount_msat delivers; + + /* Path we tried, so we can unreserve, and tell askrene the results */ + struct hop *hops; + + /* Secrets, so we can decrypt error onions */ + struct secret *shared_secrets; +}; + +/* Wrapper for pending commands (ignores return) */ +static void was_pending(const struct command_result *res) +{ + assert(res); +} + +/* Recursion, so declare now */ +static struct command_result *getroutes_for(struct command *cmd, + struct payment *payment, + struct amount_msat deliver); + +/* Pretty printing paths */ +static const char *fmt_path(const tal_t *ctx, + const struct attempt *attempt) +{ + char *s = tal_strdup(ctx, ""); + for (size_t i = 0; i < tal_count(attempt->hops); i++) { + tal_append_fmt(&s, "->%s", + fmt_pubkey(tmpctx, &attempt->hops[i].next_node)); + } + return s; +} + +static void payment_log(struct payment *payment, + enum log_level level, + const char *fmt, + ...) + PRINTF_FMT(3,4); + +/* Logging: both to the command itself and the log file */ +static void payment_log(struct payment *payment, + enum log_level level, + const char *fmt, + ...) +{ + va_list args; + const char *msg; + + va_start(args, fmt); + msg = tal_vfmt(tmpctx, fmt, args); + va_end(args); + + if (payment->cmd) + plugin_notify_message(payment->cmd, level, "%s", msg); + plugin_log(payment->plugin, level, "%"PRIu64": %s", + payment->unique_id, msg); +} + +static void attempt_log(struct attempt *attempt, + enum log_level level, + const char *fmt, + ...) + PRINTF_FMT(3,4); + +static void attempt_log(struct attempt *attempt, + enum log_level level, + const char *fmt, + ...) +{ + va_list args; + const char *msg, *path; + + va_start(args, fmt); + msg = tal_vfmt(tmpctx, fmt, args); + va_end(args); + path = fmt_path(tmpctx, attempt); + + payment_log(attempt->payment, level, "%s: %s", path, msg); +} + +#define attempt_unusual(attempt, fmt, ...) \ + attempt_log((attempt), LOG_UNUSUAL, (fmt), __VA_ARGS__) +#define attempt_info(attempt, fmt, ...) \ + attempt_log((attempt), LOG_INFORM, (fmt), __VA_ARGS__) +#define attempt_debug(attempt, fmt, ...) \ + attempt_log((attempt), LOG_DBG, (fmt), __VA_ARGS__) + +static struct command_result *ignore_result(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + void *arg) +{ + return command_still_pending(aux_cmd); +} + +static struct command_result *ignore_result_error(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct attempt *attempt) +{ + attempt_unusual(attempt, "%s failed: '%.*s'", + method, + json_tok_full_len(result), + json_tok_full(buf, result)); + return ignore_result(aux_cmd, method, buf, result, attempt); +} + +/* A request, but we don't care about result. Submit with send_payment_req */ +static struct out_req *payment_ignored_req(struct command *aux_cmd, + struct attempt *attempt, + const char *method) +{ + return jsonrpc_request_start(aux_cmd, method, + ignore_result, ignore_result_error, attempt); +} + +static struct command_result *cleanup_finished(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct payment *payment) +{ + /* payment is a child of aux_cmd, so freed now */ + return aux_command_done(aux_cmd); +} + +/* Last of all we destroy the private layer */ +static struct command_result *cleanup(struct command *aux_cmd, + struct payment *payment) +{ + struct out_req *req; + + req = jsonrpc_request_start(aux_cmd, + "askrene-remove-layer", + cleanup_finished, + cleanup_finished, + payment); + json_add_string(req->js, "layer", payment->private_layer); + return send_outreq(req); +} + +/* Last request finished after xpay command is done gets to clean up */ +static void destroy_payment_request(struct out_req *req, + struct payment *payment) +{ + for (size_t i = 0; i < tal_count(payment->requests); i++) { + if (payment->requests[i] == req) { + tal_arr_remove(&payment->requests, i); + if (tal_count(payment->requests) == 0 && payment->cmd == NULL) { + cleanup(req->cmd, payment); + } + return; + } + } + abort(); +} + +static struct command_result * +send_payment_req(struct command *aux_cmd, + struct payment *payment, struct out_req *req) +{ + tal_arr_expand(&payment->requests, req); + tal_add_destructor2(req, destroy_payment_request, payment); + return send_outreq(req); +} + +static void payment_failed(struct command *aux_cmd, + struct payment *payment, + enum jsonrpc_errcode code, + const char *fmt, + ...) + PRINTF_FMT(4,5); + +static void payment_failed(struct command *aux_cmd, + struct payment *payment, + enum jsonrpc_errcode code, + const char *fmt, + ...) +{ + va_list args; + const char *msg; + + va_start(args, fmt); + msg = tal_vfmt(tmpctx, fmt, args); + va_end(args); + + /* Only fail once */ + if (payment->cmd) { + was_pending(command_fail(payment->cmd, code, "%s", msg)); + payment->cmd = NULL; + } + + /* If no commands outstanding, we can now clean up */ + if (tal_count(payment->requests) == 0) + cleanup(aux_cmd, payment); +} + +/* The current attempt is the first to succeed: we assume all the ones + * in progress will succeed too */ +static struct amount_msat total_sent(const struct payment *payment, + const struct attempt *attempt) +{ + struct amount_msat total = attempt->hops[0].amount_in; + const struct attempt *i; + + list_for_each(&payment->current_attempts, i, list) { + if (!amount_msat_accumulate(&total, attempt->hops[0].amount_in)) + abort(); + } + return total; +} + +static void payment_succeeded(struct payment *payment, + const struct preimage *preimage, + const struct attempt *attempt) +{ + struct json_stream *js; + + /* Only succeed once */ + if (payment->cmd) { + js = jsonrpc_stream_success(payment->cmd); + json_add_preimage(js, "payment_preimage", preimage); + json_add_u64(js, "failed_parts", payment->num_failures); + json_add_u64(js, "successful_parts", + payment->total_num_attempts - payment->num_failures); + json_add_amount_msat(js, "amount_msat", payment->amount); + json_add_amount_msat(js, "amount_sent_msat", total_sent(payment, attempt)); + was_pending(command_finished(payment->cmd, js)); + payment->cmd = NULL; + } +} + +/* We usually add things we learned to the global layer, but not + * if it's a fake channel */ +static const char *layer_of(const struct payment *payment, + const struct short_channel_id_dir *scidd) +{ + struct gossmap *gossmap = get_gossmap(xpay_of(payment->plugin)); + + if (gossmap_find_chan(gossmap, &scidd->scid)) + return "xpay"; + return payment->private_layer; +} + +static void add_result_summary(struct attempt *attempt, + enum log_level level, + const char *fmt, ...) + PRINTF_FMT(3,4); + +static void add_result_summary(struct attempt *attempt, + enum log_level level, + const char *fmt, ...) +{ + va_list args; + const char *msg; + + va_start(args, fmt); + msg = tal_vfmt(tmpctx, fmt, args); + va_end(args); + + tal_append_fmt(&attempt->payment->prior_results, "%s. ", msg); + attempt_log(attempt, level, "%s", msg); +} + +static const char *describe_scidd(struct attempt *attempt, size_t index) +{ + struct short_channel_id_dir scidd = attempt->hops[index].scidd; + struct payment *payment = attempt->payment; + + assert(index < tal_count(attempt->hops)); + + /* Blinded paths? */ + if (scidd.scid.u64 < tal_count(payment->paths)) { + if (tal_count(payment->paths) == 1) + return tal_fmt(tmpctx, "the invoice's blinded path (%s)", + fmt_short_channel_id_dir(tmpctx, &scidd)); + return tal_fmt(tmpctx, "the invoice's blinded path %s (%"PRIu64" of %zu)", + fmt_short_channel_id_dir(tmpctx, &scidd), + scidd.scid.u64 + 1, + tal_count(payment->paths)); + } + + /* Routehint? Often they are a single hop. */ + if (tal_count(payment->route_hints) == 1 + && tal_count(payment->route_hints[0]) == 1) + return tal_fmt(tmpctx, "the invoice's route hint (%s)", + fmt_short_channel_id_dir(tmpctx, &scidd)); + + for (size_t i = 0; i < tal_count(payment->route_hints); i++) { + for (size_t j = 0; j < tal_count(payment->route_hints[i]); j++) { + if (short_channel_id_eq(scidd.scid, + payment->route_hints[i][j].short_channel_id)) { + return tal_fmt(tmpctx, "%s inside invoice's route hint%s", + fmt_short_channel_id_dir(tmpctx, &scidd), + tal_count(payment->route_hints) == 1 ? "" : "s"); + } + } + } + + /* Just use normal names otherwise (may be public, may be local) */ + return fmt_short_channel_id_dir(tmpctx, &scidd); +} + +static void update_knowledge_from_error(struct command *aux_cmd, + const char *buf, + const jsmntok_t *error, + struct attempt *attempt) +{ + const jsmntok_t *tok; + struct onionreply *reply; + struct out_req *req; + const u8 *replymsg; + int index; + enum onion_wire failcode; + bool from_final; + const char *failcode_name, *errmsg; + enum jsonrpc_errcode ecode; + + tok = json_get_member(buf, error, "code"); + if (!tok || !json_to_jsonrpc_errcode(buf, tok, &ecode)) + plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'", + json_tok_full_len(error), json_tok_full(buf, error)); + + if (ecode == PAY_INJECTPAYMENTONION_ALREADY_PAID) { + payment_failed(aux_cmd, attempt->payment, + PAY_INJECTPAYMENTONION_FAILED, + "Already paid this invoice successfully"); + return; + } + if (ecode != PAY_INJECTPAYMENTONION_FAILED) { + payment_failed(aux_cmd, attempt->payment, + PLUGIN_ERROR, + "Unexpected injectpaymentonion error %i: %.*s", + ecode, + json_tok_full_len(error), + json_tok_full(buf, error)); + return; + } + + tok = json_get_member(buf, error, "data"); + if (!tok) + plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'", + json_tok_full_len(error), json_tok_full(buf, error)); + tok = json_get_member(buf, tok, "onionreply"); + if (!tok) + plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'", + json_tok_full_len(error), json_tok_full(buf, error)); + reply = new_onionreply(tmpctx, take(json_tok_bin_from_hex(NULL, buf, tok))); + + replymsg = unwrap_onionreply(tmpctx, + attempt->shared_secrets, + tal_count(attempt->shared_secrets), + reply, + &index); + + /* Garbled? Blame random hop. */ + if (!replymsg) { + index = pseudorand(tal_count(attempt->hops)); + add_result_summary(attempt, LOG_UNUSUAL, + "We got a garbled error message, and chose to (randomly) to disable %s for this payment", + describe_scidd(attempt, index)); + goto disable_channel; + } + + /* We learned something about prior nodes */ + for (size_t i = 0; i < index; i++) { + req = payment_ignored_req(aux_cmd, attempt, "askrene-inform-channel"); + json_add_string(req->js, "layer", + layer_of(attempt->payment, &attempt->hops[i].scidd)); + json_add_short_channel_id_dir(req->js, + "short_channel_id_dir", + attempt->hops[i].scidd); + json_add_amount_msat(req->js, "amount_msat", + attempt->hops[i].amount_out); + json_add_string(req->js, "inform", "unconstrained"); + send_payment_req(aux_cmd, attempt->payment, req); + } + + from_final = (index == tal_count(attempt->hops)); + failcode = fromwire_peektype(replymsg); + failcode_name = onion_wire_name(failcode); + if (strstarts(failcode_name, "WIRE_")) + failcode_name = str_lowering(tmpctx, + failcode_name + + strlen("WIRE_")); + + /* For local errors, error message is informative. */ + if (index == 0) { + tok = json_get_member(buf, error, "message"); + errmsg = json_strdup(tmpctx, buf, tok); + } else + errmsg = failcode_name; + + attempt_debug(attempt, + "Error %s for path %s, from %s", + errmsg, + fmt_path(tmpctx, attempt), + from_final ? "destination" + : index == 0 ? "local node" + : fmt_pubkey(tmpctx, &attempt->hops[index-1].next_node)); + + /* Final node sent an error */ + if (from_final) { + switch (failcode) { + /* These two are deprecated */ + case WIRE_FINAL_INCORRECT_CLTV_EXPIRY: + case WIRE_FINAL_INCORRECT_HTLC_AMOUNT: + + /* These ones are weird any time (did we encode wrongly?) */ + case WIRE_INVALID_ONION_VERSION: + case WIRE_INVALID_ONION_HMAC: + case WIRE_INVALID_ONION_KEY: + case WIRE_INVALID_ONION_PAYLOAD: + + /* These should not be sent by final node */ + case WIRE_TEMPORARY_CHANNEL_FAILURE: + case WIRE_PERMANENT_CHANNEL_FAILURE: + case WIRE_REQUIRED_CHANNEL_FEATURE_MISSING: + case WIRE_UNKNOWN_NEXT_PEER: + case WIRE_AMOUNT_BELOW_MINIMUM: + case WIRE_FEE_INSUFFICIENT: + case WIRE_INCORRECT_CLTV_EXPIRY: + case WIRE_EXPIRY_TOO_FAR: + case WIRE_EXPIRY_TOO_SOON: + case WIRE_CHANNEL_DISABLED: + case WIRE_PERMANENT_NODE_FAILURE: + case WIRE_TEMPORARY_NODE_FAILURE: + case WIRE_REQUIRED_NODE_FEATURE_MISSING: + case WIRE_INVALID_ONION_BLINDING: + /* Blame hop *leading to* final node */ + index--; + goto strange_error; + + case WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS: + /* FIXME: Maybe this was actually a height + * disagreement, so check height */ + payment_failed(aux_cmd, attempt->payment, + PAY_DESTINATION_PERM_FAIL, + "Destination said it doesn't know invoice: %s", + errmsg); + return; + + case WIRE_MPP_TIMEOUT: + /* Not actually an error at all, nothing to do. */ + return; + } + } else { + /* Non-final node */ + switch (failcode) { + /* These ones are weird any time (did we encode wrongly?) */ + case WIRE_INVALID_ONION_VERSION: + case WIRE_INVALID_ONION_HMAC: + case WIRE_INVALID_ONION_KEY: + case WIRE_INVALID_ONION_PAYLOAD: + /* These should not be sent by non-final node */ + case WIRE_FINAL_INCORRECT_CLTV_EXPIRY: + case WIRE_FINAL_INCORRECT_HTLC_AMOUNT: + case WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS: + case WIRE_MPP_TIMEOUT: + goto strange_error; + + case WIRE_TEMPORARY_CHANNEL_FAILURE: + add_result_summary(attempt, LOG_DBG, + "We got %s for %s, assuming it can't carry %s", + errmsg, + describe_scidd(attempt, index), + fmt_amount_msat(tmpctx, attempt->hops[index].amount_out)); + goto channel_capacity; + + case WIRE_PERMANENT_CHANNEL_FAILURE: + case WIRE_REQUIRED_CHANNEL_FEATURE_MISSING: + case WIRE_UNKNOWN_NEXT_PEER: + case WIRE_AMOUNT_BELOW_MINIMUM: + case WIRE_FEE_INSUFFICIENT: + case WIRE_INCORRECT_CLTV_EXPIRY: + case WIRE_EXPIRY_TOO_FAR: + case WIRE_EXPIRY_TOO_SOON: + case WIRE_CHANNEL_DISABLED: + case WIRE_PERMANENT_NODE_FAILURE: + case WIRE_TEMPORARY_NODE_FAILURE: + case WIRE_REQUIRED_NODE_FEATURE_MISSING: + add_result_summary(attempt, LOG_DBG, + "We got a weird error (%s) for %s: disabling it for this payment", + errmsg, + describe_scidd(attempt, index)); + goto disable_channel; + + case WIRE_INVALID_ONION_BLINDING: + /* FIXME: This could be an MPP_TIMEOUT! */ + add_result_summary(attempt, LOG_DBG, + "We got an error from inside the blinded path %s:" + " we assume it means insufficient capacity", + fmt_short_channel_id_dir(tmpctx, + &attempt->hops[index].scidd)); + goto channel_capacity; + } + } + +strange_error: + /* We disable the erroneous channel for this */ + add_result_summary(attempt, LOG_UNUSUAL, + "Unexpected error (%s) from %s node: disabling %s for this payment", + errmsg, + from_final ? "final" : "intermediate", + describe_scidd(attempt, index)); + +disable_channel: + /* We only do this for the current payment */ + req = payment_ignored_req(aux_cmd, attempt, "askrene-update-channel"); + json_add_string(req->js, "layer", attempt->payment->private_layer); + json_add_short_channel_id_dir(req->js, + "short_channel_id_dir", + attempt->hops[index].scidd); + json_add_bool(req->js, "enabled", false); + send_payment_req(aux_cmd, attempt->payment, req); + return; + +channel_capacity: + req = payment_ignored_req(aux_cmd, attempt, "askrene-inform-channel"); + json_add_string(req->js, "layer", + layer_of(attempt->payment, &attempt->hops[index].scidd)); + json_add_short_channel_id_dir(req->js, + "short_channel_id_dir", + attempt->hops[index].scidd); + json_add_amount_msat(req->js, "amount_msat", attempt->hops[index].amount_out); + json_add_string(req->js, "inform", "constrained"); + send_payment_req(aux_cmd, attempt->payment, req); +} + +static struct command_result *unreserve_path(struct command *aux_cmd, + struct attempt *attempt) +{ + struct out_req *req; + + req = payment_ignored_req(aux_cmd, attempt, "askrene-unreserve"); + json_array_start(req->js, "path"); + for (size_t i = 0; i < tal_count(attempt->hops); i++) { + const struct hop *hop = &attempt->hops[i]; + json_object_start(req->js, NULL); + json_add_short_channel_id_dir(req->js, "short_channel_id_dir", hop->scidd); + json_add_amount_msat(req->js, "amount_msat", hop->amount_out); + json_object_end(req->js); + } + json_array_end(req->js); + return send_payment_req(aux_cmd, attempt->payment, req); +} + +static struct command_result *injectpaymentonion_failed(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *error, + struct attempt *attempt) +{ + struct payment *payment = attempt->payment; + struct amount_msat delivers = attempt->delivers; + + payment->num_failures++; + + /* Move from current_attempts to past_attempts */ + list_del_from(&payment->current_attempts, &attempt->list); + list_add(&payment->past_attempts, &attempt->list); + + /* We're no longer using this path: submit request to release it */ + unreserve_path(aux_cmd, attempt); + + /* Once reserve is removed, we can tell lightningd what we + * learned. Might fail payment! */ + update_knowledge_from_error(aux_cmd, buf, error, attempt); + + /* If xpay is done, return now */ + if (!payment->cmd) + return command_still_pending(aux_cmd); + + /* If we're not waiting for getroutes, kick one off */ + if (amount_msat_is_zero(payment->amount_being_routed)) + return getroutes_for(aux_cmd, payment, delivers); + + /* Wait for getroutes to finish */ + return command_still_pending(aux_cmd); +} + +static struct amount_msat total_being_sent(const struct payment *payment) +{ + struct attempt *attempt; + struct amount_msat sum = AMOUNT_MSAT(0); + + list_for_each(&payment->current_attempts, attempt, list) { + if (!amount_msat_accumulate(&sum, attempt->delivers)) + abort(); + } + return sum; +} + +static struct command_result *injectpaymentonion_succeeded(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct attempt *attempt) +{ + struct preimage preimage; + struct payment *payment = attempt->payment; + + if (!json_to_preimage(buf, + json_get_member(buf, result, "payment_preimage"), + &preimage)) + plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'", + json_tok_full_len(result), json_tok_full(buf, result)); + + /* Move from current_attempts to past_attempts */ + list_del_from(&payment->current_attempts, &attempt->list); + list_add(&payment->past_attempts, &attempt->list); + + attempt_info(attempt, "Success: preimage=%s", fmt_preimage(tmpctx, &preimage)); + payment_succeeded(payment, &preimage, attempt); + + /* And we're no longer using the path. */ + return unreserve_path(aux_cmd, attempt); +} + +static void append_blinded_payloads(struct sphinx_path *sp, + const struct attempt *attempt, + size_t path_num) +{ + const struct blinded_path *path = attempt->payment->paths[path_num]; + struct xpay *xpay = xpay_of(attempt->payment->plugin); + u32 final_cltv = attempt->payment->final_cltv + xpay->blockheight; + + for (size_t i = 0; i < tal_count(path->path); i++) { + bool first = (i == 0); + bool final = (i == tal_count(path->path) - 1); + const u8 *payload; + + /* BOLT #4: + * - For every node inside a blinded route: + * - MUST include the `encrypted_recipient_data` provided by the + * recipient + * - For the first node in the blinded route: + * - MUST include the `path_key` provided by the + * recipient in `current_path_key` + * - If it is the final node: + * - MUST include `amt_to_forward`, `outgoing_cltv_value` and `total_amount_msat`. + *... + * - MUST NOT include any other tlv field. + */ + payload = onion_blinded_hop(NULL, + final ? &attempt->delivers : NULL, + final ? &attempt->payment->amount : NULL, + final ? &final_cltv : NULL, + path->path[i]->encrypted_recipient_data, + first ? &path->first_path_key : NULL); + sphinx_add_hop_has_length(sp, + first ? &path->first_node_id.pubkey + : &path->path[i]->blinded_node_id, + take(payload)); + } +} + +static const u8 *create_onion(const tal_t *ctx, struct attempt *attempt) +{ + struct xpay *xpay = xpay_of(attempt->payment->plugin); + bool blinded_path = false; + struct onionpacket *packet; + struct sphinx_path *sp; + const u8 *payload, *ret; + const struct pubkey *node; + + sp = sphinx_path_new(ctx, attempt->payment->payment_hash.u.u8, + sizeof(attempt->payment->payment_hash.u.u8)); + + /* First hop is to the local node */ + node = &xpay->local_id; + + for (size_t i = 0; i < tal_count(attempt->hops); i++) { + const struct hop *hop = &attempt->hops[i]; + + if (pubkey_eq(&hop->next_node, &xpay->fakenode) + && hop->scidd.scid.u64 < tal_count(attempt->payment->paths)) { + blinded_path = true; + append_blinded_payloads(sp, attempt, hop->scidd.scid.u64); + /* This must be at the end, unless they put the fake nodeid + * in a layer, in which case it doesn't matter what we put + * in the rest of the onion. */ + break; + } + /* We tell it how much to send *out* */ + payload = onion_nonfinal_hop(NULL, &hop->scidd.scid, hop->amount_out, + hop->cltv_value_out + xpay->blockheight); + sphinx_add_hop_has_length(sp, node, take(payload)); + node = &hop->next_node; + } + + /* If we use a blinded path, final has to be special, so + * that's done in append_blinded_payloads. */ + if (!blinded_path) { + sphinx_add_hop_has_length(sp, node, + take(onion_final_hop(NULL, + attempt->delivers, + attempt->payment->final_cltv + xpay->blockheight, + attempt->payment->amount, + attempt->payment->payment_secret, + attempt->payment->payment_metadata))); + } + + /* Fails if would be too long */ + packet = create_onionpacket(attempt, sp, ROUTING_INFO_SIZE, + &attempt->shared_secrets); + if (!packet) + return NULL; + + ret = serialize_onionpacket(ctx, packet); + tal_free(packet); + return ret; +} + +static struct command_result *reserve_done(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct attempt *attempt) +{ + struct out_req *req; + const u8 *onion; + struct xpay *xpay = xpay_of(attempt->payment->plugin); + + attempt_debug(attempt, "%s", "Reserve done!"); + + onion = create_onion(tmpctx, attempt); + /* FIXME: Handle this better! */ + if (!onion) { + payment_failed(aux_cmd, attempt->payment, PAY_UNSPECIFIED_ERROR, + "Could not create payment onion: path too long!"); + return command_still_pending(aux_cmd); + } + + req = jsonrpc_request_start(aux_cmd, + "injectpaymentonion", + injectpaymentonion_succeeded, + injectpaymentonion_failed, + attempt); + json_add_hex_talarr(req->js, "onion", onion); + json_add_sha256(req->js, "payment_hash", &attempt->payment->payment_hash); + json_add_amount_msat(req->js, "amount_msat", attempt->hops[0].amount_in); + json_add_u32(req->js, "cltv_expiry", attempt->hops[0].cltv_value_in + xpay->blockheight); + json_add_u64(req->js, "partid", attempt->partid); + json_add_u64(req->js, "groupid", attempt->payment->group_id); + json_add_string(req->js, "invstring", attempt->payment->invstring); + return send_payment_req(aux_cmd, attempt->payment, req); +} + +static struct command_result *reserve_done_err(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct attempt *attempt) +{ + payment_failed(aux_cmd, attempt->payment, PAY_UNSPECIFIED_ERROR, + "Reservation failed: '%.*s'", + json_tok_full_len(result), + json_tok_full(buf, result)); + return command_still_pending(aux_cmd); +} + +static struct command_result *getroutes_done(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct payment *payment) +{ + const jsmntok_t *t, *routes; + size_t i; + struct amount_msat needs_routing, was_routing; + + payment_log(payment, LOG_DBG, "getroutes_done: %s", + payment->cmd ? "continuing" : "ignoring"); + + /* If we're finished, ignore. */ + if (!payment->cmd) + return command_still_pending(aux_cmd); + + /* Do we have more that needs routing? If so, re-ask */ + if (!amount_msat_sub(&needs_routing, + payment->amount, + total_being_sent(payment))) + abort(); + + was_routing = payment->amount_being_routed; + payment->amount_being_routed = AMOUNT_MSAT(0); + + if (!amount_msat_eq(needs_routing, was_routing)) { + payment_log(payment, LOG_DBG, + "getroutes_done: need more (was_routing %s, needs_routing %s)", + fmt_amount_msat(tmpctx, was_routing), + fmt_amount_msat(tmpctx, needs_routing)); + return getroutes_for(aux_cmd, payment, needs_routing); + } + + + routes = json_get_member(buf, result, "routes"); + payment_log(payment, LOG_DBG, "routes for %s = %.*s", + fmt_amount_msat(tmpctx, was_routing), + json_tok_full_len(result), json_tok_full(buf, result)); + json_for_each_arr(i, t, routes) { + size_t j; + const jsmntok_t *hoptok, *path; + struct out_req *req; + struct attempt *attempt = tal(payment, struct attempt); + json_to_msat(buf, json_get_member(buf, t, "amount_msat"), + &attempt->delivers); + path = json_get_member(buf, t, "path"); + attempt->hops = tal_arr(attempt, struct hop, path->size); + attempt->payment = payment; + attempt->partid = ++payment->total_num_attempts; + json_for_each_arr(j, hoptok, path) { + const char *err; + struct hop *hop = &attempt->hops[j]; + err = json_scan(tmpctx, buf, hoptok, + "{short_channel_id_dir:%" + ",amount_msat:%" + ",next_node_id:%" + ",delay:%}", + JSON_SCAN(json_to_short_channel_id_dir, + &hop->scidd), + JSON_SCAN(json_to_msat, &hop->amount_in), + JSON_SCAN(json_to_pubkey, &hop->next_node), + JSON_SCAN(json_to_u32, &hop->cltv_value_in)); + if (err) + plugin_err(aux_cmd->plugin, "Malformed routes: %s", + err); + if (j > 0) { + attempt->hops[j-1].amount_out = hop->amount_in; + attempt->hops[j-1].cltv_value_out = hop->cltv_value_in; + } + } + attempt->hops[j-1].amount_out = attempt->delivers; + attempt->hops[j-1].cltv_value_out = attempt->payment->final_cltv; + + list_add_tail(&payment->current_attempts, &attempt->list); + + /* Reserve this route */ + attempt_debug(attempt, "%s", "doing askrene-reserve"); + + req = jsonrpc_request_start(aux_cmd, + "askrene-reserve", + reserve_done, + reserve_done_err, + attempt); + json_array_start(req->js, "path"); + for (j = 0; j < tal_count(attempt->hops); j++) { + const struct hop *hop = &attempt->hops[j]; + json_object_start(req->js, NULL); + json_add_short_channel_id_dir(req->js, "short_channel_id_dir", + hop->scidd); + json_add_amount_msat(req->js, "amount_msat", hop->amount_out); + json_object_end(req->js); + } + json_array_end(req->js); + send_payment_req(aux_cmd, attempt->payment, req); + } + + payment_log(payment, LOG_DBG, "waiting..."); + return command_still_pending(aux_cmd); +} + +static struct command_result *getroutes_done_err(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *error, + struct payment *payment) +{ + int code; + const char *msg, *complaint; + + /* getroutes gives nice error messages: we may need to annotate though. */ + msg = json_strdup(tmpctx, buf, json_get_member(buf, error, "message")); + json_to_int(buf, json_get_member(buf, error, "code"), &code); + + /* Simple case: failed immediately. */ + if (payment->total_num_attempts == 0) { + payment_failed(aux_cmd, payment, code, "Failed: %s", msg); + return command_still_pending(aux_cmd); + } + + /* More elaborate explanation. */ + if (amount_msat_eq(payment->amount_being_routed, payment->amount)) + complaint = "Then routing failed"; + else + complaint = tal_fmt(tmpctx, "Then routing for remaining %s failed", + fmt_amount_msat(tmpctx, payment->amount_being_routed)); + payment_failed(aux_cmd, payment, PAY_UNSPECIFIED_ERROR, + "Failed after %"PRIu64" attempts. %s%s: %s", + payment->total_num_attempts, + payment->prior_results, + complaint, + msg); + return command_still_pending(aux_cmd); +} + +static struct command_result *getroutes_for(struct command *aux_cmd, + struct payment *payment, + struct amount_msat deliver) +{ + struct xpay *xpay = xpay_of(aux_cmd->plugin); + struct out_req *req; + + /* If we get injectpaymentonion responses, they can wait */ + payment->amount_being_routed = deliver; + + req = jsonrpc_request_start(aux_cmd, "getroutes", + getroutes_done, + getroutes_done_err, + payment); + + json_add_pubkey(req->js, "source", &xpay->local_id); + if (payment->paths) + json_add_pubkey(req->js, "destination", &xpay->fakenode); + else + json_add_pubkey(req->js, "destination", &payment->destination); + payment_log(payment, LOG_DBG, "getroutes from %s to %s", + fmt_pubkey(tmpctx, &xpay->local_id), + payment->paths + ? fmt_pubkey(tmpctx, &xpay->fakenode) + : fmt_pubkey(tmpctx, &payment->destination)); + json_add_amount_msat(req->js, "amount_msat", deliver); + json_array_start(req->js, "layers"); + /* Add local channels */ + json_add_string(req->js, NULL, "auto.localchans"); + /* We don't pay fees for ourselves */ + json_add_string(req->js, NULL, "auto.sourcefree"); + /* Add xpay global channel */ + json_add_string(req->js, NULL, "xpay"); + /* Add private layer */ + json_add_string(req->js, NULL, payment->private_layer); + /* Add user-specified layers */ + for (size_t i = 0; i < tal_count(payment->layers); i++) + json_add_string(req->js, NULL, payment->layers[i]); + json_array_end(req->js); + json_add_amount_msat(req->js, "maxfee_msat", payment->maxfee); + json_add_u32(req->js, "final_cltv", payment->final_cltv); + + return send_payment_req(aux_cmd, payment, req); +} + +/* First time, we ask getroutes for the entire payment */ +static struct command_result *start_getroutes(struct command *aux_cmd, + struct payment *payment) +{ + return getroutes_for(aux_cmd, payment, payment->amount); +} + +/* Helper to create a fake channel in temporary layer */ +static void add_fake_channel(struct command *aux_cmd, + struct request_batch *batch, + struct payment *payment, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat fee_base_msat, + u32 fee_proportional_millionths, + u16 cltv_expiry_delta) +{ + struct out_req *req; + struct short_channel_id_dir scidd; + + scidd.scid = scid; + scidd.dir = node_id_idx(src, dst); + payment_log(payment, LOG_DBG, + "Invoice gave route %s->%s (%s)", + fmt_node_id(tmpctx, src), + fmt_node_id(tmpctx, dst), + fmt_short_channel_id_dir(tmpctx, &scidd)); + req = add_to_batch(aux_cmd, batch, "askrene-create-channel"); + json_add_string(req->js, "layer", payment->private_layer); + json_add_node_id(req->js, "source", src); + json_add_node_id(req->js, "destination", dst); + json_add_short_channel_id(req->js, "short_channel_id", scid); + json_add_amount_msat(req->js, "capacity_msat", capacity); + send_payment_req(aux_cmd, payment, req); + + req = add_to_batch(aux_cmd, batch, "askrene-update-channel"); + json_add_string(req->js, "layer", payment->private_layer); + json_add_short_channel_id_dir(req->js, "short_channel_id_dir", scidd); + json_add_bool(req->js, "enabled", true); + json_add_amount_msat(req->js, "htlc_minimum_msat", htlc_min); + json_add_amount_msat(req->js, "htlc_maximum_msat", htlc_max); + json_add_amount_msat(req->js, "fee_base_msat", fee_base_msat); + json_add_u32(req->js, "fee_proportional_millionths", + fee_proportional_millionths); + json_add_u32(req->js, "cltv_expiry_delta", cltv_expiry_delta); + send_payment_req(aux_cmd, payment, req); +} + +static void add_routehint(struct request_batch *batch, + struct command *aux_cmd, + struct payment *payment, + const struct route_info *route) +{ + struct xpay *xpay = xpay_of(payment->plugin); + struct amount_msat big_cap; + struct node_id me; + + node_id_from_pubkey(&me, &xpay->local_id); + + /* We add these channels to our private layer. We start with assuming + * they have 100x the capacity we need (including fees!): we'll figure + * it out quickly if we're wrong, but this gives a success probability + * of 99%. */ + if (!amount_msat_add(&big_cap, payment->amount, payment->maxfee) + || !amount_msat_mul(&big_cap, big_cap, 100)) + big_cap = payment->amount; /* Going to fail route anyway! */ + + for (size_t i = 0; i < tal_count(route); i++) { + struct node_id next; + + if (i + 1 < tal_count(route)) { + next = route[i+1].pubkey; + } else { + node_id_from_pubkey(&next, &payment->destination); + } + + /* Don't add hints from ourselves, since we know all those, + * and the error from this would be confusing! */ + if (node_id_eq(&route[i].pubkey, &me)) + continue; + + add_fake_channel(aux_cmd, batch, payment, + &route[i].pubkey, &next, + route[i].short_channel_id, + big_cap, + /* We don't know htlc_min/max */ + AMOUNT_MSAT(0), big_cap, + amount_msat(route[i].fee_base_msat), + route[i].fee_proportional_millionths, + route[i].cltv_expiry_delta); + } +} + +/* If it fails, returns error, otherwise NULL */ +static char *add_blindedpath(const tal_t *ctx, + struct request_batch *batch, + struct command *aux_cmd, + struct payment *payment, + size_t blindedpath_num, + const struct blinded_path *path, + const struct blinded_payinfo *payinfo) +{ + struct xpay *xpay = xpay_of(payment->plugin); + struct amount_msat big_cap, per_route_reduction; + int badf; + struct short_channel_id scid; + struct node_id src, dst; + + /* BOLT-offers #12: + * - SHOULD prefer to use earlier `invoice_paths` over later ones if + * it has no other reason for preference. + */ + /* We do this by telling askrene that the first one is the largest + * capacity channel. */ + + /* We add these channels to our private layer. We start with assuming + * they have 100x the capacity we need (including fees!): we'll figure + * it out quickly if we're wrong, but this gives a success probability + * of 99%. */ + if (!amount_msat_add(&per_route_reduction, + payment->amount, payment->maxfee) + || !amount_msat_mul(&big_cap, + per_route_reduction, + 100 + (tal_count(payment->paths) - blindedpath_num))) { + /* Going to fail route anyway! */ + per_route_reduction = AMOUNT_MSAT(0); + big_cap = payment->amount; + } + + assert(path->first_node_id.is_pubkey); + + /* BOLT-offers #12: + * - For each `invoice_blindedpay`.`payinfo`: + * - MUST NOT use the corresponding `invoice_paths`.`path` + * if `payinfo`.`features` has any unknown even bits set. + * - MUST reject the invoice if this leaves no usable paths. + */ + badf = features_unsupported(plugin_feature_set(payment->plugin), + payinfo->features, + BOLT12_INVOICE_FEATURE); + if (badf != -1) + return tal_fmt(ctx, "unknown feature %i", badf); + + node_id_from_pubkey(&src, &path->first_node_id.pubkey); + node_id_from_pubkey(&dst, &xpay->fakenode); + /* We make the "scid" for the blinded path block 0, which is impossible */ + scid.u64 = blindedpath_num; + + add_fake_channel(aux_cmd, batch, payment, + &src, &dst, scid, big_cap, + payinfo->htlc_minimum_msat, + payinfo->htlc_maximum_msat, + amount_msat(payinfo->fee_base_msat), + payinfo->fee_proportional_millionths, + payinfo->cltv_expiry_delta); + return NULL; +} + +static struct command_result *log_payment_err(struct command *aux_cmd, + const char *methodname, + const char *buf, + const jsmntok_t *result, + struct payment *payment) +{ + payment_log(payment, LOG_UNUSUAL, + "%s failed: '%.*s'", + methodname, + json_tok_full_len(result), + json_tok_full(buf, result)); + return command_still_pending(aux_cmd); +} + +/* Create a layer with our payment-specific topology information */ +static struct command_result *populate_private_layer(struct command *cmd, + struct payment *payment) +{ + struct request_batch *batch; + bool all_failed; + char *errors = NULL; + struct out_req *req; + struct command *aux_cmd; + + /* Everything else is parented to a separate command, which + * can outlive the one we respond to. */ + aux_cmd = aux_command(cmd); + tal_steal(aux_cmd, payment); + batch = request_batch_new(aux_cmd, NULL, log_payment_err, start_getroutes, + payment); + req = add_to_batch(aux_cmd, batch, "askrene-create-layer"); + json_add_string(req->js, "layer", payment->private_layer); + send_payment_req(aux_cmd, payment, req); + + for (size_t i = 0; i < tal_count(payment->route_hints); i++) + add_routehint(batch, aux_cmd, payment, payment->route_hints[i]); + + all_failed = tal_count(payment->paths) ? true : false; + for (size_t i = 0; i < tal_count(payment->paths); i++) { + char *err = add_blindedpath(tmpctx, batch, + aux_cmd, payment, i, + payment->paths[i], + payment->payinfos[i]); + if (!err) { + all_failed = false; + continue; + } + if (!errors) + errors = err; + else + tal_append_fmt(&errors, ", %s", err); + } + + /* Nothing actually created yet, so this is the last point we don't use + * "payment_failed" */ + if (all_failed) + return command_fail(aux_cmd, PAY_ROUTE_NOT_FOUND, + "No usable blinded paths: %s", errors); + + return batch_done(aux_cmd, batch); +} + +static struct command_result *param_string_array(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + const char ***arr) +{ + size_t i; + const jsmntok_t *s; + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); + *arr = tal_arr(cmd, const char *, tok->size); + json_for_each_arr(i, s, tok) + (*arr)[i] = json_strdup(*arr, buffer, s); + return NULL; +} + +static struct command_result *json_xpay(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct xpay *xpay = xpay_of(cmd->plugin); + struct amount_msat *msat, *maxfee; + struct payment *payment = tal(cmd, struct payment); + char *err; + + if (!param(cmd, buffer, params, + p_req("invstring", param_invstring, &payment->invstring), + p_opt("amount_msat", param_msat, &msat), + p_opt("maxfee", param_msat, &maxfee), + p_opt("layers", param_string_array, &payment->layers), + NULL)) + return command_param_failed(); + + list_head_init(&payment->current_attempts); + list_head_init(&payment->past_attempts); + payment->plugin = cmd->plugin; + payment->cmd = cmd; + payment->amount_being_routed = AMOUNT_MSAT(0); + payment->unique_id = xpay->counter++; + payment->private_layer = tal_fmt(payment, + "xpay-%"PRIu64, payment->unique_id); + payment->group_id = pseudorand(INT64_MAX); + payment->total_num_attempts = payment->num_failures = 0; + payment->requests = tal_arr(payment, struct out_req *, 0); + payment->prior_results = tal_strdup(payment, ""); + + if (bolt12_has_prefix(payment->invstring)) { + struct gossmap *gossmap = get_gossmap(xpay); + struct tlv_invoice *b12inv + = invoice_decode(tmpctx, payment->invstring, + strlen(payment->invstring), + plugin_feature_set(cmd->plugin), + chainparams, &err); + if (!b12inv) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid bolt12 invoice: %s", err); + + payment->amount = amount_msat(*b12inv->invoice_amount); + if (msat) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot override amount for bolt12 invoices"); + payment->route_hints = NULL; + payment->payment_secret = NULL; + payment->payment_metadata = NULL; + payment->paths = tal_steal(payment, b12inv->invoice_paths); + payment->payinfos = tal_steal(payment, b12inv->invoice_blindedpay); + payment->payment_hash = *b12inv->invoice_payment_hash; + payment->destination = *b12inv->invoice_node_id; + /* Resolve introduction points if possible */ + for (size_t i = 0; i < tal_count(payment->paths); i++) { + if (!gossmap_scidd_pubkey(gossmap, &payment->paths[i]->first_node_id)) { + payment_log(payment, LOG_UNUSUAL, + "Could not resolve blinded path start %s: discarding", + fmt_sciddir_or_pubkey(tmpctx, + &payment->paths[i]->first_node_id)); + tal_arr_remove(&payment->paths, i); + tal_arr_remove(&payment->payinfos, i); + i--; + } + } + /* In case we remove them all! */ + if (tal_count(payment->paths) == 0) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not resolve any paths: unknown short_channel_id"); + } + + /* Use worst-case CLTV. */ + payment->final_cltv = 0; + for (size_t i = 0; i < tal_count(payment->payinfos); i++) { + if (payment->payinfos[i]->cltv_expiry_delta > payment->final_cltv) + payment->final_cltv = payment->payinfos[i]->cltv_expiry_delta; + } + } else { + struct bolt11 *b11 + = bolt11_decode(tmpctx, payment->invstring, + plugin_feature_set(cmd->plugin), + NULL, + chainparams, &err); + if (!b11) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid bolt11 invoice: %s", err); + payment->route_hints = tal_steal(payment, b11->routes); + payment->paths = NULL; + payment->payinfos = NULL; + if (!pubkey_from_node_id(&payment->destination, &b11->receiver_id)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid destination id %s", + fmt_node_id(tmpctx, &b11->receiver_id)); + + payment->final_cltv = b11->min_final_cltv_expiry; + payment->payment_hash = b11->payment_hash; + payment->payment_secret = tal_steal(payment, b11->payment_secret); + if (!b11->payment_secret) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "need payment_secret"); + payment->payment_metadata = tal_steal(payment, b11->metadata); + if (!b11->msat && !msat) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "amount_msat required"); + if (b11->msat && msat) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "amount_msat unnecessary"); + if (b11->msat) + payment->amount = *b11->msat; + else + payment->amount = *msat; + } + + /* Default is 5sats, or 1%, whatever is greater */ + if (!maxfee) { + if (!amount_msat_fee(&payment->maxfee, payment->amount, 0, 1000000 / 100)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid amount: fee overflows"); + payment->maxfee = amount_msat_max(payment->maxfee, + AMOUNT_MSAT(5000)); + } else + payment->maxfee = *maxfee; + + return populate_private_layer(cmd, payment); +} + +static const char *init(struct command *init_cmd, + const char *buf UNUSED, const jsmntok_t *config UNUSED) +{ + struct plugin *plugin = init_cmd->plugin; + struct xpay *xpay = xpay_of(plugin); + size_t num_cupdates_rejected; + struct json_out *js; + const char *response; + + rpc_scan(init_cmd, "getinfo", + take(json_out_obj(NULL, NULL, NULL)), + "{id:%}", JSON_SCAN(json_to_pubkey, &xpay->local_id)); + + xpay->global_gossmap = gossmap_load(xpay, + GOSSIP_STORE_FILENAME, + &num_cupdates_rejected); + if (!xpay->global_gossmap) + plugin_err(plugin, "Could not load gossmap %s: %s", + GOSSIP_STORE_FILENAME, strerror(errno)); + + if (num_cupdates_rejected) + plugin_log(plugin, LOG_DBG, + "gossmap ignored %zu channel updates", + num_cupdates_rejected); + + /* We use headercount from the backend, in case we're still syncing */ + js = json_out_new(NULL); + json_out_start(js, NULL, '{'); + json_out_add(js, "last_height", false, "0"); + json_out_end(js, '}'); + json_out_finished(js); + rpc_scan(init_cmd, "getchaininfo", take(js), + "{headercount:%}", JSON_SCAN(json_to_u32, &xpay->blockheight)); + + xpay->counter = 0; + if (!pubkey_from_hexstr("02" "0000000000000000000000000000000000000000000000000000000000000001", 66, &xpay->fakenode)) + abort(); + + /* Create xpay layer for us to use */ + jsonrpc_request_sync(tmpctx, init_cmd, "askrene-create-layer", + json_out_obj(tmpctx, "layer", "xpay"), + &response); + + return NULL; +} + +static const struct plugin_command commands[] = { + { + "xpay", + json_xpay, + }, +}; + +static struct command_result *handle_block_added(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct xpay *xpay = xpay_of(cmd->plugin); + u32 blockheight; + const char *err; + + err = json_scan(tmpctx, buf, params, + "{block_added:{height:%}}", + JSON_SCAN(json_to_u32, &blockheight)); + if (err) + plugin_err(cmd->plugin, "Bad block_added notification: %s", + err); + + /* If we were using header height, we might not have passed it yet */ + if (blockheight > xpay->blockheight) + xpay->blockheight = blockheight; + + return notification_handled(cmd); +} + +static const struct plugin_notification notifications[] = { + { + "block_added", + handle_block_added, + }, +}; + +int main(int argc, char *argv[]) +{ + setup_locale(); + + plugin_main(argv, init, take(tal(NULL, struct xpay)), + PLUGIN_RESTARTABLE, true, NULL, + commands, ARRAY_SIZE(commands), + notifications, ARRAY_SIZE(notifications), + NULL, 0, NULL, 0, NULL, 0, NULL); +} diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 8fbb64a10..8f174c057 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -121,7 +121,9 @@ def test_reserve(node_factory): def test_layers(node_factory): """Test manipulating information in layers""" - l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + # remove xpay, since it creates a layer! + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True, + opts={'disable-plugin': 'cln-xpay'}) assert l2.rpc.askrene_listlayers() == {'layers': []} with pytest.raises(RpcError, match="Unknown layer"): diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 7d34562d9..70e18af2f 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -132,3 +132,65 @@ def test_pay_fakenet(node_factory): l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=2) l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=3) + + +def test_xpay_simple(node_factory): + l1, l2, l3, l4 = node_factory.get_nodes(4, opts={'experimental-offers': None, + 'may_reconnect': True}) + node_factory.join_nodes([l1, l2, l3], wait_for_announce=True) + node_factory.join_nodes([l3, l4], announce_channels=False) + + # BOLT 11, direct peer + b11 = l2.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11'] + ret = l1.rpc.xpay(b11) + assert ret['failed_parts'] == 0 + assert ret['successful_parts'] == 1 + assert ret['amount_msat'] == 10000 + assert ret['amount_sent_msat'] == 10000 + + # Fails if we try to pay again + b11_paid = b11 + with pytest.raises(RpcError, match="Already paid"): + l1.rpc.xpay(b11_paid) + + # BOLT 11, indirect peer + b11 = l3.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11'] + ret = l1.rpc.xpay(b11) + assert ret['failed_parts'] == 0 + assert ret['successful_parts'] == 1 + assert ret['amount_msat'] == 10000 + assert ret['amount_sent_msat'] == 10001 + + # BOLT 11, routehint + b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11'] + l1.rpc.xpay(b11) + + # BOLT 12. + offer = l3.rpc.offer('any')['bolt12'] + b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice'] + l1.rpc.xpay(b12) + + # Failure from l4. + b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple2', 'test_xpay_simple2 bolt11')['bolt11'] + l4.rpc.delinvoice('test_xpay_simple2', 'unpaid') + with pytest.raises(RpcError, match="Destination said it doesn't know invoice"): + l1.rpc.xpay(b11) + + offer = l4.rpc.offer('any')['bolt12'] + b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice'] + + # Failure from l3 (with routehint) + l4.stop() + with pytest.raises(RpcError, match=r"Failed after 1 attempts\. We got temporary_channel_failure for the invoice's route hint \([0-9x]*/[01]\), assuming it can't carry 10000msat\. Then routing failed: We could not find a usable set of paths\. The shortest path is [0-9x]*->[0-9x]*->[0-9x]*, but [0-9x]*/[01]\ layer xpay-6 says max is 9999msat"): + l1.rpc.xpay(b11) + + # Failure from l3 (with blinded path) + # FIXME: We return wrong error here! + with pytest.raises(RpcError, match=r"Failed after 1 attempts\. Unexpected error \(invalid_onion_payload\) from intermediate node: disabling the invoice's blinded path \(0x0x0/[01]\) for this payment\. Then routing failed: We could not find a usable set of paths\. The destination has disabled 1 of 1 channels, leaving capacity only 0msat of 10605000msat\."): + l1.rpc.xpay(b12) + + # Restart, try pay already paid one again. + l1.restart() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + with pytest.raises(RpcError, match="Already paid"): + l1.rpc.xpay(b11_paid)