Browse Source

plugins/pay: add shadow CLTV calculation.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
plugin-timeout-inc
Rusty Russell 6 years ago
committed by Christian Decker
parent
commit
9403df8d0d
  1. 1
      plugins/Makefile
  2. 79
      plugins/pay.c
  3. 36
      tests/test_misc.py

1
plugins/Makefile

@ -26,6 +26,7 @@ PLUGIN_COMMON_OBJS := \
common/json_tok.o \
common/memleak.o \
common/param.o \
common/pseudorand.o \
common/type_to_string.o \
common/utils.o \
common/version.o \

79
plugins/pay.c

@ -3,6 +3,7 @@
#include <ccan/tal/str/str.h>
#include <ccan/time/time.h>
#include <common/bolt11.h>
#include <common/pseudorand.h>
#include <common/type_to_string.h>
#include <gossipd/gossip_constants.h>
#include <plugins/libplugin.h>
@ -47,6 +48,9 @@ struct pay_command {
/* Any routehints to use. */
struct route_info **routehints;
/* Current node during shadow route calculation. */
const char *shadow;
};
static struct command_result *start_pay_attempt(struct command *cmd,
@ -354,6 +358,78 @@ static struct command_result *start_pay_attempt(struct command *cmd,
dest, amount, cltv, max_hops, pc->riskfactor, exclude);
}
/* BOLT #7:
*
* If a route is computed by simply routing to the intended recipient and
* summing the `cltv_expiry_delta`s, then it's possible for intermediate nodes
* to guess their position in the route. Knowing the CLTV of the HTLC, the
* surrounding network topology, and the `cltv_expiry_delta`s gives an
* attacker a way to guess the intended recipient. Therefore, it's highly
* desirable to add a random offset to the CLTV that the intended recipient
* will receive, which bumps all CLTVs along the route.
*
* In order to create a plausible offset, the origin node MAY start a limited
* random walk on the graph, starting from the intended recipient and summing
* the `cltv_expiry_delta`s, and use the resulting sum as the offset. This
* effectively creates a _shadow route extension_ to the actual route and
* provides better protection against this attack vector than simply picking a
* random offset would.
*/
static struct command_result *shadow_route(struct command *cmd,
struct pay_command *pc);
static struct command_result *add_shadow_route(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct pay_command *pc)
{
/* Use reservoir sampling across the capable channels. */
const jsmntok_t *channels = json_get_member(buf, result, "channels");
const jsmntok_t *chan, *end, *best = NULL;
u64 sample;
u32 cltv, best_cltv;
end = json_next(channels);
for (chan = channels + 1; chan < end; chan = json_next(chan)) {
u64 sats, v;
json_to_u64(buf, json_get_member(buf, chan, "satoshis"), &sats);
if (sats * 1000 < pc->msatoshi)
continue;
/* Don't use if total would exceed 1/4 of our time allowance. */
json_to_number(buf, json_get_member(buf, chan, "delay"), &cltv);
if ((pc->final_cltv + cltv) * 4 > pc->maxdelay)
continue;
v = pseudorand(UINT64_MAX);
if (!best || v > sample) {
best = chan;
best_cltv = cltv;
sample = v;
}
}
if (!best)
return start_pay_attempt(cmd, pc);
pc->final_cltv += best_cltv;
pc->shadow = json_strdup(pc, buf,
json_get_member(buf, best, "destination"));
return shadow_route(cmd, pc);
}
static struct command_result *shadow_route(struct command *cmd,
struct pay_command *pc)
{
if (pseudorand(2) == 0)
return start_pay_attempt(cmd, pc);
return send_outreq(cmd, "listchannels",
add_shadow_route, forward_error, pc,
"'source' : '%s'", pc->shadow);
}
/* gossipd doesn't know much about the current state of channels; here we
* manually exclude peers which are disconnected and channels which lack
* current capacity (it will eliminate those without total capacity). */
@ -406,7 +482,7 @@ static struct command_result *listpeers_done(struct command *cmd,
}
}
return start_pay_attempt(cmd, pc);
return shadow_route(cmd, pc);
}
/* Trim route to this length by taking from the *front* of route
@ -504,6 +580,7 @@ static struct command_result *handle_pay(struct command *cmd,
pc->riskfactor = *riskfactor;
pc->final_cltv = b11->min_final_cltv_expiry;
pc->dest = type_to_string(cmd, struct pubkey, &b11->receiver_id);
pc->shadow = tal_strdup(pc, pc->dest);
pc->payment_hash = type_to_string(pc, struct sha256,
&b11->payment_hash);
pc->stoptime = timeabs_add(time_now(), time_from_sec(*retryfor));

36
tests/test_misc.py

@ -234,10 +234,12 @@ def test_htlc_out_timeout(node_factory, bitcoind, executor):
l1.daemon.wait_for_log('dev_disconnect: @WIRE_REVOKE_AND_ACK')
# Takes 6 blocks to timeout (cltv-final + 1), but we also give grace period of 1 block.
bitcoind.generate_block(5 + 1)
time.sleep(3)
assert not l1.daemon.is_in_log('hit deadline')
bitcoind.generate_block(1)
# FIXME: shadow route can add extra blocks! 50% chance of adding 6...
bitcoind.generate_block(5 + 1 + 60 + 1)
# bitcoind.generate_block(5 + 1)
# time.sleep(3)
# assert not l1.daemon.is_in_log('hit deadline')
# bitcoind.generate_block(1)
l1.daemon.wait_for_log('Offered HTLC 0 SENT_ADD_ACK_REVOCATION cltv .* hit deadline')
l1.daemon.wait_for_log('sendrawtx exit 0')
@ -294,9 +296,12 @@ def test_htlc_in_timeout(node_factory, bitcoind, executor):
l1.daemon.wait_for_log('dev_disconnect: -WIRE_REVOKE_AND_ACK')
# Deadline HTLC expiry minus 1/2 cltv-expiry delta (rounded up) (== cltv - 3). cltv is 5+1.
bitcoind.generate_block(2)
assert not l2.daemon.is_in_log('hit deadline')
bitcoind.generate_block(1)
# FIXME: shadow route can add extra blocks! 50% chance of adding 6...
shadowlen = 60
bitcoind.generate_block(2 + shadowlen + 1)
#bitcoind.generate_block(2)
#assert not l2.daemon.is_in_log('hit deadline')
#bitcoind.generate_block(1)
l2.daemon.wait_for_log('Fulfilled HTLC 0 SENT_REMOVE_COMMIT cltv .* hit deadline')
l2.daemon.wait_for_log('sendrawtx exit 0')
@ -304,14 +309,15 @@ def test_htlc_in_timeout(node_factory, bitcoind, executor):
l2.daemon.wait_for_log(' to ONCHAIN')
l1.daemon.wait_for_log(' to ONCHAIN')
# L2 will collect HTLC
l2.daemon.wait_for_log('Propose handling OUR_UNILATERAL/THEIR_HTLC by OUR_HTLC_SUCCESS_TX .* after 0 blocks')
l2.daemon.wait_for_log('sendrawtx exit 0')
bitcoind.generate_block(1)
l2.daemon.wait_for_log('Propose handling OUR_HTLC_SUCCESS_TX/DELAYED_OUTPUT_TO_US by OUR_DELAYED_RETURN_TO_WALLET .* after 5 blocks')
bitcoind.generate_block(4)
l2.daemon.wait_for_log('Broadcasting OUR_DELAYED_RETURN_TO_WALLET')
l2.daemon.wait_for_log('sendrawtx exit 0')
# L2 will collect HTLC (iff no shadow route)
if shadowlen == 0:
l2.daemon.wait_for_log('Propose handling OUR_UNILATERAL/THEIR_HTLC by OUR_HTLC_SUCCESS_TX .* after 0 blocks')
l2.daemon.wait_for_log('sendrawtx exit 0')
bitcoind.generate_block(1)
l2.daemon.wait_for_log('Propose handling OUR_HTLC_SUCCESS_TX/DELAYED_OUTPUT_TO_US by OUR_DELAYED_RETURN_TO_WALLET .* after 5 blocks')
bitcoind.generate_block(4)
l2.daemon.wait_for_log('Broadcasting OUR_DELAYED_RETURN_TO_WALLET')
l2.daemon.wait_for_log('sendrawtx exit 0')
# Now, 100 blocks it should be both done.
bitcoind.generate_block(100)

Loading…
Cancel
Save