Skip to content

Commit a12f38f

Browse files
committed
Improve JIT channel opening
1 parent 5ce8033 commit a12f38f

File tree

9 files changed

+284
-63
lines changed

9 files changed

+284
-63
lines changed

electrum/lnchannel.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo
341341
def update_unfunded_state(self) -> None:
342342
self.delete_funding_height()
343343
self.delete_closing_height()
344-
if self.get_state() in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING] and self.lnworker:
344+
if not self.lnworker:
345+
return
346+
chan_age = now() - self.storage.get('init_timestamp', 0)
347+
state = self.get_state()
348+
if state in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING]:
345349
if self.is_initiator():
346350
# set channel state to REDEEMED so that it can be removed manually
347351
# to protect ourselves against a server lying by omission,
@@ -361,8 +365,24 @@ def update_unfunded_state(self) -> None:
361365
self.set_state(ChannelState.REDEEMED)
362366
break
363367
else:
364-
if self.lnworker and (now() - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT):
365-
self.lnworker.remove_channel(self.channel_id)
368+
if chan_age > CHANNEL_OPENING_TIMEOUT:
369+
self.lnworker.remove_channel(self.channel_id)
370+
elif self.is_zeroconf() and state in [ChannelState.OPEN, ChannelState.CLOSING, ChannelState.FORCE_CLOSING]:
371+
# handling zeroconf channels with no funding tx, can happen if broadcasting fails on LSP side
372+
# or if the LSP did double spent the funding tx/never published it intentionally
373+
if state != ChannelState.OPEN or chan_age > 60 * 10 and 'init_timestamp' in self.storage:
374+
# we delete the channel if its in closing state (either initiated manually by client or by LSP on failure)
375+
# or if the channel is not seeing any funding tx after 10 minutes to prevent further usage (limit damage)
376+
self.set_state(ChannelState.REDEEMED, force=True)
377+
local_balance_sat = int(self.balance(LOCAL) // 1000)
378+
if local_balance_sat > 0:
379+
self.logger.warning(
380+
f"we may have been scammed out of {local_balance_sat} sat by our "
381+
f"JIT provider: {self.lnworker.config.ZEROCONF_TRUSTED_NODE} or he didn't use our preimage")
382+
self.lnworker.config.ZEROCONF_TRUSTED_NODE = ''
383+
self.lnworker.lnwatcher.unwatch_channel(self.get_funding_address(), self.funding_outpoint.to_str())
384+
self.lnworker.lnwatcher.adb.remove_transaction(self.funding_outpoint.txid)
385+
self.lnworker.remove_channel(self.channel_id)
366386

367387
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:
368388
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
@@ -407,6 +427,10 @@ def is_initiator(self) -> bool:
407427
def is_public(self) -> bool:
408428
pass
409429

430+
@abstractmethod
431+
def is_zeroconf(self) -> bool:
432+
pass
433+
410434
@abstractmethod
411435
def is_funding_tx_mined(self, funding_height: TxMinedInfo) -> bool:
412436
pass
@@ -660,6 +684,9 @@ def get_sweep_address(self) -> str:
660684
def has_anchors(self) -> Optional[bool]:
661685
return None
662686

687+
def is_zeroconf(self) -> bool:
688+
return False
689+
663690
def get_local_pubkey(self) -> bytes:
664691
cb = self.cb
665692
assert isinstance(cb, ChannelBackupStorage)

electrum/lnpeer.py

+39-26
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,9 @@ def on_init(self, payload):
387387
if their_networks:
388388
their_chains = list(chunks(their_networks["chains"], 32))
389389
if constants.net.rev_genesis_bytes() not in their_chains:
390-
raise GracefulDisconnect(f"no common chain found with remote. (they sent: {their_chains})")
390+
raise GracefulDisconnect(f"no common chain found with remote. "
391+
f"(they sent: {[chain.hex() for chain in their_chains]}),"
392+
f" our chain: {constants.net.rev_genesis_bytes().hex()}")
391393
# all checks passed
392394
self.lnworker.on_peer_successfully_established(self)
393395
self._received_init = True
@@ -963,6 +965,7 @@ async def channel_establishment_flow(
963965
)
964966
chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]
965967
chan.storage['has_onchain_backup'] = has_onchain_backup
968+
chan.storage['init_timestamp'] = int(time.time())
966969
if isinstance(self.transport, LNTransport):
967970
chan.add_or_update_peer_addr(self.transport.peer_addr)
968971
sig_64, _ = chan.sign_next_commitment()
@@ -1023,27 +1026,13 @@ async def on_open_channel(self, payload):
10231026
10241027
Channel configurations are initialized in this method.
10251028
"""
1026-
if self.lnworker.has_recoverable_channels():
1027-
# FIXME: we might want to keep the connection open
1028-
raise Exception('not accepting channels')
1029+
10291030
# <- open_channel
10301031
if payload['chain_hash'] != constants.net.rev_genesis_bytes():
10311032
raise Exception('wrong chain_hash')
1032-
funding_sat = payload['funding_satoshis']
1033-
push_msat = payload['push_msat']
1034-
feerate = payload['feerate_per_kw'] # note: we are not validating this
1035-
temp_chan_id = payload['temporary_channel_id']
1036-
# store the temp id now, so that it is recognized for e.g. 'error' messages
1037-
# TODO: this is never cleaned up; the dict grows unbounded until disconnect
1038-
self.temp_id_to_id[temp_chan_id] = None
10391033

10401034
open_channel_tlvs = payload.get('open_channel_tlvs')
10411035
channel_type = open_channel_tlvs.get('channel_type') if open_channel_tlvs else None
1042-
1043-
channel_opening_fee = open_channel_tlvs.get('channel_opening_fee') if open_channel_tlvs else None
1044-
if channel_opening_fee:
1045-
# todo check that the fee is reasonable
1046-
pass
10471036
# The receiving node MAY fail the channel if:
10481037
# option_channel_type was negotiated but the message doesn't include a channel_type
10491038
if self.is_channel_type() and channel_type is None:
@@ -1054,6 +1043,26 @@ async def on_open_channel(self, payload):
10541043
channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()
10551044
if not channel_type.complies_with_features(self.features):
10561045
raise Exception("sender has sent a channel type we don't support")
1046+
is_zeroconf = channel_type & channel_type.OPTION_ZEROCONF
1047+
if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):
1048+
raise Exception(f"not accepting zeroconf from node {self.pubkey}")
1049+
1050+
if self.lnworker.has_recoverable_channels() and not is_zeroconf:
1051+
# FIXME: we might want to keep the connection open
1052+
raise Exception('not accepting channels')
1053+
funding_sat = payload['funding_satoshis']
1054+
push_msat = payload['push_msat']
1055+
feerate = payload['feerate_per_kw'] # note: we are not validating this
1056+
temp_chan_id = payload['temporary_channel_id']
1057+
# store the temp id now, so that it is recognized for e.g. 'error' messages
1058+
# TODO: this is never cleaned up; the dict grows unbounded until disconnect
1059+
self.temp_id_to_id[temp_chan_id] = None
1060+
1061+
1062+
channel_opening_fee = open_channel_tlvs.get('channel_opening_fee') if open_channel_tlvs else None
1063+
if channel_opening_fee:
1064+
# todo check that the fee is reasonable
1065+
pass
10571066

10581067
if self.use_anchors():
10591068
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(
@@ -1115,9 +1124,6 @@ async def on_open_channel(self, payload):
11151124
per_commitment_point_first = secret_to_pubkey(
11161125
int.from_bytes(per_commitment_secret_first, 'big'))
11171126

1118-
is_zeroconf = channel_type & channel_type.OPTION_ZEROCONF
1119-
if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):
1120-
raise Exception(f"not accepting zeroconf from node {self.pubkey}")
11211127
min_depth = 0 if is_zeroconf else 3
11221128

11231129
accept_channel_tlvs = {
@@ -1870,7 +1876,7 @@ def log_fail_reason(reason: str):
18701876
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
18711877

18721878
if self.lnworker.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
1873-
next_peer = self.lnworker.get_peer_by_scid_alias(next_chan_scid)
1879+
next_peer = self.lnworker.get_peer_by_static_jit_scid_alias(next_chan_scid)
18741880
else:
18751881
next_peer = None
18761882

@@ -1881,6 +1887,9 @@ def log_fail_reason(reason: str):
18811887
if next_chan.can_pay(next_amount_msat_htlc):
18821888
break
18831889
else:
1890+
htlc_id = serialize_htlc_key(incoming_chan.get_scid_or_local_alias(), htlc.htlc_id)
1891+
# prevent settling the htlc until the channel opening was successfull so we can fail it if needed
1892+
self.lnworker.dont_settle_htlc_keys[htlc_id] = None
18841893
return await self.lnworker.open_channel_just_in_time(
18851894
next_peer=next_peer,
18861895
next_amount_msat_htlc=next_amount_msat_htlc,
@@ -1954,6 +1963,7 @@ async def maybe_forward_trampoline(
19541963
outer_onion: ProcessedOnionPacket,
19551964
trampoline_onion: ProcessedOnionPacket,
19561965
fw_payment_key: str,
1966+
inc_htlc_key: str,
19571967
) -> None:
19581968

19591969
forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS
@@ -2016,7 +2026,7 @@ async def maybe_forward_trampoline(
20162026

20172027
# do we have a connection to the node?
20182028
next_peer = self.lnworker.peers.get(outgoing_node_id)
2019-
if next_peer and next_peer.accepts_zeroconf():
2029+
if next_peer and next_peer.accepts_zeroconf() and self.lnworker.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
20202030
self.logger.info(f'JIT: found next_peer')
20212031
for next_chan in next_peer.channels.values():
20222032
if next_chan.can_pay(amt_to_forward):
@@ -2043,6 +2053,7 @@ async def maybe_forward_trampoline(
20432053
payment_secret=payment_secret,
20442054
trampoline_onion=next_trampoline_onion,
20452055
)
2056+
self.lnworker.dont_settle_htlc_keys[inc_htlc_key] = None
20462057
await self.lnworker.open_channel_just_in_time(
20472058
next_peer=next_peer,
20482059
next_amount_msat_htlc=amt_to_forward,
@@ -2182,16 +2193,16 @@ def maybe_fulfill_htlc(
21822193
Decide what to do with an HTLC: return preimage if it can be fulfilled, forwarding callback if it can be forwarded.
21832194
Return (preimage, (payment_key, callback)) with at most a single element not None.
21842195
"""
2196+
htlc_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)
21852197
if not processed_onion.are_we_final:
21862198
if not self.lnworker.enable_htlc_forwarding:
21872199
return None, None
21882200
# use the htlc key if we are forwarding
2189-
payment_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)
21902201
callback = lambda: self.maybe_forward_htlc(
21912202
incoming_chan=chan,
21922203
htlc=htlc,
21932204
processed_onion=processed_onion)
2194-
return None, (payment_key, callback)
2205+
return None, (htlc_key, callback)
21952206

