Skip to content

Commit

Permalink
lightningd: implement invoice hook.
Browse files Browse the repository at this point in the history
This also allows plugins to do "hold invoices" a-la LND, useful for
just-in-time inventory handling.

We're careful to handle the invoice getting paid behind our backs, and
the incoming HTLC going away.

Once @cdecker's sphinx rework is in, we can also hand the raw payload
to the invoice_payment_hook, for special effects.

Signed-off-by: Rusty Russell <[email protected]>
  • Loading branch information
rustyrussell committed Apr 12, 2019
1 parent 6630b99 commit 43d07aa
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions doc/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
128 changes: 123 additions & 5 deletions lightningd/invoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <lightningd/options.h>
#include <lightningd/peer_control.h>
#include <lightningd/peer_htlcs.h>
#include <lightningd/plugin_hook.h>
#include <lightningd/subd.h>
#include <sodium/randombytes.h>
#include <wire/wire_sync.h>
Expand Down Expand Up @@ -101,13 +102,125 @@ 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,
const struct amount_msat msat)
{
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);
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 43d07aa

Please sign in to comment.