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 43153e256..d55c5f05b 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -316,15 +316,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 195cb2479..2a1912fd5 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -840,12 +840,14 @@ static enum htlc_accepted_result htlc_accepted_hook_deserialize(const tal_t *ctx, struct lightningd *ld, const char *buffer, const jsmntok_t *toks, + /* If it specified a replacement payload (off ctx) */ + u8 **payload, /* If accepted */ struct preimage *payment_preimage, /* If rejected (tallocated off ctx) */ const u8 **failmsg) { - const jsmntok_t *resulttok, *paykeytok; + const jsmntok_t *resulttok, *paykeytok, *payloadtok; enum htlc_accepted_result result; if (!toks || !buffer) @@ -860,6 +862,17 @@ htlc_accepted_hook_deserialize(const tal_t *ctx, json_strdup(tmpctx, buffer, toks)); } + payloadtok = json_get_member(buffer, toks, "payload"); + if (payloadtok) { + *payload = json_tok_bin_from_hex(ctx, buffer, payloadtok); + if (!*payload) + fatal("Bad payload for htlc_accepted" + " hook: %.*s", + payloadtok->end - payloadtok->start, + buffer + payloadtok->start); + } else + *payload = NULL; + if (json_tok_streq(buffer, resulttok, "continue")) { return htlc_accepted_continue; } @@ -1012,7 +1025,22 @@ htlc_accepted_hook_callback(struct htlc_accepted_hook_payload *request, struct preimage payment_preimage; enum htlc_accepted_result result; const u8 *failmsg; - result = htlc_accepted_hook_deserialize(request, ld, buffer, toks, &payment_preimage, &failmsg); + u8 *payload = NULL; /* Some compilers seems to not understand this is + being set below. */ + + result = htlc_accepted_hook_deserialize(request, ld, buffer, toks, + &payload, &payment_preimage, &failmsg); + + /* If we have a replacement payload, parse it now. */ + if (payload) { + tal_free(request->payload); + tal_free(rs->raw_payload); + rs->raw_payload = tal_steal(rs, payload); + request->payload = onion_decode(request, rs, + hin->blinding, &hin->blinding_ss, + &request->failtlvtype, + &request->failtlvpos); + } switch (result) { case htlc_accepted_continue: 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")