Merge pull request #10463 from f321x/jit_2

lnwallet: zeroconf/just-in-time improvements and tests
This commit is contained in:
ThomasV
2026-04-28 10:16:34 +02:00
committed by GitHub
11 changed files with 558 additions and 73 deletions
+17 -9
View File
@@ -20,6 +20,7 @@ from .util import read_QIcon, WWLabel, MessageBoxMixin, MONOSPACE_FONT, get_icon
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from electrum.wallet import Request
class ReceiveTab(QWidget, MessageBoxMixin, Logger):
@@ -101,6 +102,9 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
self.receive_zeroconf_button = QPushButton(_('Accept'))
self.receive_zeroconf_button.clicked.connect(self.on_accept_zeroconf)
self.previous_request = None # type: Optional['Request']
self.confirmed_zeroconf_for_this_request = False # type: bool
def on_receive_rebalance():
if self.receive_rebalance_button.suggestion:
chan1, chan2, delta = self.receive_rebalance_button.suggestion
@@ -221,7 +225,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
def update_receive_widgets(self):
b = self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
self.receive_widget.update_visibility(b)
self.receive_widget.update_visibility(b, bool(self.receive_help_text.text()))
def update_current_request(self):
if len(self.request_list.selectionModel().selectedRows(0)) > 1:
@@ -229,6 +233,9 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
else:
key = self.request_list.get_current_key()
req = self.wallet.get_request(key) if key else None
if req != self.previous_request:
self.previous_request = req
self.confirmed_zeroconf_for_this_request = False
if req is None:
self.receive_e.setText('')
self.addr = self.URI = self.lnaddr = ''
@@ -243,7 +250,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
self.ln_help = help_texts.ln_help
can_rebalance = help_texts.can_rebalance()
can_swap = help_texts.can_swap()
can_zeroconf = help_texts.can_zeroconf()
can_zeroconf = help_texts.can_zeroconf() if not self.confirmed_zeroconf_for_this_request else False
self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion
self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion
self.receive_rebalance_button.setVisible(can_rebalance)
@@ -253,25 +260,26 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
self.receive_zeroconf_button.setVisible(can_zeroconf)
self.receive_zeroconf_button.setEnabled(can_zeroconf)
text, data, help_text, title = self.get_tab_data()
if self.confirmed_zeroconf_for_this_request and help_texts.can_zeroconf():
help_text = ''
# set help before receive_e so we don't flicker from qr to help
self.receive_help_text.setText(help_text)
self.receive_e.setText(text)
self.receive_qr.setData(data)
self.receive_help_text.setText(help_text)
for w in [self.receive_e, self.receive_qr]:
w.setEnabled(bool(text) and (not help_text or can_zeroconf))
w.setToolTip(help_text)
# macOS hack (similar to #4777)
self.receive_e.repaint()
# always show
if can_zeroconf:
# show the help message if zeroconf so user can first accept it and still sees the invoice
# after accepting
self.receive_widget.show_help()
self.receive_widget.setVisible(True)
self.toggle_qr_button.setEnabled(True)
self.update_receive_qr_window()
def on_accept_zeroconf(self):
self.receive_zeroconf_button.setVisible(False)
self.confirmed_zeroconf_for_this_request = True
self.receive_help_text.setText('')
self.update_receive_widgets()
def get_tab_data(self):
@@ -386,8 +394,8 @@ class ReceiveWidget(QWidget):
self.setLayout(vbox)
def update_visibility(self, is_qr):
if str(self.textedit.toPlainText()):
def update_visibility(self, is_qr: bool, show_help: bool):
if str(self.textedit.toPlainText()) and not show_help:
self.help_widget.setVisible(False)
self.textedit.setVisible(not is_qr)
self.qr.setVisible(is_qr)
+31 -19
View File
@@ -41,7 +41,7 @@ from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction, TxInput, Sighash
from .logging import Logger
from .lntransport import LNPeerAddr
from .lntransport import LNPeerAddr, extract_nodeid, ConnStringFormatError
from .lnonion import OnionRoutingFailure
from . import lnutil
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
@@ -59,7 +59,7 @@ from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo, Ma
from .lnsweep import sweep_their_ctx_to_remote_backup
from .lnhtlc import HTLCManager
from .lnmsg import encode_msg, decode_msg
from .address_synchronizer import TX_HEIGHT_LOCAL
from .address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONFIRMED
from .lnutil import CHANNEL_OPENING_TIMEOUT_BLOCKS, CHANNEL_OPENING_TIMEOUT_SEC
from .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage
from .lnutil import format_short_channel_id
@@ -224,6 +224,7 @@ class AbstractChannel(Logger, ABC):
return self._state
def is_funded(self) -> bool:
# NOTE: also true for unfunded zeroconf channels (OPEN > FUNDED)
return self.get_state() >= ChannelState.FUNDED
def is_open(self) -> bool:
@@ -375,26 +376,33 @@ class AbstractChannel(Logger, ABC):
self.logger.warning(f"dropping incoming channel, funding tx not found in mempool")
self.lnworker.remove_channel(self.channel_id)
elif self.is_zeroconf() and state in [ChannelState.OPEN, ChannelState.CLOSING, ChannelState.FORCE_CLOSING]:
chan_age = now() - self.storage['init_timestamp']
# handling zeroconf channels with no funding tx, can happen if broadcasting fails on LSP side
# or if the LSP did double spent the funding tx/never published it intentionally
# only remove a timed out OPEN channel if we are connected to the network to prevent removing it if we went
# offline before seeing the funding tx
if state != ChannelState.OPEN or chan_age > ZEROCONF_TIMEOUT and self.lnworker.network.is_connected():
# we delete the channel if its in closing state (either initiated manually by client or by LSP on failure)
# or if the channel is not seeing any funding tx after 10 minutes to prevent further usage (limit damage)
self.set_state(ChannelState.REDEEMED, force=True)
local_balance_sat = int(self.balance(LOCAL) // 1000)
if local_balance_sat > 0:
# or if the LSP did double spent the funding tx/never published it intentionally.
if not self.lnworker.wallet.is_up_to_date() or not self.lnworker.network \
or self.lnworker.network.blockchain().is_tip_stale():
# ensure we are up to date to prevent accidentally dropping a channel that is funded
return
chan_age = now() - self.storage['init_timestamp']
if chan_age > ZEROCONF_TIMEOUT:
# freeze the channel to avoid receiving even more into this unfunded channel.
# NOTE: we don't reject htlcs arriving on frozen channels, this only really
# stops us from including the channel in invoice routing hints.
if isinstance(self, Channel):
self.set_frozen_for_receiving(True)
# un-trust the LSP so the user doesn't accept another channel from the same provider
# compare the node id's as the user might already have changed to another one
if self.node_id == self.lnworker.trusted_zeroconf_node_id:
self.lnworker.config.ZEROCONF_TRUSTED_NODE = ''
if self.has_funding_timed_out():
self.lnworker.remove_channel(self.channel_id)
# remove remaining local transactions from the wallet, this will also remove child transactions (closing tx)
# self.lnworker.lnwatcher.adb.remove_transaction(self.funding_outpoint.txid)
if (local_balance_sat := int(self.balance(LOCAL) // 1000)) > 0:
self.logger.warning(
f"we may have been scammed out of {local_balance_sat} sat by our "
f"JIT provider: {self.lnworker.config.ZEROCONF_TRUSTED_NODE} or he didn't use our preimage")
self.lnworker.config.ZEROCONF_TRUSTED_NODE = ''
# FIXME this is broken: lnwatcher.unwatch_channel does not exist
self.lnworker.lnwatcher.unwatch_channel(self.get_funding_address(), self.funding_outpoint.to_str())
# remove remaining local transactions from the wallet, this will also remove child transactions (closing tx)
self.lnworker.lnwatcher.adb.remove_transaction(self.funding_outpoint.txid)
self.lnworker.remove_channel(self.channel_id)
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:
self.save_funding_height(txid=funding_txid, height=funding_height.height(), timestamp=funding_height.timestamp)
@@ -420,6 +428,9 @@ class AbstractChannel(Logger, ABC):
# remove zeroconf flag as we are now confirmed, this is to prevent an electrum server causing
# us to remove a channel later in update_unfunded_state by omitting its funding tx
self.remove_zeroconf_flag()
# unfreeze in case it was frozen in update_unfunded_state
if isinstance(self, Channel):
self.set_frozen_for_receiving(False)
def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
@@ -843,7 +854,8 @@ class Channel(AbstractChannel):
return self.is_redeemed()
def has_funding_timed_out(self):
if self.is_initiator() or self.is_funded():
funding_height = self.get_funding_height()
if self.is_initiator() or funding_height and funding_height[1] > TX_HEIGHT_UNCONFIRMED:
return False
if self.lnworker.network.blockchain().is_tip_stale() or not self.lnworker.wallet.is_up_to_date():
return False
+14 -6
View File
@@ -101,6 +101,11 @@ class Peer(Logger, EventListener):
self.pubkey = pubkey # remote pubkey
self.privkey = self.transport.privkey # local privkey
self.features = self.lnworker.features # type: LnFeatures
if lnworker == lnworker.network.lngossip or \
lnworker.config.ZEROCONF_TRUSTED_NODE and pubkey != lnworker.trusted_zeroconf_node_id:
# don't signal zeroconf support if we are client (a trusted node is configured),
# and Peer is not our trusted node
self.features &= ~LnFeatures.OPTION_ZEROCONF_OPT
self.their_features = LnFeatures(0) # type: LnFeatures
self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
assert self.node_ids[0] != self.node_ids[1]
@@ -1259,13 +1264,16 @@ class Peer(Logger, EventListener):
# store the temp id now, so that it is recognized for e.g. 'error' messages
self.temp_id_to_id[temp_chan_id] = None
self._cleanup_temp_channelids()
channel_opening_fee_tlv = open_channel_tlvs.get('channel_opening_fee', {})
channel_opening_fee = channel_opening_fee_tlv.get('channel_opening_fee')
if channel_opening_fee:
# todo check that the fee is reasonable
channel_opening_fee = open_channel_tlvs.get('channel_opening_fee', {}).get('channel_opening_fee')
if channel_opening_fee: # just-in-time channel opening
assert is_zeroconf
self.logger.info(f"just-in-time opening fee: {channel_opening_fee} msat")
pass
# the opening fee consists of the fee configured by the LSP + mining fees of the funding tx
channel_opening_fee_sat = channel_opening_fee // 1000
if channel_opening_fee_sat > funding_sat * 0.1:
# TODO: if there will be some discovery channel where LSPs announce their fees
# we should compare against the fees they announced here.
raise Exception(f"{channel_opening_fee_sat=} exceeding fee limit, rejecting channel ({funding_sat=})")
self.logger.info(f"just-in-time channel: {channel_opening_fee_sat=}")
if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(
+81 -26
View File
@@ -74,7 +74,7 @@ from .lnutil import (
OnchainChannelBackupStorage, ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget,
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage, MIN_FUNDING_SAT,
MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE, RecvMPPResolution, ReceivedMPPStatus, ReceivedMPPHtlc,
PaymentSuccess, ChannelType, LocalConfig, Keypair,
PaymentSuccess, ChannelType, LocalConfig, Keypair, ZEROCONF_TIMEOUT,
)
from .lnonion import (
decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket,
@@ -1010,7 +1010,7 @@ class LNWallet(Logger):
features = LNWALLET_FEATURES
if self.config.ENABLE_ANCHOR_CHANNELS:
features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT
if self.config.ACCEPT_ZEROCONF_CHANNELS:
if self.config.OPEN_ZEROCONF_CHANNELS:
features |= LnFeatures.OPTION_ZEROCONF_OPT
if self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS or self.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS:
features |= LnFeatures.OPTION_ONION_MESSAGE_OPT
@@ -1485,34 +1485,42 @@ class LNWallet(Logger):
payment_hash: bytes,
next_onion: OnionPacket,
) -> str:
assert self.config.OPEN_ZEROCONF_CHANNELS
# if an exception is raised during negotiation, we raise an OnionRoutingFailure.
# this will cancel the incoming HTLC
next_chan: Optional[Channel] = None
# prevent settling the htlc until the channel opening was successful so we can fail it if needed
self.dont_settle_htlcs[payment_hash.hex()] = None
try:
funding_sat = 2 * (next_amount_msat_htlc // 1000) # try to fully spend htlcs
assert self.config.ZEROCONF_CHANNEL_SIZE_PERCENT >= 120, "ZEROCONF_CHANNEL_SIZE_PERCENT below min of 120%"
assert self.config.ZEROCONF_OPENING_FEE_PPM >= 0, f"invalid {self.config.ZEROCONF_OPENING_FEE_PPM=}"
funding_sat = (self.config.ZEROCONF_CHANNEL_SIZE_PERCENT * (next_amount_msat_htlc // 1000)) // 100
password = self.wallet.get_unlocked_password() if self.wallet.has_password() else None
channel_opening_fee = next_amount_msat_htlc // 100
if channel_opening_fee // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE:
self.logger.info(f'rejecting JIT channel: payment too low')
channel_opening_base_fee_msat = (next_amount_msat_htlc * self.config.ZEROCONF_OPENING_FEE_PPM) // 1_000_000
if channel_opening_base_fee_msat // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE:
self.logger.info(
f'rejecting JIT channel: {(channel_opening_base_fee_msat // 1000)=} < {self.config.ZEROCONF_MIN_OPENING_FEE=}'
)
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low')
self.logger.info(f'channel opening fee (sats): {channel_opening_fee//1000}')
next_chan, funding_tx = await self.open_channel_with_peer(
next_peer, funding_sat,
push_sat=0,
zeroconf=True,
public=False,
opening_fee=channel_opening_fee,
opening_base_fee_msat=channel_opening_base_fee_msat,
password=password,
)
async def wait_for_channel():
while not next_chan.is_open():
await asyncio.sleep(1)
await util.wait_for2(wait_for_channel(), LN_P2P_NETWORK_TIMEOUT)
next_chan.save_remote_scid_alias(self._scid_alias_of_node(next_peer.pubkey))
self.logger.info(f'JIT channel is open')
next_amount_msat_htlc -= channel_opening_fee
self.logger.info(f'JIT channel is open (will forward htlc and await preimage now)')
self.logger.info(f'channel opening fee (sats): {channel_opening_base_fee_msat//1000} + {funding_tx.get_fee()} mining fee')
next_amount_msat_htlc -= channel_opening_base_fee_msat + funding_tx.get_fee() * 1000
if next_amount_msat_htlc < 1_000:
self.logger.info(f'rejecting JIT channel: payment too low after deducting mining fees')
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low after deducting mining fees')
# fixme: some checks are missing
htlc = next_peer.send_htlc(
chan=next_chan,
@@ -1525,12 +1533,32 @@ class LNWallet(Logger):
await asyncio.sleep(1)
await util.wait_for2(wait_for_preimage(), LN_P2P_NETWORK_TIMEOUT)
# We have been paid and can broadcast
# todo: if broadcasting raise an exception, we should try to rebroadcast
await self.network.broadcast_transaction(funding_tx)
except OnionRoutingFailure:
raise
except Exception:
# We have been paid and can broadcast.
# Channel providers should run their own, trusted Electrum server as
# we could lose funds here if the server broadcasts the tx but omits it from us
first_broadcast_ts = time.time()
while time.time() - first_broadcast_ts < ZEROCONF_TIMEOUT * 0.75:
if await self.network.try_broadcasting(funding_tx, "jit channel funding"):
break
await asyncio.sleep(30)
# we cannot rely on success of try_broadcasting to determine broadcasting success
# as broadcasting might fail with some harmless error like 'transaction already in mempool'
tx_mined_info = self.wallet.adb.get_tx_height(funding_tx.txid())
if tx_mined_info.height() > TX_HEIGHT_LOCAL:
self.logger.debug(f"found our jit channel funding tx: {tx_mined_info.height()=}")
break
else:
raise OnionRoutingFailure(
code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
data=b'failed to broadcast funding transaction',
)
except Exception as e:
self.logger.warning(f"failed to open just in time channel: {repr(e)}")
if next_chan:
await self._cleanup_failed_jit_channel(next_chan)
self._preimages.pop(payment_hash.hex(), None)
if isinstance(e, OnionRoutingFailure):
raise
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
finally:
del self.dont_settle_htlcs[payment_hash.hex()]
@@ -1538,13 +1566,30 @@ class LNWallet(Logger):
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), htlc.htlc_id)
return htlc_key
async def _cleanup_failed_jit_channel(self, chan: Channel):
"""
Removes a just in time channel where we didn't broadcast the funding
transaction, e.g. when the client didn't release the preimage.
"""
funding_height = chan.get_funding_height()
if funding_height is not None and funding_height[1] > TX_HEIGHT_LOCAL:
raise Exception("must not delete the channel if it has been broadcast")
# try to be nice and send shutdown to signal peer that this channel is dead
try:
await util.wait_for2(self.close_channel(chan.channel_id), LN_P2P_NETWORK_TIMEOUT)
except Exception:
self.logger.debug(f"sending chan shutdown to failed zeroconf peer failed ", exc_info=True)
chan.set_state(ChannelState.REDEEMED, force=True)
self.lnwatcher.adb.remove_transaction(chan.funding_outpoint.txid)
self.remove_channel(chan.channel_id)
@log_exceptions
async def open_channel_with_peer(
self, peer, funding_sat, *,
push_sat: int = 0,
public: bool = False,
zeroconf: bool = False,
opening_fee: int = None,
opening_base_fee_msat: Optional[int] = None,
password=None):
if self.config.ENABLE_ANCHOR_CHANNELS:
self.wallet.unlock(password)
@@ -1556,6 +1601,9 @@ class LNWallet(Logger):
funding_sat=funding_sat,
node_id=node_id,
fee_policy=fee_policy)
if opening_base_fee_msat:
# add funding tx fee on top of the opening fee to avoid opening channels at a loss
opening_base_fee_msat += funding_tx.get_fee() * 1000
chan, funding_tx = await self._open_channel_coroutine(
peer=peer,
funding_tx=funding_tx,
@@ -1563,7 +1611,7 @@ class LNWallet(Logger):
push_sat=push_sat,
public=public,
zeroconf=zeroconf,
opening_fee=opening_fee,
opening_fee=opening_base_fee_msat,
password=password)
return chan, funding_tx
@@ -3351,17 +3399,24 @@ class LNWallet(Logger):
return False
def can_get_zeroconf_channel(self) -> bool:
if not self.config.ACCEPT_ZEROCONF_CHANNELS and self.config.ZEROCONF_TRUSTED_NODE:
# check if zeroconf is accepted and client has trusted zeroconf node configured
if not self.config.OPEN_ZEROCONF_CHANNELS:
return False
try:
node_id = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]
except ConnStringFormatError:
# invalid connection string
node_id = self.trusted_zeroconf_node_id
if not node_id:
return False
# only return True if we are connected to the zeroconf provider
return self.lnpeermgr.get_peer_by_pubkey(node_id) is not None
@property
def trusted_zeroconf_node_id(self) -> Optional[bytes]:
if not self.config.ZEROCONF_TRUSTED_NODE:
return None
try:
return extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]
except ConnStringFormatError:
self.logger.warning(f"invalid zeroconf node connection string configured")
return None
def _suggest_channels_for_rebalance(self, direction, amount_sat) -> Sequence[Tuple[Channel, int]]:
"""
Suggest a channel and amount to send/receive with that channel, so that we will be able to receive/send amount_sat
@@ -4012,7 +4067,7 @@ class LNWallet(Logger):
# do we have a connection to the node?
next_peer = self.lnpeermgr.get_peer_by_pubkey(outgoing_node_id)
if next_peer and next_peer.accepts_zeroconf():
if next_peer and next_peer.accepts_zeroconf() and self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
self.logger.info(f'JIT: found next_peer')
for next_chan in next_peer.channels.values():
if next_chan.can_pay(amt_to_forward):
+7 -1
View File
@@ -954,9 +954,15 @@ Warning: setting this to too low will result in lots of payment failures."""),
# anchor outputs channels
ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=True, type_=bool)
# zeroconf channels
ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
OPEN_ZEROCONF_CHANNELS = ConfigVar('open_zeroconf_channels', default=False, type_=bool)
ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
# minimum absolute fee in sat for which we will open a channel just in time
ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
# fee in ppm of the outgoing htlcs value we charge for opening new channels just in time
ZEROCONF_OPENING_FEE_PPM = ConfigVar('zeroconf_opening_fee_ppm', default=10_000, type_=int)
# size of the channel the lsp opens to the client in percent of the outgoing htlcs value
# (before deducting fees). required to be at least 120% to leave some buffer for the channel reserve
ZEROCONF_CHANNEL_SIZE_PERCENT = ConfigVar('zeroconf_channel_size_percent', default=200, type_=int)
LN_UTXO_RESERVE = ConfigVar(
'ln_utxo_reserve',
default=10000,
+1 -1
View File
@@ -3455,7 +3455,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
zeroconf_nodeid = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]
except Exception:
zeroconf_nodeid = None
can_get_zeroconf_channel = (self.lnworker and self.config.ACCEPT_ZEROCONF_CHANNELS
can_get_zeroconf_channel = (self.lnworker and self.config.OPEN_ZEROCONF_CHANNELS
and self.lnworker.lnpeermgr.get_peer_by_pubkey(zeroconf_nodeid) is not None)
status = self.get_invoice_status(req)