Skip to content

Commit 496f490

Browse files
committed
Improve JIT channel opening
1 parent 764cc78 commit 496f490

File tree

8 files changed

+172
-24
lines changed

8 files changed

+172
-24
lines changed

electrum/gui/qml/qerequestdetails.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,12 @@ def expiration(self):
118118

119119
@pyqtProperty(str, notify=detailsChanged)
120120
def bolt11(self):
121-
can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0
122-
if self._req and can_receive > 0 and (self._req.get_amount_sat() or 0) <= can_receive:
123-
bolt11 = self._wallet.wallet.get_bolt11_invoice(self._req)
121+
wallet = self._wallet.wallet
122+
amount_sat = self._req.get_amount_sat() or 0
123+
can_receive = wallet.lnworker.num_sats_can_receive() if wallet.lnworker else 0
124+
will_req_zeroconf = wallet.lnworker.receive_requires_jit_channel(amount_msat=amount_sat*1000)
125+
if self._req and ((can_receive > 0 and amount_sat <= can_receive) or will_req_zeroconf):
126+
bolt11 = wallet.get_bolt11_invoice(self._req)
124127
else:
125128
return ''
126129
# encode lightning invoices as uppercase so QR encoding can use

electrum/lnpeer.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,9 @@ def on_init(self, payload):
393393
if their_networks:
394394
their_chains = list(chunks(their_networks["chains"], 32))
395395
if constants.net.rev_genesis_bytes() not in their_chains:
396-
raise GracefulDisconnect(f"no common chain found with remote. (they sent: {their_chains})")
396+
raise GracefulDisconnect(f"no common chain found with remote. "
397+
f"(they sent: {[chain.hex() for chain in their_chains]}),"
398+
f" our chain: {constants.net.rev_genesis_bytes().hex()}")
397399
# all checks passed
398400
self.lnworker.on_peer_successfully_established(self)
399401
self._received_init = True
@@ -2221,7 +2223,7 @@ async def maybe_forward_trampoline(
22212223

22222224
# do we have a connection to the node?
22232225
next_peer = self.lnworker.peers.get(outgoing_node_id)
2224-
if next_peer and next_peer.accepts_zeroconf():
2226+
if next_peer and next_peer.accepts_zeroconf() and self.lnworker.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
22252227
self.logger.info(f'JIT: found next_peer')
22262228
for next_chan in next_peer.channels.values():
22272229
if next_chan.can_pay(amt_to_forward):

electrum/lnwatcher.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def add_channel(self, outpoint: str, address: str) -> None:
6565
cb = lambda: self.check_onchain_situation(address, outpoint)
6666
self.add_callback(address, cb)
6767

68-
async def unwatch_channel(self, address, funding_outpoint):
68+
def unwatch_channel(self, address, funding_outpoint):
6969
self.logger.info(f'unwatching {funding_outpoint}')
7070
self.remove_callback(address)
7171

@@ -128,7 +128,7 @@ async def check_onchain_situation(self, address, funding_outpoint):
128128
closing_height=closing_height,
129129
keep_watching=keep_watching)
130130
if not keep_watching:
131-
await self.unwatch_channel(address, funding_outpoint)
131+
self.unwatch_channel(address, funding_outpoint)
132132

133133
async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx: Transaction) -> bool:
134134
raise NotImplementedError() # implemented by subclasses

electrum/lnworker.py

+64-11
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
create_trampoline_route_and_onion, is_legacy_relay, trampolines_by_id, hardcoded_trampoline_nodes,
7777
is_hardcoded_trampoline
7878
)
79+
from .network import TxBroadcastServerReturnedError
7980

8081
if TYPE_CHECKING:
8182
from .network import Network
@@ -1129,7 +1130,7 @@ def get_lightning_history(self) -> Dict[str, LightningHistoryItem]:
11291130
balance_msat = sum([x.amount_msat for x in out.values()])
11301131
lb = sum(chan.balance(LOCAL) if not chan.is_closed_or_closing() else 0
11311132
for chan in self.channels.values())
1132-
assert balance_msat == lb
1133+
assert balance_msat == lb, f"balance_msat: {balance_msat} != lb: {lb}"
11331134
return out
11341135

