mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 15:14:23 +01:00
pay: Filter out routehints whose entrypoint is unknown
We would happily spin on attempts that are doomed to fail because we don't know the entrypoint. Next up: remove routehints whose entrypoints are known but unreachable.
This commit is contained in:
committed by
Rusty Russell
parent
ad7f59f7a1
commit
1e1d7b387c
@@ -2313,14 +2313,17 @@ static void trim_route(struct route_info **route, size_t n)
|
||||
|
||||
/* Make sure routehints are reasonable length, and (since we assume we
|
||||
* can append), not directly to us. Note: untrusted data! */
|
||||
static struct route_info **filter_routehints(struct routehints_data *d,
|
||||
static struct route_info **filter_routehints(struct gossmap *map,
|
||||
struct payment *p,
|
||||
struct routehints_data *d,
|
||||
struct node_id *myid,
|
||||
struct route_info **hints)
|
||||
{
|
||||
const size_t max_hops = ROUTING_MAX_HOPS / 2;
|
||||
char *mods = tal_strdup(tmpctx, "");
|
||||
for (size_t i = 0; i < tal_count(hints); i++) {
|
||||
struct gossmap_node *entrynode;
|
||||
/* Trim any routehint > 10 hops */
|
||||
size_t max_hops = ROUTING_MAX_HOPS / 2;
|
||||
if (tal_count(hints[i]) > max_hops) {
|
||||
tal_append_fmt(&mods,
|
||||
"Trimmed routehint %zu (%zu hops) to %zu. ",
|
||||
@@ -2344,6 +2347,27 @@ static struct route_info **filter_routehints(struct routehints_data *d,
|
||||
tal_arr_remove(&hints, i);
|
||||
i--;
|
||||
}
|
||||
|
||||
/* If routehint entrypoint is unreachable there's no
|
||||
* point in keeping it. */
|
||||
entrynode = gossmap_find_node(map, &hints[i][0].pubkey);
|
||||
if (entrynode == NULL) {
|
||||
tal_append_fmt(&mods,
|
||||
"Removed routehint %zu because "
|
||||
"entrypoint %s is unknown. ",
|
||||
i,
|
||||
type_to_string(tmpctx, struct node_id,
|
||||
&hints[i][0].pubkey));
|
||||
plugin_log(p->plugin, LOG_DBG,
|
||||
"Removed routehint %zu because "
|
||||
"entrypoint %s is unknown. ",
|
||||
i,
|
||||
type_to_string(tmpctx, struct node_id,
|
||||
&hints[i][0].pubkey));
|
||||
tal_arr_remove(&hints, i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!streq(mods, ""))
|
||||
@@ -2616,7 +2640,7 @@ static void routehint_step_cb(struct routehints_data *d, struct payment *p)
|
||||
{
|
||||
struct route_hop hop;
|
||||
const struct payment *root = payment_root(p);
|
||||
|
||||
struct gossmap *map;
|
||||
if (p->step == PAYMENT_STEP_INITIALIZED) {
|
||||
if (root->routes == NULL)
|
||||
return payment_continue(p);
|
||||
@@ -2625,8 +2649,9 @@ static void routehint_step_cb(struct routehints_data *d, struct payment *p)
|
||||
* beginning, and every other payment will filter out the
|
||||
* exluded ones on the fly. */
|
||||
if (p->parent == NULL) {
|
||||
d->routehints = filter_routehints(d, p->local_id,
|
||||
p->routes);
|
||||
map = get_gossmap(p->plugin);
|
||||
d->routehints = filter_routehints(
|
||||
map, p, d, p->local_id, p->routes);
|
||||
/* filter_routehints modifies the array, but
|
||||
* this could trigger a resize and the resize
|
||||
* could trigger a realloc.
|
||||
|
||||
@@ -4172,3 +4172,51 @@ def test_self_pay(node_factory):
|
||||
|
||||
with pytest.raises(RpcError):
|
||||
l1.rpc.pay(inv)
|
||||
|
||||
|
||||
@unittest.skipIf(TEST_NETWORK != 'regtest', "Canned invoice is network specific")
|
||||
def test_unreachable_routehint(node_factory, bitcoind):
|
||||
"""Test that we discard routehints that we can't reach.
|
||||
|
||||
Reachability is tested by checking whether we can reach the
|
||||
entrypoint of the routehint, i.e., the first node in the
|
||||
routehint. The network we create is partitioned on purpose for
|
||||
this: first we attempt with an unknown destination and an unknown
|
||||
routehint entrypoint, later we make them known, but still
|
||||
unreachable, by connecting them without a channel.
|
||||
|
||||
"""
|
||||
|
||||
# Create a partitioned network, first without connecting it, then
|
||||
# connecting it without a channel so they can sync gossip. Notice
|
||||
# that l4 is there only to trick the deadend heuristic.
|
||||
l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
|
||||
l3, l4, l5 = node_factory.line_graph(3, wait_for_announce=True)
|
||||
entrypoint = '0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199'
|
||||
|
||||
# Generate an invoice with exactly one routehint.
|
||||
for i in range(100):
|
||||
invoice = l5.rpc.invoice(10, 'attempt{}'.format(i), 'description')['bolt11']
|
||||
decoded = l1.rpc.decodepay(invoice)
|
||||
if 'routes' in decoded and len(decoded['routes']) == 1:
|
||||
break
|
||||
|
||||
assert('routes' in decoded and len(decoded['routes']) == 1)
|
||||
|
||||
with pytest.raises(RpcError, match=r'Destination [a-f0-9]{66} is not reachable'):
|
||||
l1.rpc.pay(invoice)
|
||||
|
||||
assert(l1.daemon.is_in_log(
|
||||
r"Removed routehint 0 because entrypoint {entrypoint} is unknown.".format(
|
||||
entrypoint=entrypoint
|
||||
)
|
||||
))
|
||||
|
||||
# Now connect l2 to l3 to create a bridge, but without a
|
||||
# channel. The entrypoint will become known, but still
|
||||
# unreachable, resulting in a slightly different error message,
|
||||
# but the routehint will still be removed.
|
||||
l2.connect(l3)
|
||||
wait_for(lambda: len(l1.rpc.listnodes(entrypoint)['nodes']) == 1)
|
||||
|
||||
# TODO(cdecker) investigate why dijkstra_distance tells us the entrypoint is reachable...
|
||||
|
||||
Reference in New Issue
Block a user