offers: handle scid in blinded reply path first_node_id field.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-EXPERIMENTAL: offers: we now understand blinded paths which use a short-channel-id(+direction) as entry point.
This commit is contained in:
Rusty Russell 2024-05-09 14:12:38 +09:30
parent cb2c4963f2
commit e338452c19
4 changed files with 106 additions and 10 deletions

View file

@ -212,7 +212,7 @@ $(PLUGIN_KEYSEND_OBJS): $(PLUGIN_PAY_LIB_HEADER)
plugins/spenderp: bitcoin/block.o bitcoin/preimage.o bitcoin/psbt.o common/psbt_open.o common/json_channel_type.o common/channel_type.o common/features.o wire/peer_wiregen.o $(PLUGIN_SPENDER_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS)
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 $(JSMN_OBJS)
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)
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

View file

@ -38,6 +38,9 @@ struct sent {
/* Path to use (including self) */
struct pubkey *path;
/* When creating blinded return path, use scid not pubkey for intro node. */
struct short_channel_id_dir *dev_path_use_scidd;
/* The invreq we sent, OR the invoice we sent */
struct tlv_invoice_request *invreq;
@ -597,6 +600,7 @@ send_modern_message(struct command *cmd,
static struct blinded_path *blinded_path(const tal_t *ctx,
struct command *cmd,
const struct pubkey *ids,
const struct short_channel_id_dir *first_scidd,
const struct secret *pathsecret)
{
struct privkey first_blinding, blinding_iter;
@ -608,7 +612,10 @@ static struct blinded_path *blinded_path(const tal_t *ctx,
nhops = tal_count(ids);
assert(nhops > 0);
sciddir_or_pubkey_from_pubkey(&path->first_node_id, &ids[0]);
if (first_scidd)
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));
randombytes_buf(&first_blinding, sizeof(first_blinding));
@ -673,7 +680,8 @@ static struct command_result *make_reply_path(struct command *cmd,
for (int i = nhops - 2; i >= 0; i--)
ids[nhops - 2 - i] = sending->sent->path[i];
rpath = blinded_path(cmd, cmd, ids, sending->sent->reply_secret);
rpath = blinded_path(cmd, cmd, ids, sending->sent->dev_path_use_scidd,
sending->sent->reply_secret);
return send_modern_message(cmd, rpath, sending);
}
@ -927,6 +935,22 @@ static struct command_result *invreq_done(struct command *cmd,
return send_outreq(cmd->plugin, req);
}
static struct command_result *param_dev_scidd(struct command *cmd, const char *name,
const char *buffer, const jsmntok_t *tok,
struct short_channel_id_dir **scidd)
{
if (!plugin_developer_mode(cmd->plugin))
return command_fail_badparam(cmd, name, buffer, tok,
"not available outside --developer mode");
*scidd = tal(cmd, struct short_channel_id_dir);
if (short_channel_id_dir_from_str(buffer + tok->start, tok->end - tok->start, *scidd))
return NULL;
return command_fail_badparam(cmd, name, buffer, tok,
"should be a short_channel_id of form NxNxN/dir");
}
/* Fetches an invoice for this offer, and makes sure it corresponds. */
static struct command_result *json_fetchinvoice(struct command *cmd,
const char *buffer,
@ -950,6 +974,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
p_opt("recurrence_label", param_string, &rec_label),
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),
NULL))
return command_param_failed();
@ -1405,6 +1430,8 @@ static struct command_result *json_sendinvoice(struct command *cmd,
NULL))
return command_param_failed();
sent->dev_path_use_scidd = NULL;
/* BOLT-offers #12:
* - if the invoice is in response to an `invoice_request`:
* - MUST copy all non-signature fields from the `invoice_request`
@ -1539,6 +1566,7 @@ static struct command_result *json_dev_rawrequest(struct command *cmd,
sent->wait_timeout = *timeout;
sent->cmd = cmd;
sent->offer = NULL;
sent->dev_path_use_scidd = NULL;
/* We temporarily abuse ->path to store nodeid! */
sent->path = node_id;

