From 3d016e7249e2f4f3b258e0d5e889bb31bf598b28 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 15 Jan 2019 14:35:27 +1030 Subject: [PATCH] getroute: allow array of channels to exclude. The pay plugin will use this, rather than the current "suppress for 90 second" hacks. Signed-off-by: Rusty Russell --- contrib/pylightning/lightning/lightning.py | 7 +-- lightningd/gossip_control.c | 54 +++++++++++++++++++++- tests/test_gossip.py | 50 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index f7adc37a6..6ac9a3816 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -147,13 +147,13 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("listnodes", payload) - def getroute(self, peer_id, msatoshi, riskfactor, cltv=9, fromid=None, fuzzpercent=None, seed=None): + def getroute(self, peer_id, msatoshi, riskfactor, cltv=9, fromid=None, fuzzpercent=None, seed=None, exclude=[]): """ Show route to {id} for {msatoshi}, using {riskfactor} and optional {cltv} (default 9). If specified search from {fromid} otherwise use this node as source. Randomize the route with up to {fuzzpercent} (0.0 -> 100.0, default 5.0) using {seed} as an arbitrary-size string - seed. + seed. {exclude} is an optional array of scids[xDirection] to exclude. """ payload = { "id": peer_id, @@ -162,7 +162,8 @@ class LightningRpc(UnixDomainSocketRpc): "cltv": cltv, "fromid": fromid, "fuzzpercent": fuzzpercent, - "seed": seed + "seed": seed, + "exclude": exclude } return self.call("getroute", payload) diff --git a/lightningd/gossip_control.c b/lightningd/gossip_control.c index 1a94ff08d..feee26a81 100644 --- a/lightningd/gossip_control.c +++ b/lightningd/gossip_control.c @@ -292,6 +292,27 @@ static void json_getroute_reply(struct subd *gossip UNUSED, const u8 *reply, con was_pending(command_success(cmd, response)); } +static bool json_to_short_channel_id_with_dir(const char *buffer, + const jsmntok_t *tok, + struct short_channel_id *scid, + bool *dir) +{ + /* Ends in /0 or /1 */ + if (tok->end - tok->start < 2) + return false; + if (buffer[tok->end - 2] != '/') + return false; + if (buffer[tok->end - 1] == '0') + *dir = false; + else if (buffer[tok->end - 1] == '1') + *dir = true; + else + return false; + + return short_channel_id_from_str(buffer + tok->start, + tok->end - tok->start - 2, scid); +} + static struct command_result *json_getroute(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -300,9 +321,13 @@ static struct command_result *json_getroute(struct command *cmd, struct lightningd *ld = cmd->ld; struct pubkey *destination; struct pubkey *source; + const jsmntok_t *excludetok; u64 *msatoshi; unsigned *cltv; double *riskfactor; + struct short_channel_id *excluded; + bool *excluded_dir; + /* Higher fuzz means that some high-fee paths can be discounted * for an even larger value, increasing the scope for route * randomization (the higher-fee paths become more likely to @@ -317,16 +342,43 @@ static struct command_result *json_getroute(struct command *cmd, p_opt_def("cltv", param_number, &cltv, 9), p_opt_def("fromid", param_pubkey, &source, ld->id), p_opt_def("fuzzpercent", param_percent, &fuzz, 5.0), + p_opt("exclude", param_array, &excludetok), NULL)) return command_param_failed(); /* Convert from percentage */ *fuzz = *fuzz / 100.0; + if (excludetok) { + const jsmntok_t *t, *end = json_next(excludetok); + size_t i; + + excluded = tal_arr(cmd, struct short_channel_id, + excludetok->size); + excluded_dir = tal_arr(cmd, bool, excludetok->size); + + for (i = 0, t = excludetok + 1; + t < end; + t = json_next(t), i++) { + if (!json_to_short_channel_id_with_dir(buffer, t, + &excluded[i], + &excluded_dir[i])) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%.*s is not a valid" + " id+direction", + t->end - t->start, + buffer + t->start); + } + } + } else { + excluded = NULL; + excluded_dir = NULL; + } + u8 *req = towire_gossip_getroute_request(cmd, source, destination, *msatoshi, *riskfactor * 1000, *cltv, fuzz, - NULL, NULL); + excluded, excluded_dir); subd_req(ld->gossip, ld->gossip, req, -1, 0, json_getroute_reply, cmd); return command_still_pending(cmd); } diff --git a/tests/test_gossip.py b/tests/test_gossip.py index f476d9d69..b69f20a14 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -1,9 +1,11 @@ from fixtures import * # noqa: F401,F403 +from lightning import RpcError from utils import wait_for, TIMEOUT, only_one import json import logging import os +import pytest import struct import subprocess import time @@ -941,3 +943,51 @@ def test_gossip_notices_close(node_factory, bitcoind): l1.start() assert(l1.rpc.listchannels()['channels'] == []) assert(l1.rpc.listnodes()['nodes'] == []) + + +def test_getroute_exclude(node_factory, bitcoind): + """Test getroute's exclude argument""" + l1, l2, l3, l4 = node_factory.line_graph(4, wait_for_announce=True) + + # This should work + route = l1.rpc.getroute(l4.info['id'], 1, 1)['route'] + + # l1 id is > l2 id, so 1 means l1->l2 + chan_l1l2 = route[0]['channel'] + '/1' + chan_l2l1 = route[0]['channel'] + '/0' + + # This should not + with pytest.raises(RpcError): + l1.rpc.getroute(l4.info['id'], 1, 1, exclude=[chan_l1l2]) + + # Blocking the wrong way should be fine. + l1.rpc.getroute(l4.info['id'], 1, 1, exclude=[chan_l2l1]) + + # Now, create an alternate (better) route. + l2.rpc.connect(l4.info['id'], 'localhost', l4.port) + scid = l2.fund_channel(l4, 1000000, wait_for_active=False) + bitcoind.generate_block(5) + + # We don't wait above, because we care about it hitting l1. + l1.daemon.wait_for_logs([r'update for channel {}\(0\) now ACTIVE' + .format(scid), + r'update for channel {}\(1\) now ACTIVE' + .format(scid)]) + + # l3 id is > l2 id, so 1 means l3->l2 + # chan_l3l2 = route[1]['channel'] + '/1' + chan_l2l3 = route[1]['channel'] + '/0' + + # l4 is > l2 + # chan_l4l2 = scid + '/1' + chan_l2l4 = scid + '/0' + + # This works + l1.rpc.getroute(l4.info['id'], 1, 1, exclude=[chan_l2l3]) + + # This works + l1.rpc.getroute(l4.info['id'], 1, 1, exclude=[chan_l2l4]) + + # This doesn't + with pytest.raises(RpcError): + l1.rpc.getroute(l4.info['id'], 1, 1, exclude=[chan_l2l3, chan_l2l4])