Qt: add closing warning if we have an unconfirmed local commitment tx with htlcs
add htlc direction (offered, received) to the htlc sweep_info name regtest: add test_reedeem_received_htlcs
This commit is contained in:
@@ -74,6 +74,11 @@ MSG_REVERSE_SWAP_FUNDING_MEMPOOL = (
|
||||
"you will not get back the already pre-paid mining fees.")
|
||||
)
|
||||
|
||||
MSG_FORCE_CLOSE_WARNING = (
|
||||
_('You will need to come back online after the commitment transaction is confirmed, in order to broadcast second-stage htlc transactions.') + ' ' +
|
||||
_('If you remain offline for more than {} blocks, your channel counterparty will be able to sweep those funds.')
|
||||
)
|
||||
|
||||
MSG_FORWARD_SWAP_WARNING = (
|
||||
_('You will need to come back online after the funding transaction is confirmed, in order to settle the swap.') + ' ' +
|
||||
_('If you remain offline for more than {} blocks, your channel will be force closed and you might lose the funds you sent in the swap.')
|
||||
|
||||
@@ -274,6 +274,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
# wallet closing warning callbacks
|
||||
self.closing_warning_callbacks = [] # type: List[Callable[[], Optional[str]]]
|
||||
self.register_closing_warning_callback(self._check_ongoing_submarine_swaps_callback)
|
||||
self.register_closing_warning_callback(self._check_ongoing_force_closures)
|
||||
# banner may already be there
|
||||
if self.network and self.network.banner:
|
||||
self.console.showMessage(self.network.banner)
|
||||
@@ -2730,6 +2731,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.logger.debug(f"registering wallet closing warning callback")
|
||||
self.closing_warning_callbacks.append(warning_callback)
|
||||
|
||||
def _check_ongoing_force_closures(self) -> Optional[str]:
|
||||
from electrum.lnutil import MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
if not self.wallet.has_lightning():
|
||||
return None
|
||||
if not self.network:
|
||||
return None
|
||||
force_closes = self.wallet.lnworker.lnwatcher.get_pending_force_closes()
|
||||
if not force_closes:
|
||||
return
|
||||
# fixme: this is inaccurate, we need local_height - cltv_of_htlc
|
||||
cltv_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
msg = '\n\n'.join([
|
||||
_("Pending channel force-close"),
|
||||
messages.MSG_FORCE_CLOSE_WARNING.format(cltv_delta),
|
||||
])
|
||||
return msg
|
||||
|
||||
def _check_ongoing_submarine_swaps_callback(self) -> Optional[str]:
|
||||
"""Callback that will return a warning string if there are unconfirmed swap funding txs."""
|
||||
from electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT, LOCKTIME_DELTA_REFUND
|
||||
@@ -2749,10 +2767,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
locktime = min(swap.locktime for swap in ongoing_swaps)
|
||||
delta = locktime - self.wallet.adb.get_local_height()
|
||||
warning = messages.MSG_REVERSE_SWAP_WARNING.format(delta)
|
||||
return "".join((
|
||||
f"{str(len(ongoing_swaps))} ",
|
||||
_("pending submarine swap") if len(ongoing_swaps) == 1 else _("pending submarine swaps"),
|
||||
"\n\n",
|
||||
return "\n\n".join((
|
||||
_("Pending submarine swap"),
|
||||
warning,
|
||||
))
|
||||
|
||||
|
||||
@@ -303,12 +303,14 @@ class AbstractChannel(Logger, ABC):
|
||||
def get_remote_scid_alias(self) -> Optional[bytes]:
|
||||
return None
|
||||
|
||||
def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
|
||||
def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, SweepInfo]]:
|
||||
txid = ctx.txid()
|
||||
is_local = False
|
||||
if self._sweep_info.get(txid) is None:
|
||||
our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)
|
||||
their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)
|
||||
if our_sweep_info:
|
||||
is_local = True
|
||||
self._sweep_info[txid] = our_sweep_info
|
||||
self.logger.info(f'we (local) force closed')
|
||||
elif their_sweep_info:
|
||||
@@ -317,7 +319,7 @@ class AbstractChannel(Logger, ABC):
|
||||
else:
|
||||
self._sweep_info[txid] = {}
|
||||
self.logger.info(f'not sure who closed.')
|
||||
return self._sweep_info[txid]
|
||||
return is_local, self._sweep_info[txid]
|
||||
|
||||
def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
|
||||
return {}
|
||||
|
||||
@@ -385,7 +385,7 @@ def sweep_our_ctx(
|
||||
htlc_relative_idx=htlc_relative_idx)
|
||||
|
||||
if actual_htlc_tx is None:
|
||||
name = 'first-stage-htlc-anchors' if chan.has_anchors() else 'first-stage-htlc'
|
||||
name = 'offered-htlc' if htlc_direction == SENT else 'received-htlc'
|
||||
prevout = ctx.txid() + f':{ctx_output_idx}'
|
||||
csv_delay = 1 if chan.has_anchors() else 0
|
||||
txs[prevout] = SweepInfo(
|
||||
|
||||
@@ -8,6 +8,7 @@ from .util import TxMinedInfo, BelowDustLimit
|
||||
from .util import EventListener, event_listener, log_exceptions, ignore_exceptions
|
||||
from .transaction import Transaction, TxOutpoint
|
||||
from .logging import Logger
|
||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -31,6 +32,7 @@ class LNWatcher(Logger, EventListener):
|
||||
self.register_callbacks()
|
||||
# status gets populated when we run
|
||||
self.channel_status = {}
|
||||
self._pending_force_closes = set()
|
||||
|
||||
def start_network(self, network: 'Network'):
|
||||
self.network = network
|
||||
@@ -140,6 +142,8 @@ class LNWatcher(Logger, EventListener):
|
||||
closing_txid=closing_txid,
|
||||
closing_height=closing_height,
|
||||
keep_watching=keep_watching)
|
||||
if closing_height.conf > 0:
|
||||
self._pending_force_closes.discard(chan)
|
||||
await self.lnworker.handle_onchain_state(chan)
|
||||
|
||||
async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool:
|
||||
@@ -157,7 +161,7 @@ class LNWatcher(Logger, EventListener):
|
||||
if not chan:
|
||||
return False
|
||||
# detect who closed and get information about how to claim outputs
|
||||
sweep_info_dict = chan.sweep_ctx(closing_tx)
|
||||
is_local_ctx, sweep_info_dict = chan.get_ctx_sweep_info(closing_tx)
|
||||
keep_watching = False if sweep_info_dict else not self.adb.is_deeply_mined(closing_tx.txid())
|
||||
# create and broadcast transactions
|
||||
for prevout, sweep_info in sweep_info_dict.items():
|
||||
@@ -188,8 +192,12 @@ class LNWatcher(Logger, EventListener):
|
||||
self.maybe_add_accounting_address(spender_txid, sweep_info)
|
||||
else:
|
||||
keep_watching |= was_added
|
||||
self.maybe_add_pending_forceclose(chan, spender_txid, is_local_ctx, sweep_info, was_added)
|
||||
return keep_watching
|
||||
|
||||
def get_pending_force_closes(self):
|
||||
return self._pending_force_closes
|
||||
|
||||
def maybe_redeem(self, sweep_info: 'SweepInfo') -> bool:
|
||||
""" returns False if it was dust """
|
||||
try:
|
||||
@@ -219,7 +227,7 @@ class LNWatcher(Logger, EventListener):
|
||||
break
|
||||
else:
|
||||
return
|
||||
if sweep_info.name in ['first-stage-htlc', 'first-stage-htlc-anchors']:
|
||||
if sweep_info.name in ['offered-htlc', 'received-htlc']:
|
||||
# always consider ours
|
||||
pass
|
||||
else:
|
||||
@@ -237,3 +245,10 @@ class LNWatcher(Logger, EventListener):
|
||||
prev_tx = self.adb.get_transaction(prev_txid)
|
||||
txout = prev_tx.outputs()[int(prev_index)]
|
||||
self.lnworker.wallet._accounting_addresses.add(txout.address)
|
||||
|
||||
def maybe_add_pending_forceclose(self, chan, spender_txid, is_local_ctx, sweep_info, was_added):
|
||||
""" we are waiting for ctx to be confirmed and there are received htlcs """
|
||||
if was_added and is_local_ctx and sweep_info.name == 'received-htlc' and chan.has_anchors():
|
||||
tx_mined_status = self.adb.get_tx_height(spender_txid)
|
||||
if tx_mined_status.height == TX_HEIGHT_LOCAL:
|
||||
self._pending_force_closes.add(chan)
|
||||
|
||||
@@ -106,7 +106,7 @@ class TxBatcher(Logger):
|
||||
assert sweep_info.csv_delay >= (sweep_info.txin.get_block_based_relative_locktime() or 0)
|
||||
if sweep_info.txin and sweep_info.txout:
|
||||
# todo: don't use name, detect sighash
|
||||
if sweep_info.name == 'first-stage-htlc':
|
||||
if sweep_info.name in ['received-htlc', 'offered-htlc'] and sweep_info.csv_delay == 0:
|
||||
if sweep_info.txin.prevout not in self._legacy_htlcs:
|
||||
self.logger.info(f'received {sweep_info.name}')
|
||||
self._legacy_htlcs[sweep_info.txin.prevout] = sweep_info
|
||||
|
||||
@@ -70,8 +70,11 @@ class TestLightningAB(TestLightning):
|
||||
def test_extract_preimage(self):
|
||||
self.run_shell(['extract_preimage'])
|
||||
|
||||
def test_redeem_htlcs(self):
|
||||
self.run_shell(['redeem_htlcs'])
|
||||
def test_redeem_received_htlcs(self):
|
||||
self.run_shell(['redeem_received_htlcs'])
|
||||
|
||||
def test_redeem_offered_htlcs(self):
|
||||
self.run_shell(['redeem_offered_htlcs'])
|
||||
|
||||
def test_breach_with_unspent_htlc(self):
|
||||
self.run_shell(['breach_with_unspent_htlc'])
|
||||
|
||||
@@ -342,7 +342,8 @@ if [[ $1 == "extract_preimage" ]]; then
|
||||
fi
|
||||
|
||||
|
||||
if [[ $1 == "redeem_htlcs" ]]; then
|
||||
if [[ $1 == "redeem_offered_htlcs" ]]; then
|
||||
# alice force closes and redeems using htlc timeout
|
||||
$bob enable_htlc_settle false
|
||||
wait_for_balance alice 1
|
||||
echo "alice opens channel"
|
||||
@@ -384,6 +385,32 @@ if [[ $1 == "redeem_htlcs" ]]; then
|
||||
fi
|
||||
|
||||
|
||||
if [[ $1 == "redeem_received_htlcs" ]]; then
|
||||
# bob force closes and redeems with the preimage
|
||||
$bob enable_htlc_settle false
|
||||
wait_for_balance alice 1
|
||||
echo "alice opens channel"
|
||||
bob_node=$($bob nodeid)
|
||||
$alice open_channel $bob_node 0.15 --password=''
|
||||
new_blocks 3
|
||||
wait_until_channel_open alice
|
||||
# alice pays bob
|
||||
invoice=$($bob add_request 0.04 --lightning --memo "test" | jq -r ".lightning_invoice")
|
||||
$alice lnpay $invoice --timeout=1 || true
|
||||
unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')
|
||||
if [[ "$unsettled" == "0" ]]; then
|
||||
echo 'enable_htlc_settle did not work'
|
||||
exit 1
|
||||
fi
|
||||
$alice stop
|
||||
chan_id=$($bob list_channels | jq -r ".[0].channel_point")
|
||||
$bob close_channel $chan_id --force
|
||||
# if we exit here, bob GUI will show a warning
|
||||
new_blocks 1
|
||||
wait_for_balance bob 1.039
|
||||
fi
|
||||
|
||||
|
||||
if [[ $1 == "breach_with_unspent_htlc" ]]; then
|
||||
$bob enable_htlc_settle false
|
||||
wait_for_balance alice 1
|
||||
|
||||
Reference in New Issue
Block a user