Files
pallectrum/electrum/scripts/ln_features.py
SomberNight 7611d4c3b3 scripts: fix "cannot schedule new futures after interpreter shutdown"
- looks like around python3.9, they changed it so that
  if we don't block on the main thread, it starts to shut things down
- polling thread.join() makes Ctrl+C work. kind of.

```
$ ./electrum/scripts/txradar.py 6bde84a981e72573666fcc51c81ec3f8f4a813709bf16451dce3f106a114d392
Exception in run: RuntimeError('cannot schedule new futures after interpreter shutdown')
Traceback (most recent call last):
  File "/home/user/wspace/electrum/electrum/util.py", line 1218, in wrapper
    return await func(*args, **kwargs)
  File "/home/user/wspace/electrum/electrum/interface.py", line 649, in wrapper_func
    return await func(self, *args, **kwargs)
  File "/home/user/wspace/electrum/electrum/interface.py", line 675, in run
    await self.open_session(ssl_context=ssl_context)
  File "/home/user/wspace/electrum/electrum/interface.py", line 872, in open_session
    async with _RSClient(
  File "/home/user/.local/lib/python3.10/site-packages/aiorpcx/rawsocket.py", line 167, in __aenter__
    _transport, protocol = await self.create_connection()
  File "/home/user/wspace/electrum/electrum/interface.py", line 285, in create_connection
    return await super().create_connection()
  File "/home/user/.local/lib/python3.10/site-packages/aiorpcx/rawsocket.py", line 163, in create_connection
    return await connector.create_connection(
  File "/usr/lib/python3.10/asyncio/base_events.py", line 1036, in create_connection
    infos = await self._ensure_resolved(
  File "/usr/lib/python3.10/asyncio/base_events.py", line 1418, in _ensure_resolved
    return await loop.getaddrinfo(host, port, family=family, type=type,
  File "/usr/lib/python3.10/asyncio/base_events.py", line 863, in getaddrinfo
    return await self.run_in_executor(
  File "/usr/lib/python3.10/asyncio/base_events.py", line 821, in run_in_executor
    executor.submit(func, *args), loop=self)
  File "/usr/lib/python3.10/concurrent/futures/thread.py", line 169, in submit
    raise RuntimeError('cannot schedule new futures after '
RuntimeError: cannot schedule new futures after interpreter shutdown
```
2025-07-15 12:00:31 +00:00

