URI: 
       tln_features.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tln_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)