URI: 
       tAdd extra state to distinguish shutdown negotiation from post- negotiation, where channel should not be reestablished. See #6182 - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 2adbbee5fe2da2bcf753a4ef27ddb4a86cf6dfae
   DIR parent 680502cfb8b22b4daa8697701c3f672e2eca0763
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Fri, 29 May 2020 11:30:08 +0200
       
       Add extra state to distinguish shutdown negotiation from post-
       negotiation, where channel should not be reestablished. See #6182
       
       Diffstat:
         M electrum/lnchannel.py               |      48 +++++++++++++++++--------------
         M electrum/lnpeer.py                  |      24 +++++++++++++++---------
         M electrum/tests/test_lnpeer.py       |       7 +++++++
       
       3 files changed, 49 insertions(+), 30 deletions(-)
       ---
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -77,10 +77,11 @@ class ChannelState(IntEnum):
                                 #  - Non-funding node: has sent the funding_signed message.
            FUNDED          = 2  # Funding tx was mined (requires min_depth and tx verification)
            OPEN            = 3  # both parties have sent funding_locked
       -    CLOSING         = 4  # shutdown has been sent, and closing tx is unconfirmed.
       -    FORCE_CLOSING   = 5  # we force-closed, and closing tx is unconfirmed. (otherwise we remain OPEN)
       -    CLOSED          = 6  # closing tx has been mined
       -    REDEEMED        = 7  # we can stop watching
       +    SHUTDOWN        = 4  # shutdown has been sent.
       +    CLOSING         = 5  # closing negotiation done. we have a fully signed tx.
       +    FORCE_CLOSING   = 6  # we force-closed, and closing tx is unconfirmed. (otherwise we remain OPEN)
       +    CLOSED          = 7  # closing tx has been mined
       +    REDEEMED        = 8  # we can stop watching
        
        
        class PeerState(IntEnum):
       t@@ -95,18 +96,25 @@ state_transitions = [
            (cs.PREOPENING, cs.OPENING),
            (cs.OPENING, cs.FUNDED),
            (cs.FUNDED, cs.OPEN),
       -    (cs.OPENING, cs.CLOSING),
       -    (cs.FUNDED, cs.CLOSING),
       -    (cs.OPEN, cs.CLOSING),
       -    (cs.OPENING, cs.FORCE_CLOSING),
       -    (cs.FUNDED, cs.FORCE_CLOSING),
       -    (cs.OPEN, cs.FORCE_CLOSING),
       -    (cs.CLOSING, cs.FORCE_CLOSING),
       -    (cs.OPENING, cs.CLOSED),
       -    (cs.FUNDED, cs.CLOSED),
       -    (cs.OPEN, cs.CLOSED),
       -    (cs.CLOSING, cs.CLOSING),  # if we reestablish
       -    (cs.CLOSING, cs.CLOSED),
       +    (cs.OPENING, cs.SHUTDOWN),
       +    (cs.FUNDED, cs.SHUTDOWN),
       +    (cs.OPEN, cs.SHUTDOWN),
       +    (cs.SHUTDOWN, cs.SHUTDOWN),  # if we reestablish
       +    (cs.SHUTDOWN, cs.CLOSING),
       +    (cs.CLOSING, cs.CLOSING),
       +    # we can force close almost any time
       +    (cs.OPENING,  cs.FORCE_CLOSING),
       +    (cs.FUNDED,   cs.FORCE_CLOSING),
       +    (cs.OPEN,     cs.FORCE_CLOSING),
       +    (cs.SHUTDOWN, cs.FORCE_CLOSING),
       +    (cs.CLOSING,  cs.FORCE_CLOSING),
       +    # we can get force closed almost any time
       +    (cs.OPENING,  cs.CLOSED),
       +    (cs.FUNDED,   cs.CLOSED),
       +    (cs.OPEN,     cs.CLOSED),
       +    (cs.SHUTDOWN, cs.CLOSED),
       +    (cs.CLOSING,  cs.CLOSED),
       +    #
            (cs.FORCE_CLOSING, cs.FORCE_CLOSING),  # allow multiple attempts
            (cs.FORCE_CLOSING, cs.CLOSED),
            (cs.FORCE_CLOSING, cs.REDEEMED),
       t@@ -174,11 +182,11 @@ class AbstractChannel(Logger, ABC):
                return self.get_state() == ChannelState.OPEN
        
            def is_closing(self):
       -        return self.get_state() in [ChannelState.CLOSING, ChannelState.FORCE_CLOSING]
       +        return ChannelState.SHUTDOWN <= self.get_state() <= ChannelState.FORCE_CLOSING
        
            def is_closed(self):
                # the closing txid has been saved
       -        return self.get_state() >= ChannelState.CLOSED
       +        return self.get_state() >= ChannelState.CLOSING
        
            def is_redeemed(self):
                return self.get_state() == ChannelState.REDEEMED
       t@@ -707,8 +715,6 @@ class Channel(AbstractChannel):
                #       and the constraints are the ones imposed by their config
                ctn = self.get_next_ctn(htlc_receiver)
                chan_config = self.config[htlc_receiver]
       -        if self.is_closed():
       -            raise PaymentFailure('Channel closed')
                if self.get_state() != ChannelState.OPEN:
                    raise PaymentFailure('Channel not open', self.get_state())
                if htlc_proposer == LOCAL:
       t@@ -777,7 +783,7 @@ class Channel(AbstractChannel):
                return True
        
            def should_try_to_reestablish_peer(self) -> bool:
       -        return ChannelState.PREOPENING < self._state < ChannelState.FORCE_CLOSING and self.peer_state == PeerState.DISCONNECTED
       +        return ChannelState.PREOPENING < self._state < ChannelState.CLOSING and self.peer_state == PeerState.DISCONNECTED
        
            def get_funding_address(self):
                script = funding_output_script(self.config[LOCAL], self.config[REMOTE])
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -87,7 +87,7 @@ class Peer(Logger):
                self.temp_id_to_id = {}   # to forward error messages
                self.funding_created_sent = set() # for channels in PREOPENING
                self.funding_signed_sent = set()  # for channels in PREOPENING
       -        self.shutdown_received = {}
       +        self.shutdown_received = {} # chan_id -> asyncio.Future()
                self.announcement_signatures = defaultdict(asyncio.Queue)
                self.orphan_channel_updates = OrderedDict()
                Logger.__init__(self)
       t@@ -933,7 +933,8 @@ class Peer(Logger):
                if chan.is_funded() and chan.config[LOCAL].funding_locked_received:
                    self.mark_open(chan)
                util.trigger_callback('channel', chan)
       -        if chan.get_state() == ChannelState.CLOSING:
       +        # if we have sent a previous shutdown, it must be retransmitted (Bolt2)
       +        if chan.get_state() == ChannelState.SHUTDOWN:
                    await self.send_shutdown(chan)
        
            def send_funding_locked(self, chan: Channel):
       t@@ -1429,7 +1430,7 @@ class Peer(Logger):
                while chan.has_pending_changes(REMOTE):
                    await asyncio.sleep(0.1)
                self.send_message('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey)
       -        chan.set_state(ChannelState.CLOSING)
       +        chan.set_state(ChannelState.SHUTDOWN)
                # can fullfill or fail htlcs. cannot add htlcs, because of CLOSING state
                chan.set_can_send_ctx_updates(True)
        
       t@@ -1492,12 +1493,17 @@ class Peer(Logger):
                if not chan.constraints.is_initiator:
                    send_closing_signed()
                # add signatures
       -        closing_tx.add_signature_to_txin(txin_idx=0,
       -                                         signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(),
       -                                         sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
       -        closing_tx.add_signature_to_txin(txin_idx=0,
       -                                         signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(),
       -                                         sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
       +        closing_tx.add_signature_to_txin(
       +            txin_idx=0,
       +            signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(),
       +            sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
       +        closing_tx.add_signature_to_txin(
       +            txin_idx=0,
       +            signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(),
       +            sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
       +        # save local transaction and set state
       +        self.lnworker.wallet.add_transaction(closing_tx)
       +        chan.set_state(ChannelState.CLOSING)
                # broadcast
                await self.network.try_broadcasting(closing_tx, 'closing')
                return closing_tx.txid()
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -90,13 +90,20 @@ class MockBlockchain:
        
        
        class MockWallet:
       +
            def set_label(self, x, y):
                pass
       +
            def save_db(self):
                pass
       +
       +    def add_transaction(self, tx):
       +        pass
       +
            def is_lightning_backup(self):
                return False
        
       +
        class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
            def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue):
                Logger.__init__(self)