plugin/offers: connect if necessary for replying to invoice_request.

You can disable this with `fetchinvoice-noconnect`.

Changelog-EXPERIMENTAL: We will now reply to invoice_request messages even if reply path requires us to make an outgoing connection (LDK does this)
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2024-05-13 18:19:45 +09:30
parent 2e1274ba76
commit 8714289c8c
6 changed files with 105 additions and 32 deletions

View File

@ -753,8 +753,8 @@ corresponding functionality, which are in draft status ([bolt][bolt] #798) as [b
* **fetchinvoice-noconnect**
Specifying this prevents `fetchinvoice` and `sendinvoice` from
trying to connect directly to the offering node as a last resort.
Specifying this prevents `fetchinvoice`, `sendinvoice` and replying
to invoice request from trying to connect directly to the offering node as a last resort.
* **experimental-shutdown-wrong-funding**

View File

@ -4,6 +4,8 @@
#include <ccan/typesafe_cb/typesafe_cb.h>
#include <plugins/libplugin.h>
struct gossmap;
/**
* establish_onion_path: derive (or connect) a path to this peer.
* @cmd: the command context

View File

@ -31,6 +31,7 @@ struct pubkey id;
u32 blockheight;
u16 cltv_final;
bool offers_enabled;
bool disable_connect;
struct secret invoicesecret_base;
static struct gossmap *global_gossmap;
@ -84,8 +85,8 @@ static struct command_result *sendonionmessage_error(struct command *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)
bool convert_to_scidd(struct command *cmd,
struct sciddir_or_pubkey *sciddpk)
{
struct gossmap *gossmap = get_gossmap(cmd->plugin);
struct gossmap_chan *chan;
@ -1233,9 +1234,11 @@ static const char *init(struct plugin *p,
take(json_out_obj(NULL, NULL, NULL)),
"{configs:"
"{cltv-final:{value_int:%},"
"experimental-offers:{set:%}}}",
"experimental-offers:{set:%}},"
"fetchinvoice-noconnect?:{set:%}}",
JSON_SCAN(json_to_u16, &cltv_final),
JSON_SCAN(json_to_bool, &offers_enabled));
JSON_SCAN(json_to_bool, &offers_enabled),
JSON_SCAN(json_to_bool, &disable_connect));
rpc_scan(p, "makesecret",
take(json_out_obj(NULL, "string", INVOICE_PATH_BASE_STRING)),

View File

@ -5,6 +5,10 @@
struct command_result;
struct command;
/* If they give us an scid, do a lookup */
bool convert_to_scidd(struct command *cmd,
struct sciddir_or_pubkey *sciddpk);
/* Helper to send a reply */
struct command_result *WARN_UNUSED_RESULT
send_onion_reply(struct command *cmd,

View File

@ -5,15 +5,21 @@
#include <common/bech32_util.h>
#include <common/blindedpath.h>
#include <common/bolt12_merkle.h>
#include <common/gossmap.h>
#include <common/invoice_path_id.h>
#include <common/iso4217.h>
#include <common/json_stream.h>
#include <common/memleak.h>
#include <common/overflows.h>
#include <errno.h>
#include <plugins/establish_onion_path.h>
#include <plugins/offers.h>
#include <plugins/offers_invreq_hook.h>
#include <secp256k1_schnorrsig.h>
#include <sodium.h>
static struct gossmap *global_gossmap;
/* We need to keep the reply path around so we can reply with invoice */
struct invreq {
struct tlv_invoice_request *invreq;
@ -94,6 +100,31 @@ fail_internalerr(struct command *cmd,
return ret;
}
static void init_gossmap(struct plugin *plugin)
{
size_t num_cupdates_rejected;
global_gossmap
= notleak_with_children(gossmap_load(NULL,
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;
}
#define invreq_must_have(cmd_, ir_, fld_) \
test_field(cmd_, ir_, ir_->invreq->fld_ != NULL, #fld_, "missing")
#define invreq_must_not_have(cmd_, ir_, fld_) \
@ -995,23 +1026,16 @@ static struct command_result *listoffers_done(struct command *cmd,
return handle_amount_and_recurrence(cmd, ir, amt);
}
struct command_result *handle_invoice_request(struct command *cmd,
const u8 *invreqbin,
struct blinded_path *reply_path)
static struct command_result *invoice_request_path_done(struct command *cmd,
const struct pubkey *path,
struct invreq *ir)
{
size_t len = tal_count(invreqbin);
const u8 *cursor = invreqbin;
struct invreq *ir = tal(cmd, struct invreq);
struct out_req *req;
int bad_feature;
ir->reply_path = tal_steal(ir, reply_path);
ir->invreq = fromwire_tlv_invoice_request(cmd, &cursor, &len);
/* Now we can send error replies, because we will have connected. */
if (!ir->invreq) {
return fail_invreq(cmd, ir,
"Invalid invreq %s",
tal_hex(tmpctx, invreqbin));
return fail_invreq(cmd, ir, "Invalid invreq");
}
/* BOLT-offers #12:
@ -1025,20 +1049,6 @@ struct command_result *handle_invoice_request(struct command *cmd,
if (!ir->invreq->invreq_metadata)
return fail_invreq(cmd, ir, "Missing invreq_metadata");
/* BOLT-offers #12:
* The reader:
* ...
* - MUST fail the request if any non-signature TLV fields greater or
* equal to 160.
*/
/* BOLT-offers #12:
* Each form is signed using one or more *signature TLV elements*:
* TLV types 240 through 1000 (inclusive)
*/
if (tlv_span(invreqbin, 0, 159, NULL)
+ tlv_span(invreqbin, 240, 1000, NULL) != tal_bytelen(invreqbin))
return fail_invreq(cmd, ir, "Fields beyond 160");
/* BOLT-offers #12:
*
* The reader:
@ -1095,3 +1105,56 @@ struct command_result *handle_invoice_request(struct command *cmd,
return send_outreq(cmd->plugin, req);
}
static struct command_result *invoice_request_path_fail(struct command *cmd,
const char *why,
struct invreq *ir)
{
plugin_log(cmd->plugin, LOG_DBG,
"invoice_request path to %s failed: %s",
fmt_sciddir_or_pubkey(tmpctx, &ir->reply_path->first_node_id),
why);
return command_hook_success(cmd);
}
struct command_result *handle_invoice_request(struct command *cmd,
const u8 *invreqbin,
struct blinded_path *reply_path)
{
size_t len = tal_count(invreqbin);
const u8 *cursor = invreqbin;
struct invreq *ir = tal(cmd, struct invreq);
ir->reply_path = tal_steal(ir, reply_path);
ir->invreq = fromwire_tlv_invoice_request(cmd, &cursor, &len);
/* BOLT-offers #12:
* The reader:
* ...
* - MUST fail the request if any non-signature TLV fields greater or
* equal to 160.
*/
/* BOLT-offers #12:
* Each form is signed using one or more *signature TLV elements*:
* TLV types 240 through 1000 (inclusive)
*/
if (tlv_span(invreqbin, 0, 159, NULL)
+ tlv_span(invreqbin, 240, 1000, NULL) != tal_bytelen(invreqbin))
return fail_invreq(cmd, ir, "Fields beyond 160");
/* If they give us an sciddir, we need to convert now, to connect */
if (!reply_path->first_node_id.is_pubkey
&& !convert_to_scidd(cmd, &reply_path->first_node_id)) {
return fail_invreq(cmd, ir, "first_node_id %s not found",
fmt_sciddir_or_pubkey(tmpctx, &reply_path->first_node_id));
}
/* Before any failure, make sure we can reach first node! */
return establish_onion_path(cmd, get_gossmap(cmd->plugin), &id,
&reply_path->first_node_id.pubkey,
disable_connect ? "offers-noconnect" : NULL,
invoice_request_path_done,
invoice_request_path_fail,
ir);
}

View File

@ -7,6 +7,7 @@ extern u16 cltv_final;
extern u32 blockheight;
extern struct secret invoicesecret_base;
extern struct pubkey id;
extern bool disable_connect;
/* We got an onionmessage with an invreq! */
struct command_result *handle_invoice_request(struct command *cmd,