From a782ea75b5ec5c4c1e882025aa27e5eef63014d1 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 17 Jul 2024 12:53:00 +0930 Subject: [PATCH] plugins: move fetchinvoice functionality into offers plugin. This means only a single gossmap, and they already share the fetchinvoice-noconnect option and autoconnect code. Signed-off-by: Rusty Russell Changelog-Changed: Plugins: the `fetchinvoice` plugin has been combined into the `offers` plugin. --- plugins/Makefile | 10 +-- plugins/fetchinvoice.c | 155 ++++++++--------------------------- plugins/fetchinvoice.h | 27 ++++++ plugins/offers.c | 52 ++++++++++-- plugins/offers.h | 17 ++++ plugins/offers_invreq_hook.c | 27 ------ plugins/offers_invreq_hook.h | 6 -- plugins/offers_offer.c | 1 + plugins/offers_offer.h | 3 - tests/test_pay.py | 5 +- 10 files changed, 130 insertions(+), 173 deletions(-) create mode 100644 plugins/fetchinvoice.h diff --git a/plugins/Makefile b/plugins/Makefile index e7530431e..716f61ae6 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -31,14 +31,10 @@ PLUGIN_PAY_LIB_SRC := plugins/libplugin-pay.c PLUGIN_PAY_LIB_HEADER := plugins/libplugin-pay.h PLUGIN_PAY_LIB_OBJS := $(PLUGIN_PAY_LIB_SRC:.c=.o) -PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c plugins/offers_inv_hook.c plugins/establish_onion_path.c +PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c plugins/offers_inv_hook.c plugins/establish_onion_path.c plugins/fetchinvoice.c PLUGIN_OFFERS_OBJS := $(PLUGIN_OFFERS_SRC:.c=.o) PLUGIN_OFFERS_HEADER := $(PLUGIN_OFFERS_SRC:.c=.h) -PLUGIN_FETCHINVOICE_SRC := plugins/fetchinvoice.c plugins/establish_onion_path.c -PLUGIN_FETCHINVOICE_OBJS := $(PLUGIN_FETCHINVOICE_SRC:.c=.o) -PLUGIN_FETCHINVOICE_HEADER := - PLUGIN_SQL_SRC := plugins/sql.c PLUGIN_SQL_HEADER := PLUGIN_SQL_OBJS := $(PLUGIN_SQL_SRC:.c=.o) @@ -72,7 +68,6 @@ PLUGIN_ALL_SRC := \ $(PLUGIN_chanbackup_SRC) \ $(PLUGIN_BCLI_SRC) \ $(PLUGIN_COMMANDO_SRC) \ - $(PLUGIN_FETCHINVOICE_SRC) \ $(PLUGIN_FUNDER_SRC) \ $(PLUGIN_TOPOLOGY_SRC) \ $(PLUGIN_KEYSEND_SRC) \ @@ -97,7 +92,6 @@ C_PLUGINS := \ plugins/chanbackup \ plugins/bcli \ plugins/commando \ - plugins/fetchinvoice \ plugins/funder \ plugins/topology \ plugins/keysend \ @@ -216,8 +210,6 @@ plugins/spenderp: bitcoin/block.o bitcoin/preimage.o bitcoin/psbt.o common/psbt_ plugins/offers: $(PLUGIN_OFFERS_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/addr.o common/bolt12.o common/bolt12_merkle.o common/bolt11_json.o common/iso4217.o $(WIRE_OBJS) $(WIRE_BOLT12_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o common/blindedpath.o common/invoice_path_id.o common/blinding.o common/hmac.o common/json_blinded_path.o common/gossmap.o common/fp16.o $(JSMN_OBJS) common/dijkstra.o common/route.o common/gossmods_listpeerchannels.o -plugins/fetchinvoice: $(PLUGIN_FETCHINVOICE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/bolt12.o common/bolt12_merkle.o common/iso4217.o $(WIRE_OBJS) $(WIRE_BOLT12_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o $(JSMN_OBJS) common/gossmap.o common/fp16.o common/dijkstra.o common/route.o common/blindedpath.o common/hmac.o common/blinding.o common/gossmods_listpeerchannels.o - plugins/funder: bitcoin/psbt.o common/psbt_open.o $(PLUGIN_FUNDER_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) plugins/recover: common/gossmap.o common/fp16.o $(PLUGIN_RECOVER_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index 16269d393..8dafab0f1 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -18,13 +18,12 @@ #include #include #include +#include #include +#include #include #include -static struct gossmap *global_gossmap; -static struct pubkey local_id; -static bool disable_connect = false; static LIST_HEAD(sent_list); struct sent { @@ -334,9 +333,9 @@ badinv: return command_hook_success(cmd); } -static struct command_result *recv_modern_onion_message(struct command *cmd, - const char *buf, - const jsmntok_t *params) +struct command_result *recv_modern_onion_message(struct command *cmd, + const char *buf, + const jsmntok_t *params) { const jsmntok_t *om, *secrettok; struct sent *sent; @@ -346,6 +345,9 @@ static struct command_result *recv_modern_onion_message(struct command *cmd, om = json_get_member(buf, params, "onion_message"); secrettok = json_get_member(buf, om, "pathsecret"); + if (!secrettok) + return NULL; + json_to_secret(buf, secrettok, &pathsecret); sent = find_sent_by_secret(&pathsecret); if (!sent) { @@ -353,7 +355,7 @@ static struct command_result *recv_modern_onion_message(struct command *cmd, "No match for modern onion %.*s", json_tok_full_len(om), json_tok_full(buf, om)); - return command_hook_success(cmd); + return NULL; } plugin_log(cmd->plugin, LOG_DBG, "Received modern onion message: %.*s", @@ -367,7 +369,7 @@ static struct command_result *recv_modern_onion_message(struct command *cmd, if (sent->invreq) return handle_invreq_response(cmd, sent, buf, om); - return command_hook_success(cmd); + return NULL; } static void destroy_sent(struct sent *sent) @@ -397,31 +399,6 @@ static struct command_result *sendonionmsg_done(struct command *cmd, return command_still_pending(cmd); } -static void init_gossmap(struct plugin *plugin) -{ - size_t num_cupdates_rejected; - global_gossmap - = notleak_with_children(gossmap_load(NULL, - GOSSIP_STORE_FILENAME, - &num_cupdates_rejected)); - if (!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); -} - -static struct gossmap *get_gossmap(struct plugin *plugin) -{ - if (!global_gossmap) - init_gossmap(plugin); - else - gossmap_refresh(global_gossmap, NULL); - return global_gossmap; -} - static struct command_result *param_offer(struct command *cmd, const char *name, const char *buffer, @@ -548,7 +525,7 @@ static struct blinded_path *blinded_path(const tal_t *ctx, sciddir_or_pubkey_from_scidd(&path->first_node_id, first_scidd); else sciddir_or_pubkey_from_pubkey(&path->first_node_id, &ids[0]); - assert(pubkey_eq(&ids[nhops-1], &local_id)); + assert(pubkey_eq(&ids[nhops-1], &id)); randombytes_buf(&first_blinding, sizeof(first_blinding)); if (!pubkey_from_privkey(&first_blinding, &path->blinding)) @@ -785,7 +762,7 @@ static struct command_result *invreq_done(struct command *cmd, } } - return establish_onion_path(cmd, get_gossmap(cmd->plugin), &local_id, + return establish_onion_path(cmd, get_gossmap(cmd->plugin), &id, sent->invreq->offer_node_id, disable_connect, fetchinvoice_path_done, @@ -833,9 +810,9 @@ static struct command_result *param_dev_reply_path(struct command *cmd, const ch } /* Fetches an invoice for this offer, and makes sure it corresponds. */ -static struct command_result *json_fetchinvoice(struct command *cmd, - const char *buffer, - const jsmntok_t *params) +struct command_result *json_fetchinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *params) { struct amount_msat *msat; const char *rec_label, *payer_note; @@ -860,6 +837,10 @@ static struct command_result *json_fetchinvoice(struct command *cmd, NULL)) return command_param_failed(); + if (!offers_enabled) + return command_fail(cmd, LIGHTNINGD, + "experimental-offers not enabled"); + sent->wait_timeout = *timeout; /* BOLT-offers #12: @@ -1032,9 +1013,9 @@ static struct command_result *json_fetchinvoice(struct command *cmd, * it's actually hit the db! But using waitinvoice is also suboptimal * because we don't have libplugin infra to cancel a pending req (and I * want to rewrite our wait* API anyway) */ -static struct command_result *invoice_payment(struct command *cmd, - const char *buf, - const jsmntok_t *params) +struct command_result *invoice_payment(struct command *cmd, + const char *buf, + const jsmntok_t *params) { struct sent *i; const jsmntok_t *ptok, *preimagetok, *msattok; @@ -1134,7 +1115,7 @@ static struct command_result *createinvoice_done(struct command *cmd, "FIXME: support blinded paths!"); } - return establish_onion_path(cmd, get_gossmap(cmd->plugin), &local_id, + return establish_onion_path(cmd, get_gossmap(cmd->plugin), &id, sent->invreq->invreq_payer_id, disable_connect, sendinvoice_path_done, @@ -1284,9 +1265,9 @@ static struct command_result *param_invreq(struct command *cmd, return NULL; } -static struct command_result *json_sendinvoice(struct command *cmd, - const char *buffer, - const jsmntok_t *params) +struct command_result *json_sendinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *params) { struct amount_msat *msat; u32 *timeout; @@ -1304,6 +1285,10 @@ static struct command_result *json_sendinvoice(struct command *cmd, NULL)) return command_param_failed(); + if (!offers_enabled) + return command_fail(cmd, LIGHTNINGD, + "experimental-offers not enabled"); + sent->dev_path_use_scidd = NULL; sent->dev_reply_path = NULL; @@ -1359,7 +1344,7 @@ static struct command_result *json_sendinvoice(struct command *cmd, */ /* FIXME: Use transitory id! */ sent->inv->invoice_node_id = tal(sent->inv, struct pubkey); - sent->inv->invoice_node_id->pubkey = local_id.pubkey; + sent->inv->invoice_node_id->pubkey = id.pubkey; /* BOLT-offers #12: * - if the expiry for accepting payment is not 7200 seconds @@ -1404,9 +1389,9 @@ static struct command_result *param_raw_invreq(struct command *cmd, return NULL; } -static struct command_result *json_dev_rawrequest(struct command *cmd, - const char *buffer, - const jsmntok_t *params) +struct command_result *json_dev_rawrequest(struct command *cmd, + const char *buffer, + const jsmntok_t *params) { struct sent *sent = tal(cmd, struct sent); u32 *timeout; @@ -1426,80 +1411,10 @@ static struct command_result *json_dev_rawrequest(struct command *cmd, sent->dev_path_use_scidd = NULL; sent->dev_reply_path = NULL; - return establish_onion_path(cmd, get_gossmap(cmd->plugin), &local_id, + return establish_onion_path(cmd, get_gossmap(cmd->plugin), &id, node_id, disable_connect, fetchinvoice_path_done, fetchinvoice_path_fail, sent); } - -static const struct plugin_command commands[] = { - { - "fetchinvoice", - "payment", - "Request remote node for an invoice for this {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.", - NULL, - json_fetchinvoice, - }, - { - "sendinvoice", - "payment", - "Request remote node for to pay this {invreq}, with {label}, optional {amount_msat}, and {timeout} (default 90 seconds).", - NULL, - json_sendinvoice, - }, - { - "dev-rawrequest", - "util", - "Send {invreq} to {nodeid}, wait {timeout} (60 seconds by default)", - NULL, - json_dev_rawrequest, - .dev_only = true, - }, -}; - -static const char *init(struct plugin *p, const char *buf UNUSED, - const jsmntok_t *config UNUSED) -{ - bool exp_offers; - - rpc_scan(p, "getinfo", - take(json_out_obj(NULL, NULL, NULL)), - "{id:%}", JSON_SCAN(json_to_pubkey, &local_id)); - - rpc_scan(p, "listconfigs", - take(json_out_obj(NULL, "config", "experimental-offers")), - "{configs:{experimental-offers:{set:%}}}", - JSON_SCAN(json_to_bool, &exp_offers)); - - if (!exp_offers) - return "offers not enabled in config"; - return NULL; -} - -static const struct plugin_hook hooks[] = { - { - "onion_message_recv_secret", - recv_modern_onion_message - }, - { - "invoice_payment", - invoice_payment, - }, -}; - -int main(int argc, char *argv[]) -{ - setup_locale(); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, - commands, ARRAY_SIZE(commands), - /* No notifications */ - NULL, 0, - hooks, ARRAY_SIZE(hooks), - NULL, 0, - plugin_option("fetchinvoice-noconnect", "flag", - "Don't try to connect directly to fetch an invoice.", - flag_option, flag_jsonfmt, &disable_connect), - NULL); -} diff --git a/plugins/fetchinvoice.h b/plugins/fetchinvoice.h new file mode 100644 index 000000000..1f5ef14a3 --- /dev/null +++ b/plugins/fetchinvoice.h @@ -0,0 +1,27 @@ +#ifndef LIGHTNING_PLUGINS_FETCHINVOICE_H +#define LIGHTNING_PLUGINS_FETCHINVOICE_H +#include "config.h" + +struct command_result; +struct command; + +struct command_result *json_fetchinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + +struct command_result *json_sendinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + +struct command_result *json_dev_rawrequest(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + +struct command_result *recv_modern_onion_message(struct command *cmd, + const char *buf, + const jsmntok_t *params); +struct command_result *invoice_payment(struct command *cmd, + const char *buf, + const jsmntok_t *params); + +#endif /* LIGHTNING_PLUGINS_FETCHINVOICE_H */ diff --git a/plugins/offers.c b/plugins/offers.c index 858e9aa89..84f6f01d6 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ static void init_gossmap(struct plugin *plugin) num_cupdates_rejected); } -static struct gossmap *get_gossmap(struct plugin *plugin) +struct gossmap *get_gossmap(struct plugin *plugin) { if (!global_gossmap) init_gossmap(plugin); @@ -162,10 +163,16 @@ static struct command_result *onion_message_modern_call(struct command *cmd, { const jsmntok_t *om, *replytok, *invreqtok, *invtok; struct blinded_path *reply_path = NULL; + struct command_result *res; if (!offers_enabled) return command_hook_success(cmd); + /* FIXME: unify parsing! */ + res = recv_modern_onion_message(cmd, buf, params); + if (res) + return res; + om = json_get_member(buf, params, "onion_message"); replytok = json_get_member(buf, om, "reply_blindedpath"); if (replytok) { @@ -203,6 +210,14 @@ static const struct plugin_hook hooks[] = { "onion_message_recv", onion_message_modern_call }, + { + "onion_message_recv_secret", + onion_message_modern_call + }, + { + "invoice_payment", + invoice_payment, + }, }; static struct command_result *block_added_notify(struct command *cmd, @@ -1235,11 +1250,9 @@ static const char *init(struct plugin *p, take(json_out_obj(NULL, NULL, NULL)), "{configs:" "{cltv-final:{value_int:%}," - "experimental-offers:{set:%}}," - "fetchinvoice-noconnect?:{set:%}}", + "experimental-offers:{set:%}}}", JSON_SCAN(json_to_u16, &cltv_final), - JSON_SCAN(json_to_bool, &offers_enabled), - JSON_SCAN(json_to_bool, &disable_connect)); + JSON_SCAN(json_to_bool, &offers_enabled)); rpc_scan(p, "makesecret", take(json_out_obj(NULL, "string", INVOICE_PATH_BASE_STRING)), @@ -1271,8 +1284,31 @@ static const struct plugin_command commands[] = { NULL, json_decode, }, + { + "fetchinvoice", + "payment", + "Request remote node for an invoice for this {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.", + NULL, + json_fetchinvoice, + }, + { + "sendinvoice", + "payment", + "Request remote node for to pay this {invreq}, with {label}, optional {amount_msat}, and {timeout} (default 90 seconds).", + NULL, + json_sendinvoice, + }, + { + "dev-rawrequest", + "util", + "Send {invreq} to {nodeid}, wait {timeout} (60 seconds by default)", + NULL, + json_dev_rawrequest, + .dev_only = true, + }, }; + int main(int argc, char *argv[]) { setup_locale(); @@ -1283,5 +1319,9 @@ int main(int argc, char *argv[]) commands, ARRAY_SIZE(commands), notifications, ARRAY_SIZE(notifications), hooks, ARRAY_SIZE(hooks), - NULL, 0, NULL); + NULL, 0, + plugin_option("fetchinvoice-noconnect", "flag", + "Don't try to connect directly to fetch/pay an invoice.", + flag_option, flag_jsonfmt, &disable_connect), + NULL); } diff --git a/plugins/offers.h b/plugins/offers.h index 4ee32c6c6..5eedead4f 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -4,6 +4,20 @@ struct command_result; struct command; +struct plugin; + +/* This is me. */ +extern struct pubkey id; +/* Are offers enabled? */ +extern bool offers_enabled; +/* --fetchinvoice-noconnect */ +extern bool disable_connect; +/* --cltv-final */ +extern u16 cltv_final; +/* Current header_count */ +extern u32 blockheight; +/* Basis for invoice secrets */ +extern struct secret invoicesecret_base; /* If they give us an scid, do a lookup */ bool convert_to_scidd(struct command *cmd, @@ -14,4 +28,7 @@ struct command_result *WARN_UNUSED_RESULT send_onion_reply(struct command *cmd, struct blinded_path *reply_path, struct tlv_onionmsg_tlv *payload); + +/* Get the (latest) gossmap */ +struct gossmap *get_gossmap(struct plugin *plugin); #endif /* LIGHTNING_PLUGINS_OFFERS_H */ diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 97dfb9889..7d7cbe47c 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -19,8 +19,6 @@ #include #include -static struct gossmap *global_gossmap; - /* We need to keep the reply path around so we can reply with invoice */ struct invreq { struct tlv_invoice_request *invreq; @@ -101,31 +99,6 @@ fail_internalerr(struct command *cmd, return ret; } -static void init_gossmap(struct plugin *plugin) -{ - size_t num_cupdates_rejected; - global_gossmap - = notleak_with_children(gossmap_load(NULL, - GOSSIP_STORE_FILENAME, - &num_cupdates_rejected)); - if (!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); -} - -static struct gossmap *get_gossmap(struct plugin *plugin) -{ - if (!global_gossmap) - init_gossmap(plugin); - else - gossmap_refresh(global_gossmap, NULL); - return global_gossmap; -} - #define invreq_must_have(cmd_, ir_, fld_) \ test_field(cmd_, ir_, ir_->invreq->fld_ != NULL, #fld_, "missing") #define invreq_must_not_have(cmd_, ir_, fld_) \ diff --git a/plugins/offers_invreq_hook.h b/plugins/offers_invreq_hook.h index 4dffdc90b..055c4655c 100644 --- a/plugins/offers_invreq_hook.h +++ b/plugins/offers_invreq_hook.h @@ -3,12 +3,6 @@ #include "config.h" #include -extern u16 cltv_final; -extern u32 blockheight; -extern struct secret invoicesecret_base; -extern struct pubkey id; -extern bool disable_connect; - /* We got an onionmessage with an invreq! */ struct command_result *handle_invoice_request(struct command *cmd, const u8 *invreqbin, diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index f5c8cb79b..5ca3f8f31 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include diff --git a/plugins/offers_offer.h b/plugins/offers_offer.h index b7b25013b..e97126cd8 100644 --- a/plugins/offers_offer.h +++ b/plugins/offers_offer.h @@ -3,9 +3,6 @@ #include "config.h" #include -extern struct pubkey id; -extern bool offers_enabled; - struct command_result *json_offer(struct command *cmd, const char *buffer, const jsmntok_t *params); diff --git a/tests/test_pay.py b/tests/test_pay.py index 4121ef9bf..21a20b39d 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -4246,6 +4246,7 @@ def test_mpp_overload_payee(node_factory, bitcoind): l1.rpc.pay(inv) +@unittest.skipIf(TEST_NETWORK != 'regtest', "Canned offer is network specific") def test_offer_needs_option(node_factory): """Make sure we don't make offers without offer command""" l1 = node_factory.get_node() @@ -4254,8 +4255,8 @@ def test_offer_needs_option(node_factory): with pytest.raises(RpcError, match='experimental-offers not enabled'): l1.rpc.call('invoicerequest', {'amount': '2msat', 'description': 'simple test'}) - with pytest.raises(RpcError, match='Unknown command'): - l1.rpc.call('fetchinvoice', {'offer': 'aaaa'}) + with pytest.raises(RpcError, match='experimental-offers not enabled'): + l1.rpc.call('fetchinvoice', {'offer': 'lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqyqs5pr5v4ehg93pqfnwgkvdr57yzh6h92zg3qctvrm7w38djg67kzcm4yeg8vc4cq63s'}) # Decode still works though assert l1.rpc.decode('lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqyqs5pr5v4ehg93pqfnwgkvdr57yzh6h92zg3qctvrm7w38djg67kzcm4yeg8vc4cq63s')['valid']