URI: 
       tdetect redeemed channels (fix #5963) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 938fab86d176b4953f4ece388cdd88d27b21971b
   DIR parent f8da0f87a7dd69e8f538469d9e8ff9dae36d5f8e
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun, 16 Feb 2020 18:54:27 +0100
       
       detect redeemed channels (fix #5963)
       
       Diffstat:
         M electrum/gui/kivy/uix/dialogs/ligh… |       4 +++-
         M electrum/gui/qt/channels_list.py    |       2 +-
         M electrum/lnchannel.py               |       3 +++
         M electrum/lnwatcher.py               |      44 ++++++++++++++++++-------------
         M electrum/lnworker.py                |       3 +--
       
       5 files changed, 34 insertions(+), 22 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py
       t@@ -88,6 +88,7 @@ Builder.load_string(r'''
            id: popuproot
            data: []
            is_closed: False
       +    is_redeemed: False
            BoxLayout:
                orientation: 'vertical'
                ScrollView:
       t@@ -115,7 +116,7 @@ Builder.load_string(r'''
                        height: '48dp'
                        text: _('Delete')
                        on_release: root.remove_channel()
       -                disabled: not root.is_closed
       +                disabled: not root.is_redeemed
                    Button:
                        size_hint: 0.5, None
                        height: '48dp'
       t@@ -129,6 +130,7 @@ class ChannelDetailsPopup(Popup):
            def __init__(self, chan, app, **kwargs):
                super(ChannelDetailsPopup,self).__init__(**kwargs)
                self.is_closed = chan.is_closed()
       +        self.is_redeemed = chan.is_redeemed()
                self.app = app
                self.chan = chan
                self.title = _('Channel details')
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -112,7 +112,7 @@ class ChannelsList(MyTreeView):
                    if chan.peer_state == peer_states.GOOD:
                        menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id))
                    menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id))
       -        else:
       +        if chan.is_redeemed():
                    menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id))
                menu.exec_(self.viewport().mapToGlobal(position))
        
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -228,6 +228,9 @@ class Channel(Logger):
                # the closing txid has been saved
                return self.get_state() >= channel_states.CLOSED
        
       +    def is_redeemed(self):
       +        return self.get_state() == channel_states.REDEEMED
       +
            def _check_can_pay(self, amount_msat: int) -> None:
                # TODO check if this method uses correct ctns (should use "latest" + 1)
                if self.is_closed():
   DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -176,21 +176,24 @@ class LNWatcher(AddressSynchronizer):
                    await self.check_onchain_situation(address, outpoint)
        
            async def check_onchain_situation(self, address, funding_outpoint):
       -        keep_watching, spenders = self.inspect_tx_candidate(funding_outpoint, 0)
       +        spenders = self.inspect_tx_candidate(funding_outpoint, 0)
                funding_txid = funding_outpoint.split(':')[0]
                funding_height = self.get_tx_height(funding_txid)
                closing_txid = spenders.get(funding_outpoint)
                closing_height = self.get_tx_height(closing_txid)
       -        await self.update_channel_state(
       -            funding_outpoint, funding_txid,
       -            funding_height, closing_txid,
       -            closing_height, keep_watching)
                if closing_txid:
                    closing_tx = self.db.get_transaction(closing_txid)
                    if closing_tx:
       -                await self.do_breach_remedy(funding_outpoint, closing_tx, spenders)
       +                keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders)
                    else:
                        self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
       +                keep_watching = True
       +        else:
       +            keep_watching = True
       +        await self.update_channel_state(
       +            funding_outpoint, funding_txid,
       +            funding_height, closing_txid,
       +            closing_height, keep_watching)
                if not keep_watching:
                    await self.unwatch_channel(address, funding_outpoint)
        
       t@@ -201,32 +204,24 @@ class LNWatcher(AddressSynchronizer):
                raise NotImplementedError() # implemented by subclasses
        
            def inspect_tx_candidate(self, outpoint, n):
       -        # FIXME: instead of stopping recursion at n == 2,
       -        # we should detect which outputs are HTLCs
                prev_txid, index = outpoint.split(':')
                txid = self.db.get_spent_outpoint(prev_txid, int(index))
                result = {outpoint:txid}
                if txid is None:
                    self.channel_status[outpoint] = 'open'
       -            #self.logger.info('keep watching because outpoint is unspent')
       -            return True, result
       -        keep_watching = (self.get_tx_mined_depth(txid) != TxMinedDepth.DEEP)
       -        if keep_watching:
       +            return result
       +        if n == 0 and not self.is_deeply_mined(txid):
                    self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(txid).conf
       -            #self.logger.info('keep watching because spending tx is not deep')
                else:
                    self.channel_status[outpoint] = 'closed (deep)'
       -
                tx = self.db.get_transaction(txid)
                for i, o in enumerate(tx.outputs()):
                    if o.address not in self.get_addresses():
                        self.add_address(o.address)
       -                keep_watching = True
                    elif n < 2:
       -                k, r = self.inspect_tx_candidate(txid+':%d'%i, n+1)
       -                keep_watching |= k
       +                r = self.inspect_tx_candidate(txid+':%d'%i, n+1)
                        result.update(r)
       -        return keep_watching, result
       +        return result
        
            def get_tx_mined_depth(self, txid: str):
                if not txid:
       t@@ -247,6 +242,9 @@ class LNWatcher(AddressSynchronizer):
                else:
                    raise NotImplementedError()
        
       +    def is_deeply_mined(self, txid):
       +        return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP
       +
        
        class WatchTower(LNWatcher):
        
       t@@ -267,12 +265,16 @@ class WatchTower(LNWatcher):
                    self.add_channel(outpoint, address)
        
            async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders):
       +        keep_watching = False
                for prevout, spender in spenders.items():
                    if spender is not None:
       +                keep_watching |= not self.is_deeply_mined(spender)
                        continue
                    sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)
                    for tx in sweep_txns:
                        await self.broadcast_or_log(funding_outpoint, tx)
       +                keep_watching = True
       +        return keep_watching
        
            async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):
                height = self.get_tx_height(tx.txid()).height
       t@@ -345,6 +347,7 @@ class LNWalletWatcher(LNWatcher):
                    return
                # detect who closed and set sweep_info
                sweep_info_dict = chan.sweep_ctx(closing_tx)
       +        keep_watching = False
                self.logger.info(f'sweep_info_dict length: {len(sweep_info_dict)}')
                # create and broadcast transaction
                for prevout, sweep_info in sweep_info_dict.items():
       t@@ -360,14 +363,19 @@ class LNWalletWatcher(LNWatcher):
                            spender2 = spenders.get(spender_txid+':0')
                            if spender2:
                                self.logger.info(f'htlc is already spent {name}: {prevout}')
       +                        keep_watching |= not self.is_deeply_mined(spender2)
                            else:
                                self.logger.info(f'trying to redeem htlc {name}: {prevout}')
                                await self.try_redeem(spender_txid+':0', e_htlc_tx)
       +                        keep_watching = True
                        else:
                            self.logger.info(f'outpoint already spent {name}: {prevout}')
       +                    keep_watching |= not self.is_deeply_mined(spender_txid)
                    else:
                        self.logger.info(f'trying to redeem {name}: {prevout}')
                        await self.try_redeem(prevout, sweep_info)
       +                keep_watching = True
       +        return keep_watching
        
            @log_exceptions
            async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo') -> None:
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -1192,9 +1192,8 @@ class LNWallet(LNWorker):
                return tx.txid()
        
            def remove_channel(self, chan_id):
       -        # TODO: assert that closing tx is deep-mined and htlcs are swept
                chan = self.channels[chan_id]
       -        assert chan.is_closed()
       +        assert chan.get_state() == channel_states.REDEEMED
                with self.lock:
                    self.channels.pop(chan_id)
                    self.channel_timestamps.pop(chan_id.hex())