diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index c70c09b13..10f34db13 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -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** diff --git a/plugins/establish_onion_path.h b/plugins/establish_onion_path.h index ad2900111..d73586c3d 100644 --- a/plugins/establish_onion_path.h +++ b/plugins/establish_onion_path.h @@ -4,6 +4,8 @@ #include #include +struct gossmap; + /** * establish_onion_path: derive (or connect) a path to this peer. * @cmd: the command context diff --git a/plugins/offers.c b/plugins/offers.c index e041f158a..9b2fa26d5 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -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)), diff --git a/plugins/offers.h b/plugins/offers.h index dccdaff21..4ee32c6c6 100644 --- a/plugins/offers.h +++ b/plugins/offers.h @@ -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, diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 9f274fcc8..8b5d0ca8f 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -5,15 +5,21 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include #include #include #include +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); +} diff --git a/plugins/offers_invreq_hook.h b/plugins/offers_invreq_hook.h index 2259fe9b2..4dffdc90b 100644 --- a/plugins/offers_invreq_hook.h +++ b/plugins/offers_invreq_hook.h @@ -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,