URI: 
       tavoid leaving FORCE_CLOSING state, rebroadcast closing tx if reorged out - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit c570bc5fb1a9479264d8c36816349b0398d52991
   DIR parent 0ea87278fb18a535c5ba35eb542ea7dfd5672f18
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Mon,  5 Nov 2018 17:23:49 +0100
       
       avoid leaving FORCE_CLOSING state, rebroadcast closing tx if reorged out
       
       Diffstat:
         M electrum/gui/qt/channels_list.py    |       4 +++-
         M electrum/lnbase.py                  |      24 +++---------------------
         M electrum/lnchan.py                  |      25 ++++++++++++++++++++++++-
         M electrum/lnworker.py                |      17 ++++++++++++++---
       
       4 files changed, 44 insertions(+), 26 deletions(-)
       ---
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -1,4 +1,5 @@
        # -*- coding: utf-8 -*-
       +import traceback
        import asyncio
        from PyQt5 import QtCore, QtWidgets
        from PyQt5.QtWidgets import *
       t@@ -48,7 +49,8 @@ class ChannelsList(MyTreeWidget):
                def on_success(txid):
                    self.main_window.show_error('Channel closed' + '\n' + txid)
                def on_failure(exc_info):
       -            type_, e, traceback = exc_info
       +            type_, e, tb = exc_info
       +            traceback.print_tb(tb)
                    self.main_window.show_error('Failed to close channel:\n{}'.format(repr(e)))
                def close():
                    def task():
   DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -377,7 +377,8 @@ class Peer(PrintError):
                except:
                    pass
                for chan in self.channels.values():
       -            chan.set_state('DISCONNECTED')
       +            if chan.get_state() != 'FORCE_CLOSING':
       +                chan.set_state('DISCONNECTED')
                    self.network.trigger_callback('channel', chan)
        
            def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> Tuple[ChannelConfig, bytes]:
       t@@ -442,7 +443,7 @@ class Peer(PrintError):
                )
                payload = await self.channel_accepted[temp_channel_id].get()
                if payload.get('error'):
       -            raise Exception(payload.get('error'))
       +            raise Exception('Remote Lightning peer reported error: ' + repr(payload.get('error')))
                remote_per_commitment_point = payload['first_per_commitment_point']
                funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
                remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big')
       t@@ -1158,25 +1159,6 @@ class Peer(PrintError):
                self.print_error('Channel closed', txid)
                return txid
        
       -    async def force_close_channel(self, chan_id):
       -        chan = self.channels[chan_id]
       -        # local_commitment always gives back the next expected local_commitment,
       -        # but in this case, we want the current one. So substract one ctn number
       -        old_local_state = chan.config[LOCAL]
       -        chan.config[LOCAL]=chan.config[LOCAL]._replace(ctn=chan.config[LOCAL].ctn - 1)
       -        tx = chan.pending_local_commitment
       -        chan.config[LOCAL] = old_local_state
       -        tx.sign({bh2u(chan.config[LOCAL].multisig_key.pubkey): (chan.config[LOCAL].multisig_key.privkey, True)})
       -        remote_sig = chan.config[LOCAL].current_commitment_signature
       -        remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01"
       -        none_idx = tx._inputs[0]["signatures"].index(None)
       -        tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))
       -        assert tx.is_complete()
       -        # TODO persist FORCE_CLOSING state to disk
       -        chan.set_state('FORCE_CLOSING')
       -        self.lnworker.save_channel(chan)
       -        return await self.network.broadcast_transaction(tx)
       -
            @log_exceptions
            async def on_shutdown(self, payload):
                # length of scripts allowed in BOLT-02
   DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py
       t@@ -187,7 +187,11 @@ class Channel(PrintError):
                self.remote_commitment = self.pending_remote_commitment
        
                self._is_funding_txo_spent = None  # "don't know"
       -        self.set_state('DISCONNECTED')
       +        self._state = None
       +        if state.get('force_closed', False):
       +            self.set_state('FORCE_CLOSING')
       +        else:
       +            self.set_state('DISCONNECTED')
        
                self.lnwatcher = None
        
       t@@ -197,6 +201,8 @@ class Channel(PrintError):
                    self.log[sub].locked_in.update(self.log[sub].adds.keys())
        
            def set_state(self, state: str):
       +        if self._state == 'FORCE_CLOSING':
       +            assert state == 'FORCE_CLOSING', 'new state was not FORCE_CLOSING: ' + state
                self._state = state
        
            def get_state(self):
       t@@ -713,6 +719,7 @@ class Channel(PrintError):
                        "onion_keys": str_bytes_dict_to_save(self.onion_keys),
                        "settled_local": self.settled[LOCAL],
                        "settled_remote": self.settled[REMOTE],
       +                "force_closed": self.get_state() == 'FORCE_CLOSING',
                }
        
                # htlcs number must be monotonically increasing,
       t@@ -806,6 +813,7 @@ class Channel(PrintError):
        
            def make_closing_tx(self, local_script: bytes, remote_script: bytes,
                                fee_sat: Optional[int]=None) -> Tuple[bytes, int, str]:
       +        """ cooperative close """
                if fee_sat is None:
                    fee_sat = self.pending_local_fee
        
       t@@ -830,6 +838,21 @@ class Channel(PrintError):
                sig = ecc.sig_string_from_der_sig(der_sig[:-1])
                return sig, fee_sat, closing_tx.txid()
        
       +    def force_close_tx(self):
       +        # local_commitment always gives back the next expected local_commitment,
       +        # but in this case, we want the current one. So substract one ctn number
       +        old_local_state = self.config[LOCAL]
       +        self.config[LOCAL]=self.config[LOCAL]._replace(ctn=self.config[LOCAL].ctn - 1)
       +        tx = self.pending_local_commitment
       +        self.config[LOCAL] = old_local_state
       +        tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)})
       +        remote_sig = self.config[LOCAL].current_commitment_signature
       +        remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01"
       +        none_idx = tx._inputs[0]["signatures"].index(None)
       +        tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))
       +        assert tx.is_complete()
       +        return tx
       +
        def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
                                                         sweep_address) -> Optional[EncumberedTransaction]:
            assert isinstance(their_pcp, bytes)
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -36,6 +36,7 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
                             NUM_MAX_EDGES_IN_PAYMENT_PATH)
        from .i18n import _
        from .lnrouter import RouteEdge, is_route_sane_to_use
       +from .address_synchronizer import TX_HEIGHT_LOCAL
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -173,7 +174,8 @@ class LNWorker(PrintError):
                    return
                chan.set_funding_txo_spentness(is_spent)
                if is_spent:
       -            chan.set_state("CLOSED")
       +            if chan.get_state() != 'FORCE_CLOSING':
       +                chan.set_state("CLOSED")
                    self.channel_db.remove_channel(chan.short_channel_id)
                self.network.trigger_callback('channel', chan)
        
       t@@ -207,6 +209,13 @@ class LNWorker(PrintError):
                            await peer.bitcoin_fee_update(chan)
                        conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf
                        peer.on_network_update(chan, conf)
       +            elif chan.get_state() == 'FORCE_CLOSING':
       +                txid = chan.force_close_tx().txid()
       +                height = addr_sync.get_tx_height(txid).height
       +                self.print_error("force closing tx", txid, "height", height)
       +                if height == TX_HEIGHT_LOCAL:
       +                    self.print_error('REBROADCASTING CLOSING TX')
       +                    await self.force_close_channel(chan.channel_id)
        
            async def _open_channel_coroutine(self, peer, local_amount_sat, push_sat, password):
                # peer might just have been connected to
       t@@ -450,8 +459,10 @@ class LNWorker(PrintError):
        
            async def force_close_channel(self, chan_id):
                chan = self.channels[chan_id]
       -        peer = self.peers[chan.node_id]
       -        return await peer.force_close_channel(chan_id)
       +        tx = chan.force_close_tx()
       +        chan.set_state('FORCE_CLOSING')
       +        self.save_channel(chan)
       +        return await self.network.broadcast_transaction(tx)
        
            def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]:
                now = time.time()