URI: 
       tRefactor channel states: - persisted states are saved - state transitions are checked - transient states are stored in channel.peer_state - new channel states: 'PREOPENING', 'FUNDED' and 'REDEEMED' - upgrade storage to version 21 - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 61dfcba092c8b2ad68a5821010bd1a16fd1d57cc
   DIR parent c31fa059fe01a38ae5f382715283fbec5f6dca9a
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 29 Oct 2019 08:02:14 +0100
       
       Refactor channel states:
        - persisted states are saved
        - state transitions are checked
        - transient states are stored in channel.peer_state
        - new channel states: 'PREOPENING', 'FUNDED' and 'REDEEMED'
        - upgrade storage to version 21
       
       Diffstat:
         M electrum/gui/kivy/uix/dialogs/ligh… |       7 ++++---
         M electrum/gui/qt/channels_list.py    |       3 ++-
         M electrum/gui/qt/main_window.py      |       2 +-
         M electrum/json_db.py                 |      13 ++++++++++++-
         M electrum/lnchannel.py               |      78 ++++++++++++++++++++++---------
         M electrum/lnpeer.py                  |      28 ++++++++++++++++------------
         M electrum/lnwatcher.py               |       2 +-
         M electrum/lnworker.py                |      80 +++++++++++++++++++-------------
         M electrum/tests/regtest/regtest.sh   |       4 ++++
         M electrum/tests/test_lnchannel.py    |       6 ++++--
         M electrum/tests/test_lnpeer.py       |       7 ++++---
       
       11 files changed, 151 insertions(+), 79 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py
       t@@ -136,10 +136,11 @@ class ChannelDetailsPopup(Popup):
        
            def details(self):
                chan = self.chan
       +        status = self.app.wallet.lnworker.get_channel_status(chan)
                return {
                    _('Short Chan ID'): format_short_channel_id(chan.short_channel_id),
                    _('Initiator'): 'Local' if chan.constraints.is_initiator else 'Remote',
       -            _('State'): chan.get_state(),
       +            _('State'): status,
                    _('Local CTN'): chan.get_latest_ctn(LOCAL),
                    _('Remote CTN'): chan.get_latest_ctn(REMOTE),
                    _('Capacity'): self.app.format_amount_and_units(chan.constraints.capacity),
       t@@ -181,7 +182,7 @@ class ChannelDetailsPopup(Popup):
            def _force_close(self, b):
                if not b:
                    return
       -        if self.chan.get_state() == 'CLOSED':
       +        if self.chan.is_closed():
                    self.app.show_error(_('Channel already closed'))
                    return
                loop = self.app.wallet.network.asyncio_loop
       t@@ -223,7 +224,7 @@ class LightningChannelsDialog(Factory.Popup):
        
            def update_item(self, item):
                chan = item._chan
       -        item.status = chan.get_state()
       +        item.status = self.app.wallet.lnworker.get_channel_status(chan)
                item.short_channel_id = format_short_channel_id(chan.short_channel_id)
                l, r = self.format_fields(chan)
                item.local_balance = _('Local') + ':' + l
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -60,12 +60,13 @@ class ChannelsList(MyTreeView):
                    if bal_other != bal_minus_htlcs_other:
                        label += ' (+' + self.parent.format_amount(bal_other - bal_minus_htlcs_other) + ')'
                    labels[subject] = label
       +        status = self.lnworker.get_channel_status(chan)
                return [
                    format_short_channel_id(chan.short_channel_id),
                    bh2u(chan.node_id),
                    labels[LOCAL],
                    labels[REMOTE],
       -            chan.get_state()
       +            status
                ]
        
            def on_success(self, txid):
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -435,7 +435,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                wallet.thread = TaskThread(self, self.on_error)
                self.update_recently_visited(wallet.storage.path)
                if wallet.lnworker:
       -            wallet.lnworker.on_channels_updated()
       +            wallet.network.trigger_callback('channels_updated', wallet)
                self.need_update.set()
                # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
                # update menus
   DIR diff --git a/electrum/json_db.py b/electrum/json_db.py
       t@@ -40,7 +40,7 @@ from .logging import Logger
        
        OLD_SEED_VERSION = 4        # electrum versions < 2.0
        NEW_SEED_VERSION = 11       # electrum versions >= 2.0
       -FINAL_SEED_VERSION = 20     # electrum >= 2.7 will set this to prevent
       +FINAL_SEED_VERSION = 21     # electrum >= 2.7 will set this to prevent
                                    # old versions from overwriting new format
        
        
       t@@ -214,6 +214,7 @@ class JsonDB(Logger):
                self._convert_version_18()
                self._convert_version_19()
                self._convert_version_20()
       +        self._convert_version_21()
                self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure
        
                self._after_upgrade_tasks()
       t@@ -485,6 +486,16 @@ class JsonDB(Logger):
        
                self.put('seed_version', 20)
        
       +    def _convert_version_21(self):
       +        if not self._is_upgrade_method_needed(20, 20):
       +            return
       +        channels = self.get('channels')
       +        if channels:
       +            for channel in channels:
       +                channel['state'] = 'OPENING'
       +            self.put('channels', channels)
       +        self.put('seed_version', 21)
       +
            def _convert_imported(self):
                if not self._is_upgrade_method_needed(0, 13):
                    return
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -26,7 +26,7 @@ import os
        from collections import namedtuple, defaultdict
        import binascii
        import json
       -from enum import Enum, auto
       +from enum import IntEnum
        from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence, TYPE_CHECKING
        import time
        
       t@@ -55,6 +55,42 @@ if TYPE_CHECKING:
            from .lnworker import LNWallet
        
        
       +# lightning channel states
       +class channel_states(IntEnum):
       +    PREOPENING      = 0 # negociating
       +    OPENING         = 1 # awaiting funding tx
       +    FUNDED          = 2 # funded (requires min_depth and tx verification)
       +    OPEN            = 3 # both parties have sent funding_locked
       +    FORCE_CLOSING   = 4 # force-close tx has been broadcast
       +    CLOSING         = 5 # closing negociation
       +    CLOSED          = 6 # funding txo has been spent
       +    REDEEMED        = 7 # we can stop watching
       +
       +class peer_states(IntEnum):
       +    DISCONNECTED   = 0
       +    REESTABLISHING = 1
       +    GOOD           = 2
       +
       +cs = channel_states
       +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.CLOSED),
       +    (cs.FORCE_CLOSING, cs.CLOSED),
       +    (cs.CLOSED, cs.REDEEMED),
       +]
       +
        class ChannelJsonEncoder(json.JSONEncoder):
            def default(self, o):
                if isinstance(o, bytes):
       t@@ -136,18 +172,13 @@ class Channel(Logger):
                self.short_channel_id = ShortChannelID.normalize(state["short_channel_id"])
                self.short_channel_id_predicted = self.short_channel_id
                self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {}))
       -        self.force_closed = state.get('force_closed')
                self.data_loss_protect_remote_pcp = str_bytes_dict_from_save(state.get('data_loss_protect_remote_pcp', {}))
                self.remote_update = bfh(state.get('remote_update')) if state.get('remote_update') else None
        
                log = state.get('log')
       -        self.hm = HTLCManager(log=log,
       -                              initial_feerate=initial_feerate)
       -
       -
       -        self._is_funding_txo_spent = None  # "don't know"
       -        self._state = None
       -        self.set_state('DISCONNECTED')
       +        self.hm = HTLCManager(log=log, initial_feerate=initial_feerate)
       +        self._state = channel_states[state['state']]
       +        self.peer_state = peer_states.DISCONNECTED
                self.sweep_info = {}  # type: Dict[str, Dict[str, SweepInfo]]
                self._outgoing_channel_update = None  # type: Optional[bytes]
        
       t@@ -184,26 +215,31 @@ class Channel(Logger):
                                                                   next_per_commitment_point=None)
                self.config[LOCAL] = self.config[LOCAL]._replace(current_commitment_signature=remote_sig)
                self.hm.channel_open_finished()
       -        self.set_state('OPENING')
       +        self.peer_state = peer_states.GOOD
       +        self.set_state(channel_states.OPENING)
        
       -    def set_force_closed(self):
       -        self.force_closed = True
       -
       -    def set_state(self, state: str):
       +    def set_state(self, state):
       +        """ set on-chain state """
       +        if (self._state, state) not in state_transitions:
       +            raise Exception(f"Transition not allowed: {self._state.name} -> {state.name}")
                self._state = state
       +        self.logger.debug(f'Setting channel state: {state.name}')
       +        if self.lnworker:
       +            self.lnworker.save_channel(self)
       +            self.lnworker.network.trigger_callback('channel', self)
        
            def get_state(self):
                return self._state
        
            def is_closed(self):
       -        return self.force_closed or self.get_state() in ['CLOSED', 'CLOSING']
       +        return self.get_state() > channel_states.OPEN
        
            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():
                    raise PaymentFailure('Channel closed')
       -        if self.get_state() != 'OPEN':
       -            raise PaymentFailure('Channel not open')
       +        if self.get_state() != channel_states.OPEN:
       +            raise PaymentFailure('Channel not open', self.get_state())
                if self.available_to_spend(LOCAL) < amount_msat:
                    raise PaymentFailure(f'Not enough local balance. Have: {self.available_to_spend(LOCAL)}, Need: {amount_msat}')
                if len(self.hm.htlcs(LOCAL)) + 1 > self.config[REMOTE].max_accepted_htlcs:
       t@@ -222,12 +258,8 @@ class Channel(Logger):
                    return False
                return True
        
       -    def set_funding_txo_spentness(self, is_spent: bool):
       -        assert isinstance(is_spent, bool)
       -        self._is_funding_txo_spent = is_spent
       -
            def should_try_to_reestablish_peer(self) -> bool:
       -        return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED'
       +        return self._state < channel_states.CLOSED and self.peer_state == peer_states.DISCONNECTED
        
            def get_funding_address(self):
                script = funding_output_script(self.config[LOCAL], self.config[REMOTE])
       t@@ -624,7 +656,7 @@ class Channel(Logger):
                        "node_id": self.node_id,
                        "log": self.hm.to_save(),
                        "onion_keys": str_bytes_dict_to_save(self.onion_keys),
       -                "force_closed": self.force_closed,
       +                "state": self._state.name,
                        "data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp),
                        "remote_update": self.remote_update.hex() if self.remote_update else None
                }
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -29,7 +29,7 @@ from .logging import Logger
        from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
                              process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
                              ProcessedOnionPacket)
       -from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture
       +from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture, channel_states, peer_states
        from . import lnutil
        from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
                             RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
       t@@ -579,6 +579,7 @@ class Peer(Logger):
                    "local_config": local_config,
                    "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
                    "remote_update": None,
       +            "state": channel_states.PREOPENING.name,
                }
                chan = Channel(chan_dict,
                               sweep_address=self.lnworker.sweep_address,
       t@@ -594,10 +595,10 @@ class Peer(Logger):
                self.logger.info('received funding_signed')
                remote_sig = payload['signature']
                chan.receive_new_commitment(remote_sig, [])
       +        chan.open_with_first_pcp(remote_per_commitment_point, remote_sig)
                # broadcast funding tx
                # TODO make more robust (timeout low? server returns error?)
                await asyncio.wait_for(self.network.broadcast_transaction(funding_tx), LN_P2P_NETWORK_TIMEOUT)
       -        chan.open_with_first_pcp(remote_per_commitment_point, remote_sig)
                return chan
        
            async def on_open_channel(self, payload):
       t@@ -664,6 +665,7 @@ class Peer(Logger):
                        "local_config": local_config,
                        "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth),
                        "remote_update": None,
       +                "state": channel_states.PREOPENING.name,
                }
                chan = Channel(chan_dict,
                               sweep_address=self.lnworker.sweep_address,
       t@@ -709,11 +711,11 @@ class Peer(Logger):
            async def reestablish_channel(self, chan: Channel):
                await self.initialized.wait()
                chan_id = chan.channel_id
       -        if chan.get_state() != 'DISCONNECTED':
       +        if chan.peer_state != peer_states.DISCONNECTED:
                    self.logger.info('reestablish_channel was called but channel {} already in state {}'
                                     .format(chan_id, chan.get_state()))
                    return
       -        chan.set_state('REESTABLISHING')
       +        chan.peer_state = peer_states.REESTABLISHING
                self.network.trigger_callback('channel', chan)
                # BOLT-02: "A node [...] upon disconnection [...] MUST reverse any uncommitted updates sent by the other side"
                chan.hm.discard_unsigned_remote_updates()
       t@@ -856,11 +858,11 @@ class Peer(Logger):
                    await self.lnworker.force_close_channel(chan_id)
                    return
        
       +        chan.peer_state = peer_states.GOOD
                # note: chan.short_channel_id being set implies the funding txn is already at sufficient depth
                if their_next_local_ctn == next_local_ctn == 1 and chan.short_channel_id:
                    self.send_funding_locked(chan)
                # checks done
       -        chan.set_state('OPENING')
                if chan.config[LOCAL].funding_locked_received and chan.short_channel_id:
                    self.mark_open(chan)
                self.network.trigger_callback('channel', chan)
       t@@ -949,11 +951,12 @@ class Peer(Logger):
            def mark_open(self, chan: Channel):
                assert chan.short_channel_id is not None
                scid = chan.short_channel_id
       -        # only allow state transition to "OPEN" from "OPENING"
       -        if chan.get_state() != "OPENING":
       +        # only allow state transition from "FUNDED" to "OPEN"
       +        if chan.get_state() != channel_states.FUNDED:
       +            self.logger.info(f"cannot mark open, {chan.get_state()}")
                    return
                assert chan.config[LOCAL].funding_locked_received
       -        chan.set_state("OPEN")
       +        chan.set_state(channel_states.OPEN)
                self.network.trigger_callback('channel', chan)
                self.add_own_channel(chan)
                self.logger.info(f"CHANNEL OPENING COMPLETED for {scid}")
       t@@ -1114,7 +1117,8 @@ class Peer(Logger):
        
            async def pay(self, route: List['RouteEdge'], chan: Channel, amount_msat: int,
                          payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
       -        assert chan.get_state() == "OPEN", chan.get_state()
       +        if chan.get_state() != channel_states.OPEN:
       +            raise PaymentFailure('Channel not open')
                assert amount_msat > 0, "amount_msat is not greater zero"
                # create onion packet
                final_cltv = self.network.get_local_height() + min_final_cltv_expiry
       t@@ -1200,7 +1204,7 @@ class Peer(Logger):
                amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big')
                onion_packet = OnionPacket.from_bytes(payload["onion_routing_packet"])
                processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
       -        if chan.get_state() != "OPEN":
       +        if chan.get_state() != channel_states.OPEN:
                    raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}")
                if cltv_expiry >= 500_000_000:
                    asyncio.ensure_future(self.lnworker.force_close_channel(channel_id))
       t@@ -1255,7 +1259,7 @@ class Peer(Logger):
                    return
                outgoing_chan_upd = self.get_outgoing_gossip_channel_update_for_chan(next_chan)[2:]
                outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
       -        if next_chan.get_state() != 'OPEN':
       +        if next_chan.get_state() != channel_states.OPEN:
                    self.logger.info(f"cannot forward htlc. next_chan not OPEN: {next_chan_scid} in state {next_chan.get_state()}")
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE,
                                                        data=outgoing_chan_upd_len+outgoing_chan_upd)
       t@@ -1453,7 +1457,7 @@ class Peer(Logger):
            @log_exceptions
            async def _shutdown(self, chan: Channel, payload, is_local):
                # set state so that we stop accepting HTLCs
       -        chan.set_state('CLOSING')
       +        chan.set_state(channel_states.CLOSING)
                # wait until no HTLCs remain in either commitment transaction
                while len(chan.hm.htlcs(LOCAL)) + len(chan.hm.htlcs(REMOTE)) > 0:
                    self.logger.info('waiting for htlcs to settle...')
   DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -190,7 +190,7 @@ class LNWatcher(AddressSynchronizer):
                        return
                    self.network.trigger_callback('update_closed_channel', funding_outpoint, spenders,
                                                  funding_txid, funding_height, closing_txid,
       -                                          closing_height, closing_tx)  # FIXME sooo many args..
       +                                          closing_height, closing_tx, keep_watching)  # FIXME sooo many args..
                    # TODO: add tests for local_watchtower
                    await self.do_breach_remedy(funding_outpoint, spenders)
                if not keep_watching:
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -40,6 +40,7 @@ from .lnaddr import lnencode, LnAddr, lndecode
        from .ecc import der_sig_from_sig_string
        from .ecc_fast import is_using_fast_ecc
        from .lnchannel import Channel, ChannelJsonEncoder
       +from .lnchannel import channel_states, peer_states
        from . import lnutil
        from .lnutil import funding_output_script
        from .bitcoin import redeem_script_to_address
       t@@ -420,10 +421,21 @@ class LNWallet(LNWorker):
        
            def peer_closed(self, peer):
                for chan in self.channels_for_peer(peer.pubkey).values():
       -            chan.set_state('DISCONNECTED')
       +            chan.peer_state = peer_states.DISCONNECTED
                    self.network.trigger_callback('channel', chan)
                self.peers.pop(peer.pubkey)
        
       +    def get_channel_status(self, chan):
       +        # status displayed in the GUI
       +        cs = chan.get_state()
       +        if chan.is_closed():
       +            return cs.name
       +        peer = self.peers.get(chan.node_id)
       +        ps = chan.peer_state
       +        if ps != peer_states.GOOD:
       +            return ps.name
       +        return cs.name
       +
            def payment_completed(self, chan: Channel, direction: Direction,
                                  htlc: UpdateAddHtlc):
                chan_id = chan.channel_id
       t@@ -629,7 +641,6 @@ class LNWallet(LNWorker):
                    block_height, tx_pos, chan.funding_outpoint.output_index)
                self.logger.info(f"save_short_channel_id: {chan.short_channel_id}")
                self.save_channel(chan)
       -        self.on_channels_updated()
        
            def channel_by_txo(self, txo):
                with self.lock:
       t@@ -644,23 +655,25 @@ class LNWallet(LNWorker):
                chan = self.channel_by_txo(funding_outpoint)
                if not chan:
                    return
       -        #self.logger.debug(f'on_channel_open {funding_outpoint}')
       -        self.channel_timestamps[bh2u(chan.channel_id)] = funding_txid, funding_height.height, funding_height.timestamp, None, None, None
       -        self.storage.put('lightning_channel_timestamps', self.channel_timestamps)
       -        chan.set_funding_txo_spentness(False)
       -        # send event to GUI
       -        self.network.trigger_callback('channel', chan)
       -
       -        if self.should_channel_be_closed_due_to_expiring_htlcs(chan):
       +        if chan.get_state() == channel_states.OPEN and self.should_channel_be_closed_due_to_expiring_htlcs(chan):
                    self.logger.info(f"force-closing due to expiring htlcs")
                    await self.force_close_channel(chan.channel_id)
                    return
       -        if chan.short_channel_id is None:
       -            self.save_short_chan_id(chan)
       -        if chan.get_state() == "OPENING" and chan.short_channel_id:
       -            peer = self.peers[chan.node_id]
       -            peer.send_funding_locked(chan)
       -        elif chan.get_state() == "OPEN":
       +
       +        if chan.get_state() == channel_states.OPENING:
       +            if chan.short_channel_id is None:
       +                self.save_short_chan_id(chan)
       +            if chan.short_channel_id:
       +                chan.set_state(channel_states.FUNDED)
       +                self.channel_timestamps[bh2u(chan.channel_id)] = chan.funding_outpoint.txid, funding_height.height, funding_height.timestamp, None, None, None
       +                self.storage.put('lightning_channel_timestamps', self.channel_timestamps)
       +
       +        if chan.get_state() == channel_states.FUNDED:
       +            peer = self.peers.get(chan.node_id)
       +            if peer:
       +                peer.send_funding_locked(chan)
       +
       +        elif chan.get_state() == channel_states.OPEN:
                    peer = self.peers.get(chan.node_id)
                    if peer is None:
                        self.logger.info("peer not found for {}".format(bh2u(chan.node_id)))
       t@@ -669,7 +682,8 @@ class LNWallet(LNWorker):
                        await peer.bitcoin_fee_update(chan)
                    conf = self.lnwatcher.get_tx_height(chan.funding_outpoint.txid).conf
                    peer.on_network_update(chan, conf)
       -        elif chan.force_closed and chan.get_state() != 'CLOSED':
       +
       +        elif chan.get_state() == channel_states.FORCE_CLOSING:
                    txid = chan.force_close_tx().txid()
                    height = self.lnwatcher.get_tx_height(txid).height
                    self.logger.info(f"force closing tx {txid}, height {height}")
       t@@ -677,21 +691,27 @@ class LNWallet(LNWorker):
                        self.logger.info('REBROADCASTING CLOSING TX')
                        await self.force_close_channel(chan.channel_id)
        
       +    @ignore_exceptions
            @log_exceptions
       -    async def on_update_closed_channel(self, event, funding_outpoint, spenders, funding_txid, funding_height, closing_txid, closing_height, closing_tx):
       +    async def on_update_closed_channel(self, event, funding_outpoint, spenders, funding_txid, funding_height, closing_txid, closing_height, closing_tx, keep_watching):
                chan = self.channel_by_txo(funding_outpoint)
                if not chan:
                    return
       -        #self.logger.debug(f'on_channel_closed {funding_outpoint}')
       +
       +        # fixme: this is wasteful
                self.channel_timestamps[bh2u(chan.channel_id)] = funding_txid, funding_height.height, funding_height.timestamp, closing_txid, closing_height.height, closing_height.timestamp
                self.storage.put('lightning_channel_timestamps', self.channel_timestamps)
       -        chan.set_funding_txo_spentness(True)
       -        chan.set_state('CLOSED')
       -        self.on_channels_updated()
       -        self.network.trigger_callback('channel', chan)
       +
                # remove from channel_db
                if chan.short_channel_id is not None:
                    self.channel_db.remove_channel(chan.short_channel_id)
       +
       +        if chan.get_state() < channel_states.CLOSED:
       +            chan.set_state(channel_states.CLOSED)
       +
       +        if chan.get_state() == channel_states.CLOSED and not keep_watching:
       +            chan.set_state(channel_states.REDEEMED)
       +
                # detect who closed and set sweep_info
                sweep_info_dict = chan.sweep_ctx(closing_tx)
                self.logger.info(f'sweep_info_dict length: {len(sweep_info_dict)}')
       t@@ -800,11 +820,8 @@ class LNWallet(LNWorker):
                    temp_channel_id=os.urandom(32))
                self.save_channel(chan)
                self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
       -        self.on_channels_updated()
       -        return chan
       -
       -    def on_channels_updated(self):
                self.network.trigger_callback('channels_updated', self.wallet)
       +        return chan
        
            @log_exceptions
            async def add_peer(self, connect_str: str) -> Peer:
       t@@ -1149,7 +1166,7 @@ class LNWallet(LNWorker):
                # note: currently we add *all* our channels; but this might be a privacy leak?
                for chan in channels:
                    # check channel is open
       -            if chan.get_state() != "OPEN":
       +            if chan.get_state() != channel_states.OPEN:
                        continue
                    # check channel has sufficient balance
                    # FIXME because of on-chain fees of ctx, this check is insufficient
       t@@ -1206,7 +1223,7 @@ class LNWallet(LNWorker):
                            'channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
                            'full_channel_id': bh2u(chan.channel_id),
                            'channel_point': chan.funding_outpoint.to_str(),
       -                    'state': chan.get_state(),
       +                    'state': chan.get_state().name,
                            'remote_pubkey': bh2u(chan.node_id),
                            'local_balance': chan.balance(LOCAL)//1000,
                            'remote_balance': chan.balance(REMOTE)//1000,
       t@@ -1220,9 +1237,7 @@ class LNWallet(LNWorker):
            async def force_close_channel(self, chan_id):
                chan = self.channels[chan_id]
                tx = chan.force_close_tx()
       -        chan.set_force_closed()
       -        self.save_channel(chan)
       -        self.on_channels_updated()
       +        chan.set_state(channel_states.FORCE_CLOSING)
                try:
                    await self.network.broadcast_transaction(tx)
                except Exception as e:
       t@@ -1276,6 +1291,7 @@ class LNWallet(LNWorker):
                            if ratio < 0.5:
                                self.logger.warning(f"fee level for channel {bh2u(chan.channel_id)} is {chan_feerate} sat/kiloweight, "
                                                    f"current recommended feerate is {self.current_feerate_per_kw()} sat/kiloweight, consider force closing!")
       +                # reestablish
                        if not chan.should_try_to_reestablish_peer():
                            continue
                        peer = self.peers.get(chan.node_id, None)
   DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
       t@@ -90,6 +90,10 @@ if [[ $1 == "init" ]]; then
            new_blocks 1
        fi
        
       +if [[ $1 == "new_block" ]]; then
       +    new_blocks 1
       +fi
       +
        # start daemons. Bob is started first because he is listening
        if [[ $1 == "start" ]]; then
            $bob daemon -d
   DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
       t@@ -34,6 +34,7 @@ from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
        from electrum.lnutil import FeeUpdate
        from electrum.ecc import sig_string_from_der_sig
        from electrum.logging import console_stderr_handler
       +from electrum.lnchannel import channel_states
        
        from . import ElectrumTestCase
        
       t@@ -94,6 +95,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
                    ),
                    "node_id":other_node_id,
                    'onion_keys': {},
       +            'state': 'PREOPENING',
            }
        
        def bip32(sequence):
       t@@ -136,8 +138,8 @@ def create_test_channels(feerate=6000, local=None, remote=None):
            alice.hm.log[LOCAL]['ctn'] = 0
            bob.hm.log[LOCAL]['ctn'] = 0
        
       -    alice.set_state('OPEN')
       -    bob.set_state('OPEN')
       +    alice._state = channel_states.OPEN
       +    bob._state = channel_states.OPEN
        
            a_out = alice.get_latest_commitment(LOCAL).outputs()
            b_out = bob.get_next_commitment(REMOTE).outputs()
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -16,6 +16,7 @@ from electrum.lnpeer import Peer
        from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
        from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
        from electrum.lnutil import PaymentFailure, LnLocalFeatures
       +from electrum.lnchannel import channel_states
        from electrum.lnrouter import LNPathFinder
        from electrum.channel_db import ChannelDB
        from electrum.lnworker import LNWallet, NoPathFound
       t@@ -202,9 +203,9 @@ class TestPeer(ElectrumTestCase):
                w1.peer = p1
                w2.peer = p2
                # mark_open won't work if state is already OPEN.
       -        # so set it to OPENING
       -        self.alice_channel.set_state("OPENING")
       -        self.bob_channel.set_state("OPENING")
       +        # so set it to FUNDED
       +        self.alice_channel._state = channel_states.FUNDED
       +        self.bob_channel._state = channel_states.FUNDED
                # this populates the channel graph:
                p1.mark_open(self.alice_channel)
                p2.mark_open(self.bob_channel)