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.
from enum import IntEnum
from typing import Sequence
from PyQt5.QtCore import Qt, QItemSelectionModel
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 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)
@ -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)))

4
electrum/lnaddr.py

@ -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

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)
if TYPE_CHECKING:
from .lnrouter import RouteEdge
from .lnrouter import LNPaymentRoute
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)
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.

4
electrum/lnpeer.py

@ -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
@ -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')

7
electrum/lnrouter.py

@ -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
"""
@ -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')

18
electrum/lnutil.py

@ -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
@ -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
@ -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

39
electrum/lnworker.py

@ -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
@ -118,7 +118,9 @@ class PaymentInfo(NamedTuple):
class NoPathFound(PaymentFailure):
pass
def __str__(self):
return _('No path found')
class LNWorker(Logger):
@ -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]
@ -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()
@ -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:
@ -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
@ -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

Loading…
Cancel
Save