From 9aed5941775ef5de25e256a4fa76fa0cf3738a3c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 14 May 2024 14:01:26 +0930 Subject: [PATCH] pytest: test fetchinvoice reply path which is not a direct peer. Our fetchinvoice always creates a reply path which terminates at their peer, so we need a dev overrride for that. Signed-off-by: Rusty Russell --- plugins/fetchinvoice.c | 43 +++++++++++++++++++++++++++++++++++++----- tests/test_pay.py | 22 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index b7423b708..8fdf439ec 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -42,6 +42,9 @@ struct sent { /* When creating blinded return path, use scid not pubkey for intro node. */ struct short_channel_id_dir *dev_path_use_scidd; + /* Force reply path, for testing. */ + struct pubkey *dev_reply_path; + /* The invreq we sent, OR the invoice we sent */ struct tlv_invoice_request *invreq; @@ -603,11 +606,15 @@ static struct command_result *make_reply_path(struct command *cmd, sending->sent->reply_secret = tal(sending->sent, struct secret); randombytes_buf(sending->sent->reply_secret, sizeof(struct secret)); - /* FIXME: Could create an independent reply path, not just - * reverse existing. */ - ids = tal_arr(tmpctx, struct pubkey, nhops - 1); - for (int i = nhops - 2; i >= 0; i--) - ids[nhops - 2 - i] = sending->sent->path[i]; + if (sending->sent->dev_reply_path) { + ids = sending->sent->dev_reply_path; + } else { + /* FIXME: Could create an independent reply path, not just + * reverse existing. */ + ids = tal_arr(tmpctx, struct pubkey, nhops - 1); + for (int i = nhops - 2; i >= 0; i--) + ids[nhops - 2 - i] = sending->sent->path[i]; + } rpath = blinded_path(cmd, cmd, ids, sending->sent->dev_path_use_scidd, sending->sent->reply_secret); @@ -802,6 +809,29 @@ static struct command_result *param_dev_scidd(struct command *cmd, const char *n "should be a short_channel_id of form NxNxN/dir"); } +static struct command_result *param_dev_reply_path(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + struct pubkey **path) +{ + size_t i; + const jsmntok_t *t; + + if (!plugin_developer_mode(cmd->plugin)) + return command_fail_badparam(cmd, name, buffer, tok, + "not available outside --developer mode"); + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, "Must be array"); + + *path = tal_arr(cmd, struct pubkey, tok->size); + + json_for_each_arr(i, t, tok) { + if (!json_to_pubkey(buffer, t, &(*path)[i])) + return command_fail_badparam(cmd, name, buffer, t, "invalid pubkey"); + } + return NULL; +} + /* Fetches an invoice for this offer, and makes sure it corresponds. */ static struct command_result *json_fetchinvoice(struct command *cmd, const char *buffer, @@ -826,6 +856,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd, p_opt_def("timeout", param_number, &timeout, 60), p_opt("payer_note", param_string, &payer_note), p_opt("dev_path_use_scidd", param_dev_scidd, &sent->dev_path_use_scidd), + p_opt("dev_reply_path", param_dev_reply_path, &sent->dev_reply_path), NULL)) return command_param_failed(); @@ -1274,6 +1305,7 @@ static struct command_result *json_sendinvoice(struct command *cmd, return command_param_failed(); sent->dev_path_use_scidd = NULL; + sent->dev_reply_path = NULL; /* BOLT-offers #12: * - if the invoice is in response to an `invoice_request`: @@ -1392,6 +1424,7 @@ static struct command_result *json_dev_rawrequest(struct command *cmd, sent->cmd = cmd; sent->offer = NULL; sent->dev_path_use_scidd = NULL; + sent->dev_reply_path = NULL; return establish_onion_path(cmd, get_gossmap(cmd->plugin), &local_id, node_id, diff --git a/tests/test_pay.py b/tests/test_pay.py index b3e6dfcbf..7e25ff6af 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -4731,6 +4731,28 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind): assert l3.rpc.listpeers(l2.info['id'])['peers'] != [] +def test_fetchinvoice_disconnected_reply(node_factory, bitcoind): + """We ask for invoice, but reply path doesn't lead directly from recipient""" + l1, l2, l3 = node_factory.get_nodes(3, + opts={'experimental-offers': None, + 'may_reconnect': True, + 'dev-no-reconnect': None, + 'dev-allow-localhost': None}) + l3.rpc.connect(l2.info['id'], 'localhost', l2.port) + + # Make l1, l2 public (so l3 can auto connect). + node_factory.join_nodes([l1, l2], wait_for_announce=True) + wait_for(lambda: l3.rpc.listnodes(l1.info['id'])['nodes'] != []) + + offer = l3.rpc.offer(amount='5msat', description='test_fetchinvoice_disconnected_reply') + + # l2 sets reply_path to be l1->l2, l3 will connect to l1 to send reply. + # FIXME: if code were smarter, it would simply send via l2, and this test + # would have to get more sophisticated! + l2.rpc.fetchinvoice(offer=offer['bolt12'], dev_reply_path=[l1.info['id'], l2.info['id']]) + assert only_one(l1.rpc.listpeers(l3.info['id'])['peers']) + + def test_pay_blockheight_mismatch(node_factory, bitcoind): """Test that we can send a payment even if not caught up with the chain.