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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
# (around commit 42de4400bff5105352d0552155f73589166d162b).
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import os
|
||||
import binascii
|
||||
from pprint import pformat
|
||||
import logging
|
||||
import dataclasses
|
||||
import time
|
||||
|
||||
from electrum import bitcoin
|
||||
from electrum import lnpeer
|
||||
@@ -697,6 +699,46 @@ class TestChannel(ElectrumTestCase):
|
||||
self.alice_channel.add_htlc(new)
|
||||
self.assertIn('Not enough local balance', cm.exception.args[0])
|
||||
|
||||
def test_unfunded_channel_can_be_removed(self):
|
||||
"""
|
||||
Test that an incoming channel which stays unfunded longer than
|
||||
lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS and lnutil.CHANNEL_OPENING_TIMEOUT_SEC
|
||||
can be removed
|
||||
"""
|
||||
# set the init_height and init_timestamp
|
||||
self.current_height = 800_000
|
||||
self.bob_channel.storage['init_height'] = self.current_height
|
||||
self.alice_channel.storage['init_height'] = self.current_height
|
||||
self.bob_channel.storage['init_timestamp'] = int(time.time())
|
||||
self.alice_channel.storage['init_timestamp'] = int(time.time())
|
||||
|
||||
mock_lnworker = mock.Mock()
|
||||
mock_blockchain = mock.Mock()
|
||||
mock_lnworker.wallet = mock.Mock()
|
||||
mock_lnworker.wallet.is_up_to_date = lambda: True
|
||||
mock_blockchain.is_tip_stale = lambda: False
|
||||
mock_lnworker.network.blockchain = lambda: mock_blockchain
|
||||
mock_lnworker.network.get_local_height = lambda: self.current_height
|
||||
self.bob_channel.lnworker = mock_lnworker
|
||||
self.alice_channel.lnworker = mock_lnworker
|
||||
|
||||
# test that the non-initiator can remove the channel after timeout
|
||||
self.assertFalse(self.bob_channel.is_initiator())
|
||||
self.bob_channel._state = ChannelState.OPENING
|
||||
self.assertFalse(self.bob_channel.can_be_deleted())
|
||||
self.current_height += lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS + 1
|
||||
self.assertFalse(self.bob_channel.can_be_deleted()) # needs both block and time based timeout
|
||||
self.bob_channel.storage['init_timestamp'] -= lnutil.CHANNEL_OPENING_TIMEOUT_SEC + 1
|
||||
self.alice_channel.storage['init_timestamp'] -= lnutil.CHANNEL_OPENING_TIMEOUT_SEC + 1
|
||||
self.assertTrue(self.bob_channel.can_be_deleted()) # now both timeouts are reached
|
||||
self.current_height = 800_000 # reset to check if we can delete with just the time based timeout
|
||||
self.assertFalse(self.bob_channel.can_be_deleted())
|
||||
|
||||
# test that the initiator can't remove the channel, even after timeout
|
||||
self.current_height += lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS + 1
|
||||
self.assertTrue(self.alice_channel.is_initiator())
|
||||
self.alice_channel._state = ChannelState.OPENING
|
||||
self.assertFalse(self.alice_channel.can_be_deleted())
|
||||
|
||||
class TestChannelAnchors(TestChannel):
|
||||
TEST_ANCHOR_CHANNELS = True
|
||||
|
||||
Reference in New Issue
Block a user