From b4e93e7e3847901a01719c03524c6223633e9cbe Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 2 Jun 2025 18:06:29 +0200 Subject: [PATCH] fix: prevent lnrater from blocking if no good peers the while loop in `suggest_node_channel_open()` of lnrater would not break if there are no "good" peers available available. As a result the gui blocks and electrum has to be killed. This can happen for example on signet. This removes the tested pk from the list of candidates so each candidate gets tested only once. --- electrum/gui/qt/new_channel_dialog.py | 3 ++- electrum/lnrater.py | 32 ++++++++++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index db8fd7809..44bce8a54 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -93,7 +93,8 @@ class NewChannelDialog(WindowModalDialog): if not nodeid: self.remote_nodeid.setText("") self.remote_nodeid.setPlaceholderText( - "Please wait until the graph is synchronized to 30%, and then try again.") + _("Couldn't find suitable peer yet, try again later.") + ) else: self.remote_nodeid.setText(nodeid) self.remote_nodeid.repaint() # macOS hack for #6269 diff --git a/electrum/lnrater.py b/electrum/lnrater.py index 0ff6a3fd5..fbce9f398 100644 --- a/electrum/lnrater.py +++ b/electrum/lnrater.py @@ -18,9 +18,9 @@ from .logging import Logger from .util import profiler, get_running_loop from .lnrouter import fee_for_edge_msat from .lnutil import LnFeatures, ln_compare_features, IncompatibleLightningFeatures +from .network import Network if TYPE_CHECKING: - from .network import Network from .channel_db import Policy, NodeInfo from .lnchannel import ShortChannelID from .lnworker import LNWallet @@ -86,16 +86,12 @@ class LNRater(Logger): self._last_progress_percent = 0 def maybe_analyze_graph(self): - loop = self.network.asyncio_loop - fut = asyncio.run_coroutine_threadsafe(self._maybe_analyze_graph(), loop) - fut.result() + Network.run_from_another_thread(self._maybe_analyze_graph()) def analyze_graph(self): """Forces a graph analysis, e.g., due to external triggers like the graph info reaching 50%.""" - loop = self.network.asyncio_loop - fut = asyncio.run_coroutine_threadsafe(self._analyze_graph(), loop) - fut.result() + Network.run_from_another_thread(self._analyze_graph()) async def _maybe_analyze_graph(self): """Analyzes the graph when in early sync stage (>30%) or when caching @@ -197,7 +193,7 @@ class LNRater(Logger): self.logger.debug(pformat(channel_policies)) continue - self.logger.info(f"node statistics done, calculated statistics" + self.logger.info(f"node statistics done, calculated statistics " f"for {len(self._node_stats)} nodes") def _rate_nodes(self): @@ -234,15 +230,18 @@ class LNRater(Logger): self._node_ratings[n] = weighted_sum(heuristics, heuristics_weights) - def suggest_node_channel_open(self) -> Tuple[bytes, NodeStats]: - node_keys = list(self._node_stats.keys()) - node_ratings = list(self._node_ratings.values()) + @profiler + def suggest_node_channel_open(self) -> Optional[bytes]: + node_stats = self._node_stats.copy() + node_ratings = self._node_ratings.copy() channel_peers = self.lnworker.channel_peers() node_info: Optional["NodeInfo"] = None - while True: + while node_stats: # randomly pick nodes weighted by node_rating - pk = choices(node_keys, weights=node_ratings, k=1)[0] + pk = choices(list(node_stats.keys()), weights=list(node_ratings.values()), k=1)[0] + # remove the pk so it doesn't get tried again + node_stats.pop(pk); node_ratings.pop(pk) # node should have compatible features node_info = self.network.channel_db.get_node_infos().get(pk, None) peer_features = LnFeatures(node_info.features) @@ -265,13 +264,16 @@ class LNRater(Logger): else: continue break + else: + self.logger.info(f"no suitable channel peer found") + return None alias = node_info.alias if node_info else 'unknown node alias' self.logger.info( f"node rating for {alias}:\n" f"{pformat(self._node_stats[pk])} (score {self._node_ratings[pk]})") - return pk, self._node_stats[pk] + return pk def suggest_peer(self) -> Optional[bytes]: """Suggests a LN node to open a channel with. @@ -279,6 +281,6 @@ class LNRater(Logger): """ self.maybe_analyze_graph() if self._node_ratings: - return self.suggest_node_channel_open()[0] + return self.suggest_node_channel_open() else: return None