mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-01 17:47:30 +01:00
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:
parent
f3315ca110
commit
63441075b5
5 changed files with 106 additions and 3 deletions
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
Executable file
35
tests/plugins/replace_payload.py
Executable 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()
|
|
@ -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…
Add table
Reference in a new issue