tlnworker._pay: allow specifying path as argument - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 7153e753d1829ccdf1f0ee6821082c1f13ba0f21 DIR parent 63b18dc30f54d7a42df923475f79cb338a706c2a HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 6 May 2020 10:56:33 +0200 lnworker._pay: allow specifying path as argument not exposed to CLI/etc yet but will be used in tests soon Diffstat: M electrum/channel_db.py | 15 ++++++++++++++- M electrum/lnpeer.py | 3 ++- M electrum/lnrouter.py | 6 ++++++ M electrum/lnworker.py | 39 ++++++++++++++++++++++--------- 4 files changed, 50 insertions(+), 13 deletions(-) --- DIR diff --git a/electrum/channel_db.py b/electrum/channel_db.py t@@ -119,7 +119,7 @@ class Policy(NamedTuple): return ShortChannelID.normalize(self.key[0:8]) @property - def start_node(self): + def start_node(self) -> bytes: return self.key[8:] t@@ -732,5 +732,18 @@ class ChannelDB(SqlDB): relevant_channels.add(chan.short_channel_id) return relevant_channels + def get_endnodes_for_chan(self, short_channel_id: ShortChannelID, *, + my_channels: Dict[ShortChannelID, 'Channel'] = None) -> Optional[Tuple[bytes, bytes]]: + channel_info = self.get_channel_info(short_channel_id) + if channel_info is not None: # publicly announced channel + return channel_info.node1_id, channel_info.node2_id + # check if it's one of our own channels + if not my_channels: + return + chan = my_channels.get(short_channel_id) # type: Optional[Channel] + if not chan: + return + return chan.get_local_pubkey(), chan.node_id + def get_node_info_for_node_id(self, node_id: bytes) -> Optional['NodeInfo']: return self._nodes.get(node_id) DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -108,7 +108,8 @@ class Peer(Logger): return assert channel_id chan = self.get_channel_by_id(channel_id) - assert chan + if not chan: + raise Exception(f"channel {channel_id.hex()} not found for peer {self.pubkey.hex()}") chan.hm.store_local_update_raw_msg(raw_msg, is_commitment_signed=is_commitment_signed) if is_commitment_signed: # saving now, to ensure replaying updates works (in case of channel reestablishment) DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py t@@ -45,6 +45,9 @@ class NoChannelPolicy(Exception): super().__init__(f'cannot find channel policy for short_channel_id: {short_channel_id}') +class LNPathInconsistent(Exception): pass + + def fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_proportional_millionths: int) -> int: return fee_base_msat \ + (forwarded_amount_msat * fee_proportional_millionths // 1_000_000) t@@ -286,6 +289,9 @@ class LNPathFinder(Logger): for edge in path: node_id = edge.node_id short_channel_id = edge.short_channel_id + _endnodes = self.channel_db.get_endnodes_for_chan(short_channel_id, my_channels=my_channels) + if _endnodes and sorted(_endnodes) != sorted([prev_node_id, node_id]): + raise LNPathInconsistent("edges do not chain together") channel_policy = self.channel_db.get_policy_for_node(short_channel_id=short_channel_id, node_id=prev_node_id, my_channels=my_channels) DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -60,7 +60,8 @@ from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket from .lnmsg import decode_msg from .i18n import _ -from .lnrouter import RouteEdge, LNPaymentRoute, is_route_sane_to_use +from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use, + NoChannelPolicy, LNPathInconsistent) from .address_synchronizer import TX_HEIGHT_LOCAL from . import lnsweep from .lnwatcher import LNWalletWatcher t@@ -815,7 +816,8 @@ class LNWallet(LNWorker): return chan async def _pay(self, invoice: str, amount_sat: int = None, *, - attempts: int = 1) -> Tuple[bool, List[PaymentAttemptLog]]: + attempts: int = 1, + full_path: LNPaymentPath = None) -> Tuple[bool, List[PaymentAttemptLog]]: lnaddr = self._check_invoice(invoice, amount_sat) payment_hash = lnaddr.paymenthash key = payment_hash.hex() t@@ -837,7 +839,7 @@ class LNWallet(LNWorker): # graph updates might occur during the computation self.set_invoice_status(key, PR_ROUTING) util.trigger_callback('invoice_status', key) - route = await run_in_thread(self._create_route_from_invoice, lnaddr) + route = await run_in_thread(partial(self._create_route_from_invoice, lnaddr, full_path=full_path)) self.set_invoice_status(key, PR_INFLIGHT) util.trigger_callback('invoice_status', key) payment_attempt_log = await self._pay_to_route(route, lnaddr) t@@ -974,7 +976,8 @@ class LNWallet(LNWorker): return addr @profiler - def _create_route_from_invoice(self, decoded_invoice: 'LnAddr') -> LNPaymentRoute: + def _create_route_from_invoice(self, decoded_invoice: 'LnAddr', + *, full_path: LNPaymentPath = None) -> LNPaymentRoute: amount_msat = int(decoded_invoice.amount * COIN * 1000) invoice_pubkey = decoded_invoice.pubkey.serialize() # use 'r' field from invoice t@@ -995,12 +998,22 @@ class LNWallet(LNWorker): if len(private_route) > NUM_MAX_EDGES_IN_PAYMENT_PATH: continue border_node_pubkey = private_route[0][0] - path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, border_node_pubkey, amount_msat, - my_channels=scid_to_my_channels) + if full_path: + # user pre-selected path. check that end of given path coincides with private_route: + if [edge.short_channel_id for edge in full_path[-len(private_route):]] != [edge[1] for edge in private_route]: + continue + path = full_path[:-len(private_route)] + else: + # find path now on public graph, to border node + path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, border_node_pubkey, amount_msat, + my_channels=scid_to_my_channels) if not path: continue - route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey, - my_channels=scid_to_my_channels) + try: + route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey, + my_channels=scid_to_my_channels) + except NoChannelPolicy: + continue # we need to shift the node pubkey by one towards the destination: private_route_nodes = [edge[0] for edge in private_route][1:] + [invoice_pubkey] private_route_rest = [edge[1:] for edge in private_route] t@@ -1033,8 +1046,11 @@ class LNWallet(LNWorker): break # if could not find route using any hint; try without hint now if route is None: - path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat, - my_channels=scid_to_my_channels) + if full_path: # user pre-selected path + path = full_path + else: # find path now + path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat, + my_channels=scid_to_my_channels) if not path: raise NoPathFound() route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey, t@@ -1043,7 +1059,8 @@ class LNWallet(LNWorker): self.logger.info(f"rejecting insane route {route}") raise NoPathFound() assert len(route) > 0 - assert route[-1].node_id == invoice_pubkey + if route[-1].node_id != invoice_pubkey: + raise LNPathInconsistent("last node_id != invoice pubkey") # add features from invoice invoice_features = decoded_invoice.get_tag('9') or 0 route[-1].node_features |= invoice_features