Browse Source

lnworker: introduce PaymentAttemptLog NamedTuple

hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
b99add59c3
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 39
      electrum/gui/qt/invoice_list.py
  2. 4
      electrum/lnaddr.py
  3. 4
      electrum/lnonion.py
  4. 4
      electrum/lnpeer.py
  5. 7
      electrum/lnrouter.py
  6. 18
      electrum/lnutil.py
  7. 39
      electrum/lnworker.py

39
electrum/gui/qt/invoice_list.py

@ -24,6 +24,7 @@
# SOFTWARE. # SOFTWARE.
from enum import IntEnum from enum import IntEnum
from typing import Sequence
from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtGui import QStandardItemModel, QStandardItem
@ -34,7 +35,7 @@ from electrum.i18n import _
from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT
from electrum.util import get_request_status from electrum.util import get_request_status
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN 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, from .util import (MyTreeView, read_QIcon,
import_meta_gui, export_meta_gui, pr_icons) import_meta_gui, export_meta_gui, pr_icons)
@ -174,24 +175,34 @@ class InvoiceList(MyTreeView):
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key)) menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
menu.exec_(self.viewport().mapToGlobal(position)) 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 = WindowModalDialog(self, _("Payment log"))
d.setMinimumWidth(800)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
log_w = QTreeWidget() log_w = QTreeWidget()
log_w.setHeaderLabels([_('Route'), _('Channel ID'), _('Message'), _('Blacklist')]) log_w.setHeaderLabels([_('Route'), _('Channel ID'), _('Message'), _('Blacklist')])
for i, (route, success, failure_log) in enumerate(log): for payment_attempt_log in log:
route_str = '%d'%len(route) if not payment_attempt_log.exception:
if not success: route = payment_attempt_log.route
sender_idx, failure_msg, blacklist = failure_log route_str = '%d'%len(route)
short_channel_id = route[sender_idx+1].short_channel_id if not payment_attempt_log.success:
data = failure_msg.data sender_idx = payment_attempt_log.failure_details.sender_idx
message = repr(failure_msg.code) 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: else:
short_channel_id = route[-1].short_channel_id route_str = 'None'
message = _('Success') chan_str = 'N/A'
blacklist = False message = str(payment_attempt_log.exception)
chan_str = format_short_channel_id(short_channel_id) blacklist_msg = 'N/A'
x = QTreeWidgetItem([route_str, chan_str, message, repr(blacklist)]) x = QTreeWidgetItem([route_str, chan_str, message, blacklist_msg])
log_w.addTopLevelItem(x) log_w.addTopLevelItem(x)
vbox.addWidget(log_w) vbox.addWidget(log_w)
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))

4
electrum/lnaddr.py

@ -237,11 +237,11 @@ def lnencode(addr, privkey):
return bech32_encode(hrp, bitarray_to_u5(data)) return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr(object): 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.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags self.tags = [] if not tags else tags
self.unknown_tags = [] self.unknown_tags = []
self.paymenthash=paymenthash self.paymenthash = paymenthash
self.signature = None self.signature = None
self.pubkey = None self.pubkey = None
self.currency = constants.net.SEGWIT_HRP if currency is None else currency self.currency = constants.net.SEGWIT_HRP if currency is None else currency

4
electrum/lnonion.py

@ -36,7 +36,7 @@ from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID) NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID)
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnrouter import RouteEdge from .lnrouter import LNPaymentRoute
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
@ -188,7 +188,7 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
hmac=next_hmac) 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]: -> Tuple[List[OnionHopsDataSingle], int, int]:
"""Returns the hops_data to be used for constructing an onion packet, """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. and the amount_msat and cltv to be used on our immediate channel.

4
electrum/lnpeer.py

@ -50,7 +50,7 @@ from .lnutil import ln_dummy_address
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnworker import LNWorker, LNGossip, LNWallet from .lnworker import LNWorker, LNGossip, LNWallet
from .lnrouter import RouteEdge from .lnrouter import RouteEdge, LNPaymentRoute
from .transaction import PartialTransaction from .transaction import PartialTransaction
@ -1126,7 +1126,7 @@ class Peer(Logger):
while chan.get_latest_ctn(LOCAL) <= ctn: while chan.get_latest_ctn(LOCAL) <= ctn:
await self._local_changed_events[chan.channel_id].wait() 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: payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
if chan.get_state() != channel_states.OPEN: if chan.get_state() != channel_states.OPEN:
raise PaymentFailure('Channel not open') raise PaymentFailure('Channel not open')

7
electrum/lnrouter.py

