Skip to content

Commit ccb9773

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

File tree

6 files changed

+143
-49
lines changed

6 files changed

+143
-49
lines changed

electrum/lnpeer.py

+13-10
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
@@ -967,7 +969,7 @@ async def channel_establishment_flow(
967969
public: bool,
968970
zeroconf: bool = False,
969971
temp_channel_id: bytes,
970-
opening_fee: int = None,
972+
opening_fee_msat: int = None,
971973
) -> Tuple[Channel, 'PartialTransaction']:
972974
"""Implements the channel opening flow.
973975
@@ -1026,10 +1028,10 @@ async def channel_establishment_flow(
10261028
open_channel_tlvs['upfront_shutdown_script'] = {
10271029
'shutdown_scriptpubkey': local_config.upfront_shutdown_script
10281030
}
1029-
if opening_fee:
1031+
if opening_fee_msat:
10301032
# todo: maybe add payment hash
10311033
open_channel_tlvs['channel_opening_fee'] = {
1032-
'channel_opening_fee': opening_fee
1034+
'channel_opening_fee': opening_fee_msat
10331035
}
10341036
# for the first commitment transaction
10351037
per_commitment_secret_first = get_per_commitment_secret_from_seed(
@@ -1258,10 +1260,10 @@ async def on_open_channel(self, payload):
12581260
# store the temp id now, so that it is recognized for e.g. 'error' messages
12591261
# TODO: this is never cleaned up; the dict grows unbounded until disconnect
12601262
self.temp_id_to_id[temp_chan_id] = None
1261-
channel_opening_fee = open_channel_tlvs.get('channel_opening_fee') if open_channel_tlvs else None
1262-
if channel_opening_fee:
1263-
# todo check that the fee is reasonable
1264-
pass
1263+
channel_opening_fee_msat = open_channel_tlvs.get('channel_opening_fee') if open_channel_tlvs else None
1264+
if channel_opening_fee_msat:
1265+
if channel_opening_fee_msat // 1000 > funding_sat * 0.1:
1266+
raise Exception(f"Channel opening fee is too expensive, rejecting channel")
12651267

12661268
if self.use_anchors():
12671269
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(
@@ -1375,7 +1377,7 @@ async def on_open_channel(self, payload):
13751377
chan_dict,
13761378
lnworker=self.lnworker,
13771379
initial_feerate=feerate,
1378-
opening_fee = channel_opening_fee,
1380+
opening_fee = channel_opening_fee_msat,
13791381
)
13801382
chan.storage['init_timestamp'] = int(time.time())
13811383
if isinstance(self.transport, LNTransport):
@@ -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):
@@ -2484,6 +2486,7 @@ def log_fail_reason(reason: str):
24842486
raise exc_incorrect_or_unknown_pd
24852487
invoice_msat = info.amount_msat
24862488
if channel_opening_fee:
2489+
# TODO: we should remove the fee once paid?
24872490
invoice_msat -= channel_opening_fee
24882491

24892492
if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat):

electrum/lnutil.py

+1
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,7 @@ def list_enabled_ln_feature_bits(features: int) -> tuple[int, ...]:
17061706
return tuple(sorted(single_feature_bits))
17071707

17081708

1709+
class FeeTooLow(Exception): pass
17091710
class IncompatibleOrInsaneFeatures(Exception): pass
17101711
class UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass
17111712
class IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass

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

+122-34
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_FOR_INVOICE, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,
6363
ShortChannelID, HtlcLog, NoPathFound, InvalidGossipMsg, FeeBudgetExceeded, ImportedChannelBackupStorage,
6464
OnchainChannelBackupStorage, ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget,
65-
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage
65+
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage, ZEROCONF_TIMEOUT, FeeTooLow
6666
)
6767
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket
6868
from .lnmsg import decode_msg
@@ -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
@@ -892,6 +893,10 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv):
892893
self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self)
893894
self.onion_message_manager = OnionMessageManager(self)
894895

896+
# to keep track of the channels we sold as just-in-time provider
897+
if self.config.ACCEPT_ZEROCONF_CHANNELS:
898+
self.sold_just_in_time_channels = self.db.get_dict('sold_just_in_time_channels') # type: Dict[str, int] # channel_id -> revenue sat (opening fee - funding tx fee)
899+
895900
def has_deterministic_node_id(self) -> bool:
896901
return bool(self.db.get('lightning_xprv'))
897902

@@ -1129,7 +1134,7 @@ def get_lightning_history(self) -> Dict[str, LightningHistoryItem]:
11291134
balance_msat = sum([x.amount_msat for x in out.values()])
11301135
lb = sum(chan.balance(LOCAL) if not chan.is_closed_or_closing() else 0
11311136
for chan in self.channels.values())
1132-
assert balance_msat == lb
1137+
assert balance_msat == lb, f"balance_msat: {balance_msat} != lb: {lb}"
11331138
return out
11341139

11351140
def get_groups_for_onchain_history(self) -> Dict[str, str]:
@@ -1235,7 +1240,6 @@ def _scid_alias_of_node(self, nodeid: bytes) -> bytes:
12351240
def get_static_jit_scid_alias(self) -> bytes:
12361241
return self._scid_alias_of_node(self.node_keypair.pubkey)
12371242

1238-
@log_exceptions
12391243
async def open_channel_just_in_time(
12401244
self,
12411245
*,
@@ -1244,36 +1248,69 @@ async def open_channel_just_in_time(
12441248
next_cltv_abs: int,
12451249
payment_hash: bytes,
12461250
next_onion: OnionPacket,
1247-
) -> str:
1251+
) -> Optional[str]:
1252+
"""Wrapper around __open_channel_just_in_time to allow for cleaner htlc locking and preimage deletion"""
1253+
1254+
# prevent settling the htlc until the channel opening was successfully so we can fail it if needed
1255+
self.dont_settle_htlcs[payment_hash.hex()] = None
1256+
try:
1257+
return await self.__open_channel_just_in_time(
1258+
next_peer=next_peer,
1259+
next_amount_msat_htlc=next_amount_msat_htlc,
1260+
next_cltv_abs=next_cltv_abs,
1261+
payment_hash=payment_hash,
1262+
next_onion=next_onion,
1263+
)
1264+
except Exception:
1265+
self.logger.warning(f"failed to open jit channel", exc_info=True)
1266+
# ensure that we not accidentally store a preimage on exception
1267+
self.preimages.pop(payment_hash.hex(), None)
1268+
raise
1269+
finally:
1270+
del self.dont_settle_htlcs[payment_hash.hex()]
1271+
1272+
@log_exceptions
1273+
async def __open_channel_just_in_time(
1274+
self,
1275+
*,
1276+
next_peer: Peer,
1277+
next_amount_msat_htlc: int,
1278+
next_cltv_abs: int,
1279+
payment_hash: bytes,
1280+
next_onion: OnionPacket,
1281+
) -> Optional[str]:
12481282
# if an exception is raised during negotiation, we raise an OnionRoutingFailure.
12491283
# this will cancel the incoming HTLC
1284+
assert self.config.ZEROCONF_RELATIVE_OPENING_FEE_PPM > 0, self.config.ZEROCONF_MIN_OPENING_FEE_SAT > 0
12501285

1251-
# prevent settling the htlc until the channel opening was successfull so we can fail it if needed
1252-
self.dont_settle_htlcs[payment_hash.hex()] = None
1286+
next_chan = None
12531287
try:
12541288
funding_sat = 2 * (next_amount_msat_htlc // 1000) # try to fully spend htlcs
12551289
password = self.wallet.get_unlocked_password() if self.wallet.has_password() else None
1256-
channel_opening_fee = next_amount_msat_htlc // 100
1257-
if channel_opening_fee // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE:
1258-
self.logger.info(f'rejecting JIT channel: payment too low')
1290+
channel_opening_fee_msat = (next_amount_msat_htlc * self.config.ZEROCONF_RELATIVE_OPENING_FEE_PPM) // 1_000_000
1291+
if channel_opening_fee_msat // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE_SAT:
1292+
self.logger.info(f'rejecting JIT channel: payment too low: posible fee '
1293+
f'{channel_opening_fee_msat//1000} sat < min fee {self.config.ZEROCONF_MIN_OPENING_FEE_SAT} sat')
1294+
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low')
1295+
self.logger.info(f'jit channel opening fee (sats): {channel_opening_fee_msat//1000}')
1296+
try:
1297+
next_chan, funding_tx = await self.open_channel_with_peer(
1298+
next_peer, funding_sat,
1299+
push_sat=0,
1300+
zeroconf=True,
1301+
public=False,
1302+
opening_fee_msat=channel_opening_fee_msat,
1303+
password=password,
1304+
)
1305+
except FeeTooLow:
1306+
self.logger.info(f"rejecting JIT channel: tx fee too high in relation to revenue", exc_info=True)
12591307
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low')
1260-
self.logger.info(f'channel opening fee (sats): {channel_opening_fee//1000}')
1261-
next_chan, funding_tx = await self.open_channel_with_peer(
1262-
next_peer, funding_sat,
1263-
push_sat=0,
1264-
zeroconf=True,
1265-
public=False,
1266-
opening_fee=channel_opening_fee,
1267-
password=password,
1268-
)
12691308
async def wait_for_channel():
12701309
while not next_chan.is_open():
1271-
await asyncio.sleep(1)
1310+
await asyncio.sleep(0.1)
12721311
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-
next_amount_msat_htlc -= channel_opening_fee
1276-
# fixme: some checks are missing
1312+
self.logger.info(f'JIT channel is open (funding not broadcasted yet)')
1313+
next_amount_msat_htlc -= channel_opening_fee_msat
12771314
htlc = next_peer.send_htlc(
12781315
chan=next_chan,
12791316
payment_hash=payment_hash,
@@ -1282,30 +1319,78 @@ async def wait_for_channel():
12821319
onion=next_onion)
12831320
async def wait_for_preimage():
12841321
while self.get_preimage(payment_hash) is None:
1285-
await asyncio.sleep(1)
1322+
await asyncio.sleep(0.1)
12861323
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)
12911324
except OnionRoutingFailure:
12921325
raise
12931326
except Exception:
1327+
if next_chan:
1328+
# the chan was already established, so it has to get cleaned up again
1329+
await self.cleanup_failed_jit_channel(next_chan)
1330+
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
1331+
1332+
disable_zeroconf = True
1333+
try:
1334+
# after 10 mins `update_unfunded_state` will remove the channel on client side so we can fail here too
1335+
await util.wait_for2(self.broadcast_jit_channel_and_wait_for_mempool(next_chan, funding_tx), ZEROCONF_TIMEOUT - 30)
1336+
disable_zeroconf = False
1337+
except Exception:
1338+
# the risk of the funding tx getting mined later is low as we weren't able to get it into the mempool
1339+
await self.cleanup_failed_jit_channel(next_chan)
12941340
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
12951341
finally:
1296-
del self.dont_settle_htlcs[payment_hash.hex()]
1342+
if disable_zeroconf:
1343+
self.logger.warning(f"disabling zeroconf channels to prevent further issues. Check your wallet for consistency.")
1344+
self.config.ACCEPT_ZEROCONF_CHANNELS = False
1345+
self.features &= ~LnFeatures.OPTION_ZEROCONF_OPT
12971346

1347+
self.sold_just_in_time_channels[funding_tx.txid()] = channel_opening_fee_msat // 1000 - funding_tx.get_fee()
12981348
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), htlc.htlc_id)
12991349
return htlc_key
13001350

1351+
async def cleanup_failed_jit_channel(self, chan: Channel):
1352+
"""Closes a channel that has no published funding tx (e.g. in case of a failed jit open)"""
1353+
try:
1354+
# try to send shutdown to signal peer that channel is dead
1355+
await util.wait_for2(self.close_channel(chan.channel_id), LN_P2P_NETWORK_TIMEOUT)
1356+
except Exception:
1357+
self.logger.debug(f"chan shutdown to failed zeroconf peer failed ", exc_info=True)
1358+
chan.set_state(ChannelState.REDEEMED, force=True)
1359+
self.lnwatcher.adb.remove_transaction(chan.funding_outpoint.txid)
1360+
self.lnwatcher.unwatch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str())
1361+
self.remove_channel(chan.channel_id)
1362+
1363+
async def broadcast_jit_channel_and_wait_for_mempool(self, channel: Channel, funding_tx: Transaction) -> None:
1364+
last_broadcast_attempt = 0
1365+
while True:
1366+
if time.time() - last_broadcast_attempt > 60:
1367+
try:
1368+
await self.network.broadcast_transaction(funding_tx)
1369+
self.logger.info(f"broadcasted jit channel open txid: {funding_tx.txid()}")
1370+
except TxBroadcastServerReturnedError:
1371+
self.logger.error(f"we constructed a weird JIT funding tx. Reverting channel again.", exc_info=True)
1372+
raise
1373+
except Exception:
1374+
self.logger.warning(f"Broadcasting jit channel open tx {funding_tx.txid()} failed.", exc_info=True)
1375+
last_broadcast_attempt = time.time()
1376+
1377+
# check if the funding tx is at least in the mempool by now
1378+
funding_info = channel.get_funding_height()
1379+
if funding_info is not None:
1380+
_, height, _ = funding_info
1381+
if height > TX_HEIGHT_LOCAL:
1382+
return
1383+
1384+
await asyncio.sleep(1)
1385+
13011386
@log_exceptions
13021387
async def open_channel_with_peer(
13031388
self, peer, funding_sat, *,
13041389
push_sat: int = 0,
13051390
public: bool = False,
13061391
zeroconf: bool = False,
1307-
opening_fee: int = None,
1308-
password=None):
1392+
opening_fee_msat: int = None,
1393+
password=None) -> Tuple[Channel, PartialTransaction]:
13091394
if self.config.ENABLE_ANCHOR_CHANNELS:
13101395
self.wallet.unlock(password)
13111396
coins = self.wallet.get_spendable_coins(None)
@@ -1316,14 +1401,17 @@ async def open_channel_with_peer(
13161401
funding_sat=funding_sat,
13171402
node_id=node_id,
13181403
fee_policy=fee_policy)
1404+
if opening_fee_msat and funding_tx.get_fee() * 1000 > opening_fee_msat * 0.5:
1405+
raise FeeTooLow(f"This channel open is too expensive: fees paid: {funding_tx.get_fee()} sat. "
1406+
f"opening fee: {opening_fee_msat // 1000} sat.")
13191407
chan, funding_tx = await self._open_channel_coroutine(
13201408
peer=peer,
13211409
funding_tx=funding_tx,
13221410
funding_sat=funding_sat,
13231411
push_sat=push_sat,
13241412
public=public,
13251413
zeroconf=zeroconf,
1326-
opening_fee=opening_fee,
1414+
opening_fee_msat=opening_fee_msat,
13271415
password=password)
13281416
return chan, funding_tx
13291417

@@ -1336,7 +1424,7 @@ async def _open_channel_coroutine(
13361424
push_sat: int,
13371425
public: bool,
13381426
zeroconf=False,
1339-
opening_fee=None,
1427+
opening_fee_msat=None,
13401428
password: Optional[str],
13411429
) -> Tuple[Channel, PartialTransaction]:
13421430

@@ -1351,7 +1439,7 @@ async def _open_channel_coroutine(
13511439
push_msat=push_sat * 1000,
13521440
public=public,
13531441
zeroconf=zeroconf,
1354-
opening_fee=opening_fee,
1442+
opening_fee=opening_fee_msat,
13551443
temp_channel_id=os.urandom(32))
13561444
chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT)
13571445
util.trigger_callback('channels_updated', self.wallet)

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -830,9 +830,11 @@ 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)
835-
ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
835+
ZEROCONF_MIN_OPENING_FEE_SAT = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
836+
# fee deducted from the amount to be forwarded in ppm when opening a zeroconf channel, 1% == 100000 ppm
837+
ZEROCONF_RELATIVE_OPENING_FEE_PPM = ConfigVar('zeroconf_relative_opening_fee', default=10000, type_=float)
836838

837839
# connect to remote WT
838840
WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)

0 commit comments

Comments
 (0)