View file

@ -10,11 +10,14 @@
#include <common/bolt11.h>
#include <common/bolt11_json.h>
#include <common/bolt12_merkle.h>
#include <common/gossmap.h>
#include <common/invoice_path_id.h>
#include <common/iso4217.h>
#include <common/json_blinded_path.h>
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/memleak.h>
#include <errno.h>
#include <plugins/offers.h>
#include <plugins/offers_inv_hook.h>
#include <plugins/offers_invreq_hook.h>
@ -29,6 +32,32 @@ u32 blockheight;
u16 cltv_final;
bool offers_enabled;
struct secret invoicesecret_base;
static struct gossmap *global_gossmap;
static void init_gossmap(struct plugin *plugin)
{
size_t num_cupdates_rejected;
global_gossmap
= notleak_with_children(gossmap_load(plugin,
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 *finished(struct command *cmd,
const char *buf,
@ -52,6 +81,32 @@ static struct command_result *sendonionmessage_error(struct command *cmd,
return command_hook_success(cmd);
}
/* So, you gave us a reply scid? Let's do the lookup then! And no,
* we won't accept private channels, just public ones.
*/
static bool convert_to_scidd(struct command *cmd,
struct sciddir_or_pubkey *sciddpk)
{
struct gossmap *gossmap = get_gossmap(cmd->plugin);
struct gossmap_chan *chan;
struct gossmap_node *node;
struct node_id id;
chan = gossmap_find_chan(gossmap, &sciddpk->scidd.scid);
if (!chan)
return false;
node = gossmap_nth_node(gossmap, chan, sciddpk->scidd.dir);
gossmap_node_get_id(gossmap, node, &id);
if (!sciddir_or_pubkey_from_node_id(sciddpk, &id)) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Could not convert node %s to pubkey?",
fmt_node_id(tmpctx, &id));
return false;
}
return true;
}
struct command_result *
send_onion_reply(struct command *cmd,
struct blinded_path *reply_path,
@ -60,6 +115,13 @@ send_onion_reply(struct command *cmd,
struct out_req *req;
size_t nhops;
if (!reply_path->first_node_id.is_pubkey
&& !convert_to_scidd(cmd, &reply_path->first_node_id)) {
plugin_log(cmd->plugin, LOG_INFORM, "Unknown reply scid %s: cannot send reply",
fmt_short_channel_id_dir(tmpctx, &reply_path->first_node_id.scidd));
return command_hook_success(cmd);
}
req = jsonrpc_request_start(cmd->plugin, cmd, "sendonionmessage",
finished, sendonionmessage_error, NULL);
@ -111,13 +173,6 @@ static struct command_result *onion_message_modern_call(struct command *cmd,
plugin_err(cmd->plugin, "Invalid reply path %.*s?",
json_tok_full_len(replytok),
json_tok_full(buf, replytok));
/* FIXME: support this! */
if (!reply_path->first_node_id.is_pubkey) {
plugin_log(cmd->plugin, LOG_DBG,
"reply_blindedpath uses scid");
return command_hook_success(cmd);
}
}
invreqtok = json_get_member(buf, om, "invoice_request");

View file

@ -5577,6 +5577,19 @@ def test_pay_partial_msat(node_factory, executor):
l3pay.result(TIMEOUT)
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,
opts={'experimental-offers': None})
offer = l2.rpc.offer(amount='2msat', description='test_blinded_reply_path_scid')
chan = only_one(l1.rpc.listpeerchannels()['channels'])
scidd = "{}/{}".format(chan['short_channel_id'], chan['direction'])
inv = l1.rpc.fetchinvoice(offer=offer['bolt12'], dev_path_use_scidd=scidd)['invoice']
l1.rpc.pay(inv)
def test_pay_while_opening_channel(node_factory, bitcoind, executor):
delay_plugin = {'plugin': os.path.join(os.getcwd(),
'tests/plugins/openchannel_hook_delay.py'),