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`.
travis-debug
Rusty Russell 5 years ago
committed by Christian Decker
parent
commit
f0a916ff7a
  1. 5
      doc/PLUGINS.md
  2. 11
      lightningd/invoice.c
  3. 32
      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

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

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

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