lightningd: re-add 'offerout' functionality, as 'invoicerequest'.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2022-11-09 13:02:01 +10:30 committed by Christian Decker
parent 7906770489
commit 37bc4603b8
11 changed files with 907 additions and 28 deletions

View file

@ -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)

View file

@ -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);

View file

@ -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 \

View file

@ -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",

View file

@ -14,6 +14,7 @@
#include <common/json_param.h>
#include <common/json_stream.h>
#include <plugins/offers.h>
#include <plugins/offers_inv_hook.h>
#include <plugins/offers_invreq_hook.h>
#include <plugins/offers_offer.h>
@ -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",

326
plugins/offers_inv_hook.c Normal file
View file

@ -0,0 +1,326 @@
#include "config.h"
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/bolt12_merkle.h>
#include <common/json_stream.h>
#include <common/type_to_string.h>
#include <plugins/offers.h>
#include <plugins/offers_inv_hook.h>
#include <secp256k1_schnorrsig.h>
/* 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);
}

11
plugins/offers_inv_hook.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H
#define LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H
#include "config.h"
#include <plugins/libplugin.h>
/* 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 */

View file

@ -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);

View file

@ -8,6 +8,7 @@
#include <common/json_stream.h>
#include <common/overflows.h>
#include <plugins/offers_offer.h>
#include <sodium/randombytes.h>
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);
}

View file

@ -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 */

View file

@ -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.