Browse Source
And if we have a matching `send_invoice` offer, try to pay it! Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>ppa
Rusty Russell
4 years ago
4 changed files with 432 additions and 2 deletions
@ -0,0 +1,410 @@ |
|||
#include <bitcoin/chainparams.h> |
|||
#include <bitcoin/preimage.h> |
|||
#include <ccan/cast/cast.h> |
|||
#include <ccan/mem/mem.h> |
|||
#include <common/bech32_util.h> |
|||
#include <common/bolt12.h> |
|||
#include <common/bolt12_merkle.h> |
|||
#include <common/json_stream.h> |
|||
#include <common/overflows.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; |
|||
|
|||
const char *buf; |
|||
/* May be NULL */ |
|||
const jsmntok_t *replytok; |
|||
|
|||
/* The offer, once we've looked it up. */ |
|||
struct tlv_offer *offer; |
|||
}; |
|||
|
|||
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_invoice_error *err; |
|||
u8 *errdata; |
|||
|
|||
full_fmt = tal_fmt(tmpctx, "Failed invoice %s", |
|||
invoice_encode(tmpctx, inv->inv)); |
|||
if (inv->inv->offer_id) |
|||
tal_append_fmt(&full_fmt, " for offer %s", |
|||
type_to_string(tmpctx, struct sha256, |
|||
inv->inv->offer_id)); |
|||
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->replytok) |
|||
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! */ |
|||
|
|||
errdata = tal_arr(cmd, u8, 0); |
|||
towire_invoice_error(&errdata, err); |
|||
return send_onion_reply(cmd, inv->buf, inv->replytok, "invoice_error", errdata); |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
#define inv_must_have(cmd_, i_, fld_) \ |
|||
test_field(cmd_, i_, i_->inv->fld_ != NULL, #fld_, "missing") |
|||
#define inv_must_not_have(cmd_, i_, fld_) \ |
|||
test_field(cmd_, i_, i_->inv->fld_ == NULL, #fld_, "unexpected") |
|||
#define inv_must_equal_offer(cmd_, i_, fld_) \ |
|||
test_field_eq(cmd_, i_, i_->inv->fld_, i_->offer->fld_, #fld_) |
|||
|
|||
static struct command_result * |
|||
test_field(struct command *cmd, |
|||
const struct inv *inv, |
|||
bool test, const char *fieldname, const char *what) |
|||
{ |
|||
if (!test) |
|||
return fail_inv(cmd, inv, "%s %s", what, fieldname); |
|||
return NULL; |
|||
} |
|||
|
|||
static struct command_result * |
|||
test_field_eq(struct command *cmd, |
|||
const struct inv *inv, |
|||
const tal_t *invfield, |
|||
const tal_t *offerfield, |
|||
const char *fieldname) |
|||
{ |
|||
if (invfield && !offerfield) |
|||
return fail_inv(cmd, inv, "Unexpected %s", fieldname); |
|||
if (!invfield && offerfield) |
|||
return fail_inv(cmd, inv, "Expected %s", fieldname); |
|||
if (!memeq(invfield, tal_bytelen(invfield), |
|||
offerfield, tal_bytelen(offerfield))) |
|||
return fail_inv(cmd, inv, "Different %s", fieldname); |
|||
return NULL; |
|||
} |
|||
|
|||
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->amount); |
|||
|
|||
plugin_log(cmd->plugin, LOG_INFORM, |
|||
"Payed out %s for offer %s%s: %.*s", |
|||
type_to_string(tmpctx, struct amount_msat, &msat), |
|||
type_to_string(tmpctx, struct sha256, inv->inv->offer_id), |
|||
inv->offer->refund_for ? " (refund)": "", |
|||
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 *listoffers_done(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct inv *inv) |
|||
{ |
|||
const jsmntok_t *arr = json_get_member(buf, result, "offers"); |
|||
const jsmntok_t *offertok, *activetok, *b12tok; |
|||
bool active; |
|||
struct amount_msat amt; |
|||
char *fail; |
|||
struct out_req *req; |
|||
struct command_result *err; |
|||
|
|||
/* BOLT-offers #12:
|
|||
* - otherwise if `offer_id` is set: |
|||
* - MUST reject the invoice if the `offer_id` does not refer an |
|||
* unexpired offer with `send_invoice` |
|||
*/ |
|||
if (arr->size == 0) |
|||
return fail_inv(cmd, inv, "Unknown offer"); |
|||
|
|||
plugin_log(cmd->plugin, LOG_INFORM, |
|||
"Attempting payment of offer %.*s", |
|||
json_tok_full_len(result), |
|||
json_tok_full(buf, result)); |
|||
|
|||
offertok = arr + 1; |
|||
activetok = json_get_member(buf, offertok, "active"); |
|||
if (!activetok) { |
|||
return fail_internalerr(cmd, inv, |
|||
"Missing active: %.*s", |
|||
json_tok_full_len(offertok), |
|||
json_tok_full(buf, offertok)); |
|||
} |
|||
json_to_bool(buf, activetok, &active); |
|||
if (!active) |
|||
return fail_inv(cmd, inv, "Offer no longer available"); |
|||
|
|||
b12tok = json_get_member(buf, offertok, "bolt12"); |
|||
if (!b12tok) { |
|||
return fail_internalerr(cmd, inv, |
|||
"Missing bolt12: %.*s", |
|||
json_tok_full_len(offertok), |
|||
json_tok_full(buf, offertok)); |
|||
} |
|||
inv->offer = offer_decode(inv, |
|||
buf + b12tok->start, |
|||
b12tok->end - b12tok->start, |
|||
plugin_feature_set(cmd->plugin), |
|||
chainparams, &fail); |
|||
if (!inv->offer) { |
|||
return fail_internalerr(cmd, inv, |
|||
"Invalid offer: %s (%.*s)", |
|||
fail, |
|||
json_tok_full_len(offertok), |
|||
json_tok_full(buf, offertok)); |
|||
} |
|||
|
|||
if (inv->offer->absolute_expiry |
|||
&& time_now().ts.tv_sec >= *inv->offer->absolute_expiry) { |
|||
/* FIXME: do deloffer to disable it */ |
|||
return fail_inv(cmd, inv, "Offer expired"); |
|||
} |
|||
|
|||
if (!inv->offer->send_invoice) { |
|||
return fail_inv(cmd, inv, "Offer did not expect invoice"); |
|||
} |
|||
|
|||
/* BOLT-offers #12:
|
|||
* - MUST reject the invoice unless the following fields are equal |
|||
* or unset exactly as they are in the `offer`: |
|||
* - `refund_for` |
|||
* - `description` |
|||
* - `vendor` |
|||
*/ |
|||
err = inv_must_equal_offer(cmd, inv, refund_for); |
|||
if (err) |
|||
return err; |
|||
err = inv_must_equal_offer(cmd, inv, description); |
|||
if (err) |
|||
return err; |
|||
err = inv_must_equal_offer(cmd, inv, vendor); |
|||
if (err) |
|||
return err; |
|||
|
|||
/* BOLT-offers #12:
|
|||
* - if the offer had a `quantity_min` or `quantity_max` field: |
|||
* - MUST fail the request if there is no `quantity` field. |
|||
* - MUST fail the request if there is `quantity` is not within |
|||
* that (inclusive) range. |
|||
* - otherwise: |
|||
* - MUST fail the request if there is a `quantity` field. |
|||
*/ |
|||
if (inv->offer->quantity_min || inv->offer->quantity_max) { |
|||
err = inv_must_have(cmd, inv, quantity); |
|||
if (err) |
|||
return err; |
|||
|
|||
if (inv->offer->quantity_min && |
|||
*inv->inv->quantity < *inv->offer->quantity_min) { |
|||
return fail_inv(cmd, inv, |
|||
"quantity %"PRIu64 " < %"PRIu64, |
|||
*inv->inv->quantity, |
|||
*inv->offer->quantity_min); |
|||
} |
|||
|
|||
if (inv->offer->quantity_max && |
|||
*inv->inv->quantity > *inv->offer->quantity_max) { |
|||
return fail_inv(cmd, inv, |
|||
"quantity %"PRIu64" > %"PRIu64, |
|||
*inv->inv->quantity, |
|||
*inv->offer->quantity_max); |
|||
} |
|||
} else { |
|||
err = inv_must_not_have(cmd, inv, quantity); |
|||
if (err) |
|||
return err; |
|||
} |
|||
|
|||
/* BOLT-offers #12:
|
|||
* - MUST reject the invoice if `msat` is not present. |
|||
*/ |
|||
err = inv_must_have(cmd, inv, amount); |
|||
if (err) |
|||
return err; |
|||
|
|||
/* FIXME: Handle alternate currency conversion here! */ |
|||
if (inv->offer->currency) |
|||
return fail_inv(cmd, inv, "FIXME: support currency"); |
|||
|
|||
amt = amount_msat(*inv->inv->amount); |
|||
/* If you send an offer without an amount, you want to give away
|
|||
* unlimited money. Err, ok? */ |
|||
if (inv->offer->amount) { |
|||
struct amount_msat expected = amount_msat(*inv->offer->amount); |
|||
|
|||
/* We could allow invoices for less, I suppose. */ |
|||
if (!amount_msat_eq(expected, amt)) |
|||
return fail_inv(cmd, inv, "Expected invoice for %s", |
|||
fmt_amount_msat(tmpctx, &expected)); |
|||
} |
|||
|
|||
plugin_log(cmd->plugin, LOG_INFORM, |
|||
"Attempting payment of %s for offer %s%s", |
|||
type_to_string(tmpctx, struct amount_msat, &amt), |
|||
type_to_string(tmpctx, struct sha256, inv->inv->offer_id), |
|||
inv->offer->refund_for ? " (refund)": ""); |
|||
|
|||
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, "localofferid", inv->inv->offer_id); |
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *listoffers_error(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *err, |
|||
struct inv *inv) |
|||
{ |
|||
return fail_internalerr(cmd, inv, |
|||
"listoffers gave JSON error: %.*s", |
|||
json_tok_full_len(err), |
|||
json_tok_full(buf, err)); |
|||
} |
|||
|
|||
struct command_result *handle_invoice(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *invtok, |
|||
const jsmntok_t *replytok) |
|||
{ |
|||
const u8 *invbin = json_tok_bin_from_hex(cmd, buf, invtok); |
|||
size_t len = tal_count(invbin); |
|||
struct inv *inv = tal(cmd, struct inv); |
|||
struct out_req *req; |
|||
struct command_result *err; |
|||
int bad_feature; |
|||
struct sha256 m, shash; |
|||
|
|||
/* Make a copy of entire buffer, for later. */ |
|||
inv->buf = tal_dup_arr(inv, char, buf, replytok->end, 0); |
|||
inv->replytok = replytok; |
|||
|
|||
inv->inv = tlv_invoice_new(cmd); |
|||
if (!fromwire_invoice(&invbin, &len, inv->inv)) { |
|||
return fail_inv(cmd, inv, |
|||
"Invalid invoice %s", |
|||
tal_hex(tmpctx, invbin)); |
|||
} |
|||
|
|||
/* BOLT-offers #12:
|
|||
* |
|||
* The reader of an invoice_request: |
|||
*... |
|||
* - MUST fail the request if `features` contains unknown even bits. |
|||
*/ |
|||
bad_feature = features_unsupported(plugin_feature_set(cmd->plugin), |
|||
inv->inv->features, |
|||
BOLT11_FEATURE); |
|||
if (bad_feature != -1) { |
|||
return fail_inv(cmd, inv, |
|||
"Unsupported inv feature %i", |
|||
bad_feature); |
|||
} |
|||
|
|||
/* BOLT-offers #12:
|
|||
* |
|||
* The reader of an invoice_request: |
|||
*... |
|||
* - MUST fail the request if `chains` does not include (or imply) a |
|||
* supported chain. |
|||
*/ |
|||
if (!bolt12_chains_match(inv->inv->chains, chainparams)) { |
|||
return fail_inv(cmd, inv, |
|||
"Wrong chains %s", |
|||
tal_hex(tmpctx, inv->inv->chains)); |
|||
} |
|||
|
|||
/* BOLT-offers #12:
|
|||
* - MUST reject the invoice if `signature` is not a valid signature |
|||
* using `node_id` as described in |
|||
* [Signature Calculation](#signature-calculation). |
|||
*/ |
|||
err = inv_must_have(cmd, inv, node_id); |
|||
if (err) |
|||
return err; |
|||
|
|||
err = inv_must_have(cmd, inv, signature); |
|||
if (err) |
|||
return err; |
|||
|
|||
merkle_tlv(inv->inv->fields, &m); |
|||
sighash_from_merkle("invoice", "signature", &m, &shash); |
|||
if (secp256k1_schnorrsig_verify(secp256k1_ctx, |
|||
inv->inv->signature->u8, |
|||
shash.u.u8, |
|||
&inv->inv->node_id->pubkey) != 1) { |
|||
return fail_inv(cmd, inv, "Bad signature"); |
|||
} |
|||
|
|||
/* We don't pay random invoices off the internet, sorry. */ |
|||
err = inv_must_have(cmd, inv, offer_id); |
|||
if (err) |
|||
return err; |
|||
|
|||
/* Now find the offer. */ |
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "listoffers", |
|||
listoffers_done, listoffers_error, inv); |
|||
json_add_sha256(req->js, "offer_id", inv->inv->offer_id); |
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
@ -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! replytok could be NULL. */ |
|||
struct command_result *handle_invoice(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *invtok, |
|||
const jsmntok_t *replytok); |
|||
#endif /* LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H */ |
Loading…
Reference in new issue