diff --git a/devtools/Makefile b/devtools/Makefile index 81b620676..0badc4982 100644 --- a/devtools/Makefile +++ b/devtools/Makefile @@ -1,12 +1,12 @@ DEVTOOLS_SRC := devtools/gen_print_wire.c devtools/gen_print_onion_wire.c devtools/print_wire.c DEVTOOLS_OBJS := $(DEVTOOLS_SRC:.c=.o) -DEVTOOLS := devtools/bolt11-cli devtools/decodemsg devtools/onion devtools/dump-gossipstore devtools/gossipwith devtools/create-gossipstore devtools/mkcommit devtools/mkfunding devtools/mkclose devtools/mkgossip devtools/mkencoded devtools/checkchannels devtools/mkquery devtools/lightning-checkmessage +DEVTOOLS := devtools/bolt11-cli devtools/decodemsg devtools/onion devtools/dump-gossipstore devtools/gossipwith devtools/create-gossipstore devtools/mkcommit devtools/mkfunding devtools/mkclose devtools/mkgossip devtools/mkencoded devtools/checkchannels devtools/mkquery devtools/lightning-checkmessage devtools/topology ifeq ($(EXPERIMENTAL_FEATURES),1) DEVTOOLS += devtools/blindedpath endif DEVTOOLS_TOOL_SRC := $(DEVTOOLS:=.c) -DEVTOOLS_TOOL_OBJS := $(DEVTOOLS_TOOL_SRC:.c=.o) +DEVTOOLS_TOOL_OBJS := $(DEVTOOLS_TOOL_SRC:.c=.o) devtools/clean_topo.o DEVTOOLS_BOLT_DEPS += $(BOLT_DEPS) tools/gen/print_impl_template tools/gen/print_header_template @@ -93,9 +93,11 @@ devtools/mkquery: $(DEVTOOLS_COMMON_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/from devtools/lightning-checkmessage: $(DEVTOOLS_COMMON_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/lightning-checkmessage.o +devtools/topology: $(DEVTOOLS_COMMON_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o wire/tlvstream.o common/gossmap.o common/random_select.o common/dijkstra.o common/route.o devtools/clean_topo.o devtools/topology.o + # Make sure these depend on everything. ALL_PROGRAMS += $(DEVTOOLS) -ALL_OBJS += $(DEVTOOLS_OBJS) $(DEVTOOLS_TOOL_OBJS) +ALL_OBJS += $(DEVTOOLS_OBJS) $(DEVTOOLS_TOOL_OBJS) check-source: $(DEVTOOLS_SRC:%=check-src-include-order/%) $(DEVTOOLS_TOOLS_SRC:%=check-src-include-order/%) diff --git a/devtools/clean_topo.c b/devtools/clean_topo.c new file mode 100644 index 000000000..07f05d432 --- /dev/null +++ b/devtools/clean_topo.c @@ -0,0 +1,53 @@ +#include +#include + +static void visit(struct gossmap *map, + struct gossmap_node *n, + bool *visited) +{ + visited[gossmap_node_idx(map, n)] = true; + + for (size_t i = 0; i < n->num_chans; i++) { + int dir; + struct gossmap_chan *c = gossmap_nth_chan(map, n, i, &dir); + struct gossmap_node *peer; + + peer = gossmap_nth_node(map, c, !dir); + if (!visited[gossmap_node_idx(map, peer)]) + visit(map, peer, visited); + } +} + +void clean_topo(struct gossmap *map, bool remove_singles) +{ + struct gossmap_node *n, *next; + bool *visited; + + /* Remove channels which are not enabled in both dirs. */ + for (struct gossmap_chan *c = gossmap_first_chan(map); + c; + c = gossmap_next_chan(map, c)) { + if (!c->half[0].enabled || !c->half[1].enabled) { + gossmap_remove_chan(map, c); + } + } + + if (remove_singles) { + for (n = gossmap_first_node(map); n; n = next) { + next = gossmap_next_node(map, n); + if (n->num_chans == 1) + gossmap_remove_node(map, n); + } + } + + /* Remove isolated nodes (we assume first isn't isolated!) */ + visited = tal_arrz(NULL, bool, gossmap_max_node_idx(map)); + visit(map, gossmap_first_node(map), visited); + + for (n = gossmap_first_node(map); n; n = next) { + next = gossmap_next_node(map, n); + if (!visited[gossmap_node_idx(map, n)]) + gossmap_remove_node(map, n); + } + tal_free(visited); +} diff --git a/devtools/clean_topo.h b/devtools/clean_topo.h new file mode 100644 index 000000000..40349480c --- /dev/null +++ b/devtools/clean_topo.h @@ -0,0 +1,13 @@ +#ifndef LIGHTNING_DEVTOOLS_CLEAN_TOPO_H +#define LIGHTNING_DEVTOOLS_CLEAN_TOPO_H +#include "config.h" + +struct gossmap; + +/* Cleans topology: + * 1. Removes channels not enabled in both dirs. + * 2. (if remove_singles) remove nodes with only one connection. + * 3. Remove isolated nodes (we assume first node is well-connected!). + */ +void clean_topo(struct gossmap *map, bool remove_singles); +#endif /* LIGHTNING_DEVTOOLS_CLEAN_TOPO_H */ diff --git a/devtools/topology.c b/devtools/topology.c new file mode 100644 index 000000000..b311b1812 --- /dev/null +++ b/devtools/topology.c @@ -0,0 +1,306 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* We ignore capacity constraints */ +static bool channel_usable(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + void *unused) +{ + if (!gossmap_chan_set(c, dir)) + return false; + if (!c->half[dir].enabled) + return false; + return true; +} + +/* Note: dijkstra() sets dir to the neighbor side; i.e. c->half[dir].node_idx is the + * neighbor. */ +static bool channel_usable_to_excl(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + struct gossmap_node *excl) +{ + if (!channel_usable(map, c, dir, amount, NULL)) + return false; + + /* Don't go via excl. */ + if (c->half[dir].nodeidx == gossmap_node_idx(map, excl)) + return false; + return true; +} + +/* What nodes can reach n without going through exclude? */ +static size_t count_possible_sources(const struct gossmap *map, + struct gossmap_node *n, + struct gossmap_node *exclude, + bool is_last_node) +{ + const struct dijkstra *dij; + size_t distance_budget, num; + + dij = dijkstra(tmpctx, map, n, AMOUNT_MSAT(0), 0, + channel_usable_to_excl, route_path_shorter, exclude); + + if (is_last_node) + distance_budget = ROUTING_MAX_HOPS - 1; + else + distance_budget = ROUTING_MAX_HOPS - 2; + + assert(dijkstra_distance(dij, gossmap_node_idx(map, n)) == 0); + assert(dijkstra_distance(dij, gossmap_node_idx(map, exclude)) == UINT_MAX); + + num = 0; + for (n = gossmap_first_node(map); n; n = gossmap_next_node(map, n)) { + if (dijkstra_distance(dij, gossmap_node_idx(map, n)) <= distance_budget) + num++; + } + return num; +} + +/* Note: dijkstra() sets dir to the neighbor side; i.e. c->half[dir].node_idx is the + * neighbor. */ +static bool channel_usable_from_excl(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + struct gossmap_node *excl) +{ + if (!channel_usable(map, c, !dir, amount, NULL)) + return false; + + /* Don't go via excl. */ + if (c->half[dir].nodeidx == gossmap_node_idx(map, excl)) + return false; + return true; +} + +static size_t memcount(const void *mem, size_t len, char c) +{ + size_t count = 0; + for (size_t i = 0; i < len; i++) { + if (((char *)mem)[i] == c) + count++; + } + return count; +} + +static void visit(const struct gossmap *map, + struct gossmap_node *n, + struct gossmap_node *exclude, + bool *visited) +{ + if (n == exclude) + return; + if (visited[gossmap_node_idx(map, n)]) + return; + visited[gossmap_node_idx(map, n)] = true; + + for (size_t i = 0; i < n->num_chans; i++) { + int dir; + struct gossmap_chan *c; + c = gossmap_nth_chan(map, n, i, &dir); + + if (!channel_usable(map, c, dir, AMOUNT_MSAT(0), NULL)) + continue; + visit(map, gossmap_nth_node(map, c, !dir), exclude, visited); + } +} + +/* What nodes can n reach without going through exclude? */ +static size_t count_possible_destinations(const struct gossmap *map, + struct gossmap_node *start, + struct gossmap_node *exclude, + bool is_first_node) +{ + const struct dijkstra *dij; + size_t distance_budget, num; + + dij = dijkstra(tmpctx, map, start, AMOUNT_MSAT(0), 0, + channel_usable_from_excl, route_path_shorter, exclude); + + if (is_first_node) + distance_budget = ROUTING_MAX_HOPS - 1; + else + distance_budget = ROUTING_MAX_HOPS - 2; + + assert(dijkstra_distance(dij, gossmap_node_idx(map, start)) == 0); + assert(dijkstra_distance(dij, gossmap_node_idx(map, exclude)) == UINT_MAX); + + num = 0; + for (struct gossmap_node *n = gossmap_first_node(map); + n; + n = gossmap_next_node(map, n)) { + if (dijkstra_distance(dij, gossmap_node_idx(map, n)) <= distance_budget) + num++; +#if 0 + else + printf("Can't reach %s (%u) if we exclude %s\n", + type_to_string(tmpctx, struct node_id, + gossmap_node_get_id(map, n)), + dijkstra_distance(dij, gossmap_node_idx(map, n)), + type_to_string(tmpctx, struct node_id, + gossmap_node_get_id(map, exclude))); +#endif + } + + /* Now double-check with flood-fill. */ + bool *visited = tal_arrz(tmpctx, bool, gossmap_max_node_idx(map)); + visit(map, start, exclude, visited); + assert(memcount(visited, tal_bytelen(visited), true) == num); + return num; +} + +static bool measure_least_cost(struct gossmap *map, + struct gossmap_node *src, + struct gossmap_node *dst) +{ + const struct dijkstra *dij; + u32 srcidx = gossmap_node_idx(map, src); + /* 10ksat, budget is 0.5% */ + const struct amount_msat sent = AMOUNT_MSAT(10000000); + const struct amount_msat budget = amount_msat_div(sent, 200); + const u32 riskfactor = 0; + /* Max distance is 20 */ + const u32 distance_budget = ROUTING_MAX_HOPS; + struct amount_msat maxcost, fee; + struct route **path; + struct timemono tstart, tstop; + struct node_id srcid; + + gossmap_node_get_id(map, src, &srcid); + printf("# src %s (%u channels)\n", + type_to_string(tmpctx, struct node_id, &srcid), + src->num_chans); + + tstart = time_mono(); + dij = dijkstra(tmpctx, map, dst, + sent, riskfactor, channel_usable, + route_path_cheaper, NULL); + tstop = time_mono(); + + printf("# Time to find path: %"PRIu64" usec\n", + time_to_usec(timemono_between(tstop, tstart))); + + if (dijkstra_distance(dij, srcidx) > distance_budget) { + printf("failed (%s)\n", + dijkstra_distance(dij, srcidx) == UINT_MAX ? "unreachable" : "too far"); + return false; + } + if (!amount_msat_add(&maxcost, sent, budget)) + abort(); + if (amount_msat_greater(dijkstra_amount(dij, srcidx), maxcost)) { + printf("failed (too expensive)\n"); + return false; + } + + path = route_from_dijkstra(map, dij, src); + printf("# path length %zu\n", tal_count(path)); + if (!amount_msat_sub(&fee, dijkstra_amount(dij, srcidx), sent)) + abort(); + printf("# path fee %s\n", + type_to_string(tmpctx, struct amount_msat, &fee)); + + /* Count possible sources */ + for (size_t i = 0; i < tal_count(path); i++) { + struct gossmap_node *prev, *cur; + + /* N+1th node is at end of Nth hop */ + prev = gossmap_nth_node(map, path[i]->c, path[i]->dir); + cur = gossmap_nth_node(map, path[i]->c, !path[i]->dir); + + printf("source set size node %zu/%zu: %zu\n", + i+1, tal_count(path), + count_possible_sources(map, prev, cur, cur == dst)); + } + + /* Count possible destinations. */ + for (size_t i = 0; i < tal_count(path); i++) { + struct gossmap_node *cur, *next; + + /* N+1th node is at end of Nth hop */ + cur = gossmap_nth_node(map, path[i]->c, path[i]->dir); + next = gossmap_nth_node(map, path[i]->c, !path[i]->dir); + + printf("destination set size node %zu/%zu: %zu\n", + i, tal_count(path), + count_possible_destinations(map, next, cur, cur == src)); + } + return true; +} + +int main(int argc, char *argv[]) +{ + struct timemono tstart, tstop; + struct gossmap_node *n, *dst; + struct gossmap *map; + struct node_id dstid; + bool no_singles = false; + + setup_locale(); + setup_tmpctx(); + + opt_register_noarg("--no-single-sources", opt_set_bool, &no_singles, + "Eliminate single-channel nodes"); + opt_register_noarg("-h|--help", opt_usage_and_exit, + " |all \n" + "A topology test program.", + "Get usage information"); + opt_parse(&argc, argv, opt_log_stderr_exit); + if (argc != 4) + opt_usage_exit_fail("Expect 3 arguments"); + + tstart = time_mono(); + map = gossmap_load(NULL, argv[1]); + if (!map) + err(1, "Loading gossip store %s", argv[1]); + tstop = time_mono(); + + printf("# Time to load: %"PRIu64" msec\n", + time_to_msec(timemono_between(tstop, tstart))); + + clean_topo(map, no_singles); + printf("# Reduced to %zu nodes and %zu channels\n", + gossmap_num_nodes(map), gossmap_num_chans(map)); + + if (!node_id_from_hexstr(argv[3], strlen(argv[3]), &dstid)) + errx(1, "Bad dstid"); + dst = gossmap_find_node(map, &dstid); + if (!dst) + errx(1, "Unknown destination node '%s'", argv[3]); + + if (streq(argv[2], "all")) { + for (n = gossmap_first_node(map); + n; + n = gossmap_next_node(map, n)) { + measure_least_cost(map, n, dst); + clean_tmpctx(); + } + } else { + struct node_id srcid; + if (!node_id_from_hexstr(argv[2], strlen(argv[2]), &srcid)) + errx(1, "Bad srcid"); + n = gossmap_find_node(map, &srcid); + if (!n) + errx(1, "Unknown source node '%s'", argv[2]); + if (!measure_least_cost(map, n, dst)) + exit(1); + } + + tal_free(map); +}