devtools: add gossmap-compress to give minimal representation of gossmap topology.

Simple format, which doesn't include node information, just the channels.

Example:

```
$ ls -l gossip-store-2024-06-26
-rw------- 1 rusty rusty 98815543 Jul 26 09:47 gossip-store-2024-06-26
$ ./devtools/gossmap-compress -v compress gossip-store-2024-06-26 compressed
18693 nodes
61437 channels
46148 disabled channels (32620 no update)
9690 unique capacities
85 unique htlc_min
6867 unique htlc_max
807 unique basefee
2521 unique propfee
94 unique delay
$ ls -l compressed
-rw-rw-r-- 1 rusty rusty 1643258 Jul 26 09:51 compressed
```

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2024-08-07 10:15:55 +09:30
parent 0a73f918c1
commit cf936d296e
2 changed files with 366 additions and 1 deletions

View file

@ -1,4 +1,4 @@
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/mkquery devtools/lightning-checkmessage devtools/topology devtools/route devtools/bolt12-cli devtools/encodeaddr devtools/features devtools/fp16 devtools/rune
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/mkquery devtools/lightning-checkmessage devtools/topology devtools/route devtools/bolt12-cli devtools/encodeaddr devtools/features devtools/fp16 devtools/rune devtools/gossmap-compress
ifeq ($(HAVE_SQLITE3),1)
DEVTOOLS += devtools/checkchannels
endif
@ -57,6 +57,8 @@ devtools/bolt11-cli: $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(BITCOIN_OBJS) wire/f
devtools/encodeaddr: common/utils.o common/bech32.o devtools/encodeaddr.o
devtools/gossmap-compress: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o wire/tlvstream.o common/gossmap.o common/fp16.o devtools/gossmap-compress.o
devtools/bolt12-cli: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/bolt12_wiregen.o wire/fromwire.o wire/towire.o common/bolt12.o common/bolt12_merkle.o devtools/bolt12-cli.o common/setup.o common/iso4217.o
devtools/decodemsg: $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(BITCOIN_OBJS) $(WIRE_PRINT_OBJS) wire/fromwire.o wire/towire.o devtools/print_wire.o devtools/decodemsg.o

363
devtools/gossmap-compress.c Normal file
View file

