Browse Source

Improve handling of lightning payment status:

- Move 'handle_error_code_from_failed_htlc' to channel_db,
and call it from pay_to_route, because it should not be
called when HTLCs are forwarded.
- Replace 'payment_received' and 'payment_status'
callbacks with 'invoice_status' and 'request_status'.
- Show payment error logs in the Qt GUI
- In the invoices list, show paid invoices for which
we still have the log.
master
ThomasV 6 years ago
parent
commit
dd0be1541e
  1. 53
      electrum/channel_db.py
  2. 24
      electrum/gui/kivy/main_window.py
  3. 54
      electrum/gui/qt/invoice_list.py
  4. 45
      electrum/gui/qt/main_window.py
  5. 8
      electrum/lnchannel.py
  6. 72
      electrum/lnpeer.py
  7. 60
      electrum/lnworker.py
  8. 5
      electrum/tests/test_lnpeer.py
  9. 1
      electrum/wallet.py

53
electrum/channel_db.py

@ -39,6 +39,8 @@ from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enab
from .logging import Logger from .logging import Logger
from .lnutil import LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, format_short_channel_id, ShortChannelID from .lnutil import LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, format_short_channel_id, ShortChannelID
from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update
from .lnonion import OnionFailureCode
from .lnmsg import decode_msg
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -385,6 +387,57 @@ class ChannelDB(SqlDB):
# the update may be categorized as deprecated because of caching # the update may be categorized as deprecated because of caching
categorized_chan_upds = self.add_channel_updates([payload], verify=False) categorized_chan_upds = self.add_channel_updates([payload], verify=False)
def handle_error_code_from_failed_htlc(self, code, data, sender_idx, route):
# handle some specific error codes
failure_codes = {
OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
OnionFailureCode.AMOUNT_BELOW_MINIMUM: 8,
OnionFailureCode.FEE_INSUFFICIENT: 8,
OnionFailureCode.INCORRECT_CLTV_EXPIRY: 4,
OnionFailureCode.EXPIRY_TOO_SOON: 0,
OnionFailureCode.CHANNEL_DISABLED: 2,
}
if code in failure_codes:
offset = failure_codes[code]
channel_update_len = int.from_bytes(data[offset:offset+2], byteorder="big")
channel_update_as_received = data[offset+2: offset+2+channel_update_len]
channel_update_typed = (258).to_bytes(length=2, byteorder="big") + channel_update_as_received
# note: some nodes put channel updates in error msgs with the leading msg_type already there.
# we try decoding both ways here.
try:
message_type, payload = decode_msg(channel_update_typed)
payload['raw'] = channel_update_typed
except: # FIXME: too broad
message_type, payload = decode_msg(channel_update_as_received)
payload['raw'] = channel_update_as_received
categorized_chan_upds = self.add_channel_updates([payload])
blacklist = False
if categorized_chan_upds.good:
self.logger.info("applied channel update on our db")
#self.maybe_save_remote_update(payload)
elif categorized_chan_upds.orphaned:
# maybe it is a private channel (and data in invoice was outdated)
self.logger.info("maybe channel update is for private channel?")
start_node_id = route[sender_idx].node_id
self.add_channel_update_for_private_channel(payload, start_node_id)
elif categorized_chan_upds.expired:
blacklist = True
elif categorized_chan_upds.deprecated:
self.logger.info(f'channel update is not more recent.')
blacklist = True
else:
blacklist = True
if blacklist:
# blacklist channel after reporter node
# TODO this should depend on the error (even more granularity)
# also, we need finer blacklisting (directed edges; nodes)
try:
short_chan_id = route[sender_idx + 1].short_channel_id
except IndexError:
self.logger.info("payment destination reported error")
else:
self.network.path_finder.add_to_blacklist(short_chan_id)
def create_database(self): def create_database(self):
c = self.conn.cursor() c = self.conn.cursor()
c.execute(create_node_info) c.execute(create_node_info)

24
electrum/gui/kivy/main_window.py

