core-lightning/plugins/fetchinvoice.c
Rusty Russell 4cfd972407 common/blindedpath: expose API at a lower level.
We actually want lightningd to create these, since it wants to put the
path_id secret in the last element.  So best API is actually a generic
one, rather than separate APIs to create first and last ones.

And really, the more explicit initialization makes the users clearer.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2022-11-09 15:08:03 +01:00

1749 lines
52 KiB
C

#include "config.h"
#include <bitcoin/chainparams.h>
#include <ccan/array_size/array_size.h>
#include <ccan/cast/cast.h>
#include <ccan/json_out/json_out.h>
#include <ccan/mem/mem.h>
#include <ccan/str/hex/hex.h>
#include <ccan/tal/str/str.h>
#include <common/blindedpath.h>
#include <common/bolt12_merkle.h>
#include <common/dijkstra.h>
#include <common/gossmap.h>
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/memleak.h>
#include <common/overflows.h>
#include <common/route.h>
#include <common/type_to_string.h>
#include <errno.h>
#include <plugins/libplugin.h>
#include <secp256k1_schnorrsig.h>
#include <sodium.h>
static struct gossmap *global_gossmap;
static struct pubkey local_id;
static bool disable_connect = false;
static LIST_HEAD(sent_list);
struct sent {
/* We're in sent_invreqs, awaiting reply. */
struct list_node list;
/* The alias used by reply */
struct pubkey *reply_alias;
/* The command which sent us. */
struct command *cmd;
/* The offer we are trying to get an invoice/payment for. */
struct tlv_offer *offer;
/* Path to use (including self) */
struct pubkey *path;
/* The invreq we sent, OR the invoice we sent */
struct tlv_invoice_request *invreq;
struct tlv_invoice *inv;
struct preimage inv_preimage;
struct json_escape *inv_label;
/* How long to wait for response before giving up. */
u32 wait_timeout;
};
static struct sent *find_sent_by_alias(const struct pubkey *alias)
{
struct sent *i;
list_for_each(&sent_list, i, list) {
if (i->reply_alias && pubkey_eq(i->reply_alias, alias))
return i;
}
return NULL;
}
static const char *field_diff_(struct plugin *plugin,
const tal_t *a, const tal_t *b,
const char *fieldname)
{
/* One is set and the other isn't? */
if ((a == NULL) != (b == NULL)) {
plugin_log(plugin, LOG_DBG, "field_diff %s: a is %s, b is %s",
fieldname, a ? "set": "unset", b ? "set": "unset");
return fieldname;
}
if (!memeq(a, tal_bytelen(a), b, tal_bytelen(b))) {
plugin_log(plugin, LOG_DBG, "field_diff %s: a=%s, b=%s",
fieldname, tal_hex(tmpctx, a), tal_hex(tmpctx, b));
return fieldname;
}
return NULL;
}
#define field_diff(a, b, fieldname) \
field_diff_((cmd)->plugin, a->fieldname, b->fieldname, #fieldname)
/* Returns true if b is a with something appended. */
static bool description_is_appended(const char *a, const char *b)
{
if (!a || !b)
return false;
if (tal_bytelen(b) < tal_bytelen(a))
return false;
return memeq(a, tal_bytelen(a), b, tal_bytelen(a));
}
/* Hack to suppress warnings when we finish a different command */
static void discard_result(struct command_result *ret)
{
}
/* Returns NULL if it wasn't an error. */
static struct command_result *handle_error(struct command *cmd,
struct sent *sent,
const char *buf,
const jsmntok_t *om)
{
const u8 *data;
size_t dlen;
struct tlv_invoice_error *err;
struct json_out *details;
const jsmntok_t *errtok;
errtok = json_get_member(buf, om, "invoice_error");
if (!errtok)
return NULL;
data = json_tok_bin_from_hex(cmd, buf, errtok);
dlen = tal_bytelen(data);
details = json_out_new(cmd);
plugin_log(cmd->plugin, LOG_DBG, "errtok = %.*s",
json_tok_full_len(errtok),
json_tok_full(buf, errtok));
json_out_start(details, NULL, '{');
err = fromwire_tlv_invoice_error(cmd, &data, &dlen);
if (!err) {
plugin_log(cmd->plugin, LOG_DBG,
"Invalid invoice_error %.*s",
json_tok_full_len(errtok),
json_tok_full(buf, errtok));
json_out_addstr(details, "invoice_error_hex",
tal_strndup(tmpctx,
buf + errtok->start,
errtok->end - errtok->start));
} else {
char *failstr;
/* FIXME: with a bit more generate-wire.py support,
* we could have fieldnames and even types. */
if (err->erroneous_field)
json_out_add(details, "erroneous_field", false,
"%"PRIu64, *err->erroneous_field);
if (err->suggested_value)
json_out_addstr(details, "suggested_value",
tal_hex(tmpctx,
err->suggested_value));
/* If they don't include this, it'll be empty */
failstr = tal_strndup(tmpctx,
err->error,
tal_bytelen(err->error));
json_out_addstr(details, "error", failstr);
}
json_out_end(details, '}');
discard_result(command_done_err(sent->cmd,
OFFER_BAD_INVREQ_REPLY,
"Remote node sent failure message",
details));
return command_hook_success(cmd);
}
static struct command_result *handle_invreq_response(struct command *cmd,
struct sent *sent,
const char *buf,
const jsmntok_t *om)
{
const u8 *invbin;
const jsmntok_t *invtok;
size_t len;
struct tlv_invoice *inv;
struct sha256 merkle, sighash;
struct json_stream *out;
const char *badfield;
u64 *expected_amount;
invtok = json_get_member(buf, om, "invoice");
if (!invtok) {
plugin_log(cmd->plugin, LOG_UNUSUAL,
"Neither invoice nor invoice_request_failed in reply %.*s",
json_tok_full_len(om),
json_tok_full(buf, om));
discard_result(command_fail(sent->cmd,
OFFER_BAD_INVREQ_REPLY,
"Neither invoice nor invoice_request_failed in reply %.*s",
json_tok_full_len(om),
json_tok_full(buf, om)));
return command_hook_success(cmd);
}
invbin = json_tok_bin_from_hex(cmd, buf, invtok);
len = tal_bytelen(invbin);
inv = fromwire_tlv_invoice(cmd, &invbin, &len);
if (!inv) {
badfield = "invoice";
goto badinv;
}
#if DEVELOPER
/* Raw send? Just fwd reply. */
if (!sent->offer) {
out = jsonrpc_stream_success(sent->cmd);
json_add_string(out, "invoice", invoice_encode(tmpctx, inv));
discard_result(command_finished(sent->cmd, out));
return command_hook_success(cmd);
}
#endif /* DEVELOPER */
/* BOLT-offers #12:
* - MUST reject the invoice unless `node_id` is equal to the offer.
*/
if (!pubkey_eq(sent->offer->node_id, inv->node_id)) {
badfield = "node_id";
goto badinv;
}
/* BOLT-offers #12:
* - MUST reject the invoice if `signature` is not a valid signature
* using `node_id` as described in [Signature Calculation]
*/
merkle_tlv(inv->fields, &merkle);
sighash_from_merkle("invoice", "signature", &merkle, &sighash);
if (!inv->signature
|| !check_schnorr_sig(&sighash, &inv->node_id->pubkey, inv->signature)) {
badfield = "signature";
goto badinv;
}
/* BOLT-offers #12:
* - MUST reject the invoice if `msat` is not present.
*/
if (!inv->amount) {
badfield = "amount";
goto badinv;
}
/* BOLT-offers #12:
* - MUST reject the invoice unless `offer_id` is equal to the id of the
* offer.
*/
if ((badfield = field_diff(sent->invreq, inv, offer_id)))
goto badinv;
/* BOLT-offers #12:
* - if the invoice is a reply to an `invoice_request`:
*...
* - MUST reject the invoice unless the following fields are equal or
* unset exactly as they are in the `invoice_request:`
* - `quantity`
* - `payer_key`
* - `payer_info`
*/
/* BOLT-offers-recurrence #12:
* - if the invoice is a reply to an `invoice_request`:
*...
* - MUST reject the invoice unless the following fields are equal or
* unset exactly as they are in the `invoice_request:`
* - `quantity`
* - `recurrence_counter`
* - `recurrence_start`
* - `payer_key`
* - `payer_info`
*/
if ((badfield = field_diff(sent->invreq, inv, quantity)))
goto badinv;
if ((badfield = field_diff(sent->invreq, inv, recurrence_counter)))
goto badinv;
if ((badfield = field_diff(sent->invreq, inv, recurrence_start)))
goto badinv;
if ((badfield = field_diff(sent->invreq, inv, payer_key)))
goto badinv;
if ((badfield = field_diff(sent->invreq, inv, payer_info)))
goto badinv;
/* Get the amount we expected: firstly, if that's what we sent,
* secondly, if specified in the invoice. */
if (sent->invreq->amount) {
expected_amount = tal_dup(tmpctx, u64, sent->invreq->amount);
} else if (sent->offer->amount && !sent->offer->currency) {
expected_amount = tal(tmpctx, u64);
*expected_amount = *sent->offer->amount;
if (sent->invreq->quantity) {
/* We should never have sent this! */
if (mul_overflows_u64(*expected_amount,
*sent->invreq->quantity)) {
badfield = "quantity overflow";
goto badinv;
}
*expected_amount *= *sent->invreq->quantity;
}
} else
expected_amount = NULL;
/* BOLT-offers-recurrence #12:
* - if the offer contained `recurrence`:
* - MUST reject the invoice if `recurrence_basetime` is not set.
*/
if (sent->invreq->recurrence_counter && !inv->recurrence_basetime) {
badfield = "recurrence_basetime";
goto badinv;
}
/* BOLT-offers #12:
* - SHOULD confirm authorization if the `description` does not exactly
* match the `offer`
* - MAY highlight if `description` has simply had a change appended.
*/
/* We highlight these changes to the caller, for them to handle */
out = jsonrpc_stream_success(sent->cmd);
json_add_string(out, "invoice", invoice_encode(tmpctx, inv));
json_object_start(out, "changes");
if (field_diff(sent->offer, inv, description)) {
/* Did they simply append? */
if (description_is_appended(sent->offer->description,
inv->description)) {
size_t off = tal_bytelen(sent->offer->description);
json_add_stringn(out, "description_appended",
inv->description + off,
tal_bytelen(inv->description) - off);
} else if (!inv->description)
json_add_stringn(out, "description_removed",
sent->offer->description,
tal_bytelen(sent->offer->description));
else
json_add_stringn(out, "description",
inv->description,
tal_bytelen(inv->description));
}
/* BOLT-offers #12:
* - SHOULD confirm authorization if `issuer` does not exactly
* match the `offer`
*/
if (field_diff(sent->offer, inv, issuer)) {
if (!inv->issuer)
json_add_stringn(out, "issuer_removed",
sent->offer->issuer,
tal_bytelen(sent->offer->issuer));
else
json_add_stringn(out, "issuer",
inv->issuer,
tal_bytelen(inv->issuer));
}
/* BOLT-offers #12:
* - SHOULD confirm authorization if `msat` is not within the amount
* range authorized.
*/
/* We always tell them this unless it's trivial to calc and
* exactly as expected. */
if (!expected_amount || *inv->amount != *expected_amount) {
if (deprecated_apis)
json_add_amount_msat_only(out, "msat",
amount_msat(*inv->amount));
json_add_amount_msat_only(out, "amount_msat",
amount_msat(*inv->amount));
}
json_object_end(out);
/* We tell them about next period at this point, if any. */
if (sent->offer->recurrence) {
u64 next_counter, next_period_idx;
u64 paywindow_start, paywindow_end;
next_counter = *sent->invreq->recurrence_counter + 1;
if (sent->invreq->recurrence_start)
next_period_idx = *sent->invreq->recurrence_start
+ next_counter;
else
next_period_idx = next_counter;
/* If this was the last, don't tell them about a next! */
if (!sent->offer->recurrence_limit
|| next_period_idx <= *sent->offer->recurrence_limit) {
json_object_start(out, "next_period");
json_add_u64(out, "counter", next_counter);
json_add_u64(out, "starttime",
offer_period_start(*inv->recurrence_basetime,
next_period_idx,
sent->offer->recurrence));
json_add_u64(out, "endtime",
offer_period_start(*inv->recurrence_basetime,
next_period_idx + 1,
sent->offer->recurrence) - 1);
offer_period_paywindow(sent->offer->recurrence,
sent->offer->recurrence_paywindow,
sent->offer->recurrence_base,
*inv->recurrence_basetime,
next_period_idx,
&paywindow_start, &paywindow_end);
json_add_u64(out, "paywindow_start", paywindow_start);
json_add_u64(out, "paywindow_end", paywindow_end);
json_object_end(out);
}
}
discard_result(command_finished(sent->cmd, out));
return command_hook_success(cmd);
badinv:
plugin_log(cmd->plugin, LOG_DBG, "Failed invoice due to %s", badfield);
discard_result(command_fail(sent->cmd,
OFFER_BAD_INVREQ_REPLY,
"Incorrect %s field in %.*s",
badfield,
json_tok_full_len(invtok),
json_tok_full(buf, invtok)));
return command_hook_success(cmd);
}
static struct command_result *recv_modern_onion_message(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
const jsmntok_t *om, *aliastok;
struct sent *sent;
struct pubkey alias;
struct command_result *err;
om = json_get_member(buf, params, "onion_message");
aliastok = json_get_member(buf, om, "our_alias");
if (!aliastok || !json_to_pubkey(buf, aliastok, &alias))
return command_hook_success(cmd);
sent = find_sent_by_alias(&alias);
if (!sent) {
plugin_log(cmd->plugin, LOG_DBG,
"No match for modern onion %.*s",
json_tok_full_len(om),
json_tok_full(buf, om));
return command_hook_success(cmd);
}
plugin_log(cmd->plugin, LOG_DBG, "Received modern onion message: %.*s",
json_tok_full_len(params),
json_tok_full(buf, params));
err = handle_error(cmd, sent, buf, om);
if (err)
return err;
if (sent->invreq)
return handle_invreq_response(cmd, sent, buf, om);
return command_hook_success(cmd);
}
static void destroy_sent(struct sent *sent)
{
list_del(&sent->list);
}
/* We've received neither a reply nor a payment; return failure. */
static void timeout_sent_invreq(struct sent *sent)
{
/* This will free sent! */
discard_result(command_fail(sent->cmd, OFFER_TIMEOUT,
"Timeout waiting for response"));
}
static struct command_result *sendonionmsg_done(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_invreq, sent));
sent->cmd = cmd;
list_add_tail(&sent_list, &sent->list);
tal_add_destructor(sent, destroy_sent);
return command_still_pending(cmd);
}
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;
}
static struct command_result *param_offer(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct tlv_offer **offer)
{
char *fail;
/* BOLT-offers #12:
* - if `features` contains unknown _odd_ bits that are non-zero:
* - MUST ignore the bit.
* - if `features` contains unknown _even_ bits that are non-zero:
* - MUST NOT respond to the offer.
* - SHOULD indicate the unknown bit to the user.
*/
/* BOLT-offers #12:
* - MUST NOT set or imply any `chain_hash` not set or implied by
* the offer.
*/
*offer = offer_decode(cmd, buffer + tok->start, tok->end - tok->start,
plugin_feature_set(cmd->plugin), chainparams,
&fail);
if (!*offer)
return command_fail_badparam(cmd, name, buffer, tok,
tal_fmt(cmd,
"Unparsable offer: %s",
fail));
/* BOLT-offers #12:
*
* - if `node_id` or `description` is not set:
* - MUST NOT respond to the offer.
*/
if (!(*offer)->node_id)
return command_fail_badparam(cmd, name, buffer, tok,
"Offer does not contain a node_id");
if (!(*offer)->description)
return command_fail_badparam(cmd, name, buffer, tok,
"Offer does not contain a description");
return NULL;
}
static bool can_carry_onionmsg(const struct gossmap *map,
const struct gossmap_chan *c,
int dir,
struct amount_msat amount UNUSED,
void *arg UNUSED)
{
const struct gossmap_node *n;
/* Don't use it if either side says it's disabled */
if (!c->half[dir].enabled || !c->half[!dir].enabled)
return false;
/* Check features of recipient */
n = gossmap_nth_node(map, c, !dir);
/* 102/103 was the old EXPERIMENTAL feature bit: remove soon! */
return gossmap_node_get_feature(map, n, OPT_ONION_MESSAGES) != -1
|| gossmap_node_get_feature(map, n, 102) != -1;
}
static struct pubkey *path_to_node(const tal_t *ctx,
struct plugin *plugin,
const struct pubkey *node_id)
{
struct route_hop *r;
const struct dijkstra *dij;
const struct gossmap_node *src;
const struct gossmap_node *dst;
struct node_id dstid, local_nodeid;
struct pubkey *nodes;
struct gossmap *gossmap = get_gossmap(plugin);
node_id_from_pubkey(&dstid, node_id);
dst = gossmap_find_node(gossmap, &dstid);
if (!dst)
return NULL;
/* If we don't exist in gossip, routing can't happen. */
node_id_from_pubkey(&local_nodeid, &local_id);
src = gossmap_find_node(gossmap, &local_nodeid);
if (!src)
return NULL;
dij = dijkstra(tmpctx, gossmap, dst, AMOUNT_MSAT(0), 0,
can_carry_onionmsg, route_score_shorter, NULL);
r = route_from_dijkstra(tmpctx, gossmap, dij, src, AMOUNT_MSAT(0), 0);
if (!r)
return NULL;
nodes = tal_arr(ctx, struct pubkey, tal_count(r) + 1);
nodes[0] = local_id;
for (size_t i = 0; i < tal_count(r); i++) {
if (!pubkey_from_node_id(&nodes[i+1], &r[i].node_id)) {
plugin_err(plugin, "Could not convert nodeid %s",
type_to_string(tmpctx, struct node_id,
&r[i].node_id));
}
}
return nodes;
}
/* Marshal arguments for sending onion messages */
struct sending {
struct sent *sent;
struct tlv_onionmsg_tlv *payload;
struct command_result *(*done)(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct sent *sent);
};
static struct command_result *
send_modern_message(struct command *cmd,
struct blinded_path *reply_path,
struct sending *sending)
{
struct sent *sent = sending->sent;
struct privkey blinding_iter;
struct pubkey fwd_blinding, *node_alias;
size_t nhops = tal_count(sent->path);
struct tlv_onionmsg_tlv **payloads;
struct out_req *req;
struct tlv_encrypted_data_tlv *tlv;
/* Now create enctlvs for *forward* path. */
randombytes_buf(&blinding_iter, sizeof(blinding_iter));
if (!pubkey_from_privkey(&blinding_iter, &fwd_blinding))
return command_fail(cmd, LIGHTNINGD,
"Could not convert blinding %s to pubkey!",
type_to_string(tmpctx, struct privkey,
&blinding_iter));
/* We overallocate: this node (0) doesn't have payload or alias */
payloads = tal_arr(cmd, struct tlv_onionmsg_tlv *, nhops);
node_alias = tal_arr(cmd, struct pubkey, nhops);
for (size_t i = 1; i < nhops - 1; i++) {
payloads[i] = tlv_onionmsg_tlv_new(payloads);
tlv = tlv_encrypted_data_tlv_new(tmpctx);
tlv->next_node_id = &sent->path[i+1];
/* FIXME: Pad? */
payloads[i]->encrypted_recipient_data
= encrypt_tlv_encrypted_data(payloads[i],
&blinding_iter,
&sent->path[i],
tlv,
&blinding_iter,
&node_alias[i]);
}
/* Final payload contains the actual data. */
payloads[nhops-1] = sending->payload;
/* We don't include enctlv in final, but it gives us final alias */
tlv = tlv_encrypted_data_tlv_new(tmpctx);
if (!encrypt_tlv_encrypted_data(tmpctx,
&blinding_iter,
&sent->path[nhops-1],
tlv,
NULL,
&node_alias[nhops-1])) {
/* Should not happen! */
return command_fail(cmd, LIGHTNINGD,
"Could create final enctlv");
}
payloads[nhops-1]->reply_path = reply_path;
req = jsonrpc_request_start(cmd->plugin, cmd, "sendonionmessage",
sending->done,
forward_error,
sending->sent);
json_add_pubkey(req->js, "first_id", &sent->path[1]);
json_add_pubkey(req->js, "blinding", &fwd_blinding);
json_array_start(req->js, "hops");
for (size_t i = 1; i < nhops; i++) {
u8 *tlvbin;
json_object_start(req->js, NULL);
json_add_pubkey(req->js, "id", &node_alias[i]);
tlvbin = tal_arr(tmpctx, u8, 0);
towire_tlv_onionmsg_tlv(&tlvbin, payloads[i]);
json_add_hex_talarr(req->js, "tlv", tlvbin);
json_object_end(req->js);
}
json_array_end(req->js);
return send_outreq(cmd->plugin, req);
}
/* Lightningd gives us reply path, since we don't know secret to put
* in final so it will recognize it. */
static struct command_result *use_reply_path(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sending *sending)
{
struct blinded_path *rpath;
rpath = json_to_blinded_path(cmd, buf,
json_get_member(buf, result, "blindedpath"));
if (!rpath)
plugin_err(cmd->plugin,
"could not parse reply path %.*s?",
json_tok_full_len(result),
json_tok_full(buf, result));
/* Remember our alias we used so we can recognize reply */
sending->sent->reply_alias
= tal_dup(sending->sent, struct pubkey,
&rpath->path[tal_count(rpath->path)-1]->blinded_node_id);
return send_modern_message(cmd, rpath, sending);
}
static struct command_result *make_reply_path(struct command *cmd,
struct sending *sending)
{
struct out_req *req;
size_t nhops = tal_count(sending->sent->path);
/* FIXME: Maybe we should allow this? */
if (tal_count(sending->sent->path) == 1)
return command_fail(cmd, PAY_ROUTE_NOT_FOUND,
"Refusing to talk to ourselves");
req = jsonrpc_request_start(cmd->plugin, cmd, "blindedpath",
use_reply_path,
forward_error,
sending);
/* FIXME: Could create an independent reply path, not just
* reverse existing. */
json_array_start(req->js, "ids");
for (int i = nhops - 2; i >= 0; i--)
json_add_pubkey(req->js, NULL, &sending->sent->path[i]);
json_array_end(req->js);
return send_outreq(cmd->plugin, req);
}
static struct command_result *send_message(struct command *cmd,
struct sent *sent,
struct tlv_onionmsg_tlv *payload STEALS,
struct command_result *(*done)
(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct sent *sent))
{
struct sending *sending = tal(cmd, struct sending);
sending->sent = sent;
sending->payload = tal_steal(sending, payload);
sending->done = done;
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,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct sent *sent)
{
struct tlv_onionmsg_tlv *payload = tlv_onionmsg_tlv_new(sent);
payload->invoice_request = tal_arr(payload, u8, 0);
towire_tlv_invoice_request(&payload->invoice_request, sent->invreq);
return send_message(cmd, sent, payload, sendonionmsg_done);
}
struct connect_attempt {
struct node_id node_id;
struct command_result *(*cb)(struct command *command,
const char *buf,
const jsmntok_t *result,
struct sent *sent);
struct sent *sent;
};
static struct command_result *connected(struct command *command,
const char *buf,
const jsmntok_t *result,
struct connect_attempt *ca)
{
return ca->cb(command, buf, result, ca->sent);
}
static struct command_result *connect_failed(struct command *command,
const char *buf,
const jsmntok_t *result,
struct connect_attempt *ca)
{
return command_done_err(command, OFFER_ROUTE_NOT_FOUND,
"Failed: could not route, could not connect",
NULL);
}
/* We can't find a route, so we're going to try to connect, then just blast it
* to them. */
static struct command_result *
connect_direct(struct command *cmd,
const struct pubkey *dst,
struct command_result *(*cb)(struct command *command,
const char *buf,
const jsmntok_t *result,
struct sent *sent),
struct sent *sent)
{
struct out_req *req;
struct connect_attempt *ca = tal(cmd, struct connect_attempt);
ca->cb = cb;
ca->sent = sent;
node_id_from_pubkey(&ca->node_id, dst);
/* Make a direct path -> dst. */
sent->path = tal_arr(sent, struct pubkey, 2);
sent->path[0] = local_id;
if (!pubkey_from_node_id(&sent->path[1], &ca->node_id)) {
/* Should not happen! */
return command_done_err(cmd, LIGHTNINGD,
"Failed: could not convert to pubkey?",
NULL);
}
if (disable_connect) {
/* FIXME: This means we will fail if parity is wrong! */
plugin_notify_message(cmd, LOG_UNUSUAL,
"Cannot find route, but"
" fetchplugin-noconnect set:"
" trying direct anyway to %s",
type_to_string(tmpctx, struct pubkey,
dst));
return cb(cmd, NULL, NULL, sent);
}
req = jsonrpc_request_start(cmd->plugin, cmd, "connect", connected,
connect_failed, ca);
json_add_node_id(req->js, "id", &ca->node_id);
return send_outreq(cmd->plugin, req);
}
static struct command_result *invreq_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *t;
char *fail;
/* Get invoice request */
t = json_get_member(buf, result, "bolt12");
if (!t)
return command_fail(cmd, LIGHTNINGD,
"Missing bolt12 %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
plugin_log(cmd->plugin, LOG_DBG,
"invoice_request: %.*s",
json_tok_full_len(t),
json_tok_full(buf, t));
sent->inv = NULL;
sent->invreq = invrequest_decode(sent,
buf + t->start,
t->end - t->start,
plugin_feature_set(cmd->plugin),
chainparams,
&fail);
if (!sent->invreq)
return command_fail(cmd, LIGHTNINGD,
"Invalid invoice_request %.*s: %s",
json_tok_full_len(t),
json_tok_full(buf, t),
fail);
/* Now that's given us the previous base, check this is an OK time
* to request an invoice. */
if (sent->invreq->recurrence_counter) {
u64 *base;
const jsmntok_t *pbtok;
u64 period_idx = *sent->invreq->recurrence_counter;
if (sent->invreq->recurrence_start)
period_idx += *sent->invreq->recurrence_start;
/* BOLT-offers-recurrence #12:
* - if the offer contained `recurrence_limit`:
* - MUST NOT send an `invoice_request` for a period greater
* than `max_period`
*/
if (sent->offer->recurrence_limit
&& period_idx > *sent->offer->recurrence_limit)
return command_fail(cmd, LIGHTNINGD,
"Can't send invreq for period %"
PRIu64" (limit %u)",
period_idx,
*sent->offer->recurrence_limit);
/* BOLT-offers-recurrence #12:
* - SHOULD NOT send an `invoice_request` for a period which has
* already passed.
*/
/* If there's no recurrence_base, we need a previous payment
* for this: fortunately createinvoicerequest does that
* lookup. */
pbtok = json_get_member(buf, result, "previous_basetime");
if (pbtok) {
base = tal(tmpctx, u64);
json_to_u64(buf, pbtok, base);
} else if (sent->offer->recurrence_base)
base = &sent->offer->recurrence_base->basetime;
else {
/* happens with *recurrence_base == 0 */
assert(*sent->invreq->recurrence_counter == 0);
base = NULL;
}
if (base) {
u64 period_start, period_end, now = time_now().ts.tv_sec;
offer_period_paywindow(sent->offer->recurrence,
sent->offer->recurrence_paywindow,
sent->offer->recurrence_base,
*base, period_idx,
&period_start, &period_end);
if (now < period_start)
return command_fail(cmd, LIGHTNINGD,
"Too early: can't send until time %"
PRIu64" (in %"PRIu64" secs)",
period_start,
period_start - now);
if (now > period_end)
return command_fail(cmd, LIGHTNINGD,
"Too late: expired time %"
PRIu64" (%"PRIu64" secs ago)",
period_end,
now - period_end);
}
}
sent->path = path_to_node(sent, cmd->plugin,
sent->offer->node_id);
if (!sent->path)
return connect_direct(cmd, sent->offer->node_id,
sendinvreq_after_connect, sent);
return sendinvreq_after_connect(cmd, NULL, NULL, sent);
}
/* If they hand us the payer secret, we sign it directly, bypassing checks
* about periods etc. */
static struct command_result *
force_payer_secret(struct command *cmd,
struct sent *sent,
struct tlv_invoice_request *invreq,
const struct secret *payer_secret)
{
struct sha256 merkle, sha;
secp256k1_keypair kp;
u8 *msg;
const u8 *p;
size_t len;
if (secp256k1_keypair_create(secp256k1_ctx, &kp, payer_secret->data) != 1)
return command_fail(cmd, LIGHTNINGD, "Bad payer_secret");
invreq->payer_key = tal(invreq, struct pubkey);
/* Docs say this only happens if arguments are invalid! */
if (secp256k1_keypair_pub(secp256k1_ctx,
&invreq->payer_key->pubkey,
&kp) != 1)
plugin_err(cmd->plugin,
"secp256k1_keypair_pub failed on %s?",
type_to_string(tmpctx, struct secret, payer_secret));
/* Linearize populates ->fields */
msg = tal_arr(tmpctx, u8, 0);
towire_tlv_invoice_request(&msg, invreq);
p = msg;
len = tal_bytelen(msg);
sent->invreq = fromwire_tlv_invoice_request(cmd, &p, &len);
if (!sent->invreq)
plugin_err(cmd->plugin,
"Could not remarshall invreq %s", tal_hex(tmpctx, msg));
merkle_tlv(sent->invreq->fields, &merkle);
sighash_from_merkle("invoice_request", "signature", &merkle, &sha);
sent->invreq->signature = tal(invreq, struct bip340sig);
if (!secp256k1_schnorrsig_sign32(secp256k1_ctx,
sent->invreq->signature->u8,
sha.u.u8,
&kp,
NULL)) {
return command_fail(cmd, LIGHTNINGD,
"Failed to sign with payer_secret");
}
sent->path = path_to_node(sent, cmd->plugin,
sent->offer->node_id);
if (!sent->path)
return connect_direct(cmd, sent->offer->node_id,
sendinvreq_after_connect, sent);
return sendinvreq_after_connect(cmd, NULL, NULL, sent);
}
/* Fetches an invoice for this offer, and makes sure it corresponds. */
static struct command_result *json_fetchinvoice(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
struct amount_msat *msat;
const char *rec_label, *payer_note;
struct out_req *req;
struct tlv_invoice_request *invreq;
struct sent *sent = tal(cmd, struct sent);
struct secret *payer_secret = NULL;
u32 *timeout;
invreq = tlv_invoice_request_new(sent);
if (!param(cmd, buffer, params,
p_req("offer", param_offer, &sent->offer),
p_opt("amount_msat|msatoshi", param_msat, &msat),
p_opt("quantity", param_u64, &invreq->quantity),
p_opt("recurrence_counter", param_number,
&invreq->recurrence_counter),
p_opt("recurrence_start", param_number,
&invreq->recurrence_start),
p_opt("recurrence_label", param_string, &rec_label),
p_opt_def("timeout", param_number, &timeout, 60),
p_opt("payer_note", param_string, &payer_note),
#if DEVELOPER
p_opt("payer_secret", param_secret, &payer_secret),
#endif
NULL))
return command_param_failed();
sent->wait_timeout = *timeout;
/* BOLT-offers #12:
* - MUST set `offer_id` to the Merkle root of the offer as described
* in [Signature Calculation](#signature-calculation).
*/
invreq->offer_id = tal(invreq, struct sha256);
merkle_tlv(sent->offer->fields, invreq->offer_id);
/* Check if they are trying to send us money. */
if (sent->offer->send_invoice)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer wants an invoice, not invoice_request");
/* BOLT-offers #12:
* - SHOULD not respond to an offer if the current time is after
* `absolute_expiry`.
*/
if (sent->offer->absolute_expiry
&& time_now().ts.tv_sec > *sent->offer->absolute_expiry)
return command_fail(cmd, OFFER_EXPIRED, "Offer expired");
/* BOLT-offers-recurrence #12:
* - if the offer did not specify `amount`:
* - MUST specify `amount`.`msat` in multiples of the minimum
* lightning-payable unit (e.g. milli-satoshis for bitcoin) for
* `chain` (or for bitcoin, if there is no `chain`).
* - otherwise:
* - MAY omit `amount`.
* - if it sets `amount`:
* - MUST specify `amount`.`msat` as greater or equal to amount
* expected by the offer (before any proportional period amount).
*/
if (sent->offer->amount) {
/* FIXME: Check after quantity? */
if (msat) {
invreq->amount = tal_dup(invreq, u64,
&msat->millisatoshis); /* Raw: tu64 */
}
} else {
if (!msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"msatoshi parameter required");
invreq->amount = tal_dup(invreq, u64,
&msat->millisatoshis); /* Raw: tu64 */
}
/* BOLT-offers #12:
* - if the offer had a `quantity_min` or `quantity_max` field:
* - MUST set `quantity`
* - MUST set it within that (inclusive) range.
* - otherwise:
* - MUST NOT set `quantity`
*/
if (sent->offer->quantity_min || sent->offer->quantity_max) {
if (!invreq->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter required");
if (sent->offer->quantity_min
&& *invreq->quantity < *sent->offer->quantity_min)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be >= %"PRIu64,
*sent->offer->quantity_min);
if (sent->offer->quantity_max
&& *invreq->quantity > *sent->offer->quantity_max)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be <= %"PRIu64,
*sent->offer->quantity_max);
} else {
if (invreq->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter unnecessary");
}
/* BOLT-offers-recurrence #12:
* - if the offer contained `recurrence`:
*/
if (sent->offer->recurrence) {
/* BOLT-offers-recurrence #12:
* - for the initial request:
*...
* - MUST set `recurrence_counter` `counter` to 0.
*/
/* BOLT-offers-recurrence #12:
* - for any successive requests:
*...
* - MUST set `recurrence_counter` `counter` to one greater
* than the highest-paid invoice.
*/
if (!invreq->recurrence_counter)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"needs recurrence_counter");
/* BOLT-offers-recurrence #12:
* - if the offer contained `recurrence_base` with
* `start_any_period` non-zero:
* - MUST include `recurrence_start`
*...
* - otherwise:
* - MUST NOT include `recurrence_start`
*/
if (sent->offer->recurrence_base
&& sent->offer->recurrence_base->start_any_period) {
if (!invreq->recurrence_start)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"needs recurrence_start");
} else {
if (invreq->recurrence_start)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"unnecessary recurrence_start");
}
/* recurrence_label uniquely identifies this series of
* payments (unless they supply secret themselves)! */
if (!rec_label && !payer_secret)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"needs recurrence_label");
} else {
/* BOLT-offers-recurrence #12:
* - otherwise:
* - MUST NOT set `recurrence_counter`.
* - MUST NOT set `recurrence_start`
*/
if (invreq->recurrence_counter)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"unnecessary recurrence_counter");
if (invreq->recurrence_start)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"unnecessary recurrence_start");
}
/* BOLT-offers #12:
*
* - if the chain for the invoice is not solely bitcoin:
* - MUST specify `chains` the offer is valid for.
* - otherwise:
* - the bitcoin chain is implied as the first and only entry.
*/
if (!streq(chainparams->network_name, "bitcoin")) {
invreq->chain = tal_dup(invreq, struct bitcoin_blkid,
&chainparams->genesis_blockhash);
}
invreq->features
= plugin_feature_set(cmd->plugin)->bits[BOLT11_FEATURE];
/* invreq->payer_note is not a nul-terminated string! */
if (payer_note)
invreq->payer_note = tal_dup_arr(invreq, utf8,
payer_note, strlen(payer_note),
0);
/* They can provide a secret, and we don't assume it's our job
* to pay. */
if (payer_secret)
return force_payer_secret(cmd, sent, invreq, payer_secret);
/* Make the invoice request (fills in payer_key and payer_info) */
req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest",
&invreq_done,
&forward_error,
sent);
json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq));
if (rec_label)
json_add_string(req->js, "recurrence_label", rec_label);
return send_outreq(cmd->plugin, req);
}
/* FIXME: Using a hook here is not ideal: technically it doesn't mean
* it's actually hit the db! But using waitinvoice is also suboptimal
* because we don't have libplugin infra to cancel a pending req (and I
* want to rewrite our wait* API anyway) */
static struct command_result *invoice_payment(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct sent *i;
const jsmntok_t *ptok, *preimagetok, *msattok;
struct preimage preimage;
struct amount_msat msat;
ptok = json_get_member(buf, params, "payment");
preimagetok = json_get_member(buf, ptok, "preimage");
msattok = json_get_member(buf, ptok, "msat");
if (!preimagetok || !msattok)
plugin_err(cmd->plugin,
"Invalid invoice_payment %.*s",
json_tok_full_len(params),
json_tok_full(buf, params));
hex_decode(buf + preimagetok->start,
preimagetok->end - preimagetok->start,
&preimage, sizeof(preimage));
json_to_msat(buf, msattok, &msat);
list_for_each(&sent_list, i, list) {
struct out_req *req;
if (!i->inv)
continue;
if (!preimage_eq(&preimage, &i->inv_preimage))
continue;
/* It was paid! Success. Return as per waitinvoice. */
req = jsonrpc_request_start(cmd->plugin, i->cmd, "waitinvoice",
&forward_result,
&forward_error,
i);
json_add_escaped_string(req->js, "label", i->inv_label);
discard_result(send_outreq(cmd->plugin, req));
break;
}
return command_hook_success(cmd);
}
/* 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);
}
sent->path = path_to_node(sent, cmd->plugin,
sent->offer->node_id);
if (!sent->path)
return connect_direct(cmd, sent->offer->node_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 bool json_to_bip340sig(const char *buffer, const jsmntok_t *tok,
struct bip340sig *sig)
{
return hex_decode(buffer + tok->start, tok->end - tok->start,
sig->u8, sizeof(sig->u8));
}
static struct command_result *payersign_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *sig;
sent->inv->refund_signature = tal(sent->inv, struct bip340sig);
sig = json_get_member(buf, result, "signature");
json_to_bip340sig(buf, sig, sent->inv->refund_signature);
return sign_invoice(cmd, sent);
}
/* They're offering a refund, so we need to sign with same key as used
* in initial payment. */
static struct command_result *listsendpays_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *t, *arr = json_get_member(buf, result, "payments");
size_t i;
const u8 *public_tweak = NULL, *p;
u8 *msg;
size_t len;
struct sha256 merkle;
struct out_req *req;
/* Linearize populates ->fields */
msg = tal_arr(tmpctx, u8, 0);
towire_tlv_invoice(&msg, sent->inv);
p = msg;
len = tal_bytelen(msg);
sent->inv = fromwire_tlv_invoice(cmd, &p, &len);
if (!sent->inv)
plugin_err(cmd->plugin,
"Could not remarshall %s", tal_hex(tmpctx, msg));
merkle_tlv(sent->inv->fields, &merkle);
json_for_each_arr(i, t, arr) {
const jsmntok_t *b12tok;
struct tlv_invoice *inv;
char *fail;
b12tok = json_get_member(buf, t, "bolt12");
if (!b12tok) {
/* This could happen if they try to refund a bolt11 */
plugin_log(cmd->plugin, LOG_UNUSUAL,
"Not bolt12 string in %.*s?",
json_tok_full_len(t),
json_tok_full(buf, t));
continue;
}
inv = invoice_decode(tmpctx, buf + b12tok->start,
b12tok->end - b12tok->start,
plugin_feature_set(cmd->plugin),
chainparams,
&fail);
if (!inv) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Bad bolt12 string in %.*s?",
json_tok_full_len(t),
json_tok_full(buf, t));
continue;
}
public_tweak = inv->payer_info;
break;
}
if (!public_tweak)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Cannot find invoice %s for refund",
type_to_string(tmpctx, struct sha256,
sent->offer->refund_for));
/* BOLT-offers #12:
* - MUST set `refund_signature` to the signature of the
* `refunded_payment_hash` using prefix `refund_signature` and the
* `payer_key` from the to-be-refunded invoice.
*/
req = jsonrpc_request_start(cmd->plugin, cmd, "payersign",
&payersign_done,
&forward_error,
sent);
json_add_string(req->js, "messagename", "invoice");
json_add_string(req->js, "fieldname", "refund_signature");
json_add_sha256(req->js, "merkle", &merkle);
json_add_hex_talarr(req->js, "tweak", public_tweak);
return send_outreq(cmd->plugin, req);
}
static struct command_result *json_sendinvoice(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
struct amount_msat *msat;
struct out_req *req;
u32 *timeout;
struct sent *sent = tal(cmd, struct sent);
sent->inv = tlv_invoice_new(cmd);
sent->invreq = NULL;
sent->cmd = cmd;
/* FIXME: Support recurring send_invoice offers? */
if (!param(cmd, buffer, params,
p_req("offer", param_offer, &sent->offer),
p_req("label", param_label, &sent->inv_label),
p_opt("amount_msat|msatoshi", param_msat, &msat),
p_opt_def("timeout", param_number, &timeout, 90),
p_opt("quantity", param_u64, &sent->inv->quantity),
NULL))
return command_param_failed();
/* This is how long we'll wait for a reply for. */
sent->wait_timeout = *timeout;
/* Check they are really trying to send us money. */
if (!sent->offer->send_invoice)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer wants an invoice_request, not invoice");
/* If they don't tell us how much, base it on offer. */
if (!msat) {
if (sent->offer->currency)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer in different currency: need amount");
if (!sent->offer->amount)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer did not specify: need amount");
sent->inv->amount = tal_dup(sent->inv, u64, sent->offer->amount);
if (sent->inv->quantity)
*sent->inv->amount *= *sent->inv->quantity;
} else
sent->inv->amount = tal_dup(sent->inv, u64,
&msat->millisatoshis); /* Raw: tlv */
/* FIXME: Support blinded paths, in which case use fake nodeid */
/* BOLT-offers #12:
* - otherwise (responding to a `send_invoice` offer):
* - MUST set `node_id` to the id of the node to send payment to.
* - MUST set `description` the same as the offer.
*/
sent->inv->node_id = tal(sent->inv, struct pubkey);
sent->inv->node_id->pubkey = local_id.pubkey;
sent->inv->description
= tal_dup_talarr(sent->inv, char, sent->offer->description);
/* BOLT-offers #12:
* - MUST set (or not set) `send_invoice` the same as the offer.
*/
sent->inv->send_invoice = tal(sent->inv, struct tlv_invoice_send_invoice);
/* BOLT-offers #12:
* - MUST set `offer_id` to the id of the offer.
*/
sent->inv->offer_id = tal(sent->inv, struct sha256);
merkle_tlv(sent->offer->fields, sent->inv->offer_id);
/* BOLT-offers #12:
* - SHOULD not respond to an offer if the current time is after
* `absolute_expiry`.
*/
if (sent->offer->absolute_expiry
&& time_now().ts.tv_sec > *sent->offer->absolute_expiry)
return command_fail(cmd, OFFER_EXPIRED, "Offer expired");
/* BOLT-offers #12:
* - otherwise (responding to a `send_invoice` offer):
*...
* - if the offer had a `quantity_min` or `quantity_max` field:
* - MUST set `quantity`
* - MUST set it within that (inclusive) range.
* - otherwise:
* - MUST NOT set `quantity`
*/
if (sent->offer->quantity_min || sent->offer->quantity_max) {
if (!sent->inv->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter required");
if (sent->offer->quantity_min
&& *sent->inv->quantity < *sent->offer->quantity_min)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be >= %"PRIu64,
*sent->offer->quantity_min);
if (sent->offer->quantity_max
&& *sent->inv->quantity > *sent->offer->quantity_max)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be <= %"PRIu64,
*sent->offer->quantity_max);
} else {
if (sent->inv->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter unnecessary");
}
/* BOLT-offers #12:
* - MUST set `created_at` to the number of seconds since Midnight 1
* January 1970, UTC when the offer was created.
*/
sent->inv->created_at = tal(sent->inv, u64);
*sent->inv->created_at = time_now().ts.tv_sec;
/* BOLT-offers #12:
* - if the expiry for accepting payment is not 7200 seconds after
* `created_at`:
* - MUST set `relative_expiry` `seconds_from_creation` to the number
* of seconds after `created_at` that payment of this invoice should
* not be attempted.
*/
if (sent->wait_timeout != 7200) {
sent->inv->relative_expiry = tal(sent->inv, u32);
*sent->inv->relative_expiry = sent->wait_timeout;
}
/* BOLT-offers #12:
* - MUST set `payer_key` to the `node_id` of the offer.
*/
sent->inv->payer_key = sent->offer->node_id;
/* FIXME: recurrence? */
if (sent->offer->recurrence)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"FIXME: handle recurring send_invoice offer!");
/* BOLT-offers #12:
*
* - if the chain for the invoice is not solely bitcoin:
* - MUST specify `chains` the offer is valid for.
* - otherwise:
* - the bitcoin chain is implied as the first and only entry.
*/
if (!streq(chainparams->network_name, "bitcoin")) {
sent->inv->chain = tal_dup(sent->inv, struct bitcoin_blkid,
&chainparams->genesis_blockhash);
}
sent->inv->features
= plugin_feature_set(cmd->plugin)->bits[BOLT11_FEATURE];
randombytes_buf(&sent->inv_preimage, sizeof(sent->inv_preimage));
sent->inv->payment_hash = tal(sent->inv, struct sha256);
sha256(sent->inv->payment_hash,
&sent->inv_preimage, sizeof(sent->inv_preimage));
/* BOLT-offers #12:
* - MUST set (or not set) `refund_for` exactly as the offer did.
* - if it sets `refund_for`:
* - MUST set `refund_signature` to the signature of the
* `refunded_payment_hash` using prefix `refund_signature` and
* the `payer_key` from the to-be-refunded invoice.
* - otherwise:
* - MUST NOT set `refund_signature`
*/
if (sent->offer->refund_for) {
sent->inv->refund_for = sent->offer->refund_for;
/* Find original payment invoice */
req = jsonrpc_request_start(cmd->plugin, cmd, "listsendpays",
&listsendpays_done,
&forward_error,
sent);
json_add_sha256(req->js, "payment_hash",
sent->offer->refund_for);
return send_outreq(cmd->plugin, req);
}
return sign_invoice(cmd, sent);
}
#if DEVELOPER
static struct command_result *param_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,
&fail);
if (!*invreq)
return command_fail_badparam(cmd, name, buffer, tok,
tal_fmt(cmd,
"Unparsable invreq: %s",
fail));
return NULL;
}
static struct command_result *json_rawrequest(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
struct sent *sent = tal(cmd, struct sent);
u32 *timeout;
struct pubkey *node_id;
if (!param(cmd, buffer, params,
p_req("invreq", param_invreq, &sent->invreq),
p_req("nodeid", param_pubkey, &node_id),
p_opt_def("timeout", param_number, &timeout, 60),
NULL))
return command_param_failed();
/* This is how long we'll wait for a reply for. */
sent->wait_timeout = *timeout;
sent->cmd = cmd;
sent->offer = NULL;
sent->path = path_to_node(sent, cmd->plugin, node_id);
if (!sent->path) {
return connect_direct(cmd, node_id,
sendinvreq_after_connect, sent);
}
return sendinvreq_after_connect(cmd, NULL, NULL, sent);
}
#endif /* DEVELOPER */
static const struct plugin_command commands[] = {
{
"fetchinvoice",
"payment",
"Request remote node for an invoice for this {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.",
NULL,
json_fetchinvoice,
},
{
"sendinvoice",
"payment",
"Request remote node for to pay this send_invoice {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.",
NULL,
json_sendinvoice,
},
#if DEVELOPER
{
"dev-rawrequest",
"util",
"Send {invreq} to {nodeid}, wait {timeout} (60 seconds by default)",
NULL,
json_rawrequest,
},
#endif /* DEVELOPER */
};
static const char *init(struct plugin *p, const char *buf UNUSED,
const jsmntok_t *config UNUSED)
{
bool exp_offers;
rpc_scan(p, "getinfo",
take(json_out_obj(NULL, NULL, NULL)),
"{id:%}", JSON_SCAN(json_to_pubkey, &local_id));
rpc_scan(p, "listconfigs",
take(json_out_obj(NULL, "config", "experimental-offers")),
"{experimental-offers:%}",
JSON_SCAN(json_to_bool, &exp_offers));
if (!exp_offers)
return "offers not enabled in config";
return NULL;
}
static const struct plugin_hook hooks[] = {
{
"onion_message_ourpath",
recv_modern_onion_message
},
{
"invoice_payment",
invoice_payment,
},
};
int main(int argc, char *argv[])
{
setup_locale();
plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL,
commands, ARRAY_SIZE(commands),
/* No notifications */
NULL, 0,
hooks, ARRAY_SIZE(hooks),
NULL, 0,
plugin_option("fetchinvoice-noconnect", "flag",
"Don't try to connect directly to fetch an invoice.",
flag_option, &disable_connect),
NULL);
}