1
0

lnchannel: allow deleting unfunded incoming channels

We tried to delete incoming channels that didn't get funded after
lnutil.CHANNEL_OPENING_TIMEOUT, however an assert prevented this:

```
  3.63 | E | lnwatcher.LNWatcher.[default_wallet-LNW] | Exception in check_onchain_situation: AssertionError()
Traceback (most recent call last):
  File "/home/user/code/electrum-fork/electrum/util.py", line 1233, in wrapper
    return await func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnwatcher.py", line 117, in check_onchain_situation
    await self.update_channel_state(
    ...<5 lines>...
        keep_watching=keep_watching)
  File "/home/user/code/electrum-fork/electrum/lnwatcher.py", line 135, in update_channel_state
    chan.update_onchain_state(
    ~~~~~~~~~~~~~~~~~~~~~~~~~^
        funding_txid=funding_txid,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<2 lines>...
        closing_height=closing_height,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        keep_watching=keep_watching)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnchannel.py", line 341, in update_onchain_state
    self.update_unfunded_state()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/user/code/electrum-fork/electrum/lnchannel.py", line 382, in update_unfunded_state
    self.lnworker.remove_channel(self.channel_id)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnworker.py", line 3244, in remove_channel
    assert chan.can_be_deleted()
           ~~~~~~~~~~~~~~~~~~~^^
AssertionError
```
This commit is contained in:
f321x
2025-12-05 14:42:08 +01:00
parent 6d1e8e8619
commit c34efce984
4 changed files with 80 additions and 9 deletions

View File

@@ -60,7 +60,7 @@ 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 .lnutil import CHANNEL_OPENING_TIMEOUT
from .lnutil import CHANNEL_OPENING_TIMEOUT_BLOCKS, CHANNEL_OPENING_TIMEOUT_SEC
from .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage
from .lnutil import format_short_channel_id
from .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
@@ -356,7 +356,6 @@ class AbstractChannel(Logger, ABC):
self.delete_closing_height()
if not self.lnworker:
return
chan_age = now() - self.storage.get('init_timestamp', 0)
state = self.get_state()
if state in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING]:
if self.is_initiator():
@@ -377,11 +376,11 @@ class AbstractChannel(Logger, ABC):
self.logger.info(f'channel is double spent {inputs}')
self.set_state(ChannelState.REDEEMED)
break
else:
if chan_age > CHANNEL_OPENING_TIMEOUT:
self.lnworker.remove_channel(self.channel_id)
elif self.has_funding_timed_out():
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]:
assert self.storage.get('init_timestamp') is not None, "init_timestamp not set for zeroconf channel"
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
@@ -407,6 +406,10 @@ class AbstractChannel(Logger, ABC):
if funding_height.conf>0:
self.set_short_channel_id(ShortChannelID.from_components(
funding_height.height(), funding_height.txpos, self.funding_outpoint.output_index))
elif self.has_funding_timed_out():
self.logger.warning("dropping incoming channel, funding tx took too long to confirm")
self.lnworker.remove_channel(self.channel_id)
return
if self.get_state() == ChannelState.OPENING:
if self.is_funding_tx_mined(funding_height):
self.set_state(ChannelState.FUNDED)
@@ -549,6 +552,10 @@ class AbstractChannel(Logger, ABC):
def can_be_deleted(self) -> bool:
pass
@abstractmethod
def has_funding_timed_out(self) -> bool:
pass
@abstractmethod
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
"""Returns a list of addrs that the wallet should not use, to avoid address-reuse.
@@ -651,6 +658,9 @@ class ChannelBackup(AbstractChannel):
def can_be_deleted(self):
return self.is_imported or self.is_redeemed()
def has_funding_timed_out(self):
return False
def get_capacity(self):
lnwatcher = self.lnworker.lnwatcher
if lnwatcher:
@@ -769,7 +779,7 @@ class Channel(AbstractChannel):
self,
state: 'StoredDict', *,
name=None,
lnworker=None,
lnworker=None, # None only in unittests
initial_feerate=None,
jit_opening_fee: Optional[int] = None,
):
@@ -833,9 +843,23 @@ class Channel(AbstractChannel):
def has_onchain_backup(self):
return self.storage.get('has_onchain_backup', False)
def can_be_deleted(self):
def can_be_deleted(self) -> bool:
if self.has_funding_timed_out():
return True
return self.is_redeemed()
def has_funding_timed_out(self):
if self.is_initiator() or self.is_funded():
return False
if self.lnworker.network.blockchain().is_tip_stale() or not self.lnworker.wallet.is_up_to_date():
return False
init_height = self.storage.get('init_height', 0)
init_timestamp = self.storage.get('init_timestamp', 0)
age_blocks = self.lnworker.network.get_local_height() - init_height
age_sec = now() - init_timestamp
# some channels might not have init_height set so we check both time and block based timeouts
return age_blocks > CHANNEL_OPENING_TIMEOUT_BLOCKS and age_sec > CHANNEL_OPENING_TIMEOUT_SEC
def get_capacity(self):
return self.constraints.capacity

View File

@@ -1216,6 +1216,7 @@ class Peer(Logger, EventListener):
)
chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]
chan.storage['has_onchain_backup'] = has_onchain_backup
chan.storage['init_height'] = self.lnworker.network.get_local_height()
chan.storage['init_timestamp'] = int(time.time())
if isinstance(self.transport, LNTransport):
chan.add_or_update_peer_addr(self.transport.peer_addr)
@@ -1438,6 +1439,7 @@ class Peer(Logger, EventListener):
initial_feerate=feerate,
jit_opening_fee = channel_opening_fee,
)
chan.storage['init_height'] = self.lnworker.network.get_local_height()
chan.storage['init_timestamp'] = int(time.time())
if isinstance(self.transport, LNTransport):
chan.add_or_update_peer_addr(self.transport.peer_addr)

View File

@@ -489,7 +489,10 @@ class LNProtocolWarning(Exception):
# TODO make some of these values configurable?
REDEEM_AFTER_DOUBLE_SPENT_DELAY = 30
CHANNEL_OPENING_TIMEOUT = 24*60*60
# timeout after which we forget incoming channels if the funding tx has no confirmation
# https://github.com/lightning/bolts/commit/ba00bf8f4cd85f21bacfc03adcafd4acc7d68382
CHANNEL_OPENING_TIMEOUT_BLOCKS = 2016
CHANNEL_OPENING_TIMEOUT_SEC = 14*24*60*60 # 2 weeks
# Small capacity channels are problematic for many reasons. As the onchain fees start to become
# significant compared to the capacity, things start to break down. e.g. the counterparty