@ -15,7 +15,7 @@ from electrum.wallet import Wallet, InternalAddressCorruption
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis
from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_FAILED, PR_INFLIGHT
from electrum import blockchain from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from .i18n import _ from .i18n import _
@ -205,24 +205,26 @@ class ElectrumWindow(App):
def on_fee_histogram(self, *args): def on_fee_histogram(self, *args):
self._trigger_update_history() self._trigger_update_history()
def on_payment_received(self, event, wallet, key, status): def on_request_status(self, event, key, status):
if key not in self.wallet.requests:
return
self.update_tab('receive')
if self.request_popup and self.request_popup.key == key: if self.request_popup and self.request_popup.key == key:
self.request_popup.set_status(status) self.request_popup.set_status(status)
if status == PR_PAID: if status == PR_PAID:
self.show_info(_('Payment Received') + '\n' + key) self.show_info(_('Payment Received') + '\n' + key)
self._trigger_update_history()
def on_payment_status(self, event, key, status, *args): def on_invoice_status(self, event, key, status, log):
# todo: update single item
self.update_tab('send') self.update_tab('send')
if status == 'success': if status == PR_PAID:
self.show_info(_('Payment was sent')) self.show_info(_('Payment was sent'))
self._trigger_update_history() self._trigger_update_history()
elif status == 'progress': elif status == PR_INFLIGHT:
pass pass
elif status == 'failure': elif status == PR_FAILED:
self.show_info(_('Payment failed')) self.show_info(_('Payment failed'))
elif status == 'error':
e = args[0]
self.show_error(_('Error') + '\n' + str(e))
def _get_bu(self): def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT) decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
@ -556,10 +558,10 @@ class ElectrumWindow(App):
self.network.register_callback(self.on_fee_histogram, ['fee_histogram']) self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history']) self.network.register_callback(self.on_history, ['on_history'])
self.network.register_callback(self.on_payment_received, ['payment_received'])
self.network.register_callback(self.on_channels, ['channels']) self.network.register_callback(self.on_channels, ['channels'])
self.network.register_callback(self.on_channel, ['channel']) self.network.register_callback(self.on_channel, ['channel'])
self.network.register_callback(self.on_payment_status, ['payment_status']) self.network.register_callback(self.on_invoice_status, ['invoice_status'])
self.network.register_callback(self.on_request_status, ['request_status'])
# load wallet # load wallet
self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True)) self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
# URI passed in config # URI passed in config

54
electrum/gui/qt/invoice_list.py

@ -27,10 +27,11 @@ from enum import IntEnum
from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QHeaderView, QMenu from PyQt5.QtWidgets import QHeaderView, QMenu, QVBoxLayout, QGridLayout, QLabel
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, PR_UNPAID, PR_PAID, get_request_status 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.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import lndecode, RECEIVED from electrum.lnutil import lndecode, RECEIVED
from electrum.bitcoin import COIN from electrum.bitcoin import COIN
@ -38,6 +39,7 @@ from electrum import constants
from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT, from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT,
import_meta_gui, export_meta_gui, pr_icons) import_meta_gui, export_meta_gui, pr_icons)
from .util import CloseButton, Buttons
@ -65,12 +67,35 @@ class InvoiceList(MyTreeView):
super().__init__(parent, self.create_menu, super().__init__(parent, self.create_menu,
stretch_column=self.Columns.DESCRIPTION, stretch_column=self.Columns.DESCRIPTION,
editable_columns=[]) editable_columns=[])
self.logs = {}
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setModel(QStandardItemModel(self)) self.setModel(QStandardItemModel(self))
self.update() self.update()
def update_item(self, key, status, log):
req = self.parent.wallet.get_invoice(key)
if req is None:
return
model = self.model()
for row in range(0, model.rowCount()):
item = model.item(row, 0)
if item.data(ROLE_REQUEST_ID) == key:
break
else:
return
status_item = model.item(row, self.Columns.STATUS)
status_str = get_request_status(req)
if log:
self.logs[key] = log
if status == PR_INFLIGHT:
status_str += '... (%d)'%len(log)
status_item.setText(status_str)
status_item.setIcon(read_QIcon(pr_icons.get(status)))
def update(self): def update(self):
_list = self.parent.wallet.get_invoices() _list = self.parent.wallet.get_invoices()
# filter out paid invoices unless we have the log
_list = [x for x in _list if x and x.get('status') != PR_PAID or x.get('rhash') in self.logs]
self.model().clear() self.model().clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for idx, item in enumerate(_list): for idx, item in enumerate(_list):
@ -136,6 +161,29 @@ class InvoiceList(MyTreeView):
invoice = self.parent.wallet.get_invoice(key) invoice = self.parent.wallet.get_invoice(key)
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
if invoice['status'] == PR_UNPAID: if invoice['status'] == PR_UNPAID:
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(invoice)) menu.addAction(_("Pay"), lambda: self.parent.do_pay_invoice(invoice))
if key in self.logs:
menu.addAction(_("View log"), lambda: self.show_log(key))
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):
from .util import WindowModalDialog
log = self.logs.get(key)
d = WindowModalDialog(self, _("Payment log"))
vbox = QVBoxLayout(d)
grid = QGridLayout()
grid.addWidget(QLabel(_("Node ID")), 0, 0)
grid.addWidget(QLabel(_("Message")), 0, 1)
for i, (route, success, failure_data) in enumerate(log):
print(route[0].node_id)
if not success:
failure_node_id, failure_msg = failure_data
code, data = failure_msg.code, failure_msg.data
grid.addWidget(QLabel(failure_node_id.hex()), i+1, 0)
grid.addWidget(QLabel(repr(code)), i+1, 1)
else:
pass
vbox.addLayout(grid)
vbox.addLayout(Buttons(CloseButton(d)))
d.exec_()

