From 53faab0838490060222328e22828772e87636165 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 10:15:55 +0930 Subject: [PATCH] pytest: add routine to generate gossmap by topology. We marshal it into the "compressed" format and get the decompresser to build the actual gossmap. Signed-off-by: Rusty Russell --- tests/test_gossip.py | 78 +++++++++++++++++++++++++++- tests/utils.py | 119 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 80fa5ba62..f425de568 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -6,7 +6,7 @@ from utils import ( wait_for, TIMEOUT, only_one, sync_blockheight, expected_node_features, mine_funding_to_announce, default_ln_port, CHANNEL_SIZE, - first_scid, + first_scid, generate_gossip_store, GenChannel ) import json @@ -18,6 +18,7 @@ import struct import subprocess import time import unittest +import shutil import socket @@ -2139,6 +2140,81 @@ def test_gossip_throttle(node_factory, bitcoind, chainparams): assert set(out2) == set(out4) +def test_generate_gossip_store(node_factory): + l1 = node_factory.get_node(start=False) + chans = [GenChannel(0, 1), + GenChannel(0, 2, capacity_sats=5000), + GenChannel(0, 3), + GenChannel(0, 4)] + chans[2].half[0] = GenChannel.Half(enabled=False, + htlc_min=10, + htlc_max=5000000 - 10, + basefee=10, + propfee=10) + + chans[2].half[1] = GenChannel.Half(htlc_min=11, + htlc_max=5000000 - 11, + basefee=11, + propfee=11) + gsfile = generate_gossip_store(chans) + + # Set up l1 with this as the gossip_store + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + nodes = ['03c581bf310c4c97b05e5e6fed2f82d872f388ec9ab7f1feddfd5380ddb3c6531c', + '03091f559e2704cd80e41cd103ca4a60fd91010927674016b09f40c1d450368cf4', + '0255a0e1286c832286eda137bbefe17f21af265a08bbea481a6ea96b9f4b5f84ac', + '02ec99a74a8c8d10853e1a3b0806556abda6798a68a0cedca4c766b5f6cf314f22', + '02c5ad36f9c80ca70d4f88d50f17be2f1f481f37086dbf3433473765a0027ecd63'] + + expected = [] + chancount = 0 + for c in chans: + for d in (0, 1): + # listchannels direction 0 always lesser -> greater. + if nodes[c.node1] < nodes[c.node2]: + expected_dir = d + else: + expected_dir = d ^ 1 + channel_flags = expected_dir + if not c.half[d].enabled: + active = False + channel_flags |= 2 + else: + active = True + if d == 0: + n1 = nodes[c.node1] + n2 = nodes[c.node2] + else: + n1 = nodes[c.node2] + n2 = nodes[c.node1] + + expected.append({'source': n1, + 'destination': n2, + 'short_channel_id': '{}x{}x{}'.format(c.node1, c.node2, chancount), + 'direction': expected_dir, + 'public': True, + 'amount_msat': c.capacity_sats * 1000, + 'message_flags': 1, + 'channel_flags': channel_flags, + 'active': active, + 'last_update': 0, + 'base_fee_millisatoshi': c.half[d].basefee, + 'fee_per_millionth': c.half[d].propfee, + 'delay': c.half[d].delay, + 'htlc_minimum_msat': c.half[d].htlc_min, + 'htlc_maximum_msat': c.half[d].htlc_max, + 'features': ''}) + chancount += 1 + + # Order is not well-defined, and sets don't like dicts :( + lchans = sorted(l1.rpc.listchannels()['channels'], key=lambda x: x['source'] + x['destination']) + expected = sorted(expected, key=lambda x: x['source'] + x['destination']) + + assert lchans == expected + + def test_seeker_first_peer(node_factory, bitcoind): l1, l2, l3, l4, l5 = node_factory.get_nodes(5) diff --git a/tests/utils.py b/tests/utils.py index 2598380c2..3c92ce6e8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,8 @@ from pyln.testing.utils import env, only_one, wait_for, write_config, TailablePr import bitstring from pyln.client import Millisatoshi from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND, EXPERIMENTAL_SPLICING +import subprocess +import tempfile import time COMPAT = env("COMPAT", "1") == "1" @@ -430,3 +432,120 @@ def scriptpubkey_addr(scriptpubkey): # Modern bitcoin (at least, git master) return scriptpubkey['address'] return None + + +class GenChannel(object): + class Half(object): + def __init__(self, htlc_max, enabled=True, htlc_min=0, basefee=0, propfee=1, delay=6): + self.enabled = enabled + self.htlc_min = htlc_min + self.htlc_max = htlc_max + self.basefee = basefee + self.propfee = propfee + self.delay = delay + + def __init__(self, node1, node2, capacity_sats=1000000): + self.node1 = node1 + self.node2 = node2 + self.capacity_sats = capacity_sats + self.half = [GenChannel.Half(htlc_max=capacity_sats * 1000), + GenChannel.Half(htlc_max=capacity_sats * 1000)] + + +def generate_gossip_store(channels): + """Returns a gossip store file with the given channels in it. + """ + nodes = [] + + def write_bignum(outf, val): + if val < 253: + outf.write(val.to_bytes(1, byteorder='big')) + elif val <= 0xFFFF: + outf.write(b'\xFD') + outf.write(val.to_bytes(2, byteorder='big')) + elif val <= 0xFFFFFFFF: + outf.write(b'\xFE') + outf.write(val.to_bytes(4, byteorder='big')) + else: + outf.write(b'\xFF') + outf.write(val.to_bytes(8, byteorder='big')) + + def write_dumb_template(outf, channels, propname, illegalvals=[]): + """We don't bother uniquifing, just one entry per chan dir""" + # Template is simply all the values + write_bignum(outf, len(channels) * 2) + for c in channels: + for d in (0, 1): + v = getattr(c.half[d], propname) + assert v not in illegalvals + write_bignum(outf, v) + + # Now each entry for each channel half points into the values. + for i in range(0, len(channels) * 2): + write_bignum(outf, i) + + # First create nodes + for c in channels: + if c.node1 not in nodes: + nodes.append(c.node1) + if c.node2 not in nodes: + nodes.append(c.node2) + + cfile = tempfile.NamedTemporaryFile(prefix='gs-compressed-') + #
:= "GOSSMAP_COMPRESSv1\0" + cfile.write(b'GOSSMAP_COMPRESSv1\x00') + # := {channel_count} {start_nodeidx}*{channel_count} {end_nodeidx}*{channel_count} + write_bignum(cfile, len(channels)) + for c in channels: + write_bignum(cfile, nodes.index(c.node1)) + for c in channels: + write_bignum(cfile, nodes.index(c.node2)) + + # := * {channel_count*2} + # := {chanidx}*2+{direction} + for i, c in enumerate(channels): + for d in (0, 1): + if not c.half[d].enabled: + write_bignum(cfile, i * 2 + d) + write_bignum(cfile, len(channels) * 2) + + # := {channel_count}*{capacity_idx} + # := {capacity_count} {channel_count}*{capacity} + max_htlc_defaults = [] + write_bignum(cfile, len(channels)) + for c in channels: + write_bignum(cfile, c.capacity_sats) + max_htlc_defaults.append(c.capacity_sats) + max_htlc_defaults.append(c.capacity_sats) + + for i, _ in enumerate(channels): + write_bignum(cfile, i) + + # := {channel_ count*2}*{htlc_min_idx} + # := {htlc_min_count} {htlc_min_count}*{htlc_min} + write_dumb_template(cfile, channels, 'htlc_min') + # := {channel_count*2}*{htlc_max_idx} + # := {htlc_max_count} {htlc_max_count}*{htlc_max} + # 0 and 1 have special meanings, don't use them! + write_dumb_template(cfile, channels, 'htlc_max', [0, 1]) + # := {channel_count*2}*{basefee_idx} + # := {basefee_count} {basefee_count}*{basefee} + write_dumb_template(cfile, channels, 'basefee') + # := {channel_count*2}*{propfee_idx} + # := {propfee_count} {propfee_count}*{propfee} + write_dumb_template(cfile, channels, 'propfee') + # := {channel_count*2}*{delay_idx} + # := {delay_count} {delay_count}*{delay} + write_dumb_template(cfile, channels, 'delay') + + cfile.flush() + + outfile = tempfile.NamedTemporaryFile(prefix='gossip-store-') + subprocess.run(['devtools/gossmap-compress', + 'decompress', + cfile.name, + outfile.name], + check=True) + cfile.close() + + return outfile