#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_ZLIB #include #else /* Worst... zlib... ever! */ #define gzFile int #define gzdopen(fd, mode) (fd) #define gzclose(outf) close(outf) static int gzread(int fd, void *buf, size_t len) { if (read_all(fd, buf, len)) return len; return 0; } static int gzwrite(int fd, const void *buf, size_t len) { if (write_all(fd, buf, len)) return len; return 0; } #endif static unsigned int verbose = 0; /* All {numbers} are bigsize. * * :=
*
:= "GOSSMAP_COMPRESSv1\0" * := {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 * * := * {channel_count*2} * := {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. * * := {channel_count}*{capacity_idx} * := {capacity_count} {channel_count}*{capacity} * This is one satoshi amount per channel. * * := {channel_count*2}*{htlc_min_idx} * := {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 * * := {channel_count*2}*{htlc_max_idx} * Note that values 0 and 1 are special: 0 == channel capacity, 1 == 0.99 * channel capacity. * := {htlc_max_count} {htlc_max_count}*{htlc_max} * := {channel_count*2}*{basefee_idx} * := {basefee_count} {basefee_count}*{basefee} * := {channel_count*2}*{propfee_idx} * := {propfee_count} {propfee_count}*{propfee} * := {channel_count*2}*{delay_idx} * := {delay_count} {delay_count}*{delay} */ #define GC_HEADER "GOSSMAP_COMPRESSv1" #define GC_HEADERLEN (sizeof(GC_HEADER)) #define GOSSIP_STORE_VER ((0 << 5) | 14) 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(gzFile outf, u64 val) { u8 buf[BIGSIZE_MAX_LEN]; size_t len; len = bigsize_put(buf, val); if (gzwrite(outf, buf, len) == 0) err(1, "Writing bigsize"); } static u64 read_bigsize(gzFile inf) { u64 val; u8 buf[BIGSIZE_MAX_LEN]; if (gzread(inf, buf, 1) != 1) errx(1, "Reading bigsize"); switch (buf[0]) { case 0xfd: if (gzread(inf, buf+1, 2) != 2) errx(1, "Reading bigsize"); break; case 0xfe: if (gzread(inf, buf+1, 4) != 4) errx(1, "Reading bigsize"); break; case 0xff: if (gzread(inf, buf+1, 8) != 8) errx(1, "Reading bigsize"); break; } if (bigsize_get(buf, sizeof(buf), &val) == 0) errx(1, "Bad bigsize"); return val; } 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(gzFile outf, 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(outf, tal_count(template)); for (size_t i = 0; i < tal_count(template); i++) write_bigsize(outf, 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(outf, find_index(template, vals[i])); } } static void write_bidir_perchan(gzFile outf, 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(outf, 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; } static void pubkey_for_node(size_t nodeidx, struct pubkey *key, const struct pubkey *node_ids) { struct secret seckey; if (nodeidx < tal_count(node_ids)) { *key = node_ids[nodeidx]; return; } memset(&seckey, 1, sizeof(seckey)); memcpy(&seckey, &nodeidx, sizeof(nodeidx)); if (!pubkey_from_secret(&seckey, key)) abort(); } static void write_msg_to_gstore(int outfd, const u8 *msg TAKES) { struct gossip_hdr hdr; hdr.flags = 0; hdr.len = cpu_to_be16(tal_bytelen(msg)); hdr.timestamp = 0; hdr.crc = cpu_to_be32(crc32c(0, msg, tal_bytelen(msg))); if (!write_all(outfd, &hdr, sizeof(hdr)) || !write_all(outfd, msg, tal_bytelen(msg))) { err(1, "Writing gossip_store"); } if (taken(msg)) tal_free(msg); } /* BOLT #7: * 1. type: 256 (`channel_announcement`) * 2. data: * * [`signature`:`node_signature_1`] * * [`signature`:`node_signature_2`] * * [`signature`:`bitcoin_signature_1`] * * [`signature`:`bitcoin_signature_2`] * * [`u16`:`len`] * * [`len*byte`:`features`] * * [`chain_hash`:`chain_hash`] * * [`short_channel_id`:`short_channel_id`] * * [`point`:`node_id_1`] * * [`point`:`node_id_2`] * * [`point`:`bitcoin_key_1`] * * [`point`:`bitcoin_key_2`] */ static void write_announce(int outfd, size_t node1, size_t node2, u64 capacity, size_t i, const struct pubkey *node_ids) { struct { secp256k1_ecdsa_signature sig; struct bitcoin_blkid chain_hash; } vals; u8 *msg; struct short_channel_id scid; struct pubkey id1, id2; struct node_id nodeid1, nodeid2; memset(&vals, 0, sizeof(vals)); pubkey_for_node(node1, &id1, node_ids); pubkey_for_node(node2, &id2, node_ids); /* Nodes in pubkey order */ if (pubkey_cmp(&id1, &id2) < 0) { node_id_from_pubkey(&nodeid1, &id1); node_id_from_pubkey(&nodeid2, &id2); } else { node_id_from_pubkey(&nodeid1, &id2); node_id_from_pubkey(&nodeid2, &id1); } /* Use i to avoid clashing scids even if two nodes have > 1 channel */ if (!mk_short_channel_id(&scid, node1, node2, i & 0xFFFF)) abort(); msg = towire_channel_announcement(NULL, &vals.sig, &vals.sig, &vals.sig, &vals.sig, NULL, &vals.chain_hash, scid, &nodeid1, &nodeid2, &id1, &id1); write_msg_to_gstore(outfd, take(msg)); msg = towire_gossip_store_channel_amount(NULL, amount_sat(capacity)); write_msg_to_gstore(outfd, take(msg)); } /* BOLT #7: * 1. type: 258 (`channel_update`) * 2. data: * * [`signature`:`signature`] * * [`chain_hash`:`chain_hash`] * * [`short_channel_id`:`short_channel_id`] * * [`u32`:`timestamp`] * * [`byte`:`message_flags`] * * [`byte`:`channel_flags`] * * [`u16`:`cltv_expiry_delta`] * * [`u64`:`htlc_minimum_msat`] * * [`u32`:`fee_base_msat`] * * [`u32`:`fee_proportional_millionths`] * * [`u64`:`htlc_maximum_msat`] */ static void write_update(int outfd, size_t node1, size_t node2, size_t i, int dir, bool disabled, u64 htlc_min, u64 htlc_max, u64 basefee, u32 propfee, u16 delay, const struct pubkey *node_ids) { struct vals { secp256k1_ecdsa_signature sig; struct bitcoin_blkid chain_hash; u32 timestamp; } vals; u8 *msg; u8 message_flags, channel_flags; struct pubkey id1, id2; struct short_channel_id scid; memset(&vals, 0, sizeof(vals)); /* Use i to avoid clashing scids even if two nodes have > 1 channel */ if (!mk_short_channel_id(&scid, node1, node2, i & 0xFFFF)) abort(); /* If node ids are backward, dir is reversed */ pubkey_for_node(node1, &id1, node_ids); pubkey_for_node(node2, &id2, node_ids); if (pubkey_cmp(&id1, &id2) > 0) dir = !dir; /* BOLT #7: * The `channel_flags` bitfield is used to indicate the direction of * the channel: it identifies the node that this update originated * from and signals various options concerning the channel. The * following table specifies the meaning of its individual bits: * * | Bit Position | Name | Meaning | * | ------------- | ----------- | -------------------------------- | * | 0 | `direction` | Direction this update refers to. | * | 1 | `disable` | Disable the channel. | * * The `message_flags` bitfield is used to provide additional details about the message: * * | Bit Position | Name | * | ------------- | ---------------| * | 0 | `must_be_one` | * | 1 | `dont_forward` | */ channel_flags = dir ? 1 : 0; if (disabled) channel_flags |= 2; message_flags = 1; msg = towire_channel_update(NULL, &vals.sig, &vals.chain_hash, scid, 0, message_flags, channel_flags, delay, amount_msat(htlc_min), basefee, propfee, amount_msat(htlc_max)); write_msg_to_gstore(outfd, take(msg)); } static const u64 *read_template(const tal_t *ctx, gzFile inf, const char *what) { size_t count = read_bigsize(inf); u64 *template = tal_arr(ctx, u64, count); for (size_t i = 0; i < count; i++) template[i] = read_bigsize(inf); if (verbose) printf("%zu unique %s\n", count, what); return template; } static u64 read_val(gzFile inf, const u64 *template) { size_t idx = read_bigsize(inf); assert(idx < tal_count(template)); return template[idx]; } static char *opt_add_one(unsigned int *val) { (*val)++; return NULL; } static char *opt_nodes(const char *optarg, struct pubkey **node_ids) { char **ids = tal_strsplit(tmpctx, optarg, ",", STR_EMPTY_OK); for (size_t i = 0; ids[i]; i++) { struct pubkey n; if (!pubkey_from_hexstr(ids[i], strlen(ids[i]), &n)) return tal_fmt(tmpctx, "Invalid node id '%s'", ids[i]); tal_arr_expand(node_ids, n); } return NULL; } int main(int argc, char *argv[]) { int infd, outfd; struct pubkey *node_ids; common_setup(argv[0]); setup_locale(); node_ids = tal_arr(tmpctx, struct pubkey, 0); opt_register_noarg("--verbose|-v", opt_add_one, &verbose, "Print details (each additional gives more!)."); opt_register_arg("--nodes", opt_nodes, NULL, &node_ids, "Comma separated node ids to give first nodes."); 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; gzFile outf = gzdopen(outfd, "wb9"); 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 (gzwrite(outf, GC_HEADER, GC_HEADERLEN) == 0) 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_count} {start_nodeidx}*{channel_count} {end_nodeidx}*{channel_count} */ write_bigsize(outf, 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(outf, node_to_compr_idx[gossmap_node_idx(gossmap, n)]); /* First time reflects channel index for reader */ if (wanted_dir == 0) chans[chanidx++] = c; } } } /* := * {channel_count*2} */ /* := {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(outf, i * 2 + dir); num_disabled++; } } } write_bigsize(outf, channel_count * 2); if (verbose) printf("%zu disabled channels (%zu no update)\n", num_disabled, num_unknown); /* := {channel_count}*{capacity_idx} */ /* := {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(outf, vals, "capacities"); /* These are all of same form: one entry per direction per channel */ /* := {channel_count}*{htlc_min_idx} */ /* := {htlc_min_count} {htlc_min_count}*{htlc_min} */ /* := {channel_count}*{htlc_max_idx} */ /* := {htlc_max_count} {htlc_max_count}*{htlc_max} */ /* := {channel_count}*{basefee_idx} */ /* := {basefee_count} {basefee_count}*{basefee} */ /* := {channel_count}*{propfee_idx} */ /* := {propfee_count} {propfee_count}*{propfee} */ /* := {channel_count}*{delay_idx} */ /* := {delay_count} {delay_count}*{delay} */ write_bidir_perchan(outf, gossmap, chans, get_htlc_min, "htlc_min"); write_bidir_perchan(outf, gossmap, chans, get_htlc_max, "htlc_max"); write_bidir_perchan(outf, gossmap, chans, get_basefee, "basefee"); write_bidir_perchan(outf, gossmap, chans, get_propfee, "propfee"); write_bidir_perchan(outf, gossmap, chans, get_delay, "delay"); gzclose(outf); } else if (streq(argv[1], "decompress")) { char hdr[GC_HEADERLEN]; size_t channel_count, chanidx; const u64 *template; struct fakechan { size_t node1, node2; u64 capacity; struct halffake { u64 htlc_min, htlc_max; u32 basefee, propfee; u32 delay; bool disabled; } half[2]; } *chans; const u8 version = GOSSIP_STORE_VER; size_t disabled_count, node_limit; gzFile inf = gzdopen(infd, "rb"); if (gzread(inf, hdr, sizeof(hdr)) != sizeof(hdr) || !memeq(hdr, sizeof(hdr), GC_HEADER, GC_HEADERLEN)) errx(1, "Not a valid compressed gossmap header"); channel_count = read_bigsize(inf); if (verbose) printf("%zu channels\n", channel_count); chans = tal_arrz(tmpctx, struct fakechan, channel_count); node_limit = 0; for (size_t i = 0; i < channel_count; i++) { chans[i].node1 = read_bigsize(inf); if (chans[i].node1 >= node_limit) node_limit = chans[i].node1 + 1; } for (size_t i = 0; i < channel_count; i++) { chans[i].node2 = read_bigsize(inf); if (chans[i].node2 >= node_limit) node_limit = chans[i].node2 + 1; } /* Useful so they can map their ids back to node ids. */ for (size_t i = 0; i < node_limit; i++) { struct pubkey node_id; pubkey_for_node(i, &node_id, node_ids); printf("%s\n", fmt_pubkey(tmpctx, &node_id)); } if (verbose >= 2) { for (size_t i = 0; i < channel_count; i++) { struct pubkey id1, id2; pubkey_for_node(chans[i].node1, &id1, node_ids); pubkey_for_node(chans[i].node2, &id2, node_ids); printf("Channel %zu: %s -> %s\n", i, fmt_pubkey(tmpctx, &id1), fmt_pubkey(tmpctx, &id2)); } } disabled_count = 0; while ((chanidx = read_bigsize(inf)) < channel_count*2) { disabled_count++; chans[chanidx/2].half[chanidx%2].disabled = true; } if (verbose) printf("%zu disabled\n", disabled_count); template = read_template(tmpctx, inf, "capacities"); for (size_t i = 0; i < channel_count; i++) chans[i].capacity = read_val(inf, template); template = read_template(tmpctx, inf, "htlc_min"); for (size_t i = 0; i < channel_count; i++) { for (size_t dir = 0; dir < 2; dir++) { chans[i].half[dir].htlc_min = read_val(inf, template); } } template = read_template(tmpctx, inf, "htlc_max"); for (size_t i = 0; i < channel_count; i++) { for (size_t dir = 0; dir < 2; dir++) { u64 v = read_val(inf, template); if (v == 0) v = chans[i].capacity; else if (v == 1) v = chans[i].capacity * 0.99; chans[i].half[dir].htlc_max = v; } } template = read_template(tmpctx, inf, "basefee"); for (size_t i = 0; i < channel_count; i++) { for (size_t dir = 0; dir < 2; dir++) { chans[i].half[dir].basefee = read_val(inf, template); } } template = read_template(tmpctx, inf, "propfee"); for (size_t i = 0; i < channel_count; i++) { for (size_t dir = 0; dir < 2; dir++) { chans[i].half[dir].propfee = read_val(inf, template); } } template = read_template(tmpctx, inf, "delay"); for (size_t i = 0; i < channel_count; i++) { for (size_t dir = 0; dir < 2; dir++) { chans[i].half[dir].delay = read_val(inf, template); } } /* Now write out gossmap */ if (write(outfd, &version, 1) != 1) err(1, "Failed to write output"); for (size_t i = 0; i < channel_count; i++) { write_announce(outfd, chans[i].node1, chans[i].node2, chans[i].capacity, i, node_ids); for (size_t dir = 0; dir < 2; dir++) { write_update(outfd, chans[i].node1, chans[i].node2, i, dir, chans[i].half[dir].disabled, chans[i].half[dir].htlc_min, chans[i].half[dir].htlc_max, chans[i].half[dir].basefee, chans[i].half[dir].propfee, chans[i].half[dir].delay, node_ids); } } gzclose(inf); } else opt_usage_and_exit("Unknown command"); close(outfd); common_shutdown(); }