mirror of
https://github.com/ElementsProject/lightning.git
synced 2024-11-19 18:11:28 +01:00
37bc4603b8
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
327 lines
10 KiB
C
327 lines
10 KiB
C
#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);
|
|
}
|
|
|