180 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
Script to analyze the graph for Lightning features.
https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
"""
import asyncio
import os
import time
from typing import Optional
from aiorpcx import NetAddress
from electrum.logging import get_logger, configure_logging
from electrum.simple_config import SimpleConfig
from electrum import constants, util
from electrum.daemon import Daemon
from electrum.wallet import create_new_wallet
from electrum.util import create_and_start_event_loop, log_exceptions, bfh
from electrum.lnutil import LnFeatures
logger = get_logger(__name__)
# Configuration parameters
IS_TESTNET = False
TIMEOUT = 5 # for Lightning peer connections
WORKERS = 30 # number of workers that concurrently fetch results for feature comparison
NODES_PER_WORKER = 50
VERBOSITY = '' # for debugging set '*', otherwise ''
FLAG = LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT # chose the 'opt' flag
PRESYNC = False # should we sync the graph or take it from an already synced database?
config = SimpleConfig({"testnet": IS_TESTNET, "verbosity": VERBOSITY})
configure_logging(config)
loop, stopping_fut, loop_thread = create_and_start_event_loop()
# avoid race condition when starting network, in debug starting the asyncio loop
# takes some time
time.sleep(2)
if IS_TESTNET:
constants.BitcoinTestnet.set_as_network()
daemon = Daemon(config, listen_jsonrpc=False)
network = daemon.network
assert network.asyncio_loop.is_running()
# create empty wallet
wallet_dir = os.path.dirname(config.get_wallet_path())
wallet_path = os.path.join(wallet_dir, "ln_features_wallet_main")
if not os.path.exists(wallet_path):
create_new_wallet(path=wallet_path, config=config)
# open wallet
wallet = daemon.load_wallet(wallet_path, password=None, upgrade=True)
async def worker(work_queue: asyncio.Queue, results_queue: asyncio.Queue, flag):
"""Connects to a Lightning peer and checks whether the announced feature
from the gossip is equal to the feature in the init message.
Returns None if no connection could be made, True or False otherwise."""
count = 0
while not work_queue.empty():
if count > NODES_PER_WORKER:
return
work = await work_queue.get()
# only check non-onion addresses
addr = None # type: Optional[NetAddress]
for a in work['addrs']: # type: NetAddress
if not str(a.host).endswith(".onion"):
addr = a
if not addr:
await results_queue.put(None)
continue
connect_str = f"{work['pk'].hex()}@{addr}"
print(f"worker connecting to {connect_str}")
try:
peer = await wallet.lnworker.add_peer(connect_str)
res = await util.wait_for2(peer.initialized, TIMEOUT)
if res:
if peer.features & flag == work['features'] & flag:
await results_queue.put(True)
else:
await results_queue.put(False)
else:
await results_queue.put(None)
except Exception as e:
await results_queue.put(None)
@log_exceptions
async def node_flag_stats(opt_flag: LnFeatures, presync: False):
"""Determines statistics for feature advertisements by nodes on the Lighting
network by evaluation of the public graph.
opt_flag: The optional-flag for a feature.
presync: Sync the graph. Can take a long time and depends on the quality
of the peers. Better to use presynced graph from regular wallet use for
now.
"""
try:
await wallet.lnworker.channel_db.data_loaded.wait()
# optionally presync graph (not reliable)
if presync:
network.start_gossip()
# wait for the graph to be synchronized
while True:
await asyncio.sleep(5)
# logger.info(wallet.network.lngossip.get_sync_progress_estimate())
cur, tot, pct = wallet.network.lngossip.get_sync_progress_estimate()
print(f"graph sync progress {cur}/{tot} ({pct}%) channels")
if pct >= 100:
break
with wallet.lnworker.channel_db.lock:
nodes = wallet.lnworker.channel_db._nodes.copy()
# check how many nodes advertise opt/req flag in the gossip
n_opt = 0
n_req = 0
print(f"analyzing {len(nodes.keys())} nodes")
# 1. statistics on graph
req_flag = LnFeatures(opt_flag >> 1)
for n, nv in nodes.items():
features = LnFeatures(nv.features)
if features & opt_flag:
n_opt += 1
if features & req_flag:
n_req += 1
# analyze numbers
print(
f"opt: {n_opt} ({100 * n_opt/len(nodes)}%) "
f"req: {n_req} ({100 * n_req/len(nodes)}%)")
# 2. compare announced and actual feature set
# put nodes into a work queue
work_queue = asyncio.Queue()
results_queue = asyncio.Queue()
# fill up work
for n, nv in nodes.items():
addrs = wallet.lnworker.channel_db._addresses[n]
await work_queue.put({'pk': n, 'addrs': addrs, 'features': nv.features})
tasks = [asyncio.create_task(worker(work_queue, results_queue, opt_flag)) for i in range(WORKERS)]
try:
await asyncio.gather(*tasks)
except Exception as e:
print(e)
# analyze results
n_true = 0
n_false = 0
n_tot = 0
while not results_queue.empty():
i = results_queue.get_nowait()
n_tot += 1
if i is True:
n_true += 1
elif i is False:
n_false += 1
print(f"feature comparison - equal: {n_true} unequal: {n_false} total:{n_tot}")
finally:
stopping_fut.set_result(1)
asyncio.run_coroutine_threadsafe(
node_flag_stats(FLAG, presync=PRESYNC), loop)
while loop_thread.is_alive():
loop_thread.join(1)