From a17eb06cd29322a40132eb7d1497531a3f220dd8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 22 Aug 2025 11:09:00 +0930 Subject: [PATCH 1/9] lightningd: don't assert if plugin crashes early. If a plugin exits early, we will not exit with ret == plugins: return from this function and we will exit because ld->exit_code is set. Signed-off-by: Rusty Russell --- lightningd/plugin.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lightningd/plugin.c b/lightningd/plugin.c index e1588f246fce..3282df369936 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -2124,12 +2124,8 @@ void plugins_init(struct plugins *plugins) setenv("LIGHTNINGD_PLUGIN", "1", 1); setenv("LIGHTNINGD_VERSION", version(), 1); - if (plugins_send_getmanifest(plugins, NULL)) { - void *ret; - ret = io_loop_with_timers(plugins->ld); - log_debug(plugins->ld->log, "io_loop_with_timers: %s", __func__); - assert(ret == plugins); - } + if (plugins_send_getmanifest(plugins, NULL)) + io_loop_with_timers(plugins->ld); } static void plugin_config_cb(const char *buffer, From 88d73d4b434da6ab620ad6e33f4d6e08eaf52df4 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:02:47 +0930 Subject: [PATCH 2/9] libplugin: support multi options. Signed-off-by: Rusty Russell Changelog-Added: libplugin: support for options which accumulate if specified more than once ("multi": true). --- plugins/libplugin.c | 20 +++++++++++++++++--- plugins/libplugin.h | 18 +++++++++++------- plugins/xpay/Makefile | 2 +- tests/plugins/test_libplugin.c | 34 ++++++++++++++++++++++++++++++++++ tests/test_plugin.py | 4 ++++ 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index 614b5d3db177..a53e111902be 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -62,6 +62,8 @@ struct plugin_option { const char *depr_start, *depr_end; /* If true, allow setting after plugin has initialized */ bool dynamic; + /* If true, allow multiple settings. */ + bool multi; }; struct plugin { @@ -1262,6 +1264,7 @@ handle_getmanifest(struct command *getmanifest_cmd, json_add_string(params, "description", p->opts[i].description); json_add_deprecated(params, "deprecated", p->opts[i].depr_start, p->opts[i].depr_end); json_add_bool(params, "dynamic", p->opts[i].dynamic); + json_add_bool(params, "multi", p->opts[i].multi); if (p->opts[i].jsonfmt) p->opts[i].jsonfmt(p, params, "default", p->opts[i].arg); json_object_end(params); @@ -1598,9 +1601,19 @@ static struct command_result *handle_init(struct command *cmd, if (!popt) plugin_err(p, "lightningd specified unknown option '%s'?", name); - problem = popt->handle(p, json_strdup(tmpctx, buf, t+1), false, popt->arg); - if (problem) - plugin_err(p, "option '%s': %s", popt->name, problem); + if (popt->multi) { + size_t j; + const jsmntok_t *opt; + json_for_each_arr(j, opt, t+1) { + problem = popt->handle(p, json_strdup(tmpctx, buf, opt), false, popt->arg); + if (problem) + plugin_err(p, "option '%s': %s", popt->name, problem); + } + } else { + problem = popt->handle(p, json_strdup(tmpctx, buf, t+1), false, popt->arg); + if (problem) + plugin_err(p, "option '%s': %s", popt->name, problem); + } } if (p->init) { @@ -2449,6 +2462,7 @@ static struct plugin *new_plugin(const tal_t *ctx, o.depr_start = va_arg(ap, const char *); o.depr_end = va_arg(ap, const char *); o.dynamic = va_arg(ap, int); /* bool gets promoted! */ + o.multi = va_arg(ap, int); /* bool gets promoted! */ tal_arr_expand(&p->opts, o); } diff --git a/plugins/libplugin.h b/plugins/libplugin.h index 2cc781da81ca..2a359d3a9ebc 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -577,7 +577,7 @@ void *plugin_get_data_(struct plugin *plugin); #define plugin_get_data(plugin, type) ((type *)(plugin_get_data_(plugin))) /* Macro to define arguments */ -#define plugin_option_(name, type, description, set, jsonfmt, arg, dev_only, depr_start, depr_end, dynamic) \ +#define plugin_option_(name, type, description, set, jsonfmt, arg, dev_only, depr_start, depr_end, dynamic, multi) \ (name), \ (type), \ (description), \ @@ -594,23 +594,27 @@ void *plugin_get_data_(struct plugin *plugin); (dev_only), \ (depr_start), \ (depr_end), \ - (dynamic) + (dynamic), \ + (multi) /* jsonfmt can be NULL, but then default won't be printed */ #define plugin_option(name, type, description, set, jsonfmt, arg) \ - plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false) + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false, false) #define plugin_option_dev(name, type, description, set, jsonfmt, arg) \ - plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, false) + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, false, false) #define plugin_option_dev_dynamic(name, type, description, set, jsonfmt, arg) \ - plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, true) + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, true, false) #define plugin_option_dynamic(name, type, description, set, jsonfmt, arg) \ - plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, true) + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, true, false) #define plugin_option_deprecated(name, type, description, depr_start, depr_end, set, jsonfmt, arg) \ - plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, (depr_start), (depr_end), false) + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, (depr_start), (depr_end), false, false) + +#define plugin_option_multi(name, type, description, set, jsonfmt, arg) \ + plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false, true) /* Standard helpers */ char *u64_option(struct plugin *plugin, const char *arg, bool check_only, u64 *i); diff --git a/plugins/xpay/Makefile b/plugins/xpay/Makefile index 56f53f4451aa..a05aa076d543 100644 --- a/plugins/xpay/Makefile +++ b/plugins/xpay/Makefile @@ -10,6 +10,6 @@ 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) +$(PLUGIN_XPAY_OBJS): $(PLUGIN_XPAY_HDRS) $(PLUGIN_LIB_HEADER) 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/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c index d6f269610229..63ca527f1bdd 100644 --- a/tests/plugins/test_libplugin.c +++ b/tests/plugins/test_libplugin.c @@ -12,6 +12,7 @@ struct test_libplugin { bool self_disable; bool dont_shutdown; u32 dynamic_opt; + const char **strarr; }; static struct test_libplugin *get_test_libplugin(struct plugin *plugin) @@ -221,6 +222,13 @@ static const char *init(struct command *init_cmd, plugin_log(p, LOG_DBG, "somearg = %s", tlp->somearg); tlp->somearg = tal_free(tlp->somearg); + for (size_t i = 0; i < tal_count(tlp->strarr); i++) { + plugin_log(p, LOG_DBG, "multiopt#%zu = %s", + i, tlp->strarr[i]); + } + if (tlp->somearg) + plugin_log(p, LOG_DBG, "somearg = %s", tlp->somearg); + if (tlp->self_disable) return "Disabled via selfdisable option"; @@ -287,6 +295,25 @@ static const struct plugin_notification notifs[] = { { } }; +static char *set_multi_string_option(struct plugin *plugin, + const char *arg, + bool check_only, + const char ***arr) +{ + if (!check_only) + tal_arr_expand(arr, tal_strdup(*arr, arg)); + return NULL; +} + +static bool multi_string_jsonfmt(struct plugin *plugin, struct json_stream *js, const char *fieldname, const char ***arr) +{ + json_array_start(js, fieldname); + for (size_t i = 0; i < tal_count(*arr); i++) + json_add_string(js, NULL, (*arr)[i]); + json_array_end(js); + return true; +} + int main(int argc, char *argv[]) { setup_locale(); @@ -298,6 +325,7 @@ int main(int argc, char *argv[]) tlp->self_disable = false; tlp->dont_shutdown = false; tlp->dynamic_opt = 7; + tlp->strarr = tal_arr(tlp, const char *, 0); plugin_main(argv, init, take(tlp), PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), @@ -328,5 +356,11 @@ int main(int argc, char *argv[]) "Set me!", set_dynamic, u32_jsonfmt, &tlp->dynamic_opt), + plugin_option_multi("multiopt", + "string", + "Set me multiple times!", + set_multi_string_option, + multi_string_jsonfmt, + &tlp->strarr), NULL); } diff --git a/tests/test_plugin.py b/tests/test_plugin.py index d1c6f2938e36..b4086a6a7141 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1682,6 +1682,7 @@ def test_libplugin(node_factory): assert l1.daemon.is_in_stderr(r"somearg-deprecated=test_opt: deprecated option") del l1.daemon.opts["somearg-deprecated"] + l1.daemon.opts["multiopt"] = ['hello', 'world'] l1.start() # Test that check works as expected. @@ -1695,6 +1696,9 @@ def test_libplugin(node_factory): # This works assert l1.rpc.check('checkthis', key=["test_libplugin", "name"]) == {'command_to_check': 'checkthis'} + assert l1.daemon.is_in_log('plugin-test_libplugin: multiopt#0 = hello') + assert l1.daemon.is_in_log('plugin-test_libplugin: multiopt#1 = world') + def test_libplugin_deprecated(node_factory): """Sanity checks for plugins made with libplugin using deprecated args""" From 3caad8e435df051f3448e3f4f3168332adba7abd Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:33 +0930 Subject: [PATCH 3/9] lightningd: add payment-fronting-node option. Signed-off-by: Rusty Russell Changelog-Added: Config: `payment-fronting-node` option to specify neighbor node(s) to use for all bolt11 invoices, bolt12 offers, invoices and invoice_requests. --- doc/lightningd-config.5.md | 4 ++++ lightningd/lightningd.c | 1 + lightningd/lightningd.h | 3 +++ lightningd/options.c | 15 +++++++++++++++ 4 files changed, 23 insertions(+) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index b57e9cb19a1e..1f6579ac3c71 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -530,6 +530,10 @@ delete the others. ### Payment and invoice control options: +* **payment-fronting-node**=*nodeid* + + Always use this *nodeid* as the entry point when we generate invoices or offers. For BOLT11 invoices, this node must be a neighbor: we will use a routehint with the alias for the short channel id to provide limited privacy (we still reveal our node id). For BOLT12 invoices and offers , we provide a blinded path from the node to us, to provide better privacy. + * **disable-mpp** [plugin `pay`] Disable the multi-part payment sending support in the `pay` plugin. By default diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index d76d5b605470..fba70abfa156 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -378,6 +378,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) /* The gossip seeker automatically connects to a this many peers */ ld->autoconnect_seeker_peers = 10; + ld->fronting_nodes = tal_arr(ld, struct node_id, 0); return ld; } diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index 44a76e18d082..95c689949308 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -426,6 +426,9 @@ struct lightningd { /* Minimum number of peers seeker should maintain. */ u32 autoconnect_seeker_peers; + + /* Nodes to use for invoices / offers */ + struct node_id *fronting_nodes; }; /* Turning this on allows a tal allocation to return NULL, rather than aborting. diff --git a/lightningd/options.c b/lightningd/options.c index b55238c565bd..d33e864c7b0d 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -1278,6 +1278,16 @@ static char *opt_add_api_beg(const char *arg, struct lightningd *ld) return NULL; } +static char *opt_add_node_id(const char *arg, struct node_id **arr) +{ + struct node_id n; + if (!node_id_from_hexstr(arg, strlen(arg), &n)) + return "Unparsable nodeid"; + + tal_arr_expand(arr, n); + return NULL; +} + char *hsm_secret_arg(const tal_t *ctx, const char *arg, const u8 **hsm_secret) @@ -1598,6 +1608,10 @@ static void register_opts(struct lightningd *ld) ld, "Re-enable a long-deprecated API (which will be removed entirely next version!)"); opt_register_logging(ld); + clnopt_witharg("--payment-fronting-node", + OPT_MULTI, + opt_add_node_id, NULL, + &ld->fronting_nodes, "Put this node in all invoices and offers, and use blinded path (bolt12) or route hints (bolt11) to route to this node. Must be a neighboring node. Can be specified multiple times."); dev_register_opts(ld); } @@ -1854,6 +1868,7 @@ bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *)) || cb_arg == (void *)opt_subd_dev_disconnect || cb_arg == (void *)opt_set_crash_timeout || cb_arg == (void *)opt_add_api_beg + || cb_arg == (void *)opt_add_node_id || cb_arg == (void *)opt_force_featureset || cb_arg == (void *)opt_force_privkey || cb_arg == (void *)opt_force_bip32_seed From 7f1e5553d15fea2f63afbb0e5a449659efbf418e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:51 +0930 Subject: [PATCH 4/9] lightningd: honor `payment-fronting-node` when making bolt11 invoices. We use all the fronting nodes when creating invoices. Signed-off-by: Rusty Russell --- lightningd/invoice.c | 25 ++++++++++++++++++++++++- lightningd/routehint.c | 33 +++++++++++++++++++++++++++++---- tests/test_invoices.py | 25 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 31362ab6c7ef..25df99b9c4dc 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -662,6 +662,25 @@ static struct route_info **select_inchan_mpp(const tal_t *ctx, return routehints; } +static struct route_info **select_inchan_all(const tal_t *ctx, + struct lightningd *ld, + struct routehint_candidate + *candidates) +{ + struct route_info **routehints; + + log_debug(ld->log, "Selecting all %zu candidates", + tal_count(candidates)); + + routehints = tal_arr(ctx, struct route_info *, tal_count(candidates)); + for (size_t i = 0; i < tal_count(candidates); i++) { + routehints[i] = tal_dup(routehints, struct route_info, + candidates[i].r); + } + + return routehints; +} + /* Encapsulating struct while we wait for gossipd to give us incoming channels */ struct chanhints { bool expose_all_private; @@ -723,6 +742,10 @@ add_routehints(struct invoice_info *info, needed = info->b11->msat ? *info->b11->msat : AMOUNT_MSAT(1); + /* --payment-fronting-node means use all candidates. */ + if (tal_count(info->cmd->ld->fronting_nodes)) + info->b11->routes = select_inchan_all(info->b11, info->cmd->ld, candidates); + /* If we are not completely unpublished, try with reservoir * sampling first. * @@ -738,7 +761,7 @@ add_routehints(struct invoice_info *info, * should make an effort to avoid overlapping incoming * channels, which is done by select_inchan_mpp. */ - if (!node_unpublished) + else if (!node_unpublished) info->b11->routes = select_inchan(info->b11, info->cmd->ld, needed, diff --git a/lightningd/routehint.c b/lightningd/routehint.c index 5c0d80a432d4..b967fabc1f3d 100644 --- a/lightningd/routehint.c +++ b/lightningd/routehint.c @@ -17,6 +17,16 @@ static bool scid_in_arr(const struct short_channel_id *scidarr, return false; } +static bool is_fronting_node(const struct lightningd *ld, + const struct node_id *node) +{ + for (size_t i = 0; i < tal_count(ld->fronting_nodes); i++) { + if (node_id_eq(&ld->fronting_nodes[i], node)) + return true; + } + return false; +} + struct routehint_candidate * routehint_candidates(const tal_t *ctx, struct lightningd *ld, @@ -62,7 +72,7 @@ routehint_candidates(const tal_t *ctx, struct routehint_candidate candidate; struct amount_msat fee_base, htlc_max; struct route_info *r; - bool is_public; + bool is_public, is_fronting; r = tal(tmpctx, struct route_info); @@ -92,6 +102,17 @@ routehint_candidates(const tal_t *ctx, json_tok_full(buf, toks)); } + /* If they specify fronting nodes, always use them. */ + if (tal_count(ld->fronting_nodes)) { + if (!is_fronting_node(ld, &r->pubkey)) { + log_debug(ld->log, "%s: not a fronting node", + fmt_node_id(tmpctx, &r->pubkey)); + continue; + } + is_fronting = true; + } else + is_fronting = false; + /* Note: listincoming returns real scid or local alias if no real scid. */ candidate.c = any_channel_by_scid(ld, r->short_channel_id, true); if (!candidate.c) { @@ -133,6 +154,10 @@ routehint_candidates(const tal_t *ctx, if (expose_all_private != NULL && *expose_all_private) is_public = true; + /* Also, consider fronting nodes public */ + if (is_fronting) + is_public = true; + r->fee_base_msat = fee_base.millisatoshis; /* Raw: route_info */ /* Could wrap: if so ignore */ if (!amount_msat_eq(amount_msat(r->fee_base_msat), fee_base)) { @@ -156,7 +181,7 @@ routehint_candidates(const tal_t *ctx, continue; } /* If they give us a hint, we use even if capacity 0 */ - } else if (amount_msat_is_zero(capacity)) { + } else if (!is_fronting && amount_msat_is_zero(capacity)) { log_debug(ld->log, "%s: deadend", fmt_short_channel_id(tmpctx, r->short_channel_id)); @@ -166,8 +191,8 @@ routehint_candidates(const tal_t *ctx, continue; } - /* Is it offline? */ - if (candidate.c->owner == NULL) { + /* Is it offline? Leave it if it's fronting. */ + if (!is_fronting && candidate.c->owner == NULL) { log_debug(ld->log, "%s: offline", fmt_short_channel_id(tmpctx, r->short_channel_id)); diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 1e0260415de4..ed002c86240c 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -937,3 +937,28 @@ def test_invoice_botched_migration(node_factory, chainparams): assert ([(i['created_index'], i['label']) for i in l1.rpc.listinvoices()["invoices"]] == [(1, "made_after_bad_migration"), (2, "label1")]) assert l1.rpc.invoice(100, "test", "test")["created_index"] == 3 + + +def test_payment_fronting(node_factory): + l1, l2 = node_factory.get_nodes(2) + l3, l4 = node_factory.get_nodes(2, opts=[{'payment-fronting-node': l1.info['id']}, + {'payment-fronting-node': [l1.info['id'], l2.info['id']]}]) + + assert l3.rpc.listconfigs('payment-fronting-node') == {'configs': {'payment-fronting-node': {'sources': ['cmdline'], 'values_str': [l1.info['id']]}}} + assert l4.rpc.listconfigs('payment-fronting-node') == {'configs': {'payment-fronting-node': {'sources': ['cmdline', 'cmdline'], 'values_str': [l1.info['id'], l2.info['id']]}}} + + # l1 <----> l3 + # \ + # \-----> l4 <----> l2 + node_factory.join_nodes([l1, l3], wait_for_announce=True) + node_factory.join_nodes([l1, l4], wait_for_announce=True) + node_factory.join_nodes([l2, l4], wait_for_announce=True) + + l3inv = l3.rpc.invoice(1000, 'l3inv', 'l3inv')['bolt11'] + assert only_one(only_one(l3.rpc.decode(l3inv)['routes']))['pubkey'] == l1.info['id'] + + l4inv = l4.rpc.invoice(1000, 'l4inv', 'l4inv')['bolt11'] + assert [only_one(r)['pubkey'] for r in l4.rpc.decode(l4inv)['routes']] == [l1.info['id'], l2.info['id']] + + l1.rpc.xpay(l3inv) + l1.rpc.xpay(l4inv) From 0f52dc1b2649bce475789a09145ff4c16193cebb Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:51 +0930 Subject: [PATCH 5/9] offers: encapsulate globals in plugin_get_data() This is how modern plugins do it, and it has the benefit of not requiring extra code for memleak tracking. Signed-off-by: Rusty Russell --- plugins/fetchinvoice.c | 20 ++++++---- plugins/offers.c | 77 +++++++++++++++++++----------------- plugins/offers.h | 45 +++++++++++---------- plugins/offers_inv_hook.c | 3 +- plugins/offers_invreq_hook.c | 21 +++++----- plugins/offers_offer.c | 23 +++++++---- 6 files changed, 107 insertions(+), 82 deletions(-) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index c40889763d95..83a8f73c54a2 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -576,6 +576,7 @@ static struct command_result *establish_path_fail(struct command *cmd, static struct command_result *try_establish(struct command *cmd, struct establishing_paths *epaths) { + const struct offers_data *od = get_offers_data(cmd->plugin); struct pubkey target; if (epaths->sent->direct_dest) { @@ -596,8 +597,8 @@ static struct command_result *try_establish(struct command *cmd, epaths->sent->issuer_key = &bpath->path[tal_count(bpath->path)-1]->blinded_node_id; } - return establish_onion_path(cmd, get_gossmap(cmd->plugin), &id, &target, - disable_connect, + return establish_onion_path(cmd, get_gossmap(cmd->plugin), &od->id, &target, + od->disable_connect, establish_path_done, establish_path_fail, epaths); @@ -799,15 +800,16 @@ static struct command_result *param_dev_reply_path(struct command *cmd, const ch return NULL; } -static bool payer_key(const u8 *public_tweak, size_t public_tweak_len, +static bool payer_key(const struct offers_data *od, + const u8 *public_tweak, size_t public_tweak_len, struct pubkey *key) { struct sha256 tweakhash; - bolt12_alias_tweak(&nodealias_base, public_tweak, public_tweak_len, + bolt12_alias_tweak(&od->nodealias_base, public_tweak, public_tweak_len, &tweakhash); - *key = id; + *key = od->id; return secp256k1_ec_pubkey_tweak_add(secp256k1_ctx, &key->pubkey, tweakhash.u.u8) == 1; @@ -818,6 +820,7 @@ struct command_result *json_fetchinvoice(struct command *cmd, const char *buffer, const jsmntok_t *params) { + const struct offers_data *od = get_offers_data(cmd->plugin); struct amount_msat *msat; const char *rec_label, *payer_note; u8 *payer_metadata; @@ -988,7 +991,7 @@ struct command_result *json_fetchinvoice(struct command *cmd, rec_label, strlen(rec_label)); - bolt12_alias_tweak(&nodealias_base, + bolt12_alias_tweak(&od->nodealias_base, tweak_input, tal_bytelen(tweak_input), &tweak); @@ -1054,7 +1057,7 @@ struct command_result *json_fetchinvoice(struct command *cmd, /* We derive transient payer_id from invreq_metadata */ invreq->invreq_payer_id = tal(invreq, struct pubkey); - if (!payer_key(invreq->invreq_metadata, + if (!payer_key(od, invreq->invreq_metadata, tal_bytelen(invreq->invreq_metadata), invreq->invreq_payer_id)) { /* Doesn't happen! */ @@ -1345,6 +1348,7 @@ struct command_result *json_sendinvoice(struct command *cmd, const char *buffer, const jsmntok_t *params) { + const struct offers_data *od = get_offers_data(cmd->plugin); struct amount_msat *msat; u32 *timeout; struct sent *sent = tal(cmd, struct sent); @@ -1417,7 +1421,7 @@ struct command_result *json_sendinvoice(struct command *cmd, * - MUST set `invoice_node_id` to the final `blinded_node_id` on the path it received the invoice request */ sent->inv->invoice_node_id = tal(sent->inv, struct pubkey); - sent->inv->invoice_node_id->pubkey = id.pubkey; + sent->inv->invoice_node_id->pubkey = od->id.pubkey; /* BOLT #12: * - if the expiry for accepting payment is not 7200 seconds diff --git a/plugins/offers.c b/plugins/offers.c index 48ecff4f4fdb..099ecb494326 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -30,36 +30,31 @@ #define HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES #define ABYTES crypto_secretstream_xchacha20poly1305_ABYTES -struct pubkey id; -u32 blockheight; -u16 cltv_final; -bool disable_connect; -bool dev_invoice_bpath_scid; -struct short_channel_id *dev_invoice_internal_scid; -struct secret invoicesecret_base; -struct secret offerblinding_base; -struct secret nodealias_base; -static struct gossmap *global_gossmap; - -static void init_gossmap(struct plugin *plugin) +struct offers_data *get_offers_data(struct plugin *plugin) { - global_gossmap - = notleak_with_children(gossmap_load(plugin, - GOSSIP_STORE_FILENAME, - plugin_gossmap_logcb, - plugin)); - if (!global_gossmap) + return plugin_get_data(plugin, struct offers_data); +} + +static void init_gossmap(struct plugin *plugin, + struct offers_data *od) +{ + od->global_gossmap_ = gossmap_load(plugin, + GOSSIP_STORE_FILENAME, + plugin_gossmap_logcb, + plugin); + if (!od->global_gossmap_) plugin_err(plugin, "Could not load gossmap %s: %s", GOSSIP_STORE_FILENAME, strerror(errno)); } struct gossmap *get_gossmap(struct plugin *plugin) { - if (!global_gossmap) - init_gossmap(plugin); + struct offers_data *od = get_offers_data(plugin); + if (!od->global_gossmap_) + init_gossmap(plugin, od); else - gossmap_refresh(global_gossmap); - return global_gossmap; + gossmap_refresh(od->global_gossmap_); + return od->global_gossmap_; } /* BOLT #12: @@ -69,6 +64,7 @@ struct gossmap *get_gossmap(struct plugin *plugin) */ bool we_want_blinded_path(struct plugin *plugin, bool for_payment) { + const struct offers_data *od = get_offers_data(plugin); struct node_id local_nodeid; const struct gossmap_node *node; const u8 *nannounce; @@ -80,7 +76,7 @@ bool we_want_blinded_path(struct plugin *plugin, bool for_payment) u8 rgb_color[3], alias[32]; struct tlv_node_ann_tlvs *na_tlvs; - node_id_from_pubkey(&local_nodeid, &id); + node_id_from_pubkey(&local_nodeid, &od->id); node = gossmap_find_node(gossmap, &local_nodeid); if (!node) @@ -235,6 +231,7 @@ send_onion_reply(struct command *cmd, struct blinded_path *reply_path, struct tlv_onionmsg_tlv *payload) { + const struct offers_data *od = get_offers_data(cmd->plugin); struct onion_reply *onion_reply; onion_reply = tal(cmd, struct onion_reply); @@ -251,8 +248,8 @@ send_onion_reply(struct command *cmd, } return establish_onion_path(cmd, get_gossmap(cmd->plugin), - &id, &onion_reply->reply_path->first_node_id.pubkey, - disable_connect, + &od->id, &onion_reply->reply_path->first_node_id.pubkey, + od->disable_connect, send_onion_reply_after_established, send_onion_reply_not_established, onion_reply); @@ -425,8 +422,9 @@ static struct command_result *block_added_notify(struct command *cmd, const char *buf, const jsmntok_t *params) { + struct offers_data *od = get_offers_data(cmd->plugin); const char *err = json_scan(cmd, buf, params, "{block_added:{height:%}}", - JSON_SCAN(json_to_u32, &blockheight)); + JSON_SCAN(json_to_u32, &od->blockheight)); if (err) plugin_err(cmd->plugin, "Failed to parse block_added (%.*s): %s", json_tok_full_len(params), @@ -1515,34 +1513,36 @@ static const char *init(struct command *init_cmd, const char *buf UNUSED, const jsmntok_t *config UNUSED) { + struct offers_data *od = get_offers_data(init_cmd->plugin); + rpc_scan(init_cmd, "getinfo", take(json_out_obj(NULL, NULL, NULL)), - "{id:%}", JSON_SCAN(json_to_pubkey, &id)); + "{id:%}", JSON_SCAN(json_to_pubkey, &od->id)); rpc_scan(init_cmd, "getchaininfo", take(json_out_obj(NULL, "last_height", NULL)), - "{headercount:%}", JSON_SCAN(json_to_u32, &blockheight)); + "{headercount:%}", JSON_SCAN(json_to_u32, &od->blockheight)); rpc_scan(init_cmd, "listconfigs", take(json_out_obj(NULL, NULL, NULL)), "{configs:" "{cltv-final:{value_int:%}}}", - JSON_SCAN(json_to_u16, &cltv_final)); + JSON_SCAN(json_to_u16, &od->cltv_final)); rpc_scan(init_cmd, "makesecret", take(json_out_obj(NULL, "string", BOLT12_ID_BASE_STRING)), "{secret:%}", - JSON_SCAN(json_to_secret, &invoicesecret_base)); + JSON_SCAN(json_to_secret, &od->invoicesecret_base)); rpc_scan(init_cmd, "makesecret", take(json_out_obj(NULL, "string", "offer-blinded-path")), "{secret:%}", - JSON_SCAN(json_to_secret, &offerblinding_base)); + JSON_SCAN(json_to_secret, &od->offerblinding_base)); rpc_scan(init_cmd, "makesecret", take(json_out_obj(NULL, "string", NODE_ALIAS_BASE_STRING)), "{secret:%}", - JSON_SCAN(json_to_secret, &nodealias_base)); + JSON_SCAN(json_to_secret, &od->nodealias_base)); return NULL; } @@ -1599,22 +1599,27 @@ static bool scid_jsonfmt(struct plugin *plugin, struct json_stream *js, const ch int main(int argc, char *argv[]) { setup_locale(); + struct offers_data *od = tal(NULL, struct offers_data); + + od->disable_connect = false; + od->dev_invoice_bpath_scid = false; + od->dev_invoice_internal_scid = NULL; /* We deal in UTC; mktime() uses local time */ setenv("TZ", "", 1); - plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, + plugin_main(argv, init, take(od), PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), notifications, ARRAY_SIZE(notifications), hooks, ARRAY_SIZE(hooks), NULL, 0, plugin_option("fetchinvoice-noconnect", "flag", "Don't try to connect directly to fetch/pay an invoice.", - flag_option, flag_jsonfmt, &disable_connect), + flag_option, flag_jsonfmt, &od->disable_connect), plugin_option_dev("dev-invoice-bpath-scid", "flag", "Use short_channel_id instead of pubkey when creating a blinded payment path", - flag_option, flag_jsonfmt, &dev_invoice_bpath_scid), + flag_option, flag_jsonfmt, &od->dev_invoice_bpath_scid), plugin_option_dev("dev-invoice-internal-scid", "string", "Use short_channel_id instead of pubkey when creating a blinded payment path", - scid_option, scid_jsonfmt, &dev_invoice_internal_scid), + scid_option, scid_jsonfmt, &od->dev_invoice_internal_scid), NULL); } diff --git a/plugins/offers.h b/plugins/offers.h index e59796a9f4ea..3d77b9d01379 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -7,26 +7,31 @@ struct command; struct onion_message; struct plugin; -/* This is me. */ -extern struct pubkey id; -/* --fetchinvoice-noconnect */ -extern bool disable_connect; -/* --cltv-final */ -extern u16 cltv_final; -/* Current header_count */ -extern u32 blockheight; -/* Basis for invoice path_secrets */ -extern struct secret invoicesecret_base; -/* Base for offers path_secrets */ -extern struct secret offerblinding_base; -/* Base for node aliases for invoice requests */ -extern struct secret nodealias_base; -/* --dev-invoice-bpath-scid */ -extern bool dev_invoice_bpath_scid; -/* --dev-invoice-internal-scid */ -extern struct short_channel_id *dev_invoice_internal_scid; -/* This is me. */ -extern struct pubkey id; +/* plugin_data for this plugin */ +struct offers_data { + /* This is me. */ + struct pubkey id; + /* --fetchinvoice-noconnect */ + bool disable_connect; + /* --cltv-final */ + u16 cltv_final; + /* Current header_count */ + u32 blockheight; + /* Basis for invoice path_secrets */ + struct secret invoicesecret_base; + /* Base for offers path_secrets */ + struct secret offerblinding_base; + /* Base for node aliases for invoice requests */ + struct secret nodealias_base; + /* --dev-invoice-bpath-scid */ + bool dev_invoice_bpath_scid; + /* --dev-invoice-internal-scid */ + struct short_channel_id *dev_invoice_internal_scid; + /* Use get_gossmap() to access this! */ + struct gossmap *global_gossmap_; +}; + +struct offers_data *get_offers_data(struct plugin *plugin); /* Helper to send a reply (connecting if required), and discard result */ struct command_result *WARN_UNUSED_RESULT diff --git a/plugins/offers_inv_hook.c b/plugins/offers_inv_hook.c index 07bc1a7ed6b6..d88a88759765 100644 --- a/plugins/offers_inv_hook.c +++ b/plugins/offers_inv_hook.c @@ -217,6 +217,7 @@ struct command_result *handle_invoice(struct command *cmd, struct blinded_path *reply_path STEALS, const struct secret *secret) { + const struct offers_data *od = get_offers_data(cmd->plugin); size_t len = tal_count(invbin); struct inv *inv = tal(cmd, struct inv); struct out_req *req; @@ -242,7 +243,7 @@ struct command_result *handle_invoice(struct command *cmd, invoice_invreq_id(inv->inv, &invreq_id_nopath); inv->inv->invreq_paths = invreq_paths; - path_secret = bolt12_path_id(tmpctx, &offerblinding_base, &invreq_id_nopath); + path_secret = bolt12_path_id(tmpctx, &od->offerblinding_base, &invreq_id_nopath); if (!memeq(path_secret, tal_count(path_secret), secret, sizeof(*secret))) { if (command_dev_apis(cmd)) diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 5cefec997537..6640d4fe4501 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -247,6 +247,8 @@ static struct command_result *found_best_peer(struct command *cmd, const struct chaninfo *best, struct invreq *ir) { + struct offers_data *od = get_offers_data(cmd->plugin); + /* BOLT #12: * - MUST include `invoice_paths` containing one or more paths to the node. * - MUST specify `invoice_paths` in order of most-preferred to @@ -269,11 +271,11 @@ static struct command_result *found_best_peer(struct command *cmd, /* Make a small 1-hop path to us */ ids = tal_arr(tmpctx, struct pubkey, 2); ids[0] = best->id; - ids[1] = id; + ids[1] = od->id; /* This does nothing unless dev_invoice_internal_scid is set */ scids = tal_arrz(tmpctx, struct short_channel_id *, 2); - scids[1] = dev_invoice_internal_scid; + scids[1] = od->dev_invoice_internal_scid; /* Make basic tlvs, add payment restrictions */ etlvs = new_encdata_tlvs(tmpctx, ids, @@ -294,18 +296,18 @@ static struct command_result *found_best_peer(struct command *cmd, */ /* Give them 6 blocks, plus one per 10 minutes until expiry. */ if (ir->inv->invoice_relative_expiry) - base = blockheight + 6 + *ir->inv->invoice_relative_expiry / 600; + base = od->blockheight + 6 + *ir->inv->invoice_relative_expiry / 600; else - base = blockheight + 6 + 7200 / 600; + base = od->blockheight + 6 + 7200 / 600; etlvs[0]->payment_constraints = tal(etlvs[0], struct tlv_encrypted_data_tlv_payment_constraints); - etlvs[0]->payment_constraints->max_cltv_expiry = base + best->cltv + cltv_final; + etlvs[0]->payment_constraints->max_cltv_expiry = base + best->cltv + od->cltv_final; etlvs[0]->payment_constraints->htlc_minimum_msat = best->htlc_min.millisatoshis; /* Raw: tlv */ /* So we recognize this payment */ etlvs[1]->path_id = bolt12_path_id(etlvs[1], - &invoicesecret_base, + &od->invoicesecret_base, ir->inv->invoice_payment_hash); ir->inv->invoice_paths = tal_arr(ir->inv, struct blinded_path *, 1); @@ -316,7 +318,7 @@ static struct command_result *found_best_peer(struct command *cmd, /* If they tell us to use scidd for first point, grab * a channel from node (must exist, it's public) */ - if (dev_invoice_bpath_scid) { + if (od->dev_invoice_bpath_scid) { struct gossmap *gossmap = get_gossmap(cmd->plugin); struct node_id best_nodeid; const struct gossmap_node *n; @@ -340,7 +342,7 @@ static struct command_result *found_best_peer(struct command *cmd, ir->inv->invoice_blindedpay[0] = tal(ir->inv->invoice_blindedpay, struct blinded_payinfo); ir->inv->invoice_blindedpay[0]->fee_base_msat = best->feebase; ir->inv->invoice_blindedpay[0]->fee_proportional_millionths = best->feeppm; - ir->inv->invoice_blindedpay[0]->cltv_expiry_delta = best->cltv + cltv_final; + ir->inv->invoice_blindedpay[0]->cltv_expiry_delta = best->cltv + od->cltv_final; ir->inv->invoice_blindedpay[0]->htlc_minimum_msat = best->htlc_min; ir->inv->invoice_blindedpay[0]->htlc_maximum_msat = best->htlc_max; ir->inv->invoice_blindedpay[0]->features = NULL; @@ -766,6 +768,7 @@ static struct command_result *listoffers_done(struct command *cmd, const jsmntok_t *result, struct invreq *ir) { + const struct offers_data *od = get_offers_data(cmd->plugin); const jsmntok_t *arr = json_get_member(buf, result, "offers"); const jsmntok_t *offertok, *activetok, *b12tok; bool active; @@ -807,7 +810,7 @@ static struct command_result *listoffers_done(struct command *cmd, ir->invreq->offer_paths = NULL; invreq_offer_id(ir->invreq, &offer_id); ir->invreq->offer_paths = offer_paths; - bolt12_path_secret(&offerblinding_base, &offer_id, + bolt12_path_secret(&od->offerblinding_base, &offer_id, &blinding_path_secret); if (!secret_eq_consttime(ir->secret, &blinding_path_secret)) { /* You used the wrong blinded path for invreq */ diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index a50052cb13b1..7dc58114e4ed 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -282,6 +282,8 @@ static struct command_result *found_best_peer(struct command *cmd, const struct chaninfo *best, struct offer_info *offinfo) { + const struct offers_data *od = get_offers_data(cmd->plugin); + /* BOLT #12: * - if it is connected only by private channels: * - MUST include `offer_paths` containing one or more paths to the node from @@ -302,11 +304,11 @@ static struct command_result *found_best_peer(struct command *cmd, /* Make a small 1-hop path to us */ ids = tal_arr(tmpctx, struct pubkey, 2); ids[0] = best->id; - ids[1] = id; + ids[1] = od->id; /* So we recognize this */ /* We can check this when they try to take up offer. */ - bolt12_path_secret(&offerblinding_base, &offer_id, + bolt12_path_secret(&od->offerblinding_base, &offer_id, &blinding_path_secret); offinfo->offer->offer_paths = tal_arr(offinfo->offer, struct blinded_path *, 1); @@ -379,6 +381,7 @@ static struct command_result *param_paths(struct command *cmd, const char *name, { size_t i; const jsmntok_t *t; + const struct offers_data *od = get_offers_data(cmd->plugin); if (tok->type != JSMN_ARRAY) return command_fail_badparam(cmd, name, buffer, tok, "Must be array"); @@ -421,7 +424,7 @@ static struct command_result *param_paths(struct command *cmd, const char *name, "invalid pubkey"); } } - if (j == t->size - 1 && !pubkey_eq(&pk, &id)) + if (j == t->size - 1 && !pubkey_eq(&pk, &od->id)) return command_fail_badparam(cmd, name, buffer, p, "final pubkey must be this node"); (*paths)[i]->path[j] = pk; @@ -434,6 +437,7 @@ struct command_result *json_offer(struct command *cmd, const char *buffer, const jsmntok_t *params) { + const struct offers_data *od = get_offers_data(cmd->plugin); const char *desc, *issuer; struct tlv_offer *offer; struct offer_info *offinfo = tal(cmd, struct offer_info); @@ -531,7 +535,7 @@ struct command_result *json_offer(struct command *cmd, * - MUST set `offer_issuer_id` to the node's public key to request the * invoice from. */ - offer->offer_issuer_id = tal_dup(offer, struct pubkey, &id); + offer->offer_issuer_id = tal_dup(offer, struct pubkey, &od->id); /* Now rest of offer will not change: we use pathless offer to create secret. */ if (paths) { @@ -541,7 +545,7 @@ struct command_result *json_offer(struct command *cmd, offer_offer_id(offer, &offer_id); /* We can check this when they try to take up offer. */ - bolt12_path_secret(&offerblinding_base, &offer_id, + bolt12_path_secret(&od->offerblinding_base, &offer_id, &blinding_path_secret); offer->offer_paths = tal_arr(offer, struct blinded_path *, tal_count(paths)); @@ -603,6 +607,8 @@ static struct command_result *found_best_peer_invrequest(struct command *cmd, const struct chaninfo *best, struct invrequest_data *irdata) { + const struct offers_data *od = get_offers_data(cmd->plugin); + if (!best) { /* FIXME: Make this a warning in the result! */ plugin_log(cmd->plugin, LOG_UNUSUAL, @@ -627,11 +633,11 @@ static struct command_result *found_best_peer_invrequest(struct command *cmd, /* Make a small 1-hop path to us */ ids = tal_arr(tmpctx, struct pubkey, 2); ids[0] = best->id; - ids[1] = id; + ids[1] = od->id; /* So we recognize this */ /* We can check this when they try to take up invoice_request. */ - bolt12_path_secret(&offerblinding_base, &invreq_id, + bolt12_path_secret(&od->offerblinding_base, &invreq_id, &blinding_path_secret); plugin_log(cmd->plugin, LOG_DBG, @@ -654,6 +660,7 @@ struct command_result *json_invoicerequest(struct command *cmd, const char *buffer, const jsmntok_t *params) { + const struct offers_data *od = get_offers_data(cmd->plugin); const char *desc, *issuer, *label; struct tlv_invoice_request *invreq; struct amount_msat *msat; @@ -719,7 +726,7 @@ struct command_result *json_invoicerequest(struct command *cmd, * - MUST set `invreq_payer_id` (as it would set `offer_issuer_id` for an offer). */ /* FIXME: Allow invoicerequests using aliases! */ - invreq->invreq_payer_id = tal_dup(invreq, struct pubkey, &id); + invreq->invreq_payer_id = tal_dup(invreq, struct pubkey, &od->id); /* BOLT #12: * - if it supports bolt12 invoice request features: From 7160541a74a356b6e7c1a4af82e4c21aee68cd6f Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:52 +0930 Subject: [PATCH 6/9] lightningd: honor `payment-fronting-node` when making bolt12 offers. We use all the fronting nodes when creating offers. Signed-off-by: Rusty Russell --- plugins/offers.c | 30 ++++++++++- plugins/offers.h | 2 + plugins/offers_invreq_hook.c | 4 ++ plugins/offers_offer.c | 101 ++++++++++++++++++++++++----------- tests/test_invoices.py | 10 ++++ 5 files changed, 114 insertions(+), 33 deletions(-) diff --git a/plugins/offers.c b/plugins/offers.c index 099ecb494326..e255368114c2 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -76,6 +76,10 @@ bool we_want_blinded_path(struct plugin *plugin, bool for_payment) u8 rgb_color[3], alias[32]; struct tlv_node_ann_tlvs *na_tlvs; + /* If we're fronting, we always want a blinded path */ + if (od->fronting_nodes) + return true; + node_id_from_pubkey(&local_nodeid, &od->id); node = gossmap_find_node(gossmap, &local_nodeid); @@ -1509,6 +1513,25 @@ static struct command_result *json_decode(struct command *cmd, return command_finished(cmd, response); } +static struct pubkey *json_to_pubkeys(const tal_t *ctx, + const char *buffer, + const jsmntok_t *tok) +{ + size_t i; + const jsmntok_t *t; + struct pubkey *arr; + + if (tok->type != JSMN_ARRAY) + return NULL; + + arr = tal_arr(ctx, struct pubkey, tok->size); + json_for_each_arr(i, t, tok) { + if (!json_to_pubkey(buffer, t, &arr[i])) + return tal_free(arr); + } + return arr; +} + static const char *init(struct command *init_cmd, const char *buf UNUSED, const jsmntok_t *config UNUSED) @@ -1526,8 +1549,10 @@ static const char *init(struct command *init_cmd, rpc_scan(init_cmd, "listconfigs", take(json_out_obj(NULL, NULL, NULL)), "{configs:" - "{cltv-final:{value_int:%}}}", - JSON_SCAN(json_to_u16, &od->cltv_final)); + "{cltv-final:{value_int:%}," + "payment-fronting-node?:{values_str:%}}}", + JSON_SCAN(json_to_u16, &od->cltv_final), + JSON_SCAN_TAL(od, json_to_pubkeys, &od->fronting_nodes)); rpc_scan(init_cmd, "makesecret", take(json_out_obj(NULL, "string", BOLT12_ID_BASE_STRING)), @@ -1604,6 +1629,7 @@ int main(int argc, char *argv[]) od->disable_connect = false; od->dev_invoice_bpath_scid = false; od->dev_invoice_internal_scid = NULL; + od->fronting_nodes = NULL; /* We deal in UTC; mktime() uses local time */ setenv("TZ", "", 1); diff --git a/plugins/offers.h b/plugins/offers.h index 3d77b9d01379..4d511deaa3d9 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -23,6 +23,8 @@ struct offers_data { struct secret offerblinding_base; /* Base for node aliases for invoice requests */ struct secret nodealias_base; + /* Any --payment-fronting-node specified */ + struct pubkey *fronting_nodes; /* --dev-invoice-bpath-scid */ bool dev_invoice_bpath_scid; /* --dev-invoice-internal-scid */ diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 6640d4fe4501..a221c761c640 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -237,6 +237,10 @@ static struct command_result *create_invoicereq(struct command *cmd, return send_outreq(req); } +/* FIXME: Allow multihop! */ +/* FIXME: And add padding! */ + + /* FIXME: This is naive: * - Only creates if we have no public channels. * - Always creates a path from direct neighbor. diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 7dc58114e4ed..35faee7bbf5a 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -278,6 +278,50 @@ static struct command_result *create_offer(struct command *cmd, return send_outreq(req); } +/* Create num_node_ids paths from these node_ids to us (one hop each) */ +static struct blinded_path **offer_onehop_paths(const tal_t *ctx, + const struct offers_data *od, + const struct tlv_offer *offer, + const struct pubkey *neighbors, + size_t num_neigbors) +{ + struct pubkey *ids = tal_arr(tmpctx, struct pubkey, 2); + struct secret blinding_path_secret; + struct sha256 offer_id; + struct blinded_path **offer_paths; + + /* Note: "id" of offer minus paths */ + assert(!offer->offer_paths); + offer_offer_id(offer, &offer_id); + + offer_paths = tal_arr(ctx, struct blinded_path *, num_neigbors); + for (size_t i = 0; i < num_neigbors; i++) { + ids[0] = neighbors[i]; + ids[1] = od->id; + + /* So we recognize this */ + /* We can check this when they try to take up offer. */ + bolt12_path_secret(&od->offerblinding_base, &offer_id, + &blinding_path_secret); + + offer_paths[i] + = incoming_message_blinded_path(offer_paths, + ids, + NULL, + &blinding_path_secret); + } + return offer_paths; +} + +/* Common case of making a single path */ +static struct blinded_path **offer_onehop_path(const tal_t *ctx, + const struct offers_data *od, + const struct tlv_offer *offer, + const struct pubkey *neighbor) +{ + return offer_onehop_paths(ctx, od, offer, neighbor, 1); +} + static struct command_result *found_best_peer(struct command *cmd, const struct chaninfo *best, struct offer_info *offinfo) @@ -294,29 +338,9 @@ static struct command_result *found_best_peer(struct command *cmd, plugin_log(cmd->plugin, LOG_UNUSUAL, "No incoming channel to public peer, so no blinded path"); } else { - struct pubkey *ids; - struct secret blinding_path_secret; - struct sha256 offer_id; - - /* Note: "id" of offer minus paths */ - offer_offer_id(offinfo->offer, &offer_id); - - /* Make a small 1-hop path to us */ - ids = tal_arr(tmpctx, struct pubkey, 2); - ids[0] = best->id; - ids[1] = od->id; - - /* So we recognize this */ - /* We can check this when they try to take up offer. */ - bolt12_path_secret(&od->offerblinding_base, &offer_id, - &blinding_path_secret); - - offinfo->offer->offer_paths = tal_arr(offinfo->offer, struct blinded_path *, 1); - offinfo->offer->offer_paths[0] - = incoming_message_blinded_path(offinfo->offer->offer_paths, - ids, - NULL, - &blinding_path_secret); + offinfo->offer->offer_paths + = offer_onehop_path(offinfo->offer, od, + offinfo->offer, &best->id); } return create_offer(cmd, offinfo); @@ -325,16 +349,31 @@ static struct command_result *found_best_peer(struct command *cmd, static struct command_result *maybe_add_path(struct command *cmd, struct offer_info *offinfo) { - /* BOLT #12: - * - if it is connected only by private channels: - * - MUST include `offer_paths` containing one or more paths to the node from - * publicly reachable nodes. - */ + const struct offers_data *od = get_offers_data(cmd->plugin); + + /* Populate paths assuming not already set by dev_paths */ if (!offinfo->offer->offer_paths) { - if (we_want_blinded_path(cmd->plugin, false)) - return find_best_peer(cmd, OPT_ONION_MESSAGES, - found_best_peer, offinfo); + /* BOLT #12: + * - if it is connected only by private channels: + * - MUST include `offer_paths` containing one or more paths to the node from + * publicly reachable nodes. + */ + if (we_want_blinded_path(cmd->plugin, false)) { + /* We use *all* fronting nodes (not just "best" one) + * for offers */ + if (od->fronting_nodes) { + offinfo->offer->offer_paths + = offer_onehop_paths(offinfo->offer, od, + offinfo->offer, + od->fronting_nodes, + tal_count(od->fronting_nodes)); + } else { + return find_best_peer(cmd, OPT_ONION_MESSAGES, + found_best_peer, offinfo); + } + } } + return create_offer(cmd, offinfo); } diff --git a/tests/test_invoices.py b/tests/test_invoices.py index ed002c86240c..0db32282f0fc 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -962,3 +962,13 @@ def test_payment_fronting(node_factory): l1.rpc.xpay(l3inv) l1.rpc.xpay(l4inv) + + # Now test offers. + l3offer = l3.rpc.offer(1000, 'l3offer', 'l3offer')['bolt12'] + assert only_one(l3.rpc.decode(l3offer)['offer_paths'])['first_node_id'] == l1.info['id'] + + l4offer = l4.rpc.offer(1000, 'l4offer', 'l4offer')['bolt12'] + assert [r['first_node_id'] for r in l4.rpc.decode(l4offer)['offer_paths']] == [l1.info['id'], l2.info['id']] + + l3invb12 = l1.rpc.fetchinvoice(l3offer)['invoice'] + l4invb12 = l1.rpc.fetchinvoice(l4offer)['invoice'] From 8dcb077336c2a98d732c6b06b49eb24fcc0a45a8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:52 +0930 Subject: [PATCH 7/9] offers: modify find_best_peer() to only select from fronting nodes if set. Signed-off-by: Rusty Russell --- plugins/offers.c | 25 +++++++++++++++++++++++++ plugins/offers.h | 5 +++-- plugins/offers_invreq_hook.c | 2 +- plugins/offers_offer.c | 3 ++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/plugins/offers.c b/plugins/offers.c index e255368114c2..904922786b81 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -320,9 +320,20 @@ struct find_best_peer_data { const struct chaninfo *, void *); int needed_feature; + const struct pubkey *fronting_only; void *arg; }; +static bool is_in_pubkeys(const struct pubkey *pubkeys, + const struct pubkey *k) +{ + for (size_t i = 0; i < tal_count(pubkeys); i++) { + if (pubkey_eq(&pubkeys[i], k)) + return true; + } + return false; +} + static struct command_result *listincoming_done(struct command *cmd, const char *method, const char *buf, @@ -367,6 +378,18 @@ static struct command_result *listincoming_done(struct command *cmd, } ci.feebase = feebase.millisatoshis; /* Raw: feebase */ + if (data->fronting_only) { + if (!is_in_pubkeys(data->fronting_only, &ci.id)) + continue; + + /* If disconnected, don't eliminate, simply + * consider it last. */ + if (!enabled) { + ci.capacity = AMOUNT_MSAT(0); + enabled = true; + } + } + /* Don't pick a peer which is disconnected */ if (!enabled) continue; @@ -392,6 +415,7 @@ static struct command_result *listincoming_done(struct command *cmd, struct command_result *find_best_peer_(struct command *cmd, int needed_feature, + const struct pubkey *fronting_only, struct command_result *(*cb)(struct command *, const struct chaninfo *, void *), @@ -402,6 +426,7 @@ struct command_result *find_best_peer_(struct command *cmd, data->cb = cb; data->arg = arg; data->needed_feature = needed_feature; + data->fronting_only = fronting_only; req = jsonrpc_request_start(cmd, "listincoming", listincoming_done, forward_error, data); return send_outreq(req); diff --git a/plugins/offers.h b/plugins/offers.h index 4d511deaa3d9..c448a9cf986c 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -86,13 +86,14 @@ struct chaninfo { /* Calls listpeerchannels, then cb with best peer (if any!) which has needed_feature */ struct command_result *find_best_peer_(struct command *cmd, int needed_feature, + const struct pubkey *fronting_only, struct command_result *(*cb)(struct command *, const struct chaninfo *, void *), void *arg); -#define find_best_peer(cmd, needed_feature, cb, arg) \ - find_best_peer_((cmd), (needed_feature), \ +#define find_best_peer(cmd, needed_feature, fronting_only, cb, arg) \ + find_best_peer_((cmd), (needed_feature), (fronting_only), \ typesafe_cb_preargs(struct command_result *, void *, \ (cb), (arg), \ struct command *, \ diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index a221c761c640..8327026e5f3b 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -361,7 +361,7 @@ static struct command_result *add_blindedpaths(struct command *cmd, if (!we_want_blinded_path(cmd->plugin, true)) return create_invoicereq(cmd, ir); - return find_best_peer(cmd, OPT_ROUTE_BLINDING, + return find_best_peer(cmd, OPT_ROUTE_BLINDING, NULL, found_best_peer, ir); } diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 35faee7bbf5a..69569e8987c3 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -369,6 +369,7 @@ static struct command_result *maybe_add_path(struct command *cmd, tal_count(od->fronting_nodes)); } else { return find_best_peer(cmd, OPT_ONION_MESSAGES, + NULL, found_best_peer, offinfo); } } @@ -779,7 +780,7 @@ struct command_result *json_invoicerequest(struct command *cmd, idata->invreq = invreq; idata->single_use = *single_use; idata->label = label; - return find_best_peer(cmd, OPT_ONION_MESSAGES, + return find_best_peer(cmd, OPT_ONION_MESSAGES, NULL, found_best_peer_invrequest, idata); } From 2068bef957344a6c17c0959baced7fa16b5bff8c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:52 +0930 Subject: [PATCH 8/9] offers: honor `payment-fronting-nodes` when creating invoices. Signed-off-by: Rusty Russell --- plugins/offers_invreq_hook.c | 10 +++++++++- tests/test_invoices.py | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 8327026e5f3b..5b384ada3b1a 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -261,6 +261,12 @@ static struct command_result *found_best_peer(struct command *cmd, * for each `blinded_path` in `paths`, in order. */ if (!best) { + /* Don't allow bare invoices if they explicitly told us to front */ + if (od->fronting_nodes) { + return fail_invreq(cmd, ir, + "Could not find path from payment-fronting-node"); + } + /* Note: since we don't make one, createinvoice adds a dummy. */ plugin_log(cmd->plugin, LOG_UNUSUAL, "No incoming channel for %s, so no blinded path", @@ -358,10 +364,12 @@ static struct command_result *found_best_peer(struct command *cmd, static struct command_result *add_blindedpaths(struct command *cmd, struct invreq *ir) { + const struct offers_data *od = get_offers_data(cmd->plugin); + if (!we_want_blinded_path(cmd->plugin, true)) return create_invoicereq(cmd, ir); - return find_best_peer(cmd, OPT_ROUTE_BLINDING, NULL, + return find_best_peer(cmd, OPT_ROUTE_BLINDING, od->fronting_nodes, found_best_peer, ir); } diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 0db32282f0fc..727b4cc9af70 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -972,3 +972,10 @@ def test_payment_fronting(node_factory): l3invb12 = l1.rpc.fetchinvoice(l3offer)['invoice'] l4invb12 = l1.rpc.fetchinvoice(l4offer)['invoice'] + + assert only_one(l3.rpc.decode(l3invb12)['invoice_paths'])['first_node_id'] == l1.info['id'] + # Given multiple, it will pick one. + assert only_one(l3.rpc.decode(l3invb12)['invoice_paths'])['first_node_id'] in (l1.info['id'], l2.info['id']) + + l1.rpc.xpay(l3invb12) + l1.rpc.xpay(l4invb12) From 49e4d09ced78b9cd41110f4fee99a3e8407534f8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 23 Aug 2025 12:03:52 +0930 Subject: [PATCH 9/9] offers: honor `payment-fronting-nodes` when creating invoice_requests. Signed-off-by: Rusty Russell --- plugins/offers_offer.c | 10 +++++++--- tests/test_invoices.py | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 69569e8987c3..9a1bafe2752b 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -650,6 +650,12 @@ static struct command_result *found_best_peer_invrequest(struct command *cmd, const struct offers_data *od = get_offers_data(cmd->plugin); if (!best) { + /* Don't allow bare invoices if they explicitly told us to front */ + if (od->fronting_nodes) { + return command_fail(cmd, LIGHTNINGD, + "Could not find neighbour fronting node"); + } + /* FIXME: Make this a warning in the result! */ plugin_log(cmd->plugin, LOG_UNUSUAL, "No incoming channel to public peer, so no blinded path for invoice request"); @@ -773,14 +779,12 @@ struct command_result *json_invoicerequest(struct command *cmd, * - MUST set `invreq_features`.`features` to the bitmap of features. */ - /* FIXME: We only set blinded path if private/noaddr, we should allow - * setting otherwise! */ if (we_want_blinded_path(cmd->plugin, false)) { struct invrequest_data *idata = tal(cmd, struct invrequest_data); idata->invreq = invreq; idata->single_use = *single_use; idata->label = label; - return find_best_peer(cmd, OPT_ONION_MESSAGES, NULL, + return find_best_peer(cmd, OPT_ONION_MESSAGES, od->fronting_nodes, found_best_peer_invrequest, idata); } diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 727b4cc9af70..eba0654bf62a 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -979,3 +979,12 @@ def test_payment_fronting(node_factory): l1.rpc.xpay(l3invb12) l1.rpc.xpay(l4invb12) + + # Balance so l3 can pay ->l1->l4. + l3inv2 = l3.rpc.invoice(10000000, 'l3inv2', 'l3inv2')['bolt11'] + l1.rpc.xpay(l3inv2) + + # When l3 creates an invoice request, it will also use the fronting nodes. + l3invreq = l3.rpc.invoicerequest(amount=1000, description='l3invreq')['bolt12'] + assert only_one(l3.rpc.decode(l3invreq)['invreq_paths'])['first_node_id'] == l1.info['id'] + l4.rpc.sendinvoice(invreq=l3invreq, label='l3invreq')