plugins/offers: add dev-paths option for specifying offers blinded paths.

This will let us test, at least, as we implement fetching invoices from
them.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2024-07-17 12:53:25 +09:30 committed by Vincenzo Palazzo
parent 92eb84d45f
commit 27578d5c1d
4 changed files with 159 additions and 2 deletions

View file

@ -36,6 +36,7 @@ u16 cltv_final;
bool offers_enabled;
bool disable_connect;
struct secret invoicesecret_base;
struct secret offerblinding_base;
static struct gossmap *global_gossmap;
static void init_gossmap(struct plugin *plugin)
@ -1324,6 +1325,11 @@ static const char *init(struct plugin *p,
"{secret:%}",
JSON_SCAN(json_to_secret, &invoicesecret_base));
rpc_scan(p, "makesecret",
take(json_out_obj(NULL, "string", "offer-blinded-path")),
"{secret:%}",
JSON_SCAN(json_to_secret, &offerblinding_base));
return NULL;
}

View file

@ -17,8 +17,13 @@ extern bool disable_connect;
extern u16 cltv_final;
/* Current header_count */
extern u32 blockheight;
/* Basis for invoice secrets */
/* Basis for invoice path_secrets */
extern struct secret invoicesecret_base;
/* Base for offers path_secrets */
extern struct secret offerblinding_base;
/* This is me. */
extern struct pubkey id;
/* If they give us an scid, do a lookup */
bool convert_to_scidd(struct command *cmd,

View file

@ -3,9 +3,11 @@
#include <ccan/array_size/array_size.h>
#include <ccan/tal/str/str.h>
#include <common/bolt12.h>
#include <common/invoice_path_id.h>
#include <common/iso4217.h>
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/onion_message.h>
#include <common/overflows.h>
#include <plugins/offers.h>
#include <plugins/offers_offer.h>
@ -286,6 +288,105 @@ static struct command_result *currency_done(struct command *cmd,
return create_offer(cmd, offinfo);
}
static bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok,
struct short_channel_id_dir *scidd)
{
jsmntok_t scidtok, numtok;
u32 dir;
if (!split_tok(buffer, tok, '/', &scidtok, &numtok))
return false;
if (!json_to_short_channel_id(buffer, &scidtok, &scidd->scid))
return false;
if (!json_to_u32(buffer, &numtok, &dir) || (dir > 1))
return false;
scidd->dir = dir;
return true;
}
static bool json_to_sciddir_or_pubkey(const char *buffer, const jsmntok_t *tok,
struct sciddir_or_pubkey *sciddir_or_pubkey)
{
struct pubkey pk;
struct short_channel_id_dir scidd;
if (json_to_pubkey(buffer, tok, &pk)) {
sciddir_or_pubkey_from_pubkey(sciddir_or_pubkey, &pk);
return true;
} else if (json_to_short_channel_id_dir(buffer, tok, &scidd)) {
sciddir_or_pubkey_from_scidd(sciddir_or_pubkey, &scidd);
return true;
}
return false;
}
struct path {
/* Optional: a scid as the entry point */
struct short_channel_id_dir *first_scidd;
/* A node id for every element on the path */
struct pubkey *path;
};
static struct command_result *param_paths(struct command *cmd, const char *name,
const char *buffer, const jsmntok_t *tok,
struct path ***paths)
{
size_t i;
const jsmntok_t *t;
if (tok->type != JSMN_ARRAY)
return command_fail_badparam(cmd, name, buffer, tok, "Must be array");
*paths = tal_arr(cmd, struct path *, tok->size);
json_for_each_arr(i, t, tok) {
size_t j;
const jsmntok_t *p;
if (t->type != JSMN_ARRAY || t->size == 0) {
return command_fail_badparam(cmd, name, buffer, t,
"Must be array of non-empty arrays");
}
(*paths)[i] = tal(*paths, struct path);
(*paths)[i]->path = tal_arr((*paths)[i], struct pubkey, t->size);
json_for_each_arr(j, p, t) {
struct pubkey pk;
if (j == 0) {
struct sciddir_or_pubkey init;
if (!json_to_sciddir_or_pubkey(buffer, p, &init)) {
return command_fail_badparam(cmd, name, buffer, p,
"invalid pubkey/sciddir");
}
if (!init.is_pubkey) {
(*paths)[i]->first_scidd = tal_dup((*paths)[i],
struct short_channel_id_dir,
&init.scidd);
if (!convert_to_scidd(cmd, &init))
return command_fail_badparam(cmd, name, buffer, p,
"unknown sciddir");
} else {
(*paths)[i]->first_scidd = NULL;
}
pk = init.pubkey;
} else {
if (!json_to_pubkey(buffer, p, &pk)) {
return command_fail_badparam(cmd, name, buffer, p,
"invalid pubkey");
}
}
if (j == t->size - 1 && !pubkey_eq(&pk, &id))
return command_fail_badparam(cmd, name, buffer, p,
"final pubkey must be this node");
(*paths)[i]->path[j] = pk;
}
}
return NULL;
}
struct command_result *json_offer(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
@ -293,6 +394,7 @@ struct command_result *json_offer(struct command *cmd,
const char *desc, *issuer;
struct tlv_offer *offer;
struct offer_info *offinfo = tal(cmd, struct offer_info);
struct path **paths;
offinfo->offer = offer = tlv_offer_new(offinfo);
@ -318,7 +420,7 @@ struct command_result *json_offer(struct command *cmd,
p_opt("recurrence_start_any_period",
param_recurrence_start_any_period,
&offer->offer_recurrence_base),
/* FIXME: hints support! */
p_opt("dev_paths", param_paths, &paths),
NULL))
return command_param_failed();
@ -389,6 +491,32 @@ struct command_result *json_offer(struct command *cmd,
*/
offer->offer_node_id = tal_dup(offer, struct pubkey, &id);
/* Now rest of offer will not change: we use pathless offer to create secret. */
if (paths) {
const u8 *path_secret;
struct secret blinding_path_secret;
struct sha256 offer_id;
/* Note: "id" of offer minus paths */
offer_offer_id(offer, &offer_id);
/* We can check this when they try to take up offer. */
path_secret = invoice_path_id(tmpctx, &offerblinding_base, &offer_id);
assert(tal_count(path_secret) == sizeof(blinding_path_secret));
memcpy(&blinding_path_secret, path_secret, sizeof(blinding_path_secret));
offer->offer_paths = tal_arr(offer, struct blinded_path *, tal_count(paths));
for (size_t i = 0; i < tal_count(paths); i++) {
offer->offer_paths[i] = incoming_message_blinded_path(offer->offer_paths,
paths[i]->path,
NULL,
&blinding_path_secret);
/* Override entry point if they said to */
if (paths[i]->first_scidd)
sciddir_or_pubkey_from_scidd(&offer->offer_paths[i]->first_node_id,
paths[i]->first_scidd);
}
}
/* If they specify a different currency, warn if we can't
* convert it! */
if (offer->offer_currency) {

View file

@ -5674,6 +5674,24 @@ def test_pay_while_opening_channel(node_factory, bitcoind, executor):
l1.rpc.pay(inv['bolt11'])
def test_offer_paths(node_factory):
# Need to announce channels to use their scid in offers anyway!
l1, l2, l3 = node_factory.line_graph(3,
opts={'experimental-offers': None},
wait_for_announce=True)
chan = only_one(l1.rpc.listpeerchannels()['channels'])
scidd = "{}/{}".format(chan['short_channel_id'], chan['direction'])
offer = l2.rpc.offer(amount='100sat', description='test_offer_paths',
dev_paths=[[scidd, l2.info['id']], [l3.info['id'], l2.info['id']]])
paths = l1.rpc.decode(offer['bolt12'])['offer_paths']
assert len(paths) == 2
assert paths[0]['first_scid'] == chan['short_channel_id']
assert paths[0]['first_scid_dir'] == chan['direction']
assert paths[1]['first_node_id'] == l3.info['id']
def test_pay_legacy_forward(node_factory, bitcoind, executor):
"""We removed legacy in 22.11, and LND will still send them for
route hints! See