@ -87,7 +87,10 @@ class RouteEdge(NamedTuple):
return True 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. """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 called when we are paying; so e.g. lower cltv is better
""" """
@ -238,7 +241,7 @@ class LNPathFinder(Logger):
edge_startnode = edge_endnode edge_startnode = edge_endnode
return path 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) assert isinstance(from_node_id, bytes)
if path is None: if path is None:
raise Exception('cannot create route from None path') raise Exception('cannot create route from None path')

18
electrum/lnutil.py

@ -5,7 +5,7 @@
from enum import IntFlag, IntEnum from enum import IntFlag, IntEnum
import json import json
from collections import namedtuple 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 import re
from aiorpcx import NetAddress from aiorpcx import NetAddress
@ -24,6 +24,8 @@ from .keystore import BIP32_KeyStore
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnchannel import Channel from .lnchannel import Channel
from .lnrouter import LNPaymentRoute
from .lnonion import OnionRoutingFailureMessage
HTLC_TIMEOUT_WEIGHT = 663 HTLC_TIMEOUT_WEIGHT = 663
@ -116,6 +118,20 @@ class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
return "{}:{}".format(self.txid, self.output_index) 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 LightningError(Exception): pass
class LightningPeerConnectionClosed(LightningError): pass class LightningPeerConnectionClosed(LightningError): pass
class UnableToDeriveSecret(LightningError): pass class UnableToDeriveSecret(LightningError): pass

39
electrum/lnworker.py

@ -51,13 +51,13 @@ from .lnutil import (Outpoint, LNPeerAddr,
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id, UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id,
ShortChannelID) ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails)
from .lnutil import ln_dummy_address from .lnutil import ln_dummy_address
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode from .lnonion import OnionFailureCode
from .lnmsg import decode_msg from .lnmsg import decode_msg
from .i18n import _ 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 .address_synchronizer import TX_HEIGHT_LOCAL
from . import lnsweep from . import lnsweep
from .lnwatcher import LNWatcher from .lnwatcher import LNWatcher
@ -118,7 +118,9 @@ class PaymentInfo(NamedTuple):
class NoPathFound(PaymentFailure): class NoPathFound(PaymentFailure):
pass def __str__(self):
return _('No path found')
class LNWorker(Logger): class LNWorker(Logger):
@ -345,7 +347,7 @@ class LNWallet(LNWorker):
self.preimages = self.storage.get('lightning_preimages', {}) # RHASH -> preimage self.preimages = self.storage.get('lightning_preimages', {}) # RHASH -> preimage
self.sweep_address = wallet.get_receiving_address() self.sweep_address = wallet.get_receiving_address()
self.lock = threading.RLock() 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! # note: accessing channels (besides simple lookup) needs self.lock!
self.channels = {} # type: Dict[bytes, Channel] self.channels = {} # type: Dict[bytes, Channel]
@ -891,7 +893,7 @@ class LNWallet(LNWorker):
return chan return chan
@log_exceptions @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) lnaddr = self._check_invoice(invoice, amount_sat)
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
key = payment_hash.hex() key = payment_hash.hex()
@ -905,23 +907,23 @@ class LNWallet(LNWorker):
self.save_payment_info(info) self.save_payment_info(info)
self.wallet.set_label(key, lnaddr.get_description()) self.wallet.set_label(key, lnaddr.get_description())
log = self.logs[key] log = self.logs[key]
success = False
for i in range(attempts): for i in range(attempts):
try: try:
route = await self._create_route_from_invoice(decoded_invoice=lnaddr) route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
except NoPathFound: except NoPathFound as e:
success = False log.append(PaymentAttemptLog(success=False, exception=e))
break break
self.network.trigger_callback('invoice_status', key, PR_INFLIGHT) 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: if success:
log.append((route, True, preimage))
break break
else:
log.append((route, False, failure_log))
self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED) self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED)
return success 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 short_channel_id = route[0].short_channel_id
chan = self.get_channel_by_short_id(short_channel_id) chan = self.get_channel_by_short_id(short_channel_id)
if not chan: if not chan:
@ -948,8 +950,13 @@ class LNWallet(LNWorker):
self.logger.info("payment destination reported error") self.logger.info("payment destination reported error")
else: else:
self.network.path_finder.add_to_blacklist(short_chan_id) self.network.path_finder.add_to_blacklist(short_chan_id)
failure_log = (sender_idx, failure_msg, blacklist) failure_log = PaymentAttemptFailureDetails(sender_idx=sender_idx,
return success, preimage, failure_log 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): def handle_error_code_from_failed_htlc(self, failure_msg, sender_idx, route, peer):
code, data = failure_msg.code, failure_msg.data code, data = failure_msg.code, failure_msg.data
@ -1011,11 +1018,11 @@ class LNWallet(LNWorker):
f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}")) f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}"))
return addr 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) amount_msat = int(decoded_invoice.amount * COIN * 1000)
invoice_pubkey = decoded_invoice.pubkey.serialize() invoice_pubkey = decoded_invoice.pubkey.serialize()
# use 'r' field from invoice # use 'r' field from invoice
route = None # type: Optional[List[RouteEdge]] route = None # type: Optional[LNPaymentRoute]
# only want 'r' tags # only want 'r' tags
r_tags = list(filter(lambda x: x[0] == 'r', decoded_invoice.tags)) r_tags = list(filter(lambda x: x[0] == 'r', decoded_invoice.tags))
# strip the tag type, it's implicitly 'r' now # strip the tag type, it's implicitly 'r' now

Loading…
Cancel
Save