From 80813f2233d62e537426e20ec6ffae7808e82ecc Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 17 Jun 2020 13:03:19 +0200 Subject: [PATCH] paymod: Implement keysend sending support as a native RPC command This makes use of the payment modifier structure to just add the preimage to the TLV payload for the last hop. Changelog-Added: JSON-RPC: The `keysend` command allows sending to a node without requiring an invoice first. --- plugins/keysend.c | 169 +++++++++++++++++++++++++++++++++++++++ tests/plugins/keysend.py | 118 --------------------------- tests/test_pay.py | 5 +- 3 files changed, 170 insertions(+), 122 deletions(-) delete mode 100755 tests/plugins/keysend.py diff --git a/plugins/keysend.c b/plugins/keysend.c index 5c6075f05..bf45f1edd 100644 --- a/plugins/keysend.c +++ b/plugins/keysend.c @@ -2,17 +2,186 @@ #include #include #include +#include #include #define PREIMAGE_TLV_TYPE 5482373484 #define KEYSEND_FEATUREBIT 55 +static unsigned int maxdelay_default; +static struct node_id my_id; + +/***************************************************************************** + * Keysend modifier + * ================ + * + * The keysend modifier adds the payment preimage to the TLV payload. This + * enables the recipient to accept the payment despite it not correspondin to + * an invoice that the recipient created. Keysend does not provide any proof + * or payment, but does not require an out-of-band communication round to get + * an invoice first. + */ + +/* FIXME: If we have more than one plugin using keysend we can move this to + * libplugin-pay.c */ + +struct keysend_data { + struct preimage preimage; +}; + +REGISTER_PAYMENT_MODIFIER_HEADER(keysend, struct keysend_data); + +static struct keysend_data *keysend_init(struct payment *p) +{ + struct keysend_data *d; + struct sha256 payment_hash; + if (p->parent == NULL) { + /* If we are the root payment we generate a random preimage + * and populate the preimage field in the keysend_data and the + * payment_hash in the payment. */ + d = tal(p, struct keysend_data); + randombytes_buf(&d->preimage, sizeof(d->preimage)); + ccan_sha256(&payment_hash, &d->preimage, sizeof(d->preimage)); + p->payment_hash = tal_dup(p, struct sha256, &payment_hash); + return d; + } else { + /* If we are a child payment (retry or split) we copy the + * parent's information, since the payment_hash needs to match + * in order to be collated at the recipient. */ + return payment_mod_keysend_get_data(p->parent); + } +} + +static void keysend_cb(struct keysend_data *d, struct payment *p) { + struct route_hop *last_hop; + struct createonion_hop *last_payload; + size_t hopcount; + u8 *raw_preimage; + + if (p->step == PAYMENT_STEP_GOT_ROUTE) { + /* Force the last step to be a TLV, we might not have an + * announcement and it still supports it. Required later when + * we adjust the payload. */ + last_hop = &p->route[tal_count(p->route) - 1]; + last_hop->style = ROUTE_HOP_TLV; + } + + if (p->step != PAYMENT_STEP_ONION_PAYLOAD) + return payment_continue(p); + + raw_preimage = tal_dup_arr(p->createonion_request, u8, d->preimage.r, + sizeof(d->preimage), 0); + + hopcount = tal_count(p->createonion_request->hops); + last_payload = &p->createonion_request->hops[hopcount - 1]; + tlvstream_set_raw(&last_payload->tlv_payload->fields, PREIMAGE_TLV_TYPE, + take(raw_preimage)); + + return payment_continue(p); +} + +REGISTER_PAYMENT_MODIFIER(keysend, struct keysend_data *, keysend_init, + keysend_cb); +/* + * End of keysend modifier + *****************************************************************************/ static void init(struct plugin *p, const char *buf UNUSED, const jsmntok_t *config UNUSED) { + const char *field; + + field = rpc_delve(tmpctx, p, "getinfo", + take(json_out_obj(NULL, NULL, NULL)), ".id"); + if (!node_id_from_hexstr(field, strlen(field), &my_id)) + plugin_err(p, "getinfo didn't contain valid id: '%s'", field); + + field = + rpc_delve(tmpctx, p, "listconfigs", + take(json_out_obj(NULL, "config", "max-locktime-blocks")), + ".max-locktime-blocks"); + maxdelay_default = atoi(field); +} + +struct payment_modifier *pay_mods[8] = { + &keysend_pay_mod, + &local_channel_hints_pay_mod, + &directpay_pay_mod, + &shadowroute_pay_mod, + &exemptfee_pay_mod, + &waitblockheight_pay_mod, + &retry_pay_mod, + NULL, +}; + +static struct command_result *json_keysend(struct command *cmd, const char *buf, + const jsmntok_t *params) +{ + struct payment *p; + const char *label; + struct amount_msat *exemptfee, *msat; + struct node_id *destination; + u64 *maxfee_pct_millionths; + u32 *maxdelay; + unsigned int *retryfor; +#if DEVELOPER + bool *use_shadow; +#endif + p = payment_new(NULL, cmd, NULL /* No parent */, pay_mods); + if (!param(cmd, buf, params, + p_req("destination", param_node_id, &destination), + p_req("msatoshi", param_msat, &msat), + p_opt("label", param_string, &label), + p_opt_def("maxfeepercent", param_millionths, + &maxfee_pct_millionths, 500000), + p_opt_def("retry_for", param_number, &retryfor, 60), + p_opt_def("maxdelay", param_number, &maxdelay, + maxdelay_default), + p_opt_def("exemptfee", param_msat, &exemptfee, AMOUNT_MSAT(5000)), +#if DEVELOPER + p_opt_def("use_shadow", param_bool, &use_shadow, true), +#endif + NULL)) + return command_param_failed(); + + p->local_id = &my_id; + p->json_buffer = tal_steal(p, buf); + p->json_toks = params; + p->destination = tal_steal(p, destination); + p->payment_secret = NULL; + p->amount = *msat; + p->invoice = NULL; + p->bolt11 = NULL; + p->why = "Initial attempt"; + p->constraints.cltv_budget = *maxdelay; + p->deadline = timeabs_add(time_now(), time_from_sec(*retryfor)); + p->getroute->riskfactorppm = 10000000; + + if (!amount_msat_fee(&p->constraints.fee_budget, p->amount, 0, + *maxfee_pct_millionths / 100)) { + tal_free(p); + return command_fail( + cmd, JSONRPC2_INVALID_PARAMS, + "Overflow when computing fee budget, fee rate too high."); + } + p->constraints.cltv_budget = *maxdelay; + + payment_mod_exemptfee_get_data(p)->amount = *exemptfee; +#if DEVELOPER + payment_mod_shadowroute_get_data(p)->use_shadow = *use_shadow; +#endif + p->label = tal_steal(p, label); + payment_start(p); + return command_still_pending(cmd); } static const struct plugin_command commands[] = { + { + "keysend", + "payment", + "Send a payment without an invoice to a node", + "Send an unsolicited payment of {amount} to {destination}, by providing the recipient the necessary information to claim the payment", + json_keysend + }, }; static struct command_result * diff --git a/tests/plugins/keysend.py b/tests/plugins/keysend.py deleted file mode 100755 index 73ce4a8b7..000000000 --- a/tests/plugins/keysend.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -"""Temporary keysend plugin until we implement it in C - -This plugin is just used to test the ability to receive keysend payments until -we implement it in `plugins/keysend.c`. Most of this code is borrowed from the -noise plugin. - -""" - -from pyln.client import Plugin, RpcError -from pyln.proto.onion import TlvPayload, Tu32Field, Tu64Field -from binascii import hexlify -import os -import hashlib -import struct - - -plugin = Plugin() -TLV_KEYSEND_PREIMAGE = 5482373484 - - -def serialize_payload(n, blockheight): - """Serialize a legacy payload. - """ - block, tx, out = n['channel'].split('x') - payload = hexlify(struct.pack( - "!cQQL", b'\x00', - int(block) << 40 | int(tx) << 16 | int(out), - int(n['amount_msat']), - blockheight + n['delay'])).decode('ASCII') - payload += "00" * 12 - return payload - - -def buildpath(plugin, node_id, payload, amt, exclusions): - blockheight = plugin.rpc.getinfo()['blockheight'] - route = plugin.rpc.getroute(node_id, amt, 10, exclude=exclusions)['route'] - first_hop = route[0] - # Need to shift the parameters by one hop - hops = [] - for h, n in zip(route[:-1], route[1:]): - # We tell the node h about the parameters to use for n (a.k.a. h + 1) - hops.append({ - "type": "legacy", - "pubkey": h['id'], - "payload": serialize_payload(n, blockheight) - }) - - pl = TlvPayload() - pl.fields.append(Tu64Field(2, amt)) - pl.fields.append(Tu32Field(4, route[-1]['delay'])) - - for f in payload.fields: - pl.add_field(f.typenum, f.value) - - # The last hop has a special payload: - hops.append({ - "type": "tlv", - "pubkey": route[-1]['id'], - "payload": hexlify(pl.to_bytes()).decode('ASCII'), - }) - print(f"Keysend payload {hexlify(pl.to_bytes())}") - return first_hop, hops, route - - -def deliver(node_id, payload, amt, payment_hash, max_attempts=5): - """Do your best to deliver `payload` to `node_id`. - """ - exclusions = [] - payment_hash = hexlify(payment_hash).decode('ASCII') - - for attempt in range(max_attempts): - plugin.log("Starting attempt {} to deliver message to {}".format(attempt, node_id)) - - first_hop, hops, route = buildpath(plugin, node_id, payload, amt, exclusions) - onion = plugin.rpc.createonion(hops=hops, assocdata=payment_hash) - - plugin.rpc.sendonion( - onion=onion['onion'], - first_hop=first_hop, - payment_hash=payment_hash, - shared_secrets=onion['shared_secrets'], - ) - try: - plugin.rpc.waitsendpay(payment_hash=payment_hash) - return {'route': route, 'payment_hash': payment_hash, 'attempt': attempt} - except RpcError as e: - failcode = e.error['data']['failcode'] - failingidx = e.error['data']['erring_index'] - if failcode == 16399 or failingidx == len(hops): - return { - 'route': route, - 'payment_hash': payment_hash, - 'attempt': attempt + 1 - } - - plugin.log("Retrying delivery.") - - # TODO Store the failing channel in the exclusions - raise ValueError('Could not reach destination {node_id}'.format(node_id=node_id)) - - -@plugin.method('keysend') -def keysend(node_id, amount, plugin): - payload = TlvPayload() - payment_key = os.urandom(32) - payment_hash = hashlib.sha256(payment_key).digest() - payload.add_field(TLV_KEYSEND_PREIMAGE, payment_key) - res = deliver( - node_id, - payload, - amt=amount, - payment_hash=payment_hash - ) - return res - - -plugin.run() diff --git a/tests/test_pay.py b/tests/test_pay.py index ac8f4e315..7d8b8cd50 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -3045,11 +3045,8 @@ def test_excluded_adjacent_routehint(node_factory, bitcoind, compat): def test_keysend(node_factory): - # Use a temporary python plugin until we implement a native one - plugin_path = os.path.join(os.getcwd(), 'tests/plugins/keysend.py') - opts = {'plugin': plugin_path} amt = 10000 - l1, l2, l3 = node_factory.line_graph(3, opts=opts, wait_for_announce=True) + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) # The keysend featurebit must be set in the announcement, i.e., l1 should # learn that l3 supports keysends.