diff --git a/electrum/commands.py b/electrum/commands.py index 2d55c4bb0..191bd123c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1204,12 +1204,13 @@ class Commands: @command('n') async def clear_ln_blacklist(self): if self.network.path_finder: - self.network.path_finder.liquidity_hints.clear_blacklist() + self.network.path_finder.clear_blacklist() @command('n') async def reset_liquidity_hints(self): if self.network.path_finder: self.network.path_finder.liquidity_hints.reset_liquidity_hints() + self.network.path_finder.clear_blacklist() @command('wnl') async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None): diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index 167134b99..b2872ff3b 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -27,6 +27,7 @@ import queue from collections import defaultdict from typing import Sequence, Tuple, Optional, Dict, TYPE_CHECKING, Set import time +import threading from threading import RLock import attr from math import inf @@ -42,7 +43,6 @@ if TYPE_CHECKING: DEFAULT_PENALTY_BASE_MSAT = 500 # how much base fee we apply for unknown sending capability of a channel DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH = 100 # how much relative fee we apply for unknown sending capability of a channel -BLACKLIST_DURATION = 3600 # how long (in seconds) a channel remains blacklisted HINT_DURATION = 3600 # how long (in seconds) a liquidity hint remains valid @@ -173,7 +173,7 @@ def is_fee_sane(fee_msat: int, *, payment_amount_msat: int) -> bool: class LiquidityHint: """Encodes the amounts that can and cannot be sent over the direction of a - channel and whether the channel is blacklisted. + channel. A LiquidityHint is the value of a dict, which is keyed to node ids and the channel. @@ -184,7 +184,6 @@ class LiquidityHint: self._cannot_send_forward = None self._can_send_backward = None self._cannot_send_backward = None - self.blacklist_timestamp = 0 self.hint_timestamp = 0 self._inflight_htlcs_forward = 0 self._inflight_htlcs_backward = 0 @@ -297,10 +296,8 @@ class LiquidityHint: self._inflight_htlcs_backward = max(0, self._inflight_htlcs_forward - 1) def __repr__(self): - is_blacklisted = False if not self.blacklist_timestamp else int(time.time()) - self.blacklist_timestamp < BLACKLIST_DURATION return f"forward: can send: {self._can_send_forward} msat, cannot send: {self._cannot_send_forward} msat, htlcs: {self._inflight_htlcs_forward}\n" \ - f"backward: can send: {self._can_send_backward} msat, cannot send: {self._cannot_send_backward} msat, htlcs: {self._inflight_htlcs_backward}\n" \ - f"blacklisted: {is_blacklisted}" + f"backward: can send: {self._can_send_backward} msat, cannot send: {self._cannot_send_backward} msat, htlcs: {self._inflight_htlcs_backward}\n" class LiquidityHintMgr: @@ -382,22 +379,6 @@ class LiquidityHintMgr: inflight_htlc_fee = num_inflight_htlcs * success_fee return success_fee + inflight_htlc_fee - @with_lock - def add_to_blacklist(self, channel_id: ShortChannelID): - hint = self.get_hint(channel_id) - now = int(time.time()) - hint.blacklist_timestamp = now - - @with_lock - def get_blacklist(self) -> Set[ShortChannelID]: - now = int(time.time()) - return set(k for k, v in self._liquidity_hints.items() if now - v.blacklist_timestamp < BLACKLIST_DURATION) - - @with_lock - def clear_blacklist(self): - for k, v in self._liquidity_hints.items(): - v.blacklist_timestamp = 0 - @with_lock def reset_liquidity_hints(self): for k, v in self._liquidity_hints.items(): @@ -417,6 +398,33 @@ class LNPathFinder(Logger): Logger.__init__(self) self.channel_db = channel_db self.liquidity_hints = LiquidityHintMgr() + self._edge_blacklist = dict() # type: Dict[ShortChannelID, int] # scid -> expiration + self._blacklist_lock = threading.Lock() + + def _is_edge_blacklisted(self, short_channel_id: ShortChannelID, *, now: int) -> bool: + blacklist_expiration = self._edge_blacklist.get(short_channel_id) + if blacklist_expiration is None: + return False + if blacklist_expiration < now: + return False + return True + + def add_edge_to_blacklist( + self, + short_channel_id: ShortChannelID, + *, + now: int = None, + duration: int = 3600, # seconds + ) -> None: + if now is None: + now = int(time.time()) + with self._blacklist_lock: + blacklist_expiration = self._edge_blacklist.get(short_channel_id, 0) + self._edge_blacklist[short_channel_id] = max(blacklist_expiration, now + duration) + + def clear_blacklist(self): + with self._blacklist_lock: + self._edge_blacklist = dict() def update_liquidity_hints( self, @@ -450,7 +458,7 @@ class LNPathFinder(Logger): def _edge_cost( self, *, - short_channel_id: bytes, + short_channel_id: ShortChannelID, start_node: bytes, end_node: bytes, payment_amt_msat: int, @@ -458,10 +466,13 @@ class LNPathFinder(Logger): is_mine=False, my_channels: Dict[ShortChannelID, 'Channel'] = None, private_route_edges: Dict[ShortChannelID, RouteEdge] = None, + now: int, # unix ts ) -> Tuple[float, int]: """Heuristic cost (distance metric) of going through a channel. Returns (heuristic_cost, fee_for_edge_msat). """ + if self._is_edge_blacklisted(short_channel_id, now=now): + return float('inf'), 0 if private_route_edges is None: private_route_edges = {} channel_info = self.channel_db.get_channel_info( @@ -537,12 +548,12 @@ class LNPathFinder(Logger): # run Dijkstra # The search is run in the REVERSE direction, from nodeB to nodeA, # to properly calculate compound routing fees. - blacklist = self.liquidity_hints.get_blacklist() distance_from_start = defaultdict(lambda: float('inf')) distance_from_start[nodeB] = 0 previous_hops = {} # type: Dict[bytes, PathEdge] nodes_to_explore = queue.PriorityQueue() nodes_to_explore.put((0, invoice_amount_msat, nodeB)) # order of fields (in tuple) matters! + now = int(time.time()) # main loop of search while nodes_to_explore.qsize() > 0: @@ -569,7 +580,7 @@ class LNPathFinder(Logger): for edge_channel_id in channels_for_endnode: assert isinstance(edge_channel_id, bytes) - if blacklist and edge_channel_id in blacklist: + if self._is_edge_blacklisted(edge_channel_id, now=now): continue channel_info = self.channel_db.get_channel_info( edge_channel_id, my_channels=my_sending_channels, private_route_edges=private_route_edges) @@ -589,7 +600,9 @@ class LNPathFinder(Logger): ignore_costs=(edge_startnode == nodeA), is_mine=is_mine, my_channels=my_sending_channels, - private_route_edges=private_route_edges) + private_route_edges=private_route_edges, + now=now, + ) alt_dist_to_neighbour = distance_from_start[edge_endnode] + edge_cost if alt_dist_to_neighbour < distance_from_start[edge_startnode]: distance_from_start[edge_startnode] = alt_dist_to_neighbour diff --git a/electrum/lnworker.py b/electrum/lnworker.py index e411be0b6..48830d160 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1604,7 +1604,7 @@ class LNWallet(LNWorker): else: blacklist = True if blacklist: - self.network.path_finder.liquidity_hints.add_to_blacklist(failing_channel) + self.network.path_finder.add_edge_to_blacklist(short_channel_id=failing_channel) def _handle_chanupd_from_failed_htlc(self, payload, *, route, sender_idx) -> Tuple[bool, bool]: blacklist = False