Skip to content

Commit 764cc78

Browse files
authored
Merge pull request #9590 from f321x/jit-update-unfunded-state
Handle unfunded zeroconf channels in update_unfunded_state
2 parents 8011ae0 + 6fd833c commit 764cc78

File tree

4 files changed

+56
-4
lines changed

4 files changed

+56
-4
lines changed

electrum/lnchannel.py

+51-3
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
ShortChannelID, map_htlcs_to_ctx_output_idxs,
5656
fee_for_htlc_output, offered_htlc_trim_threshold_sat,
5757
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
58-
ChannelType, LNProtocolWarning)
58+
ChannelType, LNProtocolWarning, ZEROCONF_TIMEOUT)
5959
from .lnsweep import sweep_our_ctx, sweep_their_ctx
6060
from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo
6161
from .lnsweep import sweep_their_ctx_to_remote_backup
@@ -345,7 +345,11 @@ def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo
345345
def update_unfunded_state(self) -> None:
346346
self.delete_funding_height()
347347
self.delete_closing_height()
348-
if self.get_state() in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING] and self.lnworker:
348+
if not self.lnworker:
349+
return
350+
chan_age = now() - self.storage.get('init_timestamp', 0)
351+
state = self.get_state()
352+
if state in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING]:
349353
if self.is_initiator():
350354
# set channel state to REDEEMED so that it can be removed manually
351355
# to protect ourselves against a server lying by omission,
@@ -365,15 +369,39 @@ def update_unfunded_state(self) -> None:
365369
self.set_state(ChannelState.REDEEMED)
366370
break
367371
else:
368-
if self.lnworker and (now() - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT):
372+
if chan_age > CHANNEL_OPENING_TIMEOUT:
369373
self.lnworker.remove_channel(self.channel_id)
374+
elif self.is_zeroconf() and state in [ChannelState.OPEN, ChannelState.CLOSING, ChannelState.FORCE_CLOSING]:
375+
assert self.storage.get('init_timestamp') is not None, "init_timestamp not set for zeroconf channel"
376+
# handling zeroconf channels with no funding tx, can happen if broadcasting fails on LSP side
377+
# or if the LSP did double spent the funding tx/never published it intentionally
378+
# only remove a timed out OPEN channel if we are connected to the network to prevent removing it if we went
379+
# offline before seeing the funding tx
380+
if state != ChannelState.OPEN or chan_age > ZEROCONF_TIMEOUT and self.lnworker.network.is_connected():
381+
# we delete the channel if its in closing state (either initiated manually by client or by LSP on failure)
382+
# or if the channel is not seeing any funding tx after 10 minutes to prevent further usage (limit damage)
383+
self.set_state(ChannelState.REDEEMED, force=True)
384+
local_balance_sat = int(self.balance(LOCAL) // 1000)
385+
if local_balance_sat > 0:
386+
self.logger.warning(
387+
f"we may have been scammed out of {local_balance_sat} sat by our "
388+
f"JIT provider: {self.lnworker.config.ZEROCONF_TRUSTED_NODE} or he didn't use our preimage")
389+
self.lnworker.config.ZEROCONF_TRUSTED_NODE = ''
390+
self.lnworker.lnwatcher.unwatch_channel(self.get_funding_address(), self.funding_outpoint.to_str())
391+
# remove remaining local transactions from the wallet, this will also remove child transactions (closing tx)
392+
self.lnworker.lnwatcher.adb.remove_transaction(self.funding_outpoint.txid)
393+
self.lnworker.remove_channel(self.channel_id)
370394

371395
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:
372396
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
373397
self.delete_closing_height()
374398
if funding_height.conf>0:
375399
self.set_short_channel_id(ShortChannelID.from_components(
376400
funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
401+
if self.is_zeroconf():
402+
# remove zeroconf flag as we are now confirmed, this is to prevent an electrum server causing
403+
# us to remove a channel later in update_unfunded_state by omitting its funding tx
404+
self.remove_zeroconf_flag()
377405
if self.get_state() == ChannelState.OPENING:
378406
if self.is_funding_tx_mined(funding_height):
379407
self.set_state(ChannelState.FUNDED)
@@ -411,6 +439,14 @@ def is_initiator(self) -> bool:
411439
def is_public(self) -> bool:
412440
pass
413441

442+
@abstractmethod
443+
def is_zeroconf(self) -> bool:
444+
pass
445+
446+
@abstractmethod
447+
def remove_zeroconf_flag(self) -> None:
448+
pass
449+
414450
@abstractmethod
415451
def is_funding_tx_mined(self, funding_height: TxMinedInfo) -> bool:
416452
pass
@@ -664,6 +700,12 @@ def get_sweep_address(self) -> str:
664700
def has_anchors(self) -> Optional[bool]:
665701
return None
666702

703+
def is_zeroconf(self) -> bool:
704+
return False
705+
706+
def remove_zeroconf_flag(self) -> None:
707+
pass
708+
667709
def get_local_pubkey(self) -> bytes:
668710
cb = self.cb
669711
assert isinstance(cb, ChannelBackupStorage)
@@ -906,6 +948,12 @@ def is_zeroconf(self) -> bool:
906948
channel_type = ChannelType(self.storage.get('channel_type'))
907949
return bool(channel_type & ChannelType.OPTION_ZEROCONF)
908950

951+
def remove_zeroconf_flag(self) -> None:
952+
if not self.is_zeroconf():
953+
return
954+
channel_type = ChannelType(self.storage.get('channel_type'))
955+
self.storage['channel_type'] = channel_type & ~ChannelType.OPTION_ZEROCONF
956+
909957
def get_sweep_address(self) -> str:
910958
# TODO: in case of unilateral close with pending HTLCs, this address will be reused
911959
if self.has_anchors():

electrum/lnpeer.py

+1
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ async def channel_establishment_flow(
11611161
)
11621162
chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]
11631163
chan.storage['has_onchain_backup'] = has_onchain_backup
1164+
chan.storage['init_timestamp'] = int(time.time())
11641165
if isinstance(self.transport, LNTransport):
11651166
chan.add_or_update_peer_addr(self.transport.peer_addr)
11661167
sig_64, _ = chan.sign_next_commitment()

electrum/lnutil.py

+3
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@ class LNProtocolWarning(Exception):
503503

504504
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016
505505

506+
# timeout after which we consider a zeroconf channel without funding tx to be failed
507+
ZEROCONF_TIMEOUT = 60 * 10
508+
506509
class RevocationStore:
507510
# closely based on code in lightningnetwork/lnd
508511

electrum/wallet.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):
610610
def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):
611611
if self.adb != adb:
612612
return
613-
if not self.tx_is_related(tx):
613+
if not tx or not self.tx_is_related(tx):
614614
return
615615
self.clear_tx_parents_cache()
616616
util.trigger_callback('removed_transaction', self, tx)

0 commit comments

Comments
 (0)