electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

ln_features.py (6086B)


      1 #!/usr/bin/env python3
      2 """
      3 Script to analyze the graph for Lightning features.
      4 
      5 https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
      6 """
      7 
      8 import asyncio
      9 import os
     10 import time
     11 
     12 from electrum.logging import get_logger, configure_logging
     13 from electrum.simple_config import SimpleConfig
     14 from electrum import constants
     15 from electrum.daemon import Daemon
     16 from electrum.wallet import create_new_wallet
     17 from electrum.util import create_and_start_event_loop, log_exceptions, bh2u, bfh
     18 from electrum.lnutil import LnFeatures
     19 
     20 logger = get_logger(__name__)
     21 
     22 
     23 # Configuration parameters
     24 IS_TESTNET = False
     25 TIMEOUT = 5  # for Lightning peer connections
     26 WORKERS = 30  # number of workers that concurrently fetch results for feature comparison
     27 NODES_PER_WORKER = 50
     28 VERBOSITY = ''  # for debugging set '*', otherwise ''
     29 FLAG = LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT  # chose the 'opt' flag
     30 PRESYNC = False  # should we sync the graph or take it from an already synced database?
     31 
     32 
     33 config = SimpleConfig({"testnet": IS_TESTNET, "verbosity": VERBOSITY})
     34 configure_logging(config)
     35 
     36 loop, stopping_fut, loop_thread = create_and_start_event_loop()
     37 # avoid race condition when starting network, in debug starting the asyncio loop
     38 # takes some time
     39 time.sleep(2)
     40 
     41 if IS_TESTNET:
     42     constants.set_testnet()
     43 daemon = Daemon(config, listen_jsonrpc=False)
     44 network = daemon.network
     45 assert network.asyncio_loop.is_running()
     46 
     47 # create empty wallet
     48 wallet_dir = os.path.dirname(config.get_wallet_path())
     49 wallet_path = os.path.join(wallet_dir, "ln_features_wallet_main")
     50 if not os.path.exists(wallet_path):
     51     create_new_wallet(path=wallet_path, config=config)
     52 
     53 # open wallet
     54 wallet = daemon.load_wallet(wallet_path, password=None, manual_upgrades=False)
     55 wallet.start_network(network)
     56 
     57 
     58 async def worker(work_queue: asyncio.Queue, results_queue: asyncio.Queue, flag):
     59     """Connects to a Lightning peer and checks whether the announced feature
     60     from the gossip is equal to the feature in the init message.
     61 
     62     Returns None if no connection could be made, True or False otherwise."""
     63     count = 0
     64     while not work_queue.empty():
     65         if count > NODES_PER_WORKER:
     66             return
     67         work = await work_queue.get()
     68 
     69         # only check non-onion addresses
     70         addr = None
     71         for a in work['addrs']:
     72             if not "onion" in a[0]:
     73                 addr = a
     74         if not addr:
     75             await results_queue.put(None)
     76             continue
     77 
     78         # handle ipv4/ipv6
     79         if ':' in addr[0]:
     80             connect_str = f"{bh2u(work['pk'])}@[{addr.host}]:{addr.port}"
     81         else:
     82             connect_str = f"{bh2u(work['pk'])}@{addr.host}:{addr.port}"
     83 
     84         print(f"worker connecting to {connect_str}")
     85         try:
     86             peer = await wallet.lnworker.add_peer(connect_str)
     87             res = await asyncio.wait_for(peer.initialized, TIMEOUT)
     88             if res:
     89                 if peer.features & flag == work['features'] & flag:
     90                     await results_queue.put(True)
     91                 else:
     92                     await results_queue.put(False)
     93             else:
     94                 await results_queue.put(None)
     95         except Exception as e:
     96             await results_queue.put(None)
     97 
     98 
     99 @log_exceptions
    100 async def node_flag_stats(opt_flag: LnFeatures, presync: False):
    101     """Determines statistics for feature advertisements by nodes on the Lighting
    102     network by evaluation of the public graph.
    103 
    104     opt_flag: The optional-flag for a feature.
    105     presync: Sync the graph. Can take a long time and depends on the quality
    106         of the peers. Better to use presynced graph from regular wallet use for
    107         now.
    108     """
    109     try:
    110         await wallet.lnworker.channel_db.data_loaded.wait()
    111 
    112         # optionally presync graph (not relyable)
    113         if presync:
    114             network.start_gossip()
    115 
    116             # wait for the graph to be synchronized
    117             while True:
    118                 await asyncio.sleep(5)
    119 
    120                 # logger.info(wallet.network.lngossip.get_sync_progress_estimate())
    121                 cur, tot, pct = wallet.network.lngossip.get_sync_progress_estimate()
    122                 print(f"graph sync progress {cur}/{tot} ({pct}%) channels")
    123                 if pct >= 100:
    124                     break
    125 
    126         with wallet.lnworker.channel_db.lock:
    127             nodes = wallet.lnworker.channel_db._nodes.copy()
    128 
    129         # check how many nodes advertize opt/req flag in the gossip
    130         n_opt = 0
    131         n_req = 0
    132         print(f"analyzing {len(nodes.keys())} nodes")
    133 
    134         # 1. statistics on graph
    135         req_flag = LnFeatures(opt_flag >> 1)
    136         for n, nv in nodes.items():
    137             features = LnFeatures(nv.features)
    138             if features & opt_flag:
    139                 n_opt += 1
    140             if features & req_flag:
    141                 n_req += 1
    142 
    143         # analyze numbers
    144         print(
    145             f"opt: {n_opt} ({100 * n_opt/len(nodes)}%) "
    146             f"req: {n_req} ({100 * n_req/len(nodes)}%)")
    147 
    148         # 2. compare announced and actual feature set
    149         # put nodes into a work queue
    150         work_queue = asyncio.Queue()
    151         results_queue = asyncio.Queue()
    152 
    153         # fill up work
    154         for n, nv in nodes.items():
    155             addrs = wallet.lnworker.channel_db._addresses[n]
    156             await work_queue.put({'pk': n, 'addrs': addrs, 'features': nv.features})
    157         tasks = [asyncio.create_task(worker(work_queue, results_queue, opt_flag)) for i in range(WORKERS)]
    158         try:
    159             await asyncio.gather(*tasks)
    160         except Exception as e:
    161             print(e)
    162         # analyze results
    163         n_true = 0
    164         n_false = 0
    165         n_tot = 0
    166         while not results_queue.empty():
    167             i = results_queue.get_nowait()
    168             n_tot += 1
    169             if i is True:
    170                 n_true += 1
    171             elif i is False:
    172                 n_false += 1
    173         print(f"feature comparison - equal: {n_true} unequal: {n_false} total:{n_tot}")
    174 
    175     finally:
    176         stopping_fut.set_result(1)
    177 
    178 asyncio.run_coroutine_threadsafe(
    179     node_flag_stats(FLAG, presync=PRESYNC), loop)