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)