From 63441075b5c9996dfc090cdd0647c709f2ac0da2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 7 Apr 2020 16:40:30 +0930 Subject: [PATCH] lightningd: allow htlc_accepted hook to replace onion payload. Signed-off-by: Rusty Russell Changelog-added: `htlc_accepted` hook can now offer a replacement onion `payload`. --- doc/PLUGINS.md | 5 +++++ lightningd/invoice.c | 11 +++++++-- lightningd/peer_htlcs.c | 38 +++++++++++++++++++++++++++++++- tests/plugins/replace_payload.py | 35 +++++++++++++++++++++++++++++ tests/test_plugin.py | 20 +++++++++++++++++ 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100755 tests/plugins/replace_payload.py diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 39233af2f..05905ac3f 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -803,6 +803,11 @@ This means that the plugin does not want to do anything special and if we're the recipient, or attempt to forward it otherwise. Notice that the usual checks such as sufficient fees and CLTV deltas are still enforced. +It can also replace the `onion.payload` by specifying a `payload` in +the response. This will be re-parsed; it's useful for removing onion +fields which a plugin doesn't want lightningd to consider. + + ```json { "result": "fail", diff --git a/lightningd/invoice.c b/lightningd/invoice.c index cf4199218..e05f324dc 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -314,15 +314,22 @@ invoice_check_payment(const tal_t *ctx, * - MUST fail the HTLC. */ if (feature_is_set(details->features, COMPULSORY_FEATURE(OPT_VAR_ONION)) - && !payment_secret) + && !payment_secret) { + log_debug(ld->log, "Attept to pay %s without secret", + type_to_string(tmpctx, struct sha256, &details->rhash)); return tal_free(details); + } if (payment_secret) { struct secret expected; invoice_secret(&details->r, &expected); - if (!secret_eq_consttime(payment_secret, &expected)) + if (!secret_eq_consttime(payment_secret, &expected)) { + log_debug(ld->log, "Attept to pay %s with wrong secret", + type_to_string(tmpctx, struct sha256, + &details->rhash)); return tal_free(details); + } } /* BOLT #4: diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 2185046e3..b71513b80 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -853,6 +853,20 @@ htlc_accepted_hook_try_resolve(struct htlc_accepted_hook_payload *request, } } +static u8 *prepend_length(const tal_t *ctx, const u8 *payload TAKES) +{ + u8 buf[BIGSIZE_MAX_LEN], *ret; + size_t len; + + len = bigsize_put(buf, tal_bytelen(payload)); + ret = tal_arr(ctx, u8, len + tal_bytelen(payload)); + memcpy(ret, buf, len); + memcpy(ret + len, payload, tal_bytelen(payload)); + if (taken(payload)) + tal_free(payload); + return ret; +} + /** * Callback when a plugin answers to the htlc_accepted hook */ @@ -860,10 +874,12 @@ static bool htlc_accepted_hook_deserialize(struct htlc_accepted_hook_payload *re const char *buffer, const jsmntok_t *toks) { + struct route_step *rs = request->route_step; struct htlc_in *hin = request->hin; struct lightningd *ld = request->ld; struct preimage payment_preimage; - const jsmntok_t *resulttok, *paykeytok; + const jsmntok_t *resulttok, *paykeytok, *payloadtok; + u8 *payload; if (!toks || !buffer) return true; @@ -877,6 +893,26 @@ static bool htlc_accepted_hook_deserialize(struct htlc_accepted_hook_payload *re json_strdup(tmpctx, buffer, toks)); } + payloadtok = json_get_member(buffer, toks, "payload"); + if (payloadtok) { + payload = json_tok_bin_from_hex(rs, buffer, payloadtok); + if (!payload) + fatal("Bad payload for htlc_accepted" + " hook: %.*s", + payloadtok->end - payloadtok->start, + buffer + payloadtok->start); + tal_free(request->payload); + tal_free(rs->raw_payload); + + rs->raw_payload = prepend_length(rs, take(payload)); + request->payload = onion_decode(request, rs, + hin->blinding, &hin->blinding_ss, + &request->failtlvtype, + &request->failtlvpos); + + } else + payload = NULL; + if (json_tok_streq(buffer, resulttok, "continue")) { return true; } diff --git a/tests/plugins/replace_payload.py b/tests/plugins/replace_payload.py new file mode 100755 index 000000000..0c55c2c41 --- /dev/null +++ b/tests/plugins/replace_payload.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Plugin that replaces HTLC payloads. + +This feature is important if we want to accept an HTLC tlv field not +accepted by lightningd. +""" +from pyln.client import Plugin + +plugin = Plugin() + + +@plugin.hook("htlc_accepted") +def on_htlc_accepted(htlc, onion, plugin, **kwargs): + # eg. '2902017b04016d0821fff5b6bd5018c8731aa0496c3698ef49f132ef9a3000c94436f4957e79a2f8827b' + # (but values change depending on pay's randomness!) + if plugin.replace_payload == 'corrupt_secret': + if onion['payload'][18] == '0': + newpayload = onion['payload'][:18] + '1' + onion['payload'][19:] + else: + newpayload = onion['payload'][:18] + '0' + onion['payload'][19:] + else: + newpayload = plugin.replace_payload + print("payload was:{}".format(onion['payload'])) + print("payload now:{}".format(newpayload)) + + return {'result': 'continue', 'payload': newpayload} + + +@plugin.method('setpayload') +def setpayload(plugin, payload: bool): + plugin.replace_payload = payload + return {} + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index aa48b554b..797a4fed2 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1180,3 +1180,23 @@ def test_feature_set(node_factory): assert fs['node'] == expected_features() assert fs['channel'] == '' assert 'invoice' in fs + + +def test_replacement_payload(node_factory): + """Test that htlc_accepted plugin hook can replace payload""" + plugin = os.path.join(os.path.dirname(__file__), 'plugins/replace_payload.py') + l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": plugin}]) + + # Replace with an invalid payload. + l2.rpc.call('setpayload', ['0000']) + inv = l2.rpc.invoice(123, 'test_replacement_payload', 'test_replacement_payload')['bolt11'] + with pytest.raises(RpcError, match=r"WIRE_INVALID_ONION_PAYLOAD \(reply from remote\)"): + l1.rpc.pay(inv) + + # Replace with valid payload, but corrupt payment_secret + l2.rpc.call('setpayload', ['corrupt_secret']) + + with pytest.raises(RpcError, match=r"WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS \(reply from remote\)"): + l1.rpc.pay(inv) + + assert l2.daemon.wait_for_log("Attept to pay.*with wrong secret")