11351136
def get_groups_for_onchain_history(self) -> Dict[str, str]:
@@ -1250,6 +1251,7 @@ async def open_channel_just_in_time(
12501251

12511252
# prevent settling the htlc until the channel opening was successfull so we can fail it if needed
12521253
self.dont_settle_htlcs[payment_hash.hex()] = None
1254+
funding_tx = None
12531255
try:
12541256
funding_sat = 2 * (next_amount_msat_htlc // 1000) # try to fully spend htlcs
12551257
password = self.wallet.get_unlocked_password() if self.wallet.has_password() else None
@@ -1268,10 +1270,9 @@ async def open_channel_just_in_time(
12681270
)
12691271
async def wait_for_channel():
12701272
while not next_chan.is_open():
1271-
await asyncio.sleep(1)
1273+
await asyncio.sleep(0.1)
12721274
await util.wait_for2(wait_for_channel(), LN_P2P_NETWORK_TIMEOUT)
1273-
next_chan.save_remote_scid_alias(self._scid_alias_of_node(next_peer.pubkey))
1274-
self.logger.info(f'JIT channel is open')
1275+
self.logger.info(f'JIT channel is open (funding not broadcasted yet)')
12751276
next_amount_msat_htlc -= channel_opening_fee
12761277
# fixme: some checks are missing
12771278
htlc = next_peer.send_htlc(
@@ -1282,30 +1283,82 @@ async def wait_for_channel():
12821283
onion=next_onion)
12831284
async def wait_for_preimage():
12841285
while self.get_preimage(payment_hash) is None:
1285-
await asyncio.sleep(1)
1286-
await util.wait_for2(wait_for_preimage(), LN_P2P_NETWORK_TIMEOUT)
1287-
1288-
# We have been paid and can broadcast
1289-
# todo: if broadcasting raise an exception, we should try to rebroadcast
1290-
await self.network.broadcast_transaction(funding_tx)
1286+
await asyncio.sleep(0.1)
1287+
try:
1288+
await util.wait_for2(wait_for_preimage(), LN_P2P_NETWORK_TIMEOUT)
1289+
except asyncio.TimeoutError:
1290+
self.logger.info(
1291+
f"jit opening didn't get preimage, removing chan {next_chan.get_id_for_log()} again")
1292+
await self.cleanup_failed_jit_channel(next_chan)
1293+
raise
12911294
except OnionRoutingFailure:
12921295
raise
12931296
except Exception:
12941297
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
1298+
1299+
disable_zeroconf = True
1300+
try:
1301+
# after 10 mins `update_unfunded_state` will remove the channel on client side so we can fail here too
1302+
await util.wait_for2(self.broadcast_jit_channel_and_wait_for_mempool(next_chan, funding_tx), 60 * 10 - 30)
1303+
disable_zeroconf = False
1304+
except Exception:
1305+
# the risk of the funding tx getting mined later is low as we weren't able to get it into the mempool
1306+
self.preimages.pop(payment_hash.hex(), None)
1307+
await self.cleanup_failed_jit_channel(next_chan)
1308+
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
12951309
finally:
1310+
if disable_zeroconf:
1311+
self.logger.warning(f"disabling zeroconf channels to prevent further issues. Check your wallet for consistency.")
1312+
self.config.ACCEPT_ZEROCONF_CHANNELS = False
1313+
self.features &= ~LnFeatures.OPTION_ZEROCONF_OPT
12961314
del self.dont_settle_htlcs[payment_hash.hex()]
12971315

12981316
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), htlc.htlc_id)
12991317
return htlc_key
13001318

