Browse Source

lightningd: allow htlc_accepted hook to replace onion payload.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-added: `htlc_accepted` hook can now offer a replacement onion `payload`.
register-keysend-plugin
Rusty Russell 5 years ago
parent
commit
63441075b5
  1. 5
      doc/PLUGINS.md
  2. 11
      lightningd/invoice.c
  3. 38
      lightningd/peer_htlcs.c
  4. 35
      tests/plugins/replace_payload.py
  5. 20
      tests/test_plugin.py

5
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",

11
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:

38
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;
}

35
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()

20
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")

Loading…
Cancel
Save