diff --git a/devtools/bolt12-cli.c b/devtools/bolt12-cli.c index 9547900be..58ea2d21b 100644 --- a/devtools/bolt12-cli.c +++ b/devtools/bolt12-cli.c @@ -349,10 +349,10 @@ static bool print_recurrence_counter_with_base(const u32 *recurrence_counter, return true; } -static void print_payment_hash(const struct sha256 *payment_hash) +static void print_hash(const char *fieldname, const struct sha256 *hash) { - printf("invoice_payment_hash: %s\n", - type_to_string(tmpctx, struct sha256, payment_hash)); + printf("%s: %s\n", + fieldname, type_to_string(tmpctx, struct sha256, hash)); } static void print_relative_expiry(u64 *created_at, u32 *relative) @@ -511,12 +511,15 @@ int main(int argc, char *argv[]) } if (streq(hrp, "lno")) { + struct sha256 offer_id; const struct tlv_offer *offer = offer_decode(ctx, argv[2], strlen(argv[2]), NULL, NULL, &fail); if (!offer) errx(ERROR_BAD_DECODE, "Bad offer: %s", fail); + offer_offer_id(offer, &offer_id); + print_hash("offer_id", &offer_id); if (offer->offer_chains) print_offer_chains(offer->offer_chains); if (offer->offer_amount) @@ -545,12 +548,20 @@ int main(int argc, char *argv[]) if (!print_extra_fields(offer->fields)) well_formed = false; } else if (streq(hrp, "lnr")) { + struct sha256 offer_id, invreq_id; const struct tlv_invoice_request *invreq = invrequest_decode(ctx, argv[2], strlen(argv[2]), NULL, NULL, &fail); if (!invreq) errx(ERROR_BAD_DECODE, "Bad invreq: %s", fail); + if (invreq->offer_node_id) { + invreq_offer_id(invreq, &offer_id); + print_hash("offer_id", &offer_id); + } + invreq_invreq_id(invreq, &invreq_id); + print_hash("invreq_id", &invreq_id); + /* FIXME: We can do more intra-field checking! */ if (must_have(invreq, invreq_metadata)) print_hex("invreq_metadata", invreq->invreq_metadata); @@ -572,7 +583,7 @@ int main(int argc, char *argv[]) well_formed &= print_utf8("offer_issuer", invreq->offer_issuer); if (invreq->offer_quantity_max) print_u64("offer_quantity_max", *invreq->offer_quantity_max); - if (must_have(invreq, offer_node_id)) + if (invreq->offer_node_id) print_node_id("offer_node_id", invreq->offer_node_id); if (invreq->offer_recurrence) well_formed &= print_recurrance(invreq->offer_recurrence, @@ -607,12 +618,22 @@ int main(int argc, char *argv[]) if (!print_extra_fields(invreq->fields)) well_formed = false; } else if (streq(hrp, "lni")) { + struct sha256 offer_id, invreq_id; const struct tlv_invoice *invoice = invoice_decode(ctx, argv[2], strlen(argv[2]), NULL, NULL, &fail); if (!invoice) errx(ERROR_BAD_DECODE, "Bad invoice: %s", fail); + if (invoice->invreq_payer_id) { + if (invoice->offer_node_id) { + invoice_offer_id(invoice, &offer_id); + print_hash("offer_id", &offer_id); + } + invoice_invreq_id(invoice, &invreq_id); + print_hash("invreq_id", &invreq_id); + } + /* FIXME: We can do more intra-field checking! */ if (must_have(invoice, invreq_metadata)) print_hex("invreq_metadata", invoice->invreq_metadata); @@ -670,7 +691,7 @@ int main(int argc, char *argv[]) print_relative_expiry(invoice->invoice_created_at, invoice->invoice_relative_expiry); if (must_have(invoice, invoice_payment_hash)) - print_payment_hash(invoice->invoice_payment_hash); + print_hash("invoice_payment_hash", invoice->invoice_payment_hash); if (must_have(invoice, invoice_amount)) print_msat("invoice_amount", *invoice->invoice_amount); if (invoice->invoice_fallbacks) diff --git a/lightningd/offer.c b/lightningd/offer.c index 89996e586..4f1975cfe 100644 --- a/lightningd/offer.c +++ b/lightningd/offer.c @@ -396,7 +396,7 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, struct json_stream *response; u64 *prev_basetime = NULL; struct sha256 merkle; - bool *save, *single_use; + bool *save, *single_use, *exposeid; enum offer_status status; struct sha256 invreq_id; const char *b12str; @@ -404,6 +404,7 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, if (!param(cmd, buffer, params, p_req("bolt12", param_b12_invreq, &invreq), p_req("savetodb", param_bool, &save), + p_opt_def("exposeid", param_bool, &exposeid, false), p_opt("recurrence_label", param_label, &label), p_opt_def("single_use", param_bool, &single_use, true), NULL)) @@ -449,10 +450,14 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, } invreq->invreq_payer_id = tal(invreq, struct pubkey); - if (!payer_key(cmd->ld, - invreq->invreq_metadata, - tal_bytelen(invreq->invreq_metadata), - invreq->invreq_payer_id)) { + if (*exposeid) { + if (!pubkey_from_node_id(invreq->invreq_payer_id, + &cmd->ld->id)) + fatal("Our ID is invalid?"); + } else if (!payer_key(cmd->ld, + invreq->invreq_metadata, + tal_bytelen(invreq->invreq_metadata), + invreq->invreq_payer_id)) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Invalid tweak"); } @@ -466,8 +471,8 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, merkle_tlv(invreq->fields, &merkle); invreq->signature = tal(invreq, struct bip340sig); hsm_sign_b12(cmd->ld, "invoice_request", "signature", - &merkle, invreq->invreq_metadata, invreq->invreq_payer_id, - invreq->signature); + &merkle, *exposeid ? NULL : invreq->invreq_metadata, + invreq->invreq_payer_id, invreq->signature); b12str = invrequest_encode(cmd, invreq); diff --git a/plugins/Makefile b/plugins/Makefile index d56827285..87791c906 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -31,13 +31,13 @@ PLUGIN_PAY_LIB_SRC := plugins/libplugin-pay.c PLUGIN_PAY_LIB_HEADER := plugins/libplugin-pay.h PLUGIN_PAY_LIB_OBJS := $(PLUGIN_PAY_LIB_SRC:.c=.o) -PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c +PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c plugins/offers_inv_hook.c PLUGIN_OFFERS_OBJS := $(PLUGIN_OFFERS_SRC:.c=.o) PLUGIN_OFFERS_HEADER := $(PLUGIN_OFFERS_SRC:.c=.h) PLUGIN_FETCHINVOICE_SRC := plugins/fetchinvoice.c PLUGIN_FETCHINVOICE_OBJS := $(PLUGIN_FETCHINVOICE_SRC:.c=.o) -PLUGIN_FETCHINVOICE_HEADER := +PLUGIN_FETCHINVOICE_HEADER := PLUGIN_SPENDER_SRC := \ plugins/spender/fundchannel.c \ diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index 972043c92..7870268b6 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -707,6 +707,32 @@ static struct command_result *send_message(struct command *cmd, return make_reply_path(cmd, sending); } +/* We've received neither a reply nor a payment; return failure. */ +static void timeout_sent_inv(struct sent *sent) +{ + struct json_out *details = json_out_new(sent); + + json_out_start(details, NULL, '{'); + json_out_addstr(details, "invstring", invoice_encode(tmpctx, sent->inv)); + json_out_end(details, '}'); + + /* This will free sent! */ + discard_result(command_done_err(sent->cmd, OFFER_TIMEOUT, + "Failed: timeout waiting for response", + details)); +} + +static struct command_result *prepare_inv_timeout(struct command *cmd, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + struct sent *sent) +{ + tal_steal(cmd, plugin_timer(cmd->plugin, + time_from_sec(sent->wait_timeout), + timeout_sent_inv, sent)); + return sendonionmsg_done(cmd, buf, result, sent); +} + /* We've connected (if we tried), so send the invreq. */ static struct command_result * sendinvreq_after_connect(struct command *cmd, @@ -944,7 +970,7 @@ force_payer_secret(struct command *cmd, } sent->path = path_to_node(sent, cmd->plugin, - sent->invreq->offer_node_id); + sent->invreq->invreq_payer_id); if (!sent->path) return connect_direct(cmd, sent->invreq->offer_node_id, sendinvreq_after_connect, sent); @@ -1198,7 +1224,83 @@ static struct command_result *invoice_payment(struct command *cmd, return command_hook_success(cmd); } -#if DEVELOPER +/* We've connected (if we tried), so send the invoice. */ +static struct command_result * +sendinvoice_after_connect(struct command *cmd, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + struct sent *sent) +{ + struct tlv_onionmsg_tlv *payload = tlv_onionmsg_tlv_new(sent); + + payload->invoice = tal_arr(payload, u8, 0); + towire_tlv_invoice(&payload->invoice, sent->inv); + + return send_message(cmd, sent, payload, prepare_inv_timeout); +} + +static struct command_result *createinvoice_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct sent *sent) +{ + const jsmntok_t *invtok = json_get_member(buf, result, "bolt12"); + char *fail; + + /* Replace invoice with signed one */ + tal_free(sent->inv); + sent->inv = invoice_decode(sent, + buf + invtok->start, + invtok->end - invtok->start, + plugin_feature_set(cmd->plugin), + chainparams, + &fail); + if (!sent->inv) { + plugin_log(cmd->plugin, LOG_BROKEN, + "Bad createinvoice %.*s: %s", + json_tok_full_len(invtok), + json_tok_full(buf, invtok), + fail); + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Bad createinvoice response %s", fail); + } + + /* BOLT-offers #12: + * - if it sends an invoice in response: + * - MUST use `offer_paths` if present, otherwise MUST use + * `invreq_payer_id` as the node id to send to. + */ + /* FIXME! */ + if (sent->invreq->offer_paths) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "FIXME: support blinded paths!"); + } + + sent->path = path_to_node(sent, cmd->plugin, + sent->invreq->invreq_payer_id); + if (!sent->path) + return connect_direct(cmd, sent->invreq->invreq_payer_id, + sendinvoice_after_connect, sent); + + return sendinvoice_after_connect(cmd, NULL, NULL, sent); +} + +static struct command_result *sign_invoice(struct command *cmd, + struct sent *sent) +{ + struct out_req *req; + + /* Get invoice signature and put in db so we can receive payment */ + req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoice", + &createinvoice_done, + &forward_error, + sent); + json_add_string(req->js, "invstring", invoice_encode(tmpctx, sent->inv)); + json_add_preimage(req->js, "preimage", &sent->inv_preimage); + json_add_escaped_string(req->js, "label", sent->inv_label); + return send_outreq(cmd->plugin, req); +} + static struct command_result *param_invreq(struct command *cmd, const char *name, const char *buffer, @@ -1206,6 +1308,231 @@ static struct command_result *param_invreq(struct command *cmd, struct tlv_invoice_request **invreq) { char *fail; + int badf; + u8 *wire; + struct sha256 merkle, sighash; + + /* BOLT-offers #12: + * - if `invreq_chain` is not present: + * - MUST fail the request if bitcoin is not a supported chain. + * - otherwise: + * - MUST fail the request if `invreq_chain`.`chain` is not a + * supported chain. + */ + *invreq = invrequest_decode(cmd, + buffer + tok->start, tok->end - tok->start, + plugin_feature_set(cmd->plugin), + chainparams, + &fail); + if (!*invreq) + return command_fail_badparam(cmd, name, buffer, tok, + tal_fmt(cmd, + "Unparsable invoice_request: %s", + fail)); + /* BOLT-offers #12: + * The reader: + * - MUST fail the request if `invreq_payer_id` or `invreq_metadata` + * are not present. + * - MUST fail the request if any non-signature TLV fields greater or + * equal to 160. + * - if `invreq_features` contains unknown _odd_ bits that are + * non-zero: + * - MUST ignore the bit. + * - if `invreq_features` contains unknown _even_ bits that are + * non-zero: + * - MUST fail the request. + */ + if (!(*invreq)->invreq_payer_id) + return command_fail_badparam(cmd, name, buffer, tok, + "Missing invreq_payer_id"); + + if (!(*invreq)->invreq_metadata) + return command_fail_badparam(cmd, name, buffer, tok, + "Missing invreq_metadata"); + + wire = tal_arr(tmpctx, u8, 0); + towire_tlv_invoice_request(&wire, *invreq); + if (tlv_span(wire, 160, 239, NULL) != 0 + || tlv_span(wire, 1001, UINT64_MAX, NULL) != 0) { + return command_fail_badparam(cmd, name, buffer, tok, + "Invalid high-numbered fields"); + } + + badf = features_unsupported(plugin_feature_set(cmd->plugin), + (*invreq)->invreq_features, + BOLT12_INVREQ_FEATURE); + if (badf != -1) { + return command_fail_badparam(cmd, name, buffer, tok, + tal_fmt(tmpctx, + "unknown feature %i", + badf)); + } + + /* BOLT-offers #12: + * - MUST fail the request if `signature` is not correct as detailed in [Signature + * Calculation](#signature-calculation) using the `invreq_payer_id`. + */ + merkle_tlv((*invreq)->fields, &merkle); + sighash_from_merkle("invoice_request", "signature", &merkle, &sighash); + + if (!(*invreq)->signature) + return command_fail_badparam(cmd, name, buffer, tok, + "Missing signature"); + if (!check_schnorr_sig(&sighash, + &(*invreq)->invreq_payer_id->pubkey, + (*invreq)->signature)) + return command_fail_badparam(cmd, name, buffer, tok, + "Invalid signature"); + + /* Plugin handles these automatically, you shouldn't send one + * manually. */ + if ((*invreq)->offer_node_id) { + return command_fail_badparam(cmd, name, buffer, tok, + "This is based on an offer?"); + } + + /* BOLT-offers #12: + * - otherwise (no `offer_node_id`, not a response to our offer): + * - MUST fail the request if any of the following are present: + * - `offer_chains`, `offer_features` or `offer_quantity_max`. + * - MUST fail the request if `invreq_amount` is not present. + */ + if ((*invreq)->offer_chains) + return command_fail_badparam(cmd, name, buffer, tok, + "Unexpected offer_chains"); + if ((*invreq)->offer_features) + return command_fail_badparam(cmd, name, buffer, tok, + "Unexpected offer_features"); + if ((*invreq)->offer_quantity_max) + return command_fail_badparam(cmd, name, buffer, tok, + "Unexpected offer_quantity_max"); + if (!(*invreq)->invreq_amount) + return command_fail_badparam(cmd, name, buffer, tok, + "Missing invreq_amount"); + + /* BOLT-offers #12: + * - otherwise (no `offer_node_id`, not a response to our offer): + *... + * - MAY use `offer_amount` (or `offer_currency`) for informational display to user. + */ + if ((*invreq)->offer_amount && (*invreq)->offer_currency) { + plugin_notify_message(cmd, LOG_INFORM, + "invoice_request offers %.*s%"PRIu64" as %s", + (int)tal_bytelen((*invreq)->offer_currency), + (*invreq)->offer_currency, + *(*invreq)->offer_amount, + fmt_amount_msat(tmpctx, + amount_msat(*(*invreq)->invreq_amount))); + } + return NULL; +} + +static struct command_result *json_sendinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct amount_msat *msat; + u32 *timeout; + struct sent *sent = tal(cmd, struct sent); + + sent->offer = NULL; + sent->cmd = cmd; + + /* FIXME: Support recurring invoice_requests? */ + if (!param(cmd, buffer, params, + p_req("invreq", param_invreq, &sent->invreq), + p_req("label", param_label, &sent->inv_label), + p_opt("amount_msat", param_msat, &msat), + p_opt_def("timeout", param_number, &timeout, 90), + NULL)) + return command_param_failed(); + + /* BOLT-offers #12: + * The writer: + * - MUST copy all non-signature fields from the invreq (including + * unknown fields). + */ + sent->inv = invoice_for_invreq(sent, sent->invreq); + + /* This is how long we'll wait for a reply for. */ + sent->wait_timeout = *timeout; + + /* BOLT-offers #12: + * - if `invreq_amount` is present: + * - MUST set `invoice_amount` to `invreq_amount` + * - otherwise: + * - MUST set `invoice_amount` to the *expected amount*. + */ + if (!msat) + sent->inv->invoice_amount = tal_dup(sent->inv, u64, + sent->invreq->invreq_amount); + else + sent->inv->invoice_amount = tal_dup(sent->inv, u64, + &msat->millisatoshis); /* Raw: tlv */ + + /* BOLT-offers #12: + * - MUST set `invoice_created_at` to the number of seconds since Midnight 1 + * January 1970, UTC when the offer was created. + * - MUST set `invoice_amount` to the minimum amount it will accept, in units of + * the minimal lightning-payable unit (e.g. milli-satoshis for bitcoin) for + * `invreq_chain`. + */ + sent->inv->invoice_created_at = tal(sent->inv, u64); + *sent->inv->invoice_created_at = time_now().ts.tv_sec; + + /* FIXME: Support blinded paths, in which case use fake nodeid */ + + /* BOLT-offers #12: + * - MUST set `invoice_payment_hash` to the SHA256 hash of the + * `payment_preimage` that will be given in return for payment. + */ + randombytes_buf(&sent->inv_preimage, sizeof(sent->inv_preimage)); + sent->inv->invoice_payment_hash = tal(sent->inv, struct sha256); + sha256(sent->inv->invoice_payment_hash, + &sent->inv_preimage, sizeof(sent->inv_preimage)); + + /* BOLT-offers #12: + * - if `offer_node_id` is present: + * - MUST set `invoice_node_id` to `offer_node_id`. + * - otherwise: + * - MUST set `invoice_node_id` to a valid public key. + */ + /* FIXME: Use transitory id! */ + sent->inv->invoice_node_id = tal(sent->inv, struct pubkey); + sent->inv->invoice_node_id->pubkey = local_id.pubkey; + + /* BOLT-offers #12: + * - if the expiry for accepting payment is not 7200 seconds + * after `invoice_created_at`: + * - MUST set `invoice_relative_expiry`.`seconds_from_creation` + * to the number of seconds after `invoice_created_at` that + * payment of this invoice should not be attempted. + */ + if (sent->wait_timeout != 7200) { + sent->inv->invoice_relative_expiry = tal(sent->inv, u32); + *sent->inv->invoice_relative_expiry = sent->wait_timeout; + } + + /* FIXME: recurrence? */ + if (sent->inv->offer_recurrence) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "FIXME: handle recurring invreq?"); + + sent->inv->invoice_features + = plugin_feature_set(cmd->plugin)->bits[BOLT12_INVOICE_FEATURE]; + + return sign_invoice(cmd, sent); +} + +#if DEVELOPER +/* This version doesn't do sanity checks! */ +static struct command_result *param_raw_invreq(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct tlv_invoice_request **invreq) +{ + char *fail; *invreq = invrequest_decode(cmd, buffer + tok->start, tok->end - tok->start, plugin_feature_set(cmd->plugin), chainparams, @@ -1227,7 +1554,7 @@ static struct command_result *json_rawrequest(struct command *cmd, struct pubkey *node_id; if (!param(cmd, buffer, params, - p_req("invreq", param_invreq, &sent->invreq), + p_req("invreq", param_raw_invreq, &sent->invreq), p_req("nodeid", param_pubkey, &node_id), p_opt_def("timeout", param_number, &timeout, 60), NULL)) @@ -1256,6 +1583,13 @@ static const struct plugin_command commands[] = { NULL, json_fetchinvoice, }, + { + "sendinvoice", + "payment", + "Request remote node for to pay this {invreq}, with {label}, optional {amount_msat}, and {timeout} (default 90 seconds).", + NULL, + json_sendinvoice, + }, #if DEVELOPER { "dev-rawrequest", diff --git a/plugins/offers.c b/plugins/offers.c index 89e0a3340..59e7aeb8f 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -89,7 +90,7 @@ static struct command_result *onion_message_modern_call(struct command *cmd, const char *buf, const jsmntok_t *params) { - const jsmntok_t *om, *replytok, *invreqtok; + const jsmntok_t *om, *replytok, *invreqtok, *invtok; struct blinded_path *reply_path = NULL; if (!offers_enabled) @@ -117,6 +118,13 @@ static struct command_result *onion_message_modern_call(struct command *cmd, "invoice_request without reply_path"); } + invtok = json_get_member(buf, om, "invoice"); + if (invtok) { + const u8 *invbin = json_tok_bin_from_hex(tmpctx, buf, invtok); + if (invbin) + return handle_invoice(cmd, invbin, reply_path); + } + return command_hook_success(cmd); } @@ -1070,6 +1078,13 @@ static const struct plugin_command commands[] = { "Create an offer for invoices of {amount} with {description}, optional {issuer}, internal {label}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use}", json_offer }, + { + "invoicerequest", + "payment", + "Create an invoice_request to send money", + "Create an invoice_request to pay invoices of {amount} with {description}, optional {issuer}, internal {label}, and {absolute_expiry}", + json_invoicerequest + }, { "decode", "utility", diff --git a/plugins/offers_inv_hook.c b/plugins/offers_inv_hook.c new file mode 100644 index 000000000..ebe43977a --- /dev/null +++ b/plugins/offers_inv_hook.c @@ -0,0 +1,326 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* We need to keep the reply path around so we can reply if error */ +struct inv { + struct tlv_invoice *inv; + struct sha256 invreq_id; + + /* May be NULL */ + struct blinded_path *reply_path; + + /* The invreq, once we've looked it up. */ + struct tlv_invoice_request *invreq; +}; + +static struct command_result *WARN_UNUSED_RESULT +fail_inv_level(struct command *cmd, + const struct inv *inv, + enum log_level l, + const char *fmt, va_list ap) +{ + char *full_fmt, *msg; + struct tlv_onionmsg_tlv *payload; + struct tlv_invoice_error *err; + + full_fmt = tal_fmt(tmpctx, "Failed invoice"); + if (inv->inv) { + tal_append_fmt(&full_fmt, " %s", + invoice_encode(tmpctx, inv->inv)); + } + tal_append_fmt(&full_fmt, ": %s", fmt); + + msg = tal_vfmt(tmpctx, full_fmt, ap); + plugin_log(cmd->plugin, l, "%s", msg); + + /* Only reply if they gave us a path */ + if (!inv->reply_path) + return command_hook_success(cmd); + + /* Don't send back internal error details. */ + if (l == LOG_BROKEN) + msg = "Internal error"; + + err = tlv_invoice_error_new(cmd); + /* Remove NUL terminator */ + err->error = tal_dup_arr(err, char, msg, strlen(msg), 0); + /* FIXME: Add suggested_value / erroneous_field! */ + + payload = tlv_onionmsg_tlv_new(tmpctx); + payload->invoice_error = tal_arr(payload, u8, 0); + towire_tlv_invoice_error(&payload->invoice_error, err); + return send_onion_reply(cmd, inv->reply_path, payload); +} + +static struct command_result *WARN_UNUSED_RESULT +fail_inv(struct command *cmd, + const struct inv *inv, + const char *fmt, ...) +{ + va_list ap; + struct command_result *ret; + + va_start(ap, fmt); + ret = fail_inv_level(cmd, inv, LOG_DBG, fmt, ap); + va_end(ap); + + return ret; +} + +static struct command_result *WARN_UNUSED_RESULT +fail_internalerr(struct command *cmd, + const struct inv *inv, + const char *fmt, ...) +{ + va_list ap; + struct command_result *ret; + + va_start(ap, fmt); + ret = fail_inv_level(cmd, inv, LOG_BROKEN, fmt, ap); + va_end(ap); + + return ret; +} + +static struct command_result *pay_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct inv *inv) +{ + struct amount_msat msat = amount_msat(*inv->inv->invoice_amount); + + plugin_log(cmd->plugin, LOG_INFORM, + "Payed out %s for invreq %s: %.*s", + type_to_string(tmpctx, struct amount_msat, &msat), + type_to_string(tmpctx, struct sha256, &inv->invreq_id), + json_tok_full_len(result), + json_tok_full(buf, result)); + return command_hook_success(cmd); +} + +static struct command_result *pay_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct inv *inv) +{ + const jsmntok_t *msgtok = json_get_member(buf, error, "message"); + + return fail_inv(cmd, inv, "pay attempt failed: %.*s", + json_tok_full_len(msgtok), + json_tok_full(buf, msgtok)); +} + +static struct command_result *listinvreqs_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct inv *inv) +{ + const jsmntok_t *arr = json_get_member(buf, result, "invoicerequests"); + const jsmntok_t *activetok; + bool active; + struct amount_msat amt; + struct out_req *req; + struct sha256 merkle, sighash; + + /* BOLT-offers #12: + * A reader of an invoice: + *... + * - if the invoice is a response to an `invoice_request`: + * - MUST reject the invoice if all fields less than type 160 do not exactly match the `invoice_request`. + * - if `offer_node_id` is present (invoice_request for an offer): + * - MUST reject the invoice if `invoice_node_id` is not equal to `offer_node_id`. + * - otherwise (invoice_request without an offer): + * - MAY reject the invoice if it cannot confirm that `invoice_node_id` is correct, out-of-band. + * + * - otherwise: (a invoice presented without being requested, eg. scanned by user): + */ + + /* Since the invreq_id hashes all fields < 160, we know it matches */ + if (arr->size == 0) + return fail_inv(cmd, inv, "Unknown invoice_request %s", + type_to_string(tmpctx, struct sha256, &inv->invreq_id)); + + activetok = json_get_member(buf, arr + 1, "active"); + if (!activetok) { + return fail_internalerr(cmd, inv, + "Missing active: %.*s", + json_tok_full_len(arr), + json_tok_full(buf, arr)); + } + json_to_bool(buf, activetok, &active); + if (!active) + return fail_inv(cmd, inv, "invoice_request no longer available"); + + /* We only save ones without offers to the db! */ + assert(!inv->inv->offer_node_id); + + /* BOLT-offers #12: + * - MUST reject the invoice if `signature` is not a valid signature + * using `invoice_node_id` as described in [Signature + * Calculation](#signature-calculation). + */ + if (!inv->inv->signature) + return fail_inv(cmd, inv, "invoice missing signature"); + + merkle_tlv(inv->inv->fields, &merkle); + sighash_from_merkle("invoice", "signature", &merkle, &sighash); + if (!check_schnorr_sig(&sighash, &inv->inv->invoice_node_id->pubkey, inv->inv->signature)) + return fail_inv(cmd, inv, "invalid invoice signature"); + + /* BOLT-offers #12: + * - SHOULD confirm authorization if `invoice_amount`.`msat` is not + * within the amount range authorized. + */ + /* Because there's no offer, we had to set invreq_amount */ + if (*inv->inv->invoice_amount > *inv->inv->invreq_amount) + return fail_inv(cmd, inv, "invoice amount is too large"); + + /* FIXME: Create a hook for validating the invoice_node_id! */ + amt = amount_msat(*inv->inv->invoice_amount); + plugin_log(cmd->plugin, LOG_INFORM, + "Attempting payment of %s for invoice_request %s", + type_to_string(tmpctx, struct amount_msat, &amt), + type_to_string(tmpctx, struct sha256, &inv->invreq_id)); + + req = jsonrpc_request_start(cmd->plugin, cmd, "pay", + pay_done, pay_error, inv); + json_add_string(req->js, "bolt11", invoice_encode(tmpctx, inv->inv)); + json_add_sha256(req->js, "localinvreqid", &inv->invreq_id); + return send_outreq(cmd->plugin, req); +} + +static struct command_result *listinvreqs_error(struct command *cmd, + const char *buf, + const jsmntok_t *err, + struct inv *inv) +{ + return fail_internalerr(cmd, inv, + "listinvoicerequests gave JSON error: %.*s", + json_tok_full_len(err), + json_tok_full(buf, err)); +} + +struct command_result *handle_invoice(struct command *cmd, + const u8 *invbin, + struct blinded_path *reply_path STEALS) +{ + size_t len = tal_count(invbin); + struct inv *inv = tal(cmd, struct inv); + struct out_req *req; + int bad_feature; + u64 invexpiry; + + inv->reply_path = tal_steal(inv, reply_path); + + inv->inv = fromwire_tlv_invoice(cmd, &invbin, &len); + if (!inv->inv) { + return fail_inv(cmd, inv, + "Invalid invoice %s", + tal_hex(tmpctx, invbin)); + } + invoice_invreq_id(inv->inv, &inv->invreq_id); + + /* BOLT-offers #12: + * A reader of an invoice: + * - MUST reject the invoice if `invoice_amount` is not present. + * - MUST reject the invoice if `invoice_created_at` is not present. + * - MUST reject the invoice if `invoice_payment_hash` is not present. + * - MUST reject the invoice if `invoice_node_id` is not present. + */ + if (!inv->inv->invoice_amount) + return fail_inv(cmd, inv, "Missing invoice_amount"); + if (!inv->inv->invoice_created_at) + return fail_inv(cmd, inv, "Missing invoice_created_at"); + if (!inv->inv->invoice_payment_hash) + return fail_inv(cmd, inv, "Missing invoice_payment_hash"); + if (!inv->inv->invoice_node_id) + return fail_inv(cmd, inv, "Missing invoice_node_id"); + + /* BOLT-offers #12: + * A reader of an invoice: + *... + * - if `invoice_features` contains unknown _odd_ bits that are non-zero: + * - MUST ignore the bit. + * - if `invoice_features` contains unknown _even_ bits that are non-zero: + * - MUST reject the invoice. + */ + bad_feature = features_unsupported(plugin_feature_set(cmd->plugin), + inv->inv->invoice_features, + BOLT12_INVOICE_FEATURE); + if (bad_feature != -1) { + return fail_inv(cmd, inv, + "Unsupported invoice feature %i", + bad_feature); + } + + /* BOLT-offers #12: + * A reader of an invoice: + *... + * - if `invoice_relative_expiry` is present: + * - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `invoice_created_at` plus `seconds_from_creation`. + * - otherwise: + * - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `invoice_created_at` plus 7200. + */ + if (inv->inv->invoice_relative_expiry) + invexpiry = *inv->inv->invoice_created_at + *inv->inv->invoice_relative_expiry; + else + invexpiry = *inv->inv->invoice_created_at + BOLT12_DEFAULT_REL_EXPIRY; + if (time_now().ts.tv_sec > invexpiry) + return fail_inv(cmd, inv, "Expired invoice"); + + /* BOLT-offers #12: + * A reader of an invoice: + *... + * - MUST reject the invoice if `invoice_paths` is not present or is empty. + * - MUST reject the invoice if `invoice_blindedpay` is not present. + * - MUST reject the invoice if `invoice_blindedpay` does not contain exactly one `blinded_payinfo` per `invoice_paths`.`blinded_path`. + */ + if (!inv->inv->invoice_paths) + return fail_inv(cmd, inv, "Missing invoice_paths"); + if (!inv->inv->invoice_blindedpay) + return fail_inv(cmd, inv, "Missing invoice_blindedpay"); + if (tal_count(inv->inv->invoice_blindedpay) + != tal_count(inv->inv->invoice_paths)) + return fail_inv(cmd, inv, + "Mismatch between invoice_blindedpay and invoice_paths"); + + /* BOLT-offers #12: + * A reader of an invoice: + *... + * - For each `invoice_blindedpay`.`payinfo`: + * - MUST NOT use the corresponding `invoice_paths`.`path` if + * `payinfo`.`features` has any unknown even bits set. + * - MUST reject the invoice if this leaves no usable paths. + */ + for (size_t i = 0; i < tal_count(inv->inv->invoice_blindedpay); i++) { + bad_feature = features_unsupported(plugin_feature_set(cmd->plugin), + inv->inv->invoice_blindedpay[i]->features, + /* FIXME: Technically a different feature set? */ + BOLT12_INVOICE_FEATURE); + if (bad_feature == -1) + continue; + + tal_arr_remove(&inv->inv->invoice_paths, i); + tal_arr_remove(&inv->inv->invoice_blindedpay, i); + i--; + } + if (tal_count(inv->inv->invoice_paths) == 0) { + return fail_inv(cmd, inv, + "Unsupported feature for all paths (%i)", + bad_feature); + } + + /* Now find the invoice_request. */ + req = jsonrpc_request_start(cmd->plugin, cmd, "listinvoicerequests", + listinvreqs_done, listinvreqs_error, inv); + json_add_sha256(req->js, "invreq_id", &inv->invreq_id); + return send_outreq(cmd->plugin, req); +} + diff --git a/plugins/offers_inv_hook.h b/plugins/offers_inv_hook.h new file mode 100644 index 000000000..164b85388 --- /dev/null +++ b/plugins/offers_inv_hook.h @@ -0,0 +1,11 @@ +#ifndef LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H +#define LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H +#include "config.h" +#include + +/* We got an onionmessage with an invoice! reply_path could be NULL. */ +struct command_result *handle_invoice(struct command *cmd, + const u8 *invbin, + struct blinded_path *reply_path STEALS); + +#endif /* LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H */ diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 1b7575c2e..26ff2b682 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -940,9 +940,11 @@ static struct command_result *listoffers_done(struct command *cmd, } /* BOLT-offers #12: - * The writer: - * - MUST copy all non-signature fields from the invreq (including - * unknown fields). + * The writer of an invoice: + *... + * - if the invoice is in response to an `invoice_request`: + * - MUST copy all non-signature fields from the invreq (including + * unknown fields). */ ir->inv = invoice_for_invreq(cmd, ir->invreq); assert(ir->inv->invreq_payer_id); diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 5c2302035..648bc7d52 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -8,6 +8,7 @@ #include #include #include +#include static bool msat_or_any(const char *buffer, const jsmntok_t *tok, @@ -230,14 +231,14 @@ static struct command_result *check_result(struct command *cmd, &active)) { return command_fail(cmd, LIGHTNINGD, - "Bad createoffer status reply %.*s", + "Bad createoffer/createinvoicerequest status reply %.*s", json_tok_full_len(result), json_tok_full(buf, result)); } if (!active) return command_fail(cmd, OFFER_ALREADY_EXISTS, - "Offer already exists, but isn't active"); + "Already exists, but isn't active"); /* Otherwise, push through the result. */ return forward_result(cmd, buf, result, arg); @@ -384,3 +385,89 @@ struct command_result *json_offer(struct command *cmd, return create_offer(cmd, offinfo); } + +struct command_result *json_invoicerequest(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + const char *desc, *issuer, *label; + struct tlv_invoice_request *invreq; + struct out_req *req; + struct amount_msat *msat; + bool *single_use; + + invreq = tlv_invoice_request_new(cmd); + + if (!param(cmd, buffer, params, + p_req("amount", param_msat, &msat), + p_req("description", param_escaped_string, &desc), + p_opt("issuer", param_escaped_string, &issuer), + p_opt("label", param_escaped_string, &label), + p_opt("absolute_expiry", param_u64, + &invreq->offer_absolute_expiry), + p_opt_def("single_use", param_bool, &single_use, true), + NULL)) + return command_param_failed(); + + if (!offers_enabled) + return command_fail(cmd, LIGHTNINGD, + "experimental-offers not enabled"); + + /* BOLT-offers #12: + * - otherwise (not responding to an offer): + * - MUST set (or not set) `offer_metadata`, `offer_description`, `offer_absolute_expiry`, `offer_paths` and `offer_issuer` as it would for an offer. + * - MUST set `invreq_payer_id` as it would set `offer_node_id` for an offer. + * - MUST NOT include `signature`, `offer_chains`, `offer_amount`, `offer_currency`, `offer_features`, `offer_quantity_max` or `offer_node_id` + * - if the chain for the invoice is not solely bitcoin: + * - MUST specify `invreq_chain` the offer is valid for. + * - MUST set `invreq_amount`. + */ + invreq->offer_description + = tal_dup_arr(invreq, char, desc, strlen(desc), 0); + if (issuer) { + invreq->offer_issuer + = tal_dup_arr(invreq, char, issuer, strlen(issuer), 0); + } + + if (!streq(chainparams->network_name, "bitcoin")) { + invreq->invreq_chain + = tal_dup(invreq, struct bitcoin_blkid, + &chainparams->genesis_blockhash); + } + /* BOLT-offers #12: + * - if it sets `invreq_amount`: + * - MUST set `msat` in multiples of the minimum lightning-payable unit + * (e.g. milli-satoshis for bitcoin) for `invreq_chain` (or for bitcoin, if there is no `invreq_chain`). + */ + invreq->invreq_amount + = tal_dup(invreq, u64, &msat->millisatoshis); /* Raw: wire */ + + /* FIXME: enable blinded paths! */ + + /* BOLT-offers #12: + * - MUST set `invreq_metadata` to an unpredictable series of bytes. + */ + /* BOLT-offers #12: + * - otherwise (not responding to an offer): + *... + * - MUST set `invreq_payer_id` as it would set `offer_node_id` for an offer. + */ + /* createinvoicerequest sets these! */ + + /* BOLT-offers #12: + * - if it supports bolt12 invoice request features: + * - MUST set `invreq_features`.`features` to the bitmap of features. + */ + req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest", + check_result, forward_error, + invreq); + json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq)); + json_add_bool(req->js, "savetodb", true); + /* FIXME: Allow invoicerequests using aliases! */ + json_add_bool(req->js, "exposeid", true); + json_add_bool(req->js, "single_use", *single_use); + if (label) + json_add_string(req->js, "label", label); + return send_outreq(cmd->plugin, req); +} + diff --git a/plugins/offers_offer.h b/plugins/offers_offer.h index b815dc1cb..b7b25013b 100644 --- a/plugins/offers_offer.h +++ b/plugins/offers_offer.h @@ -10,7 +10,7 @@ struct command_result *json_offer(struct command *cmd, const char *buffer, const jsmntok_t *params); -struct command_result *json_offerout(struct command *cmd, - const char *buffer, - const jsmntok_t *params); +struct command_result *json_invoicerequest(struct command *cmd, + const char *buffer, + const jsmntok_t *params); #endif /* LIGHTNING_PLUGINS_OFFERS_OFFER_H */ diff --git a/tests/test_pay.py b/tests/test_pay.py index 327fdc34a..aa5d71848 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -4379,6 +4379,9 @@ def test_offer_needs_option(node_factory): l1 = node_factory.get_node() with pytest.raises(RpcError, match='experimental-offers not enabled'): l1.rpc.call('offer', {'amount': '1msat', 'description': 'test'}) + with pytest.raises(RpcError, match='experimental-offers not enabled'): + l1.rpc.call('invoicerequest', {'amount': '2msat', + 'description': 'simple test'}) with pytest.raises(RpcError, match='Unknown command'): l1.rpc.call('fetchinvoice', {'offer': 'aaaa'}) @@ -4730,7 +4733,7 @@ def test_fetchinvoice(node_factory, bitcoind): 'description': 'simple test'}) assert offer1['created'] is False l3.rpc.call('disableoffer', {'offer_id': offer1['offer_id']}) - with pytest.raises(RpcError, match="1000.*Offer already exists, but isn't active"): + with pytest.raises(RpcError, match="1000.*Already exists, but isn't active"): l3.rpc.call('offer', {'amount': '2msat', 'description': 'simple test'}) @@ -4800,6 +4803,16 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind): l3.rpc.call('fetchinvoice', {'offer': offer['bolt12']}) assert l3.rpc.listpeers(l2.info['id'])['peers'] != [] + # Similarly for an invoice_request. + l3.rpc.disconnect(l2.info['id']) + invreq = l2.rpc.call('invoicerequest', {'amount': '2msat', + 'description': 'simple test'}) + # Ofc l2 can't actually pay it! + with pytest.raises(RpcError, match='pay attempt failed: "Ran out of routes to try'): + l3.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], 'label': 'payme!'}) + + assert l3.rpc.listpeers(l2.info['id'])['peers'] != [] + # But if we create a channel l3->l1->l2 (and balance!), l2 can! node_factory.join_nodes([l3, l1], wait_for_announce=True) # Make sure l2 knows about it @@ -4810,7 +4823,7 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind): wait_for(lambda: only_one(only_one(l2.rpc.listpeers(l1.info['id'])['peers'])['channels'])['spendable_msat'] != Millisatoshi(0)) l3.rpc.disconnect(l2.info['id']) - l3.rpc.call('fetchinvoice', {'offer': offer['bolt12']}) + l3.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], 'label': 'payme for real!'}) # It will have autoconnected, to send invoice (since l1 says it doesn't do onion messages!) assert l3.rpc.listpeers(l2.info['id'])['peers'] != [] @@ -4850,6 +4863,71 @@ def test_dev_rawrequest(node_factory): assert 'invoice' in ret +def test_sendinvoice(node_factory, bitcoind): + l2opts = {'experimental-offers': None} + l1, l2 = node_factory.line_graph(2, wait_for_announce=True, + opts=[{'experimental-offers': None}, + l2opts]) + + # Simple offer to send money (balances channel a little) + invreq = l1.rpc.call('invoicerequest', {'amount': '100000sat', + 'description': 'simple test'}) + + # Fetchinvoice will refuse, since it's not an offer. + with pytest.raises(RpcError, match='unexpected prefix lnr'): + l2.rpc.call('fetchinvoice', {'offer': invreq['bolt12']}) + + # Pay will refuse, since it's not an invoice. + with pytest.raises(RpcError, match='unexpected prefix lnr'): + l2.rpc.call('fetchinvoice', {'offer': invreq['bolt12']}) + + # used will be false + assert only_one(l1.rpc.call('listinvoicerequests', [invreq['invreq_id']])['invoicerequests'])['used'] is False + + # sendinvoice should work. + out = l2.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], + 'label': 'test sendinvoice 1'}) + assert out['label'] == 'test sendinvoice 1' + assert out['description'] == 'simple test' + assert 'bolt12' in out + assert 'payment_hash' in out + assert out['status'] == 'paid' + assert 'payment_preimage' in out + assert 'expires_at' in out + assert out['amount_msat'] == Millisatoshi(100000000) + assert 'pay_index' in out + assert out['amount_received_msat'] == Millisatoshi(100000000) + + # Note, if we're slow, this fails with "Offer no longer available", + # *but* if it hasn't heard about payment success yet, l2 will fail + # simply because payments are already pending. + with pytest.raises(RpcError, match='no longer available|pay attempt failed'): + l2.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], + 'label': 'test sendinvoice 2'}) + + # Technically, l1 may not have gotten payment success, so we need to wait. + wait_for(lambda: only_one(l1.rpc.call('listinvoicerequests', [invreq['invreq_id']])['invoicerequests'])['used'] is True) + + # Offer with issuer: we must copy issuer into our invoice! + invreq = l1.rpc.call('invoicerequest', {'amount': '10000sat', + 'description': 'simple test', + 'issuer': "clightning test suite"}) + + out = l2.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], + 'label': 'test sendinvoice 3'}) + assert out['label'] == 'test sendinvoice 3' + assert out['description'] == 'simple test' + assert 'issuer' not in out + assert 'bolt12' in out + assert 'payment_hash' in out + assert out['status'] == 'paid' + assert 'payment_preimage' in out + assert 'expires_at' in out + assert out['amount_msat'] == Millisatoshi(10000000) + assert 'pay_index' in out + assert out['amount_received_msat'] == Millisatoshi(10000000) + + def test_self_pay(node_factory): """Repro test for issue 4345: pay ourselves via the pay plugin.