45
electrum/gui/qt/main_window.py

@ -73,7 +73,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from electrum.exchange_rate import FxThread from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.logging import Logger from electrum.logging import Logger
from electrum.paymentrequest import PR_PAID from electrum.util import PR_PAID, PR_UNPAID, PR_INFLIGHT, PR_FAILED
from electrum.util import pr_expiration_values from electrum.util import pr_expiration_values
from .exception_window import Exception_Hook from .exception_window import Exception_Hook
@ -232,8 +232,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
interests = ['wallet_updated', 'network_updated', 'blockchain_updated', interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status', 'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes', 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
'on_history', 'channel', 'channels', 'payment_received', 'on_history', 'channel', 'channels',
'payment_status'] 'invoice_status', 'request_status']
# To avoid leaking references to "self" that prevent the # To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be # window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be # methods of this class only, and specifically not be
@ -382,8 +382,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
elif event == 'channel': elif event == 'channel':
self.channels_list.update_single_row.emit(*args) self.channels_list.update_single_row.emit(*args)
self.update_status() self.update_status()
elif event == 'payment_status': elif event == 'request_status':
self.on_payment_status(*args) self.on_request_status(*args)
elif event == 'invoice_status':
self.on_invoice_status(*args)
elif event == 'status': elif event == 'status':
self.update_status() self.update_status()
elif event == 'banner': elif event == 'banner':
@ -401,10 +403,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.fee_slider.update() self.fee_slider.update()
self.require_fee_update = True self.require_fee_update = True
self.history_model.on_fee_histogram() self.history_model.on_fee_histogram()
elif event == 'payment_received':
wallet, key, status = args
if wallet == self.wallet:
self.notify(_('Payment received') + '\n' + key)
else: else:
self.logger.info(f"unexpected network event: {event} {args}") self.logger.info(f"unexpected network event: {event} {args}")
@ -1682,24 +1680,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
amount_sat = self.amount_e.get_amount() amount_sat = self.amount_e.get_amount()
attempts = LN_NUM_PAYMENT_ATTEMPTS attempts = LN_NUM_PAYMENT_ATTEMPTS
def task(): def task():
self.wallet.lnworker.pay(invoice, amount_sat, attempts) try:
self.wallet.lnworker.pay(invoice, amount_sat, attempts)
except Exception as e:
self.show_error(str(e))
self.do_clear() self.do_clear()
self.wallet.thread.add(task) self.wallet.thread.add(task)
self.invoice_list.update() self.invoice_list.update()
def on_payment_status(self, key, status, *args): def on_request_status(self, key, status):
# todo: check that key is in this wallet's invoice list if key not in self.wallet.requests:
self.invoice_list.update() return
if status == 'success': if status == PR_PAID:
self.notify(_('Payment received') + '\n' + key)
def on_invoice_status(self, key, status, log):
if key not in self.wallet.invoices:
return
self.invoice_list.update_item(key, status, log)
if status == PR_PAID:
self.show_message(_('Payment succeeded')) self.show_message(_('Payment succeeded'))
self.need_update.set() self.need_update.set()
elif status == 'progress': elif status == PR_FAILED:
print('on_payment_status', key, status, args)
elif status == 'failure':
self.show_error(_('Payment failed')) self.show_error(_('Payment failed'))
elif status == 'error': else:
e = args[0] pass
self.show_error(_('Error') + '\n' + str(e))
def read_invoice(self): def read_invoice(self):
if self.check_send_tab_payto_line_and_show_errors(): if self.check_send_tab_payto_line_and_show_errors():

8
electrum/lnchannel.py