1319+
async def cleanup_failed_jit_channel(self, chan: Channel):
1320+
"""Closes a channel that has no published funding tx (e.g. in case of a failed jit open)"""
1321+
try:
1322+
# try to send shutdown to signal peer that channel is dead
1323+
await util.wait_for2(self.close_channel(chan.channel_id), LN_P2P_NETWORK_TIMEOUT)
1324+
except Exception:
1325+
self.logger.debug(f"chan shutdown to failed zeroconf peer failed ", exc_info=True)
1326+
chan.set_state(ChannelState.REDEEMED, force=True)
1327+
self.lnwatcher.adb.remove_transaction(chan.funding_outpoint.txid)
1328+
self.lnwatcher.unwatch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str())
1329+
self.remove_channel(chan.channel_id)
1330+
1331+
async def broadcast_jit_channel_and_wait_for_mempool(self, channel: Channel, funding_tx: Transaction) -> None:
1332+
last_broadcast_attempt = 0
1333+
while True:
1334+
if time.time() - last_broadcast_attempt > 60:
1335+
try:
1336+
await self.network.broadcast_transaction(funding_tx)
1337+
self.logger.info(f"broadcasted jit channel open txid: {funding_tx.txid()}")
1338+
except TxBroadcastServerReturnedError:
1339+
self.logger.error(f"we constructed a weird JIT funding tx. Reverting channel again.", exc_info=True)
1340+
raise
1341+
except Exception:
1342+
self.logger.warning(f"Broadcasting jit channel open tx {funding_tx.txid()} failed.", exc_info=True)
1343+
last_broadcast_attempt = time.time()
1344+
1345+
# check if the funding tx is at least in the mempool by now
1346+
funding_info = channel.get_funding_height()
1347+
if funding_info is not None:
1348+
_, height, _ = funding_info
1349+
if height > TX_HEIGHT_LOCAL:
1350+
return
1351+
1352+
await asyncio.sleep(1)
1353+
13011354
@log_exceptions
13021355
async def open_channel_with_peer(
13031356
self, peer, funding_sat, *,
13041357
push_sat: int = 0,
13051358
public: bool = False,
13061359
zeroconf: bool = False,
13071360
opening_fee: int = None,
1308-
password=None):
1361+
password=None) -> Tuple[Channel, PartialTransaction]:
13091362
if self.config.ENABLE_ANCHOR_CHANNELS:
13101363
self.wallet.unlock(password)
13111364
coins = self.wallet.get_spendable_coins(None)

electrum/plugins/watchtower/watchtower.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ async def f():
189189
return self.network.run_from_another_thread(f())
190190

191191
async def unwatch_channel(self, address, funding_outpoint):
192-
await super().unwatch_channel(address, funding_outpoint)
192+
super().unwatch_channel(address, funding_outpoint)
193193
await self.sweepstore.remove_sweep_tx(funding_outpoint)
194194
await self.sweepstore.remove_channel(funding_outpoint)
195195
if funding_outpoint in self.tx_progress:

electrum/simple_config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ def __setattr__(self, name, value):
830830
# anchor outputs channels
831831
ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=False, type_=bool)
832832
# zeroconf channels
833-
ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
833+
ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=True, type_=bool)
834834
ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
835835
ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
836836

tests/regtest.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,17 @@ class TestLightningJIT(TestLightning):
135135
}
136136
}
137137

138-
def test_just_in_time(self):
139-
self.run_shell(['just_in_time'])
138+
def test_just_in_time_no_channel(self):
139+
# jit payment to node without existing channels
140+
self.run_shell(['just_in_time_no_channel'])
141+
142+
def test_just_in_time_existing_channels(self):
143+
# payment should not open additional channels as there is already a sufficient channel
144+
self.run_shell(['just_in_time_existing_channels'])
145+
146+
def test_just_in_time_failed_htlc(self):
147+
# jit client fails htlc, LSP doesn't get preimage. No channel should be opened.
148+
self.run_shell(['just_in_time_failed_htlc'])
140149

141150

142151
class TestLightningJITTrampoline(TestLightningJIT):

tests/regtest/regtest.sh

+83-2
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ if [[ $1 == "watchtower" ]]; then
481481
wait_until_spent $ctx_id $output_index # alice's to_local gets punished
482482
fi
483483

