From f00f832b96b5faa63d7bb7103293d4f7c44bc0a9 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 17 Jul 2024 18:36:11 +0930 Subject: [PATCH] plugins/pay: pay to invoices where first hop is a short_channel_id_dir. Changelog-Added: Protocol: pay can now pay to bolt12 invoices if entry to blinded hop is specified as a short_channel_id (rather than node id). Signed-off-by: Rusty Russell --- plugins/libplugin-pay.c | 8 +++++++- plugins/libplugin-pay.h | 3 +++ plugins/offers.c | 4 ++++ plugins/offers.h | 3 ++- plugins/offers_invreq_hook.c | 21 +++++++++++++++++++++ plugins/pay.c | 8 +++++--- tests/test_pay.py | 23 +++++++++++++++++++++-- 7 files changed, 63 insertions(+), 7 deletions(-) diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index bde4d9a3b..c7db43a50 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -35,13 +35,19 @@ static void init_gossmap(struct plugin *plugin) num_channel_updates_rejected); } -static struct gossmap *get_gossmap(struct payment *payment) +struct gossmap *get_raw_gossmap(struct payment *payment) { assert(!got_gossmap); if (!global_gossmap) init_gossmap(payment->plugin); else gossmap_refresh(global_gossmap, NULL); + return global_gossmap; +} + +static struct gossmap *get_gossmap(struct payment *payment) +{ + get_raw_gossmap(payment); got_gossmap = true; assert(payment->mods); gossmap_apply_localmods(global_gossmap, payment->mods); diff --git a/plugins/libplugin-pay.h b/plugins/libplugin-pay.h index 9d2d92cb2..c2f918399 100644 --- a/plugins/libplugin-pay.h +++ b/plugins/libplugin-pay.h @@ -494,6 +494,9 @@ void payment_abort(struct payment *p, const char *fmt, ...) PRINTF_FMT(2,3); struct payment *payment_root(struct payment *p); struct payment_tree_result payment_collect_result(struct payment *p); +/* If you need an unmodified gossmap */ +struct gossmap *get_raw_gossmap(struct payment *payment); + /* Add fields for successful payment: result can be NULL for selfpay */ void json_add_payment_success(struct json_stream *js, struct payment *p, diff --git a/plugins/offers.c b/plugins/offers.c index d29a6c83d..6e27ae073 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -35,6 +35,7 @@ u32 blockheight; u16 cltv_final; bool offers_enabled; bool disable_connect; +bool dev_invoice_bpath_scid; struct secret invoicesecret_base; struct secret offerblinding_base; static struct gossmap *global_gossmap; @@ -1451,5 +1452,8 @@ int main(int argc, char *argv[]) plugin_option("fetchinvoice-noconnect", "flag", "Don't try to connect directly to fetch/pay an invoice.", flag_option, flag_jsonfmt, &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), NULL); } diff --git a/plugins/offers.h b/plugins/offers.h index f22d8e313..b7a821f06 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -21,7 +21,8 @@ extern u32 blockheight; extern struct secret invoicesecret_base; /* Base for offers path_secrets */ extern struct secret offerblinding_base; - +/* --dev-invoice-bpath-scid */ +extern bool dev_invoice_bpath_scid; /* This is me. */ extern struct pubkey id; diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 8d378bc3c..201959d3e 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -304,6 +304,27 @@ static struct command_result *found_best_peer(struct command *cmd, cast_const2(const struct tlv_encrypted_data_tlv **, etlvs), ids); + /* 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) { + struct gossmap *gossmap = get_gossmap(cmd->plugin); + struct node_id best_nodeid; + const struct gossmap_node *n; + const struct gossmap_chan *c; + struct short_channel_id_dir scidd; + + node_id_from_pubkey(&best_nodeid, &best->id); + n = gossmap_find_node(gossmap, &best_nodeid); + c = gossmap_nth_chan(gossmap, n, 0, &scidd.dir); + + scidd.scid = gossmap_chan_scid(gossmap, c); + sciddir_or_pubkey_from_scidd(&ir->inv->invoice_paths[0]->first_node_id, + &scidd); + plugin_log(cmd->plugin, LOG_DBG, "dev_invoice_bpath_scid: start is %s", + fmt_sciddir_or_pubkey(tmpctx, + &ir->inv->invoice_paths[0]->first_node_id)); + } + /* FIXME: This should be a "normal" feerate and range. */ ir->inv->invoice_blindedpay = tal_arr(ir->inv, struct blinded_payinfo *, 1); ir->inv->invoice_blindedpay[0] = tal(ir->inv->invoice_blindedpay, struct blinded_payinfo); diff --git a/plugins/pay.c b/plugins/pay.c index e42ca465e..fe3f1838f 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -1289,10 +1289,12 @@ static struct command_result *json_pay(struct command *cmd, /* FIXME: do MPP across these! We choose first one. */ p->blindedpath = tal_steal(p, b12->invoice_paths[0]); p->blindedpay = tal_steal(p, b12->invoice_blindedpay[0]); - /* FIXME: support this! */ - if (!p->blindedpath->first_node_id.is_pubkey) { + + if (!gossmap_scidd_pubkey(get_raw_gossmap(p), &p->blindedpath->first_node_id)) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "First hop of blinding is an scid: not supported!"); + "First hop of blinding scid %s unknown", + fmt_short_channel_id_dir(tmpctx, + &p->blindedpath->first_node_id.scidd)); } p->min_final_cltv_expiry = p->blindedpay->cltv_expiry_delta; diff --git a/tests/test_pay.py b/tests/test_pay.py index 1ab00478d..9adda4d33 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -5624,9 +5624,11 @@ 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={'experimental-offers': None}) + opts={'experimental-offers': None, + 'may_reconnect': True}) l3 = node_factory.get_node(options={'experimental-offers': None, - 'cltv-final': 120}) + 'cltv-final': 120}, + may_reconnect=True) # Private channel. node_factory.join_nodes([l2, l3], announce_channels=False) @@ -5647,6 +5649,23 @@ def test_blindedpath_privchan(node_factory, bitcoind): l1.rpc.pay(inv['invoice']) + # Now try when l3 uses scid for entry point of blinded path. + l3.stop() + l3.daemon.opts['dev-invoice-bpath-scid'] = None + l3.start() + l3.rpc.connect(l2.info['id'], 'localhost', l2.port) + + chan = only_one(l1.rpc.listchannels(source=l2.info['id'])['channels']) + + inv = l2.rpc.fetchinvoice(offer['bolt12']) + decode = l1.rpc.decode(inv['invoice']) + assert len(decode['invoice_paths']) == 1 + assert 'first_node_id' not in decode['invoice_paths'][0] + assert decode['invoice_paths'][0]['first_scid'] == chan['short_channel_id'] + assert decode['invoice_paths'][0]['first_scid_dir'] == chan['direction'] + + l1.rpc.pay(inv['invoice']) + def test_blinded_reply_path_scid(node_factory): """Check that we handle a blinded path which begins with a scid instead of a nodeid"""