@ -38,6 +38,7 @@ from .crypto import sha256, sha256d
from .transaction import Transaction from .transaction import Transaction
from .logging import Logger from .logging import Logger
from .lnonion import decode_onion_error
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx, get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey, sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey,
@ -578,6 +579,13 @@ class Channel(Logger):
htlc = log['adds'][htlc_id] htlc = log['adds'][htlc_id]
return htlc.payment_hash return htlc.payment_hash
def decode_onion_error(self, reason, route, htlc_id):
failure_msg, sender_idx = decode_onion_error(
reason,
[x.node_id for x in route],
self.onion_keys[htlc_id])
return failure_msg, sender_idx
def receive_htlc_settle(self, preimage, htlc_id): def receive_htlc_settle(self, preimage, htlc_id):
self.logger.info("receive_htlc_settle") self.logger.info("receive_htlc_settle")
log = self.hm.log[LOCAL] log = self.hm.log[LOCAL]

72
electrum/lnpeer.py

@ -86,7 +86,6 @@ class Peer(Logger):
self.announcement_signatures = defaultdict(asyncio.Queue) self.announcement_signatures = defaultdict(asyncio.Queue)
self.closing_signed = defaultdict(asyncio.Queue) self.closing_signed = defaultdict(asyncio.Queue)
# #
self.attempted_route = {}
self.orphan_channel_updates = OrderedDict() self.orphan_channel_updates = OrderedDict()
self._local_changed_events = defaultdict(asyncio.Event) self._local_changed_events = defaultdict(asyncio.Event)
self._remote_changed_events = defaultdict(asyncio.Event) self._remote_changed_events = defaultdict(asyncio.Event)
@ -1096,7 +1095,6 @@ class Peer(Logger):
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
chan.receive_fail_htlc(htlc_id) chan.receive_fail_htlc(htlc_id)
local_ctn = chan.get_latest_ctn(LOCAL) local_ctn = chan.get_latest_ctn(LOCAL)
asyncio.ensure_future(self._handle_error_code_from_failed_htlc(payload, channel_id, htlc_id))
asyncio.ensure_future(self._on_update_fail_htlc(channel_id, htlc_id, local_ctn, reason)) asyncio.ensure_future(self._on_update_fail_htlc(channel_id, htlc_id, local_ctn, reason))
@log_exceptions @log_exceptions
@ -1106,75 +1104,6 @@ class Peer(Logger):
payment_hash = chan.get_payment_hash(htlc_id) payment_hash = chan.get_payment_hash(htlc_id)
self.lnworker.payment_failed(payment_hash, reason) self.lnworker.payment_failed(payment_hash, reason)
@log_exceptions
async def _handle_error_code_from_failed_htlc(self, payload, channel_id, htlc_id):
chan = self.channels[channel_id]
key = (channel_id, htlc_id)
try:
route = self.attempted_route[key] # type: List[RouteEdge]
except KeyError:
# the remote might try to fail an htlc after we restarted...
# attempted_route is not persisted, so we will get here then
self.logger.info("UPDATE_FAIL_HTLC. cannot decode! attempted route is MISSING. {}".format(key))
return
error_reason = payload["reason"]
failure_msg, sender_idx = decode_onion_error(
error_reason,
[x.node_id for x in route],
chan.onion_keys[htlc_id])
code, data = failure_msg.code, failure_msg.data
self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}")
self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
# handle some specific error codes
failure_codes = {
OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
OnionFailureCode.AMOUNT_BELOW_MINIMUM: 8,
OnionFailureCode.FEE_INSUFFICIENT: 8,
OnionFailureCode.INCORRECT_CLTV_EXPIRY: 4,
OnionFailureCode.EXPIRY_TOO_SOON: 0,
OnionFailureCode.CHANNEL_DISABLED: 2,
}
if code in failure_codes:
offset = failure_codes[code]
channel_update_len = int.from_bytes(data[offset:offset+2], byteorder="big")
channel_update_as_received = data[offset+2: offset+2+channel_update_len]
channel_update_typed = (258).to_bytes(length=2, byteorder="big") + channel_update_as_received
# note: some nodes put channel updates in error msgs with the leading msg_type already there.
# we try decoding both ways here.
try:
message_type, payload = decode_msg(channel_update_typed)
payload['raw'] = channel_update_typed
except: # FIXME: too broad
message_type, payload = decode_msg(channel_update_as_received)
payload['raw'] = channel_update_as_received
categorized_chan_upds = self.channel_db.add_channel_updates([payload])
blacklist = False
if categorized_chan_upds.good:
self.logger.info("applied channel update on our db")
self.maybe_save_remote_update(payload)
elif categorized_chan_upds.orphaned:
# maybe it is a private channel (and data in invoice was outdated)
self.logger.info("maybe channel update is for private channel?")
start_node_id = route[sender_idx].node_id
self.channel_db.add_channel_update_for_private_channel(payload, start_node_id)
elif categorized_chan_upds.expired:
blacklist = True
elif categorized_chan_upds.deprecated:
self.logger.info(f'channel update is not more recent.')
blacklist = True
else:
blacklist = True
if blacklist:
# blacklist channel after reporter node
# TODO this should depend on the error (even more granularity)
# also, we need finer blacklisting (directed edges; nodes)
try:
short_chan_id = route[sender_idx + 1].short_channel_id
except IndexError:
self.logger.info("payment destination reported error")
else:
self.network.path_finder.add_to_blacklist(short_chan_id)
def maybe_send_commitment(self, chan: Channel): def maybe_send_commitment(self, chan: Channel):
# REMOTE should revoke first before we can sign a new ctx # REMOTE should revoke first before we can sign a new ctx
if chan.hm.is_revack_pending(REMOTE): if chan.hm.is_revack_pending(REMOTE):
@ -1215,7 +1144,6 @@ class Peer(Logger):
htlc = chan.add_htlc(htlc) htlc = chan.add_htlc(htlc)
remote_ctn = chan.get_latest_ctn(REMOTE) remote_ctn = chan.get_latest_ctn(REMOTE)
chan.onion_keys[htlc.htlc_id] = secret_key chan.onion_keys[htlc.htlc_id] = secret_key
self.attempted_route[(chan.channel_id, htlc.htlc_id)] = route
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}") self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
self.send_message("update_add_htlc", self.send_message("update_add_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,

60
electrum/lnworker.py

@ -21,7 +21,8 @@ import dns.exception
from . import constants from . import constants
from . import keystore from . import keystore
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, profiler from .util import profiler
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED
from .util import PR_TYPE_LN from .util import PR_TYPE_LN
from .keystore import BIP32_KeyStore from .keystore import BIP32_KeyStore
from .bitcoin import COIN from .bitcoin import COIN
@ -92,6 +93,9 @@ class PaymentInfo(NamedTuple):
status: int status: int
class NoPathFound(PaymentFailure):
pass
class LNWorker(Logger): class LNWorker(Logger):
def __init__(self, xprv): def __init__(self, xprv):
@ -825,19 +829,9 @@ class LNWallet(LNWorker):
""" """
Can be called from other threads Can be called from other threads
""" """
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
key = bh2u(addr.paymenthash)
coro = self._pay(invoice, amount_sat, attempts) coro = self._pay(invoice, amount_sat, attempts)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try: success = fut.result()
success = fut.result()
except Exception as e:
self.network.trigger_callback('payment_status', key, 'error', e)
return
if success:
self.network.trigger_callback('payment_status', key, 'success')
else:
self.network.trigger_callback('payment_status', key, 'failure')
def get_channel_by_short_id(self, short_channel_id: ShortChannelID) -> Channel: def get_channel_by_short_id(self, short_channel_id: ShortChannelID) -> Channel:
with self.lock: with self.lock:
@ -848,9 +842,10 @@ class LNWallet(LNWorker):
@log_exceptions @log_exceptions
async def _pay(self, invoice, amount_sat=None, attempts=1): async def _pay(self, invoice, amount_sat=None, attempts=1):
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
key = bh2u(lnaddr.paymenthash) payment_hash = lnaddr.paymenthash
key = payment_hash.hex()
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
status = self.get_payment_status(lnaddr.paymenthash) status = self.get_payment_status(payment_hash)
if status == PR_PAID: if status == PR_PAID:
raise PaymentFailure(_("This invoice has been paid already")) raise PaymentFailure(_("This invoice has been paid already"))
if status == PR_INFLIGHT: if status == PR_INFLIGHT:
@ -859,13 +854,22 @@ class LNWallet(LNWorker):
self.save_payment_info(info) self.save_payment_info(info)
self._check_invoice(invoice, amount_sat) self._check_invoice(invoice, amount_sat)
self.wallet.set_label(key, lnaddr.get_description()) self.wallet.set_label(key, lnaddr.get_description())
log = []
for i in range(attempts): for i in range(attempts):
route = await self._create_route_from_invoice(decoded_invoice=lnaddr) try:
self.network.trigger_callback('payment_status', key, 'progress', i) route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
success, preimage, reason = await self._pay_to_route(route, lnaddr) except NoPathFound:
success = False
break
self.network.trigger_callback('invoice_status', key, PR_INFLIGHT, log)
success, preimage, failure_node_id, failure_msg = await self._pay_to_route(route, lnaddr)
if success: if success:
return True log.append((route, True, preimage))
return False break
else:
log.append((route, False, (failure_node_id, failure_msg)))
self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED, log)
return success
async def _pay_to_route(self, route, lnaddr): async def _pay_to_route(self, route, lnaddr):
short_channel_id = route[0].short_channel_id short_channel_id = route[0].short_channel_id
@ -877,7 +881,18 @@ class LNWallet(LNWorker):
peer = self.peers[route[0].node_id] peer = self.peers[route[0].node_id]
htlc = await peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry()) htlc = await peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT) self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT)
return await self.await_payment(lnaddr.paymenthash) success, preimage, reason = await self.await_payment(lnaddr.paymenthash)
if success:
failure_node_id = None
failure_msg = None
else:
failure_msg, sender_idx = chan.decode_onion_error(reason, route, htlc.htlc_id)
failure_node_id = route[sender_idx].node_id
code, data = failure_msg.code, failure_msg.data
self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}")
self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
self.channel_db.handle_error_code_from_failed_htlc(code, data, sender_idx, route)
return success, preimage, failure_node_id, failure_msg
@staticmethod @staticmethod
def _check_invoice(invoice, amount_sat=None): def _check_invoice(invoice, amount_sat=None):
@ -945,11 +960,11 @@ class LNWallet(LNWorker):
if route is None: if route is None:
path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat, channels) path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat, channels)
if not path: if not path:
raise PaymentFailure(_("No path found")) raise NoPathFound()
route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey) route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey)
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()): if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
self.logger.info(f"rejecting insane route {route}") self.logger.info(f"rejecting insane route {route}")
raise PaymentFailure(_("No path found")) raise NoPathFound()
return route return route
def add_request(self, amount_sat, message, expiry): def add_request(self, amount_sat, message, expiry):
@ -1052,6 +1067,7 @@ class LNWallet(LNWorker):
def payment_received(self, payment_hash: bytes): def payment_received(self, payment_hash: bytes):
self.set_payment_status(payment_hash, PR_PAID) self.set_payment_status(payment_hash, PR_PAID)
self.network.trigger_callback('request_status', payment_hash.hex(), PR_PAID)
async def _calc_routing_hints_for_invoice(self, amount_sat): async def _calc_routing_hints_for_invoice(self, amount_sat):
"""calculate routing hints (BOLT-11 'r' field)""" """calculate routing hints (BOLT-11 'r' field)"""

