From 6e4ff6a7d283ef199f93625c3b1ecb73e56b8c10 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 14 Feb 2025 11:47:21 +1030 Subject: [PATCH] offers: add a blinded path if we have no advertized address. Suggested-by: Matt Corallo Fixes: https://github.com/ElementsProject/lightning/issues/7806 Changelog-Changed: Offers: we will use a blinded path if we have no advertized address (so payers wouldn't be able to connect directly). --- plugins/offers.c | 43 ++++++++++++++++++++++++++++++++++++ plugins/offers.h | 2 ++ plugins/offers_invreq_hook.c | 6 +---- plugins/offers_offer.c | 9 ++------ tests/test_pay.py | 25 +++++++++++++++++++-- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/plugins/offers.c b/plugins/offers.c index 62d210a63..781367ea0 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -62,6 +62,49 @@ struct gossmap *get_gossmap(struct plugin *plugin) return global_gossmap; } +/* 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. + */ +bool we_want_blinded_path(struct plugin *plugin) +{ + struct node_id local_nodeid; + const struct gossmap_node *node; + const u8 *nannounce; + const struct gossmap *gossmap = get_gossmap(plugin); + struct node_id our_id; + secp256k1_ecdsa_signature signature; + u32 timestamp; + u8 *addresses, *features; + u8 rgb_color[3], alias[32]; + struct tlv_node_ann_tlvs *na_tlvs; + + node_id_from_pubkey(&local_nodeid, &id); + + node = gossmap_find_node(gossmap, &local_nodeid); + if (!node) + return true; + + /* Matt Corallo also suggests we do this (for now) if we don't + * advertize an address to connect to. */ + + /* We expect to know our own node announcements, but just in case. */ + nannounce = gossmap_node_get_announce(tmpctx, gossmap, node); + if (!nannounce) + return true; + + if (!fromwire_node_announcement(tmpctx, nannounce, + &signature, &features, + ×tamp, + &our_id, rgb_color, alias, + &addresses, + &na_tlvs)) + return true; + + return tal_count(fromwire_wireaddr_array(tmpctx, addresses)) == 0; +} + static struct command_result *finished(struct command *cmd, const char *method, const char *buf, diff --git a/plugins/offers.h b/plugins/offers.h index 6b01a33d9..97f8b367e 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -92,4 +92,6 @@ struct command_result *find_best_peer_(struct command *cmd, const struct chaninfo *), \ (arg)) +/* Do we want a blinded path from a peer? */ +bool we_want_blinded_path(struct plugin *plugin); #endif /* LIGHTNING_PLUGINS_OFFERS_H */ diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 85763ea7e..7669ad1e7 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -352,11 +352,7 @@ static struct command_result *found_best_peer(struct command *cmd, static struct command_result *add_blindedpaths(struct command *cmd, struct invreq *ir) { - struct node_id local_nodeid; - - /* Don't bother if we're public */ - node_id_from_pubkey(&local_nodeid, &id); - if (gossmap_find_node(get_gossmap(cmd->plugin), &local_nodeid)) + if (!we_want_blinded_path(cmd->plugin)) create_invoicereq(cmd, ir); return find_best_peer(cmd, OPT_ROUTE_BLINDING, diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index ac2684615..85b516515 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -329,10 +329,7 @@ static struct command_result *maybe_add_path(struct command *cmd, * publicly reachable nodes. */ if (!offinfo->offer->offer_paths) { - struct node_id local_nodeid; - - node_id_from_pubkey(&local_nodeid, &id); - if (!gossmap_find_node(get_gossmap(cmd->plugin), &local_nodeid)) + if (we_want_blinded_path(cmd->plugin)) return find_best_peer(cmd, OPT_ONION_MESSAGES, found_best_peer, offinfo); } @@ -661,7 +658,6 @@ struct command_result *json_invoicerequest(struct command *cmd, struct tlv_invoice_request *invreq; struct amount_msat *msat; bool *single_use; - struct node_id local_nodeid; invreq = tlv_invoice_request_new(cmd); @@ -732,8 +728,7 @@ struct command_result *json_invoicerequest(struct command *cmd, /* FIXME: We only set blinded path if private, we should allow * setting otherwise! */ - node_id_from_pubkey(&local_nodeid, &id); - if (!gossmap_find_node(get_gossmap(cmd->plugin), &local_nodeid)) { + if (we_want_blinded_path(cmd->plugin)) { struct invrequest_data *idata = tal(cmd, struct invrequest_data); idata->invreq = invreq; idata->single_use = *single_use; diff --git a/tests/test_pay.py b/tests/test_pay.py index a17f336c7..4a9cd0e57 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -5633,8 +5633,10 @@ def test_pay_partial_msat(node_factory, executor): def test_blindedpath_privchan(node_factory, bitcoind): l1, l2 = node_factory.line_graph(2, wait_for_announce=True, - opts={'may_reconnect': True}) - l3 = node_factory.get_node(options={'cltv-final': 120}, + opts={'may_reconnect': True, + 'dev-allow-localhost': None}) + l3 = node_factory.get_node(options={'cltv-final': 120, + 'dev-allow-localhost': None}, may_reconnect=True) # Private channel. @@ -5674,6 +5676,25 @@ def test_blindedpath_privchan(node_factory, bitcoind): l1.rpc.pay(inv['invoice']) +def test_blindedpath_noaddr(node_factory, bitcoind): + l1, l2 = node_factory.line_graph(2, wait_for_announce=True, + opts={'dev-allow-localhost': None}) + + # Another channel. + l3 = node_factory.get_node() + + node_factory.join_nodes([l2, l3], wait_for_announce=True) + # Make sure l3 knows about l1-l2, so will add route hint. + wait_for(lambda: l3.rpc.listnodes(l1.info['id']) != {'nodes': []}) + + offer = l3.rpc.offer(1000, 'test_pay_blindedpath_nodeaddr') + assert only_one(l1.rpc.decode(offer['bolt12'])['offer_paths'])['first_node_id'] == l2.info['id'] + + # But l2 has a public address, so doesn't bother. + offer = l2.rpc.offer(1000, 'test_pay_blindedpath_nodeaddr') + assert 'offer_paths' not in l1.rpc.decode(offer['bolt12']) + + def test_blinded_reply_path_scid(node_factory): """Check that we handle a blinded path which begins with a scid instead of a nodeid""" l1, l2 = node_factory.line_graph(2, wait_for_announce=True)