@ -0,0 +1,363 @@
#include "config.h"
#include <ccan/asort/asort.h>
#include <ccan/err/err.h>
#include <ccan/opt/opt.h>
#include <ccan/read_write_all/read_write_all.h>
#include <ccan/tal/grab_file/grab_file.h>
#include <ccan/tal/str/str.h>
#include <common/bigsize.h>
#include <common/gossmap.h>
#include <common/setup.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
static bool verbose = false;
/* All {numbers} are bigsize.
*
* <FILE> := <HEADER> <CHANNEL_ENDS> <CAPACITIES> <DISABLEDS> <HTLC_MINS> <HTLC_MAXS> <BASEFEES> <PROPFEES> <DELAYS>
* <HEADER> := "GOSSMAP_COMPRESSv1\0"
* <CHANNEL_ENDS> := {channel_count} {start_nodeidx}*{channel_count} {end_nodeidx}*{channel_count}
* This describes each attached channel, eg if there are two
* channels, node 0 to node 1 and node 0 to node 2, this would be:
* 2 0 0 1 2
*
* <DISABLEDS> := <DISABLED>* {channel_count*2}
* <DISABLED> := {chanidx}*2+{direction}
* Selection of disabled channels and directions, expected to only be a few. Indexes into the
* first channel_ends array. Terminated by invalid index.
*
* <CAPACITIES> := <CAPACITY_TEMPLATES> {channel_count}*{capacity_idx}
* <CAPACITY_TEMPLATES> := {capacity_count} {channel_count}*{capacity}
* This is one satoshi amount per channel.
*
* <HTLC_MINS> := <HTLC_MIN_TEMPLATES> {channel_count*2}*{htlc_min_idx}
* <HTLC_MIN_TEMPLATES> := {htlc_min_count} {htlc_min_count}*{htlc_min}
* These templates are all of the same form. A set of values, followed by
* an index into these values for each direction of each channel, in order
* 1. 0'th channel 1st direction
* 2. 0'th channel 2nd direction
* 3. 1'st channel 1st direction
* 4. 1'st channel 2nd direction
*
* <HTLC_MAXS> := <HTLC_MAX_TEMPLATES> {channel_count*2}*{htlc_max_idx}
* Note that values 0 and 1 are special: 0 == channel capacity, 1 == 0.99 * channel capacity.
* <HTLC_MAX_TEMPLATES> := {htlc_max_count} {htlc_max_count}*{htlc_max}
* <BASEFEES> := <BASEFEE_TEMPLATES> {channel_count*2}*{basefee_idx}
* <BASEFEE_TEMPLATES> := {basefee_count} {basefee_count}*{basefee}
* <PROPFEES> := <PROPFEE_TEMPLATES> {channel_count*2}*{propfee_idx}
* <PROPFEE_TEMPLATES> := {propfee_count} {propfee_count}*{propfee}
* <DELAYS> := <DELAY_TEMPLATES> {channel_count*2}*{delay_idx}
* <DELAY_TEMPLATES> := {delay_count} {delay_count}*{delay}
*/
#define GC_HEADER "GOSSMAP_COMPRESSv1"
#define GC_HEADERLEN (sizeof(GC_HEADER))
static int cmp_node_num_chans(struct gossmap_node *const *a,
struct gossmap_node *const *b,
void *unused)
{
return (int)(*a)->num_chans - (int)(*b)->num_chans;
}
static void write_bigsize(int outfd, u64 val)
{
u8 buf[BIGSIZE_MAX_LEN];
size_t len;
len = bigsize_put(buf, val);
if (!write_all(outfd, buf, len))
errx(1, "Writing bigsize");
}
static int cmp_u64(const u64 *a,
const u64 *b,
void *unused)
{
if (*a > *b)
return 1;
else if (*a < *b)
return -1;
return 0;
}
static const u64 *deduplicate(const tal_t *ctx, const u64 *vals)
{
u64 *sorted;
u64 *dedup;
size_t n;
/* Sort and remove dups */
sorted = tal_dup_talarr(tmpctx, u64, vals);
asort(sorted, tal_count(sorted), cmp_u64, NULL);
dedup = tal_arr(ctx, u64, tal_count(sorted));
n = 0;
dedup[n++] = sorted[0];
for (size_t i = 1; i < tal_count(sorted); i++) {
if (sorted[i] == dedup[n-1])
continue;
dedup[n++] = sorted[i];
}
tal_resize(&dedup, n);
return dedup;
}
static size_t find_index(const u64 *template, u64 val)
{
for (size_t i = 0; i < tal_count(template); i++) {
if (template[i] == val)
return i;
}
abort();
}
/* All templates are of the same form. Output all the distinct values, then
* write out which one is used by each channel */
static void write_template_and_values(int outfd, const u64 *vals, const char *what)
{
/* Sort and remove dups */
const u64 *template = deduplicate(tmpctx, vals);
if (verbose)
printf("%zu unique %s\n", tal_count(template), what);
assert(tal_count(vals) >= tal_count(template));
/* Write template. */
write_bigsize(outfd, tal_count(template));
for (size_t i = 0; i < tal_count(template); i++)
write_bigsize(outfd, template[i]);
/* Tie every channel into the template. O(N^2) but who
* cares? */
for (size_t i = 0; i < tal_count(vals); i++) {
write_bigsize(outfd, find_index(template, vals[i]));
}
}
static void write_bidir_perchan(int outfd,
struct gossmap *gossmap,
struct gossmap_chan **chans,
u64 (*get_value)(struct gossmap *,
const struct gossmap_chan *,
int),
const char *what)
{
u64 *vals = tal_arr(tmpctx, u64, tal_count(chans) * 2);
for (size_t i = 0; i < tal_count(chans); i++) {
for (size_t dir = 0; dir < 2; dir++) {
if (chans[i]->half[dir].enabled)
vals[i*2+dir] = get_value(gossmap, chans[i], dir);
else
vals[i*2+dir] = 0;
}
}
write_template_and_values(outfd, vals, what);
}
static u64 get_htlc_min(struct gossmap *gossmap,
const struct gossmap_chan *chan,
int dir)
{
struct amount_msat msat;
gossmap_chan_get_update_details(gossmap, chan, dir,
NULL, NULL, NULL, NULL, NULL, &msat, NULL);
return msat.millisatoshis; /* Raw: compressed format */
}
static u64 get_htlc_max(struct gossmap *gossmap,
const struct gossmap_chan *chan,
int dir)
{
struct amount_msat msat, capacity_msat;
struct amount_sat capacity_sats;
gossmap_chan_get_capacity(gossmap, chan, &capacity_sats);
gossmap_chan_get_update_details(gossmap, chan, dir,
NULL, NULL, NULL, NULL, NULL, NULL, &msat);
/* Special value for the common case of "max_htlc == capacity" */
if (amount_msat_eq_sat(msat, capacity_sats)) {
return 0;
}
/* Other common case: "max_htlc == 99% capacity" */
if (amount_sat_to_msat(&capacity_msat, capacity_sats)
&& amount_msat_scale(&capacity_msat, capacity_msat, 0.99)
&& amount_msat_eq(msat, capacity_msat)) {
return 1;
}
return msat.millisatoshis; /* Raw: compressed format */
}
static u64 get_basefee(struct gossmap *gossmap,
const struct gossmap_chan *chan,
int dir)
{
u32 basefee;
gossmap_chan_get_update_details(gossmap, chan, dir,
NULL, NULL, NULL, &basefee, NULL, NULL, NULL);
return basefee;
}
static u64 get_propfee(struct gossmap *gossmap,
const struct gossmap_chan *chan,
int dir)
{
u32 propfee;
gossmap_chan_get_update_details(gossmap, chan, dir,
NULL, NULL, NULL, NULL, &propfee, NULL, NULL);
return propfee;
}
static u64 get_delay(struct gossmap *gossmap,
const struct gossmap_chan *chan,
int dir)
{
return chan->half[dir].delay;
}
int main(int argc, char *argv[])
{
int infd, outfd;
common_setup(argv[0]);
setup_locale();
opt_register_noarg("--verbose|-v", opt_set_bool, &verbose,
"Print details.");
opt_register_noarg("--help|-h", opt_usage_and_exit,
"[decompress|compress] infile outfile"
"Compress or decompress a gossmap file",
"Print this message.");
opt_parse(&argc, argv, opt_log_stderr_exit);
if (argc != 4)
opt_usage_and_exit("Needs 4 arguments");
infd = open(argv[2], O_RDONLY);
if (infd < 0)
opt_usage_and_exit(tal_fmt(tmpctx, "Cannot open %s for reading: %s",
argv[2], strerror(errno)));
outfd = open(argv[3], O_WRONLY|O_CREAT|O_TRUNC, 0666);
if (outfd < 0)
opt_usage_and_exit(tal_fmt(tmpctx, "Cannot open %s for writing: %s",
argv[3], strerror(errno)));
if (streq(argv[1], "compress")) {
struct gossmap_node **nodes, *n;
size_t *node_to_compr_idx;
size_t node_count, channel_count;
struct gossmap_chan **chans, *c;
struct gossmap *gossmap = gossmap_load_fd(tmpctx, infd, NULL, NULL, NULL);
if (!gossmap)
opt_usage_and_exit("Cannot read gossmap");
nodes = tal_arr(gossmap, struct gossmap_node *, gossmap_max_node_idx(gossmap));
for (node_count = 0, n = gossmap_first_node(gossmap);
n;
n = gossmap_next_node(gossmap, n), node_count++) {
nodes[node_count] = n;
}
tal_resize(&nodes, node_count);
if (verbose)
printf("%zu nodes\n", node_count);
/* nodes with most channels go first */
asort(nodes, tal_count(nodes), cmp_node_num_chans, NULL);
/* Create map of gossmap index to compression index */
node_to_compr_idx = tal_arr(nodes, size_t, gossmap_max_node_idx(gossmap));
for (size_t i = 0; i < tal_count(nodes); i++)
node_to_compr_idx[gossmap_node_idx(gossmap, nodes[i])] = i;
if (!write_all(outfd, GC_HEADER, GC_HEADERLEN))
err(1, "Writing header");
/* Now, output channels. First get exact count. */
for (channel_count = 0, c = gossmap_first_chan(gossmap);
c;
c = gossmap_next_chan(gossmap, c)) {
channel_count++;
}
if (verbose)
printf("%zu channels\n", channel_count);
chans = tal_arr(gossmap, struct gossmap_chan *, channel_count);
/* * <CHANNEL_ENDS> := {channel_count} {start_nodeidx}*{channel_count} {end_nodeidx}*{channel_count} */
write_bigsize(outfd, channel_count);
size_t chanidx = 0;
/* We iterate nodes to get to channels. This gives us nicer ordering for compression */
for (size_t wanted_dir = 0; wanted_dir < 2; wanted_dir++) {
for (n = gossmap_first_node(gossmap); n; n = gossmap_next_node(gossmap, n)) {
for (size_t i = 0; i < n->num_chans; i++) {
int dir;
c = gossmap_nth_chan(gossmap, n, i, &dir);
if (dir != wanted_dir)
continue;
write_bigsize(outfd,
node_to_compr_idx[gossmap_node_idx(gossmap, n)]);
/* First time reflects channel index for reader */
if (wanted_dir == 0)
chans[chanidx++] = c;
}
}
}
/* <DISABLEDS> := <DISABLED>* {channel_count*2} */
/* <DISABLED> := {chanidx}*2+{direction} */
size_t num_disabled = 0;
size_t num_unknown = 0;
for (size_t i = 0; i < channel_count; i++) {
for (size_t dir = 0; dir < 2; dir++) {
if (chans[i]->cupdate_off[dir] == 0)
num_unknown++;
if (!chans[i]->half[dir].enabled) {
write_bigsize(outfd, i * 2 + dir);
num_disabled++;
}
}
}
write_bigsize(outfd, channel_count * 2);
if (verbose)
printf("%zu disabled channels (%zu no update)\n", num_disabled, num_unknown);
/* <CAPACITIES> := <CAPACITY_TEMPLATES> {channel_count}*{capacity_idx} */
/* <CAPACITY_TEMPLATES> := {capacity_count} {capacity_count}*{capacity} */
u64 *vals = tal_arr(chans, u64, channel_count);
for (size_t i = 0; i < channel_count; i++) {
struct amount_sat sats;
gossmap_chan_get_capacity(gossmap, chans[i], &sats);
vals[i] = sats.satoshis; /* Raw: compression format */
}
write_template_and_values(outfd, vals, "capacities");
/* These are all of same form: one entry per direction per channel */
/* <HTLC_MINS> := <HTLC_MIN_TEMPLATES> {channel_count}*{htlc_min_idx} */
/* <HTLC_MIN_TEMPLATES> := {htlc_min_count} {htlc_min_count}*{htlc_min} */
/* <HTLC_MAXS> := <HTLC_MAX_TEMPLATES> {channel_count}*{htlc_max_idx} */
/* <HTLC_MAX_TEMPLATES> := {htlc_max_count} {htlc_max_count}*{htlc_max} */
/* <BASEFEES> := <BASEFEE_TEMPLATES> {channel_count}*{basefee_idx} */
/* <BASEFEE_TEMPLATES> := {basefee_count} {basefee_count}*{basefee} */
/* <PROPFEES> := <PROPFEE_TEMPLATES> {channel_count}*{propfee_idx} */
/* <PROPFEE_TEMPLATES> := {propfee_count} {propfee_count}*{propfee} */
/* <DELAYS> := <DELAY_TEMPLATES> {channel_count}*{delay_idx} */
/* <DELAY_TEMPLATES> := {delay_count} {delay_count}*{delay} */
write_bidir_perchan(outfd, gossmap, chans, get_htlc_min, "htlc_min");
write_bidir_perchan(outfd, gossmap, chans, get_htlc_max, "htlc_max");
write_bidir_perchan(outfd, gossmap, chans, get_basefee, "basefee");
write_bidir_perchan(outfd, gossmap, chans, get_propfee, "propfee");
write_bidir_perchan(outfd, gossmap, chans, get_delay, "delay");
} else if (streq(argv[1], "decompress")) {
errx(1, "NYI");
} else
opt_usage_and_exit("Unknown command");
common_shutdown();
}