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`.
This commit is contained in:
Rusty Russell 2020-04-07 16:40:30 +09:30
parent f3315ca110
commit 63441075b5
5 changed files with 106 additions and 3 deletions

View file

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

View file

@ -314,16 +314,23 @@ 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:
*

View file

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

View file

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

View file

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