21962207
def log_fail_reason(reason: str):
21972208
self.logger.info(
@@ -2260,7 +2271,8 @@ def log_fail_reason(reason: str):
22602271
inc_cltv_abs=htlc.cltv_abs, # TODO: use max or enforce same value across mpp parts
22612272
outer_onion=processed_onion,
22622273
trampoline_onion=trampoline_onion,
2263-
fw_payment_key=payment_key)
2274+
fw_payment_key=payment_key,
2275+
inc_htlc_key=htlc_key)
22642276
return None, (payment_key, callback)
22652277

22662278
# TODO don't accept payments twice for same invoice
@@ -2853,10 +2865,11 @@ async def wrapped_callback():
28532865
forwarding_coro = forwarding_callback()
28542866
try:
28552867
next_htlc = await forwarding_coro
2856-
if next_htlc:
2868+
if next_htlc and payment_key in self.lnworker.active_forwardings:
28572869
htlc_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)
28582870
self.lnworker.active_forwardings[payment_key].append(next_htlc)
28592871
self.lnworker.downstream_to_upstream_htlc[next_htlc] = htlc_key
2872+
self.lnworker.dont_settle_htlc_keys.pop(htlc_key, None)
28602873
except OnionRoutingFailure as e:
28612874
if len(self.lnworker.active_forwardings[payment_key]) == 0:
28622875
self.lnworker.save_forwarding_failure(payment_key, failure_message=e)
@@ -2890,7 +2903,7 @@ async def wrapped_callback():
28902903
return None, None, error_bytes
28912904
if error_reason:
28922905
raise error_reason
2893-
if preimage:
2906+
if preimage and forwarding_key not in self.lnworker.dont_settle_htlc_keys:
28942907
return preimage, None, None
28952908
return None, None, None
28962909

electrum/lnwatcher.py

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

70-
async def unwatch_channel(self, address, funding_outpoint):
70+
def unwatch_channel(self, address, funding_outpoint):
7171
self.logger.info(f'unwatching {funding_outpoint}')
7272
self.remove_callback(address)
7373

@@ -130,7 +130,7 @@ async def check_onchain_situation(self, address, funding_outpoint):
130130
closing_height=closing_height,
131131
keep_watching=keep_watching)
132132
if not keep_watching:
133-
await self.unwatch_channel(address, funding_outpoint)
133+
self.unwatch_channel(address, funding_outpoint)
134134

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

0 commit comments

Comments
 (0)