5
electrum/tests/test_lnpeer.py

@ -18,7 +18,7 @@ from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
from electrum.lnutil import PaymentFailure, LnLocalFeatures from electrum.lnutil import PaymentFailure, LnLocalFeatures
from electrum.lnrouter import LNPathFinder from electrum.lnrouter import LNPathFinder
from electrum.channel_db import ChannelDB from electrum.channel_db import ChannelDB
from electrum.lnworker import LNWallet from electrum.lnworker import LNWallet, NoPathFound
from electrum.lnmsg import encode_msg, decode_msg from electrum.lnmsg import encode_msg, decode_msg
from electrum.logging import console_stderr_handler from electrum.logging import console_stderr_handler
from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID
@ -251,9 +251,8 @@ class TestPeer(ElectrumTestCase):
# check if a tx (commitment transaction) was broadcasted: # check if a tx (commitment transaction) was broadcasted:
assert q1.qsize() == 1 assert q1.qsize() == 1
with self.assertRaises(PaymentFailure) as e: with self.assertRaises(NoPathFound) as e:
run(w1._create_route_from_invoice(decoded_invoice=addr)) run(w1._create_route_from_invoice(decoded_invoice=addr))
self.assertEqual(str(e.exception), 'No path found')
peer = w1.peers[route[0].node_id] peer = w1.peers[route[0].node_id]
# AssertionError is ok since we shouldn't use old routes, and the # AssertionError is ok since we shouldn't use old routes, and the

1
electrum/wallet.py

@ -558,7 +558,6 @@ class Abstract_Wallet(AddressSynchronizer):
def get_invoices(self): def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()] out = [self.get_invoice(key) for key in self.invoices.keys()]
out = [x for x in out if x and x.get('status') != PR_PAID]
out.sort(key=operator.itemgetter('time')) out.sort(key=operator.itemgetter('time'))
return out return out

Loading…
Cancel
Save