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)