484-
if [[ $1 == "just_in_time" ]]; then
484+
if [[ $1 == "just_in_time_no_channel" ]]; then
485485
bob_node=$($bob nodeid)
486486
$alice setconfig zeroconf_trusted_node $bob_node
487487
$alice setconfig use_recoverable_channels false
@@ -493,7 +493,88 @@ if [[ $1 == "just_in_time" ]]; then
493493
echo "carol pays alice"
494494
# note: set amount to 0.001 to test failure: 'payment too low'
495495
invoice=$($alice add_request 0.01 --lightning -m "invoice" | jq -r ".lightning_invoice")
496-
$carol lnpay $invoice
496+
success=$($carol lnpay $invoice | jq -r ".success")
497+
if [[ "$success" != "true" ]]; then
498+
echo "jit payment failed"
499+
exit 1
500+
fi
501+
# try again, multiple jit openings should work without issues
502+
new_blocks 3
503+
echo "carol pays alice again"
504+
invoice=$($alice add_request 0.04 --lightning -m "invoice" | jq -r ".lightning_invoice")
505+
success=$($carol lnpay $invoice | jq -r ".success")
506+
if [[ "$success" != "true" ]]; then
507+
echo "jit payment failed"
508+
exit 1
509+
fi
510+
alice_chan_count=$($alice list_channels | jq '. | length')
511+
if [[ "$alice_chan_count" != "2" ]]; then
512+
echo "alice should have two jit channels"
513+
exit 1
514+
fi
515+
fi
516+
517+
if [[ $1 == "just_in_time_existing_channels" ]]; then
518+
bob_node=$($bob nodeid)
519+
alice_node=$($alice nodeid)
520+
$alice setconfig zeroconf_trusted_node $bob_node
521+
$alice setconfig use_recoverable_channels false
522+
wait_for_balance bob 1
523+
wait_for_balance carol 1
524+
echo "carol opens channel with bob"
525+
$carol open_channel $bob_node 0.15 --password=''
526+
echo "bob opens channel with alice"
527+
$bob open_channel $alice_node 0.15 --password=''
528+
new_blocks 3
529+
wait_until_channel_open carol
530+
wait_until_channel_open bob
531+
echo "carol pays alice"
532+
# this should not open an additional channel
533+
invoice=$($alice add_request 0.12 --lightning -m "invoice" | jq -r ".lightning_invoice")
534+
success=$($carol lnpay $invoice | jq -r ".success")
535+
if [[ "$success" != "true" ]]; then
536+
echo "jit payment failed"
537+
exit 1
538+
fi
539+
alice_chan_count=$($alice list_channels | jq '. | length')
540+
if [[ "$alice_chan_count" != "1" ]]; then
541+
echo "alice should not have channel an additional channel"
542+
exit 1
543+
fi
544+
fi
545+
546+
547+
if [[ $1 == "just_in_time_failed_htlc" ]]; then
548+
bob_node=$($bob nodeid)
549+
$alice setconfig zeroconf_trusted_node $bob_node
550+
$alice setconfig use_recoverable_channels false
551+
$alice setconfig test_fail_htlcs_with_temp_node_failure true
552+
wait_for_balance carol 1
553+
echo "carol opens channel with bob"
554+
$carol open_channel $bob_node 0.15 --password=''
555+
new_blocks 3
556+
wait_until_channel_open carol
557+
echo "carol tries to pay alice (but alice will fail htlc)"
558+
invoice=$($alice add_request 0.02 --lightning -m "invoice" | jq -r ".lightning_invoice")
559+
success=$($carol lnpay $invoice | jq -r ".success") # this will fail
560+
561+
bob_chan_count=$($bob list_channels | jq '. | length')
562+
if [[ "$bob_chan_count" != "1" ]]; then # failed jit channels should be removed directly
563+
echo "bob should have only one channel\n$($bob list_channels)"
564+
exit 1
565+
fi
566+
alice_chan_state=$($alice list_channels | jq -r '.[0].state')
567+
if [[ "$alice_chan_state" != "CLOSING" ]]; then
568+
echo "alice failed JIT channel should be in closing state\n$($alice list_channels)"
569+
exit 1
570+
fi
571+
# after a new block triggers one iteration of update_unfunded_state the channel should be removed
572+
new_blocks 1
573+
alice_chan_count=$($alice list_channels | jq '. | length')
574+
if [[ "$alice_chan_count" != "0" ]]; then
575+
echo "alice should have no channel\n$($alice list_channels)"
576+
exit 1
577+
fi
497578
fi
498579

499580
if [[ $1 == "unixsockets" ]]; then

0 commit comments

Comments
 (0)