diff --git a/CHANGELOG.md b/CHANGELOG.md index a0de80fde..93fa6b24d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - JSON API: `listpeers` status now shows how many confirmations until channel is open (#2405) - Config: Adds parameter `min-capacity-sat` to reject tiny channels. - JSON API: `listforwards` now includes the time an HTLC was received and when it was resolved. Both are expressed as UNIX timestamps to facilitate parsing (Issue [#2491](https://github.com/ElementsProject/lightning/issues/2491), PR [#2528](https://github.com/ElementsProject/lightning/pull/2528)) +- JSON API: new plugin `invoice_payment` hook for intercepting invoices before they're paid. ### Changed diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 6f05ae7ce..9ee4ed82a 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -288,5 +288,26 @@ It is currently extremely restricted: Any response but "true" will cause lightningd to error without committing to the database! +#### `invoice_payment` + +This hook is called whenever a valid payment for an unpaid invoice has arrived. + +```json +{ + "payment": { + "label": "unique-label-for-invoice", + "preimage": "0000000000000000000000000000000000000000000000000000000000000000", + "msat": "10000msat" + } +} +``` + +The hook is sparse on purpose, since the plugin can use the JSON-RPC +`listinvoices` command to get additional details about this invoice. +It can return a non-zero `failure_code` field as defined for final +nodes in [BOLT 4][bolt4-failure-codes], or otherwise an empty object +to accept the payment. + [jsonrpc-spec]: https://www.jsonrpc.org/specification [jsonrpc-notification-spec]: https://www.jsonrpc.org/specification#notification +[bolt4-failure-codes]: https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 4609f483e..98f166e0e 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -101,6 +102,117 @@ static void wait_on_invoice(const struct invoice *invoice, void *cmd) tell_waiter_deleted((struct command *) cmd); } +struct invoice_payment_hook_payload { + struct lightningd *ld; + /* Set to NULL if it is deleted while waiting for plugin */ + struct htlc_in *hin; + /* What invoice it's trying to pay. */ + const struct json_escaped *label; + /* Amount it's offering. */ + struct amount_msat msat; + /* Preimage we'll give it if succeeds. */ + struct preimage preimage; + /* FIXME: Include raw payload! */ +}; + +static void +invoice_payment_serialize(struct invoice_payment_hook_payload *payload, + struct json_stream *stream) +{ + json_object_start(stream, "payment"); + json_add_escaped_string(stream, "label", payload->label); + json_add_hex(stream, "preimage", + &payload->preimage, sizeof(payload->preimage)); + json_add_string(stream, "msat", + type_to_string(tmpctx, struct amount_msat, + &payload->msat)); + json_object_end(stream); /* .payment */ +} + +/* We cheat and return 0 (not a valid onion_type) for "OK" */ +static enum onion_type +invoice_payment_deserialize(const tal_t *ctx, const char *buffer, + const jsmntok_t *toks) +{ + const jsmntok_t *resulttok, *t; + unsigned int val; + + resulttok = json_get_member(buffer, toks, "result"); + if (!resulttok) + fatal("Invalid invoice_payment_hook response: %.*s", + toks[0].end - toks[1].start, buffer); + + t = json_get_member(buffer, resulttok, "failure_code"); + if (!t) + return 0; + + if (!json_to_number(buffer, t, &val)) + fatal("Invalid invoice_payment_hook failure_code: %.*s", + toks[0].end - toks[1].start, buffer); + + /* UPDATE isn't valid for final nodes to return, and I think we + * assert elsewhere that we don't do this! */ + if (val & UPDATE) + fatal("Invalid invoice_payment_hook UPDATE failure_code: %.*s", + toks[0].end - toks[1].start, buffer); + + return val; +} + +/* Peer dies? Remove hin ptr from payload so we know to ignore plugin return */ +static void invoice_payload_remove_hin(struct htlc_in *hin, + struct invoice_payment_hook_payload *payload) +{ + assert(payload->hin == hin); + payload->hin = NULL; +} + +static void +invoice_payment_hook_cb(struct invoice_payment_hook_payload *payload, + enum onion_type failcode) +{ + struct lightningd *ld = payload->ld; + struct invoice invoice; + + tal_del_destructor2(payload->hin, invoice_payload_remove_hin, payload); + /* We want to free this, whatever happens. */ + tal_steal(tmpctx, payload); + + /* If peer dies or something, this can happen. */ + if (!payload->hin) { + log_debug(ld->log, "invoice '%s' paying htlc_in has gone!", + payload->label->s); + return; + } + + /* If invoice gets paid meanwhile (plugin responds out-of-order?) then + * we can also fail */ + if (!wallet_invoice_find_by_label(ld->wallet, &invoice, payload->label)) { + failcode = WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS; + fail_htlc(payload->hin, failcode); + return; + } + + if (failcode != 0) { + fail_htlc(payload->hin, failcode); + return; + } + + log_info(ld->log, "Resolved invoice '%s' with amount %s", + payload->label->s, + type_to_string(tmpctx, struct amount_msat, &payload->msat)); + wallet_invoice_resolve(ld->wallet, invoice, payload->msat); + fulfill_htlc(payload->hin, &payload->preimage); +} + +REGISTER_PLUGIN_HOOK(invoice_payment, + invoice_payment_hook_cb, + struct invoice_payment_hook_payload *, + invoice_payment_serialize, + struct invoice_payment_hook_payload *, + invoice_payment_deserialize, + enum onion_type); + void invoice_try_pay(struct lightningd *ld, struct htlc_in *hin, const struct sha256 *payment_hash, @@ -108,6 +220,7 @@ void invoice_try_pay(struct lightningd *ld, { struct invoice invoice; const struct invoice_details *details; + struct invoice_payment_hook_payload *payload; if (!wallet_invoice_find_unpaid(ld->wallet, &invoice, payment_hash)) { fail_htlc(hin, WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS); @@ -146,11 +259,16 @@ void invoice_try_pay(struct lightningd *ld, } } - log_info(ld->log, "Resolved invoice '%s' with amount %s", - details->label->s, - type_to_string(tmpctx, struct amount_msat, &msat)); - wallet_invoice_resolve(ld->wallet, invoice, msat); - fulfill_htlc(hin, &details->r); + payload = tal(ld, struct invoice_payment_hook_payload); + payload->ld = ld; + payload->label = tal_steal(payload, details->label); + payload->msat = msat; + payload->preimage = details->r; + payload->hin = hin; + tal_add_destructor2(hin, invoice_payload_remove_hin, payload); + + log_debug(ld->log, "Calling hook for invoice '%s'", details->label->s); + plugin_hook_call_invoice_payment(ld, payload, payload); } static bool hsm_sign_b11(const u5 *u5bytes,