tlnworker: introduce PaymentAttemptLog NamedTuple - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit b99add59c336ef039ae4905287cedc9bf8ef8733 DIR parent 24ebc77d76cb342502a8443c4edc2a1feff8f067 HTML Author: SomberNight <somber.night@protonmail.com> Date: Tue, 10 Dec 2019 03:17:57 +0100 lnworker: introduce PaymentAttemptLog NamedTuple Diffstat: M electrum/gui/qt/invoice_list.py | 39 ++++++++++++++++++++----------- M electrum/lnaddr.py | 4 ++-- M electrum/lnonion.py | 4 ++-- M electrum/lnpeer.py | 4 ++-- M electrum/lnrouter.py | 7 +++++-- M electrum/lnutil.py | 18 +++++++++++++++++- M electrum/lnworker.py | 39 ++++++++++++++++++------------- 7 files changed, 76 insertions(+), 39 deletions(-) --- DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py t@@ -24,6 +24,7 @@ # SOFTWARE. from enum import IntEnum +from typing import Sequence from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtGui import QStandardItemModel, QStandardItem t@@ -34,7 +35,7 @@ from electrum.i18n import _ from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT from electrum.util import get_request_status from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN -from electrum.lnutil import format_short_channel_id +from electrum.lnutil import PaymentAttemptLog from .util import (MyTreeView, read_QIcon, import_meta_gui, export_meta_gui, pr_icons) t@@ -174,24 +175,34 @@ class InvoiceList(MyTreeView): menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key)) menu.exec_(self.viewport().mapToGlobal(position)) - def show_log(self, key, log): + def show_log(self, key, log: Sequence[PaymentAttemptLog]): d = WindowModalDialog(self, _("Payment log")) + d.setMinimumWidth(800) vbox = QVBoxLayout(d) log_w = QTreeWidget() log_w.setHeaderLabels([_('Route'), _('Channel ID'), _('Message'), _('Blacklist')]) - for i, (route, success, failure_log) in enumerate(log): - route_str = '%d'%len(route) - if not success: - sender_idx, failure_msg, blacklist = failure_log - short_channel_id = route[sender_idx+1].short_channel_id - data = failure_msg.data - message = repr(failure_msg.code) + for payment_attempt_log in log: + if not payment_attempt_log.exception: + route = payment_attempt_log.route + route_str = '%d'%len(route) + if not payment_attempt_log.success: + sender_idx = payment_attempt_log.failure_details.sender_idx + failure_msg = payment_attempt_log.failure_details.failure_msg + blacklist_msg = str(payment_attempt_log.failure_details.is_blacklisted) + short_channel_id = route[sender_idx+1].short_channel_id + data = failure_msg.data + message = repr(failure_msg.code) + else: + short_channel_id = route[-1].short_channel_id + message = _('Success') + blacklist_msg = str(False) + chan_str = str(short_channel_id) else: - short_channel_id = route[-1].short_channel_id - message = _('Success') - blacklist = False - chan_str = format_short_channel_id(short_channel_id) - x = QTreeWidgetItem([route_str, chan_str, message, repr(blacklist)]) + route_str = 'None' + chan_str = 'N/A' + message = str(payment_attempt_log.exception) + blacklist_msg = 'N/A' + x = QTreeWidgetItem([route_str, chan_str, message, blacklist_msg]) log_w.addTopLevelItem(x) vbox.addWidget(log_w) vbox.addLayout(Buttons(CloseButton(d))) DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py t@@ -237,11 +237,11 @@ def lnencode(addr, privkey): return bech32_encode(hrp, bitarray_to_u5(data)) class LnAddr(object): - def __init__(self, paymenthash=None, amount=None, currency=None, tags=None, date=None): + def __init__(self, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None): self.date = int(time.time()) if not date else int(date) self.tags = [] if not tags else tags self.unknown_tags = [] - self.paymenthash=paymenthash + self.paymenthash = paymenthash self.signature = None self.pubkey = None self.currency = constants.net.SEGWIT_HRP if currency is None else currency DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py t@@ -36,7 +36,7 @@ from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH, NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID) if TYPE_CHECKING: - from .lnrouter import RouteEdge + from .lnrouter import LNPaymentRoute HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 t@@ -188,7 +188,7 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, hmac=next_hmac) -def calc_hops_data_for_payment(route: List['RouteEdge'], amount_msat: int, final_cltv: int) \ +def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_cltv: int) \ -> Tuple[List[OnionHopsDataSingle], int, int]: """Returns the hops_data to be used for constructing an onion packet, and the amount_msat and cltv to be used on our immediate channel. DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -50,7 +50,7 @@ from .lnutil import ln_dummy_address if TYPE_CHECKING: from .lnworker import LNWorker, LNGossip, LNWallet - from .lnrouter import RouteEdge + from .lnrouter import RouteEdge, LNPaymentRoute from .transaction import PartialTransaction t@@ -1126,7 +1126,7 @@ class Peer(Logger): while chan.get_latest_ctn(LOCAL) <= ctn: await self._local_changed_events[chan.channel_id].wait() - async def pay(self, route: List['RouteEdge'], chan: Channel, amount_msat: int, + async def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int, payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc: if chan.get_state() != channel_states.OPEN: raise PaymentFailure('Channel not open') DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py t@@ -87,7 +87,10 @@ class RouteEdge(NamedTuple): return True -def is_route_sane_to_use(route: List[RouteEdge], invoice_amount_msat: int, min_final_cltv_expiry: int) -> bool: +LNPaymentRoute = Sequence[RouteEdge] + + +def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_final_cltv_expiry: int) -> bool: """Run some sanity checks on the whole route, before attempting to use it. called when we are paying; so e.g. lower cltv is better """ t@@ -238,7 +241,7 @@ class LNPathFinder(Logger): edge_startnode = edge_endnode return path - def create_route_from_path(self, path, from_node_id: bytes) -> List[RouteEdge]: + def create_route_from_path(self, path, from_node_id: bytes) -> LNPaymentRoute: assert isinstance(from_node_id, bytes) if path is None: raise Exception('cannot create route from None path') DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -5,7 +5,7 @@ from enum import IntFlag, IntEnum import json from collections import namedtuple -from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set +from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence import re from aiorpcx import NetAddress t@@ -24,6 +24,8 @@ from .keystore import BIP32_KeyStore if TYPE_CHECKING: from .lnchannel import Channel + from .lnrouter import LNPaymentRoute + from .lnonion import OnionRoutingFailureMessage HTLC_TIMEOUT_WEIGHT = 663 t@@ -116,6 +118,20 @@ class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])): return "{}:{}".format(self.txid, self.output_index) +class PaymentAttemptFailureDetails(NamedTuple): + sender_idx: int + failure_msg: 'OnionRoutingFailureMessage' + is_blacklisted: bool + + +class PaymentAttemptLog(NamedTuple): + success: bool + route: Optional['LNPaymentRoute'] = None + preimage: Optional[bytes] = None + failure_details: Optional[PaymentAttemptFailureDetails] = None + exception: Optional[Exception] = None + + class LightningError(Exception): pass class LightningPeerConnectionClosed(LightningError): pass class UnableToDeriveSecret(LightningError): pass DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -51,13 +51,13 @@ from .lnutil import (Outpoint, LNPeerAddr, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id, - ShortChannelID) + ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails) from .lnutil import ln_dummy_address from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .lnonion import OnionFailureCode from .lnmsg import decode_msg from .i18n import _ -from .lnrouter import RouteEdge, is_route_sane_to_use +from .lnrouter import RouteEdge, LNPaymentRoute, is_route_sane_to_use from .address_synchronizer import TX_HEIGHT_LOCAL from . import lnsweep from .lnwatcher import LNWatcher t@@ -118,7 +118,9 @@ class PaymentInfo(NamedTuple): class NoPathFound(PaymentFailure): - pass + def __str__(self): + return _('No path found') + class LNWorker(Logger): t@@ -345,7 +347,7 @@ class LNWallet(LNWorker): self.preimages = self.storage.get('lightning_preimages', {}) # RHASH -> preimage self.sweep_address = wallet.get_receiving_address() self.lock = threading.RLock() - self.logs = defaultdict(list) + self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # note: accessing channels (besides simple lookup) needs self.lock! self.channels = {} # type: Dict[bytes, Channel] t@@ -891,7 +893,7 @@ class LNWallet(LNWorker): return chan @log_exceptions - async def _pay(self, invoice, amount_sat=None, attempts=1): + async def _pay(self, invoice, amount_sat=None, attempts=1) -> bool: lnaddr = self._check_invoice(invoice, amount_sat) payment_hash = lnaddr.paymenthash key = payment_hash.hex() t@@ -905,23 +907,23 @@ class LNWallet(LNWorker): self.save_payment_info(info) self.wallet.set_label(key, lnaddr.get_description()) log = self.logs[key] + success = False for i in range(attempts): try: route = await self._create_route_from_invoice(decoded_invoice=lnaddr) - except NoPathFound: - success = False + except NoPathFound as e: + log.append(PaymentAttemptLog(success=False, exception=e)) break self.network.trigger_callback('invoice_status', key, PR_INFLIGHT) - success, preimage, failure_log = await self._pay_to_route(route, lnaddr) + payment_attempt_log = await self._pay_to_route(route, lnaddr) + log.append(payment_attempt_log) + success = payment_attempt_log.success if success: - log.append((route, True, preimage)) break - else: - log.append((route, False, failure_log)) self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED) return success - async def _pay_to_route(self, route, lnaddr): + async def _pay_to_route(self, route: LNPaymentRoute, lnaddr: LnAddr) -> PaymentAttemptLog: short_channel_id = route[0].short_channel_id chan = self.get_channel_by_short_id(short_channel_id) if not chan: t@@ -948,8 +950,13 @@ class LNWallet(LNWorker): self.logger.info("payment destination reported error") else: self.network.path_finder.add_to_blacklist(short_chan_id) - failure_log = (sender_idx, failure_msg, blacklist) - return success, preimage, failure_log + failure_log = PaymentAttemptFailureDetails(sender_idx=sender_idx, + failure_msg=failure_msg, + is_blacklisted=blacklist) + return PaymentAttemptLog(route=route, + success=success, + preimage=preimage, + failure_details=failure_log) def handle_error_code_from_failed_htlc(self, failure_msg, sender_idx, route, peer): code, data = failure_msg.code, failure_msg.data t@@ -1011,11 +1018,11 @@ class LNWallet(LNWorker): f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}")) return addr - async def _create_route_from_invoice(self, decoded_invoice) -> List[RouteEdge]: + async def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute: amount_msat = int(decoded_invoice.amount * COIN * 1000) invoice_pubkey = decoded_invoice.pubkey.serialize() # use 'r' field from invoice - route = None # type: Optional[List[RouteEdge]] + route = None # type: Optional[LNPaymentRoute] # only want 'r' tags r_tags = list(filter(lambda x: x[0] == 'r', decoded_invoice.tags)) # strip the tag type, it's implicitly 'r' now