URI: 
       tReplace wallet backup with channel backups - channels can be backed up individually - backups are added to lnwatcher - AbstractChannel ancestor class - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 8f41aeb783048ca01a2474d738ece1ee2ade28fb
   DIR parent e5b1596b69a1c77b3d8d2b235b65e624a2383921
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Fri, 13 Mar 2020 11:44:29 +0100
       
       Replace wallet backup with channel backups
        - channels can be backed up individually
        - backups are added to lnwatcher
        - AbstractChannel ancestor class
       
       Diffstat:
         M electrum/commands.py                |      10 ++++++++++
         M electrum/gui/kivy/main_window.py    |      20 +++++++++++++++++---
         M electrum/gui/kivy/uix/dialogs/ligh… |     126 ++++++++++++++++++++++++++++++-
         M electrum/gui/qt/__init__.py         |       1 -
         M electrum/gui/qt/channels_list.py    |      51 ++++++++++++++++++++++---------
         M electrum/gui/qt/main_window.py      |      59 +++++++++++++++++++++++--------
         M electrum/gui/qt/settings_dialog.py  |      14 --------------
         M electrum/lnchannel.py               |     402 ++++++++++++++++++++-----------
         M electrum/lnpeer.py                  |      26 ++++++++++++++------------
         M electrum/lnsweep.py                 |       2 +-
         M electrum/lnutil.py                  |      70 ++++++++++++++++++++++++++++++-
         M electrum/lnwatcher.py               |       1 +
         M electrum/lnworker.py                |     108 +++++++++++++++++++++++++++++--
         M electrum/transaction.py             |       4 ++++
         M electrum/wallet.py                  |      23 +++++++++++++++--------
         M electrum/wallet_db.py               |       4 +++-
       
       16 files changed, 705 insertions(+), 216 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -1050,6 +1050,16 @@ class Commands:
                coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)
                return await coro
        
       +    @command('w')
       +    async def export_channel_backup(self, channel_point, wallet: Abstract_Wallet = None):
       +        txid, index = channel_point.split(':')
       +        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
       +        return wallet.lnworker.export_channel_backup(chan_id)
       +
       +    @command('w')
       +    async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):
       +        return wallet.lnworker.import_channel_backup(encrypted)
       +
            @command('wn')
            async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
                """ return the current commitment transaction of a channel """
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -408,6 +408,9 @@ class ElectrumWindow(App):
                if data.startswith('bitcoin:'):
                    self.set_URI(data)
                    return
       +        if data.startswith('channel_backup:'):
       +            self.import_channel_backup(data[15:])
       +            return
                bolt11_invoice = maybe_extract_bolt11_invoice(data)
                if bolt11_invoice is not None:
                    self.set_ln_invoice(bolt11_invoice)
       t@@ -727,9 +730,6 @@ class ElectrumWindow(App):
                d.open()
        
            def lightning_channels_dialog(self):
       -        if not self.wallet.has_lightning():
       -            self.show_error('Lightning not enabled on this wallet')
       -            return
                if self._channels_dialog is None:
                    self._channels_dialog = LightningChannelsDialog(self)
                self._channels_dialog.open()
       t@@ -1303,3 +1303,17 @@ class ElectrumWindow(App):
                        self.show_error("Invalid PIN")
                        return
                self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
       +
       +    def import_channel_backup(self, encrypted):
       +        d = Question(_('Import Channel Backup?'), lambda b: self._import_channel_backup(b, encrypted))
       +        d.open()
       +
       +    def _import_channel_backup(self, b, encrypted):
       +        if not b:
       +            return
       +        try:
       +            self.wallet.lnbackups.import_channel_backup(encrypted)
       +        except Exception as e:
       +            self.show_error("failed to import backup" + '\n' + str(e))
       +            return
       +        self.lightning_channels_dialog()
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py
       t@@ -198,9 +198,118 @@ Builder.load_string(r'''
                        text: _('Delete')
                        on_release: root.remove_channel()
                        disabled: not root.is_redeemed
       +
       +<ChannelBackupPopup@Popup>:
       +    id: popuproot
       +    data: []
       +    is_closed: False
       +    is_redeemed: False
       +    node_id:''
       +    short_id:''
       +    initiator:''
       +    capacity:''
       +    funding_txid:''
       +    closing_txid:''
       +    state:''
       +    is_open:False
       +    BoxLayout:
       +        padding: '12dp', '12dp', '12dp', '12dp'
       +        spacing: '12dp'
       +        orientation: 'vertical'
       +        ScrollView:
       +            scroll_type: ['bars', 'content']
       +            scroll_wheel_distance: dp(114)
       +            BoxLayout:
       +                orientation: 'vertical'
       +                height: self.minimum_height
       +                size_hint_y: None
       +                spacing: '5dp'
       +                BoxLabel:
       +                    text: _('Channel ID')
       +                    value: root.short_id
       +                BoxLabel:
       +                    text: _('State')
       +                    value: root.state
       +                BoxLabel:
       +                    text: _('Initiator')
       +                    value: root.initiator
       +                BoxLabel:
       +                    text: _('Capacity')
       +                    value: root.capacity
       +                Widget:
       +                    size_hint: 1, 0.1
       +                TopLabel:
       +                    text: _('Remote Node ID')
       +                TxHashLabel:
       +                    data: root.node_id
       +                    name: _('Remote Node ID')
       +                TopLabel:
       +                    text: _('Funding Transaction')
       +                TxHashLabel:
       +                    data: root.funding_txid
       +                    name: _('Funding Transaction')
       +                    touch_callback: lambda: app.show_transaction(root.funding_txid)
       +                TopLabel:
       +                    text: _('Closing Transaction')
       +                    opacity: int(bool(root.closing_txid))
       +                TxHashLabel:
       +                    opacity: int(bool(root.closing_txid))
       +                    data: root.closing_txid
       +                    name: _('Closing Transaction')
       +                    touch_callback: lambda: app.show_transaction(root.closing_txid)
       +                Widget:
       +                    size_hint: 1, 0.1
       +        Widget:
       +            size_hint: 1, 0.05
       +        BoxLayout:
       +            size_hint: 1, None
       +            height: '48dp'
       +            Button:
       +                size_hint: 0.5, None
       +                height: '48dp'
       +                text: _('Request force-close')
       +                on_release: root.request_force_close()
       +                disabled: root.is_closed
       +            Button:
       +                size_hint: 0.5, None
       +                height: '48dp'
       +                text: _('Delete')
       +                on_release: root.remove_backup()
        ''')
        
        
       +class ChannelBackupPopup(Popup):
       +
       +    def __init__(self, chan, app, **kwargs):
       +        super(ChannelBackupPopup,self).__init__(**kwargs)
       +        self.chan = chan
       +        self.app = app
       +
       +    def request_force_close(self):
       +        msg = _('Request force close?')
       +        Question(msg, self._request_force_close).open()
       +
       +    def _request_force_close(self, b):
       +        if not b:
       +            return
       +        loop = self.app.wallet.network.asyncio_loop
       +        coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnbackups.request_force_close(self.chan.channel_id), loop)
       +        try:
       +            coro.result(5)
       +            self.app.show_info(_('Channel closed'))
       +        except Exception as e:
       +            self.app.show_info(_('Could not close channel: ') + repr(e)) # repr because str(Exception()) == ''
       +
       +    def remove_backup(self):
       +        msg = _('Delete backup?')
       +        Question(msg, self._remove_backup).open()
       +
       +    def _remove_backup(self, b):
       +        if not b:
       +            return
       +        self.app.wallet.lnbackups.remove_channel_backup(self.chan.channel_id)
       +        self.dismiss()
       +
        class ChannelDetailsPopup(Popup):
        
            def __init__(self, chan, app, **kwargs):
       t@@ -282,7 +391,11 @@ class LightningChannelsDialog(Factory.Popup):
                self.update()
        
            def show_item(self, obj):
       -        p = ChannelDetailsPopup(obj._chan, self.app)
       +        chan = obj._chan
       +        if chan.is_backup():
       +            p = ChannelBackupPopup(chan, self.app)
       +        else:
       +            p = ChannelDetailsPopup(chan, self.app)
                p.open()
        
            def format_fields(self, chan):
       t@@ -305,7 +418,7 @@ class LightningChannelsDialog(Factory.Popup):
            def update_item(self, item):
                chan = item._chan
                item.status = chan.get_state_for_GUI()
       -        item.short_channel_id = format_short_channel_id(chan.short_channel_id)
       +        item.short_channel_id = chan.short_id_for_GUI()
                l, r = self.format_fields(chan)
                item.local_balance = _('Local') + ':' + l
                item.remote_balance = _('Remote') + ': ' + r
       t@@ -317,10 +430,13 @@ class LightningChannelsDialog(Factory.Popup):
                if not self.app.wallet:
                    return
                lnworker = self.app.wallet.lnworker
       -        for i in lnworker.channels.values():
       +        channels = list(lnworker.channels.values()) if lnworker else []
       +        lnbackups = self.app.wallet.lnbackups
       +        backups = list(lnbackups.channel_backups.values())
       +        for i in channels + backups:
                    item = Factory.LightningChannelItem()
                    item.screen = self
       -            item.active = i.node_id in lnworker.peers
       +            item.active = i.node_id in (lnworker.peers if lnworker else [])
                    item._chan = i
                    self.update_item(item)
                    channel_cards.add_widget(item)
       t@@ -328,5 +444,7 @@ class LightningChannelsDialog(Factory.Popup):
        
            def update_can_send(self):
                lnworker = self.app.wallet.lnworker
       +        if not lnworker:
       +            return
                self.can_send = self.app.format_amount_and_units(lnworker.num_sats_can_send())
                self.can_receive = self.app.format_amount_and_units(lnworker.num_sats_can_receive())
   DIR diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
       t@@ -235,7 +235,6 @@ class ElectrumGui(Logger):
                run_hook('on_new_window', w)
                w.warn_if_testnet()
                w.warn_if_watching_only()
       -        w.warn_if_lightning_backup()
                return w
        
            def count_wizards_in_progress(func):
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -57,6 +57,7 @@ class ChannelsList(MyTreeView):
                self.update_single_row.connect(self.do_update_single_row)
                self.network = self.parent.network
                self.lnworker = self.parent.wallet.lnworker
       +        self.lnbackups = self.parent.wallet.lnbackups
                self.setSortingEnabled(True)
        
            def format_fields(self, chan):
       t@@ -78,7 +79,7 @@ class ChannelsList(MyTreeView):
                else:
                    node_alias = ''
                return [
       -            format_short_channel_id(chan.short_channel_id),
       +            chan.short_id_for_GUI(),
                    bh2u(chan.node_id),
                    node_alias,
                    '' if closed else labels[LOCAL],
       t@@ -106,14 +107,11 @@ class ChannelsList(MyTreeView):
            def force_close(self, channel_id):
                chan = self.lnworker.channels[channel_id]
                to_self_delay = chan.config[REMOTE].to_self_delay
       -        if self.lnworker.wallet.is_lightning_backup():
       -            msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?')
       -        else:
       -            msg = _('Force-close channel?') + '\n\n'\
       -                  + _(f'Funds retrieved from this channel will not be available before {to_self_delay} blocks after forced closure.') + ' '\
       -                  + _('After that delay, funds will be sent to an address derived from your wallet seed.') + '\n\n'\
       -                  + _('In the meantime, channel funds will not be recoverable from your seed, and will be lost if you lose your wallet.') + ' '\
       -                  + _('To prevent that, you should backup your wallet if you have not already done so.')
       +        msg = _('Force-close channel?') + '\n\n'\
       +              + _(f'Funds retrieved from this channel will not be available before {to_self_delay} blocks after forced closure.') + ' '\
       +              + _('After that delay, funds will be sent to an address derived from your wallet seed.') + '\n\n'\
       +              + _('In the meantime, channel funds will not be recoverable from your seed, and might be lost if you lose your wallet.') + ' '\
       +              + _('To prevent that, you should have a backup of this channel on another device.')
                if self.parent.question(msg):
                    def task():
                        coro = self.lnworker.force_close_channel(channel_id)
       t@@ -124,6 +122,22 @@ class ChannelsList(MyTreeView):
                if self.main_window.question(_('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.')):
                    self.lnworker.remove_channel(channel_id)
        
       +    def remove_channel_backup(self, channel_id):
       +        if self.main_window.question(_('Remove channel backup?')):
       +            self.lnbackups.remove_channel_backup(channel_id)
       +
       +    def export_channel_backup(self, channel_id):
       +        data = self.lnworker.export_channel_backup(channel_id)
       +        self.main_window.show_qrcode('channel_backup:' + data, 'channel backup')
       +
       +    def request_force_close(self, channel_id):
       +        def task():
       +            coro = self.lnbackups.request_force_close(channel_id)
       +            return self.network.run_from_another_thread(coro)
       +        def on_success(b):
       +            self.main_window.show_message('success')
       +        WaitingDialog(self, 'please wait..', task, on_success, self.on_failure)
       +
            def create_menu(self, position):
                menu = QMenu()
                menu.setSeparatorsCollapsible(True)  # consecutive separators are merged together
       t@@ -140,6 +154,11 @@ class ChannelsList(MyTreeView):
                if not item:
                    return
                channel_id = idx.sibling(idx.row(), self.Columns.NODE_ID).data(ROLE_CHANNEL_ID)
       +        if channel_id in self.lnbackups.channel_backups:
       +            menu.addAction(_("Request force-close"), lambda: self.request_force_close(channel_id))
       +            menu.addAction(_("Delete"), lambda: self.remove_channel_backup(channel_id))
       +            menu.exec_(self.viewport().mapToGlobal(position))
       +            return
                chan = self.lnworker.channels[channel_id]
                menu.addAction(_("Details..."), lambda: self.parent.show_channel(channel_id))
                cc = self.add_copy_menu(menu, idx)
       t@@ -163,7 +182,6 @@ 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))
       -            menu.addSeparator()
                else:
                    item = chan.get_closing_height()
                    if item:
       t@@ -171,6 +189,8 @@ class ChannelsList(MyTreeView):
                        closing_tx = self.lnworker.lnwatcher.db.get_transaction(txid)
                        if closing_tx:
                            menu.addAction(_("View closing transaction"), lambda: self.parent.show_transaction(closing_tx))
       +        menu.addSeparator()
       +        menu.addAction(_("Export backup"), lambda: self.export_channel_backup(channel_id))
                if chan.is_redeemed():
                    menu.addSeparator()
                    menu.addAction(_("Delete"), lambda: self.remove_channel(channel_id))
       t@@ -195,13 +215,13 @@ class ChannelsList(MyTreeView):
            def do_update_rows(self, wallet):
                if wallet != self.parent.wallet:
                    return
       -        lnworker = self.parent.wallet.lnworker
       -        if not lnworker:
       -            return
       -        self.update_can_send(lnworker)
       +        channels = list(wallet.lnworker.channels.values()) if wallet.lnworker else []
       +        backups = list(wallet.lnbackups.channel_backups.values())
       +        if wallet.lnworker:
       +            self.update_can_send(wallet.lnworker)
                self.model().clear()
                self.update_headers(self.headers)
       -        for chan in lnworker.channels.values():
       +        for chan in channels + backups:
                    items = [QtGui.QStandardItem(x) for x in self.format_fields(chan)]
                    self.set_editability(items)
                    if self._default_item_bg_brush is None:
       t@@ -212,6 +232,7 @@ class ChannelsList(MyTreeView):
                    items[self.Columns.REMOTE_BALANCE].setFont(QFont(MONOSPACE_FONT))
                    self._update_chan_frozen_bg(chan=chan, items=items)
                    self.model().insertRow(0, items)
       +
                self.sortByColumn(self.Columns.SHORT_CHANID, Qt.DescendingOrder)
        
            def _update_chan_frozen_bg(self, *, chan: Channel, items: Sequence[QStandardItem]):
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -221,8 +221,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                        tabs.addTab(tab, icon, description.replace("&", ""))
        
                add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses")
       -        if self.wallet.has_lightning():
       -            add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels")
       +        add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels")
                add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo")
                add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts")
                add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole"), "console")
       t@@ -524,18 +523,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    ])
                    self.show_warning(msg, title=_('Watch-only wallet'))
        
       -    def warn_if_lightning_backup(self):
       -        if self.wallet.is_lightning_backup():
       -            msg = '\n\n'.join([
       -                _("This file is a backup of a lightning wallet."),
       -                _("You will not be able to perform lightning payments using this file, and the lightning balance displayed in this wallet might be outdated.") + ' ' + \
       -                _("If you have lost the original wallet file, you can use this file to trigger a forced closure of your channels."),
       -                _("Do you want to have your channels force-closed?")
       -            ])
       -            if self.question(msg, title=_('Lightning Backup')):
       -                self.network.maybe_init_lightning()
       -                self.wallet.lnworker.start_network(self.network)
       -
            def warn_if_testnet(self):
                if not constants.net.TESTNET:
                    return
       t@@ -572,14 +559,44 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
                self.gui_object.new_window(filename)
        
       +    def select_backup_dir(self, b):
       +        name = self.config.get('backup_dir', '')
       +        dirname = QFileDialog.getExistingDirectory(self, "Select your SSL certificate file", name)
       +        if dirname:
       +            self.config.set_key('backup_dir', dirname)
       +            self.backup_dir_e.setText(dirname)
       +
            def backup_wallet(self):
       +        d = WindowModalDialog(self, _("File Backup"))
       +        vbox = QVBoxLayout(d)
       +        grid = QGridLayout()
       +        backup_help = ""
       +        backup_dir = self.config.get('backup_dir')
       +        backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
       +        msg = _('Please select a backup directory')
       +        if self.wallet.lnworker and self.wallet.lnworker.channels:
       +            msg += '\n\n' + ' '.join([
       +                _("Note that lightning channels will be converted to channel backups."),
       +                _("You cannot use channel backups to perform lightning payments."),
       +                _("Channel backups can only be used to request your channels to be closed.")
       +            ])
       +        self.backup_dir_e = QPushButton(backup_dir)
       +        self.backup_dir_e.clicked.connect(self.select_backup_dir)
       +        grid.addWidget(backup_dir_label, 1, 0)
       +        grid.addWidget(self.backup_dir_e, 1, 1)
       +        vbox.addLayout(grid)
       +        vbox.addWidget(WWLabel(msg))
       +        vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
       +        if not d.exec_():
       +            return
                try:
                    new_path = self.wallet.save_backup()
                except BaseException as reason:
                    self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
                    return
                if new_path:
       -            self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
       +            msg = _("A copy of your wallet file was created in")+" '%s'" % str(new_path)
       +            self.show_message(msg, title=_("Wallet backup created"))
                else:
                    self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not created"))
        
       t@@ -2524,6 +2541,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
                    return
        
       +    def import_channel_backup(self, encrypted):
       +        if not self.question('Import channel backup?'):
       +            return
       +        try:
       +            self.wallet.lnbackups.import_channel_backup(encrypted)
       +        except Exception as e:
       +            self.show_error("failed to import backup" + '\n' + str(e))
       +            return
       +
            def read_tx_from_qrcode(self):
                from electrum import qrscanner
                try:
       t@@ -2537,6 +2563,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if str(data).startswith("bitcoin:"):
                    self.pay_to_URI(data)
                    return
       +        if data.startswith('channel_backup:'):
       +            self.import_channel_backup(data[15:])
       +            return
                # else if the user scanned an offline signed tx
                tx = self.tx_from_text(data)
                if not tx:
   DIR diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
       t@@ -146,13 +146,6 @@ class SettingsDialog(WindowModalDialog):
                # lightning
                lightning_widgets = []
        
       -        backup_help = _("""If you configure a backup directory, a backup of your wallet file will be saved everytime you create a new channel.\n\nA backup file cannot be used as a wallet; it can only be used to retrieve the funds locked in your channels, by requesting your channels to be force closed (using data loss protection).\n\nIf the remote node is online, they will force-close your channels when you open the backup file. Note that a backup is not strictly necessary for that; if the remote party is online but they cannot reach you because you lost your wallet file, they should eventually close your channels, and your funds should be sent to an address recoverable from your seed (using static_remotekey).\n\nIf the remote node is not online, you can use the backup file to force close your channels, but only at the risk of losing all your funds in the channel, because you will be broadcasting an old state.""")
       -        backup_dir = self.config.get('backup_dir')
       -        backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
       -        self.backup_dir_e = QPushButton(backup_dir)
       -        self.backup_dir_e.clicked.connect(self.select_backup_dir)
       -        lightning_widgets.append((backup_dir_label, self.backup_dir_e))
       -
                help_persist = _("""If this option is checked, Electrum will persist as a daemon after
        you close all your wallet windows. Your local watchtower will keep
        running, and it will protect your channels even if your wallet is not
       t@@ -554,13 +547,6 @@ that is always connected to the internet. Configure a port if you want it to be 
                if alias:
                    self.window.fetch_alias()
        
       -    def select_backup_dir(self, b):
       -        name = self.config.get('backup_dir', '')
       -        dirname = QFileDialog.getExistingDirectory(self, "Select your SSL certificate file", name)
       -        if dirname:
       -            self.config.set_key('backup_dir', dirname)
       -            self.backup_dir_e.setText(dirname)
       -
            def select_ssl_certfile(self, b):
                name = self.config.get('ssl_certfile', '')
                filename, __ = QFileDialog.getOpenFileName(self, "Select your SSL certificate file", name)
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -56,6 +56,8 @@ from .lnhtlc import HTLCManager
        from .lnmsg import encode_msg, decode_msg
        from .address_synchronizer import TX_HEIGHT_LOCAL
        from .lnutil import CHANNEL_OPENING_TIMEOUT
       +from .lnutil import ChannelBackupStorage
       +from .lnutil import format_short_channel_id
        
        if TYPE_CHECKING:
            from .lnworker import LNWallet
       t@@ -121,19 +123,256 @@ def htlcsum(htlcs):
            return sum([x.amount_msat for x in htlcs])
        
        
       -class Channel(Logger):
       +class AbstractChannel(Logger):
       +
       +    def set_short_channel_id(self, short_id: ShortChannelID) -> None:
       +        self.short_channel_id = short_id
       +        self.storage["short_channel_id"] = short_id
       +
       +    def get_id_for_log(self) -> str:
       +        scid = self.short_channel_id
       +        if scid:
       +            return str(scid)
       +        return self.channel_id.hex()
       +
       +    def set_state(self, state: channel_states) -> None:
       +        """ set on-chain state """
       +        old_state = self._state
       +        if (old_state, state) not in state_transitions:
       +            raise Exception(f"Transition not allowed: {old_state.name} -> {state.name}")
       +        self.logger.debug(f'Setting channel state: {old_state.name} -> {state.name}')
       +        self._state = state
       +        self.storage['state'] = self._state.name
       +
       +    def get_state(self) -> channel_states:
       +        return self._state
       +
       +    def is_open(self):
       +        return self.get_state() == channel_states.OPEN
       +
       +    def is_closing(self):
       +        return self.get_state() in [channel_states.CLOSING, channel_states.FORCE_CLOSING]
       +
       +    def is_closed(self):
       +        # 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 save_funding_height(self, txid, height, timestamp):
       +        self.storage['funding_height'] = txid, height, timestamp
       +
       +    def get_funding_height(self):
       +        return self.storage.get('funding_height')
       +
       +    def delete_funding_height(self):
       +        self.storage.pop('funding_height', None)
       +
       +    def save_closing_height(self, txid, height, timestamp):
       +        self.storage['closing_height'] = txid, height, timestamp
       +
       +    def get_closing_height(self):
       +        return self.storage.get('closing_height')
       +
       +    def delete_closing_height(self):
       +        self.storage.pop('closing_height', None)
       +
       +    def create_sweeptxs_for_our_ctx(self, ctx):
       +        return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
       +
       +    def create_sweeptxs_for_their_ctx(self, ctx):
       +        return create_sweeptxs_for_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
       +
       +    def is_backup(self):
       +        return False
       +
       +    def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
       +        txid = ctx.txid()
       +        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 not None:
       +                self.sweep_info[txid] = our_sweep_info
       +                self.logger.info(f'we force closed')
       +            elif their_sweep_info is not None:
       +                self.sweep_info[txid] = their_sweep_info
       +                self.logger.info(f'they force closed.')
       +            else:
       +                self.sweep_info[txid] = {}
       +                self.logger.info(f'not sure who closed.')
       +        return self.sweep_info[txid]
       +
       +    # ancestor for Channel and ChannelBackup
       +    def update_onchain_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching):
       +        # note: state transitions are irreversible, but
       +        # save_funding_height, save_closing_height are reversible
       +        if funding_height.height == TX_HEIGHT_LOCAL:
       +            self.update_unfunded_state()
       +        elif closing_height.height == TX_HEIGHT_LOCAL:
       +            self.update_funded_state(funding_txid, funding_height)
       +        else:
       +            self.update_closed_state(funding_txid, funding_height, closing_txid, closing_height, keep_watching)
       +
       +    def update_unfunded_state(self):
       +        self.delete_funding_height()
       +        self.delete_closing_height()
       +        if self.get_state() in [channel_states.PREOPENING, channel_states.OPENING, channel_states.FORCE_CLOSING] and self.lnworker:
       +            if self.is_initiator():
       +                # set channel state to REDEEMED so that it can be removed manually
       +                # to protect ourselves against a server lying by omission,
       +                # we check that funding_inputs have been double spent and deeply mined
       +                inputs = self.storage.get('funding_inputs', [])
       +                if not inputs:
       +                    self.logger.info(f'channel funding inputs are not provided')
       +                    self.set_state(channel_states.REDEEMED)
       +                for i in inputs:
       +                    spender_txid = self.lnworker.wallet.db.get_spent_outpoint(*i)
       +                    if spender_txid is None:
       +                        continue
       +                    if spender_txid != self.funding_outpoint.txid:
       +                        tx_mined_height = self.lnworker.wallet.get_tx_height(spender_txid)
       +                        if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY:
       +                            self.logger.info(f'channel is double spent {inputs}')
       +                            self.set_state(channel_states.REDEEMED)
       +                            break
       +            else:
       +                now = int(time.time())
       +                if now - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT:
       +                    self.lnworker.remove_channel(self.channel_id)
       +
       +    def update_funded_state(self, funding_txid, funding_height):
       +        self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp)
       +        self.delete_closing_height()
       +        if self.get_state() == channel_states.OPENING:
       +            if self.is_funding_tx_mined(funding_height):
       +                self.set_state(channel_states.FUNDED)
       +                self.set_short_channel_id(ShortChannelID.from_components(
       +                    funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
       +                self.logger.info(f"save_short_channel_id: {self.short_channel_id}")
       +
       +    def update_closed_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching):
       +        self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp)
       +        self.save_closing_height(closing_txid, closing_height.height, closing_height.timestamp)
       +        if self.get_state() < channel_states.CLOSED:
       +            conf = closing_height.conf
       +            if conf > 0:
       +                self.set_state(channel_states.CLOSED)
       +            else:
       +                # we must not trust the server with unconfirmed transactions
       +                # if the remote force closed, we remain OPEN until the closing tx is confirmed
       +                pass
       +        if self.get_state() == channel_states.CLOSED and not keep_watching:
       +            self.set_state(channel_states.REDEEMED)
       +
       +
       +class ChannelBackup(AbstractChannel):
       +    """
       +    current capabilities:
       +      - detect force close
       +      - request force close
       +      - sweep my ctx to_local
       +    future:
       +      - will need to sweep their ctx to_remote
       +    """
       +
       +    def __init__(self, cb: ChannelBackupStorage, *, sweep_address=None, lnworker=None):
       +        self.name = None
       +        Logger.__init__(self)
       +        self.cb = cb
       +        self.sweep_info = {}  # type: Dict[str, Dict[str, SweepInfo]]
       +        self.sweep_address = sweep_address
       +        self.storage = {} # dummy storage
       +        self._state = channel_states.OPENING
       +        self.config = {}
       +        self.config[LOCAL] = LocalConfig.from_seed(
       +            channel_seed=cb.channel_seed,
       +            to_self_delay=cb.local_delay,
       +            # dummy values
       +            static_remotekey=None,
       +            dust_limit_sat=None,
       +            max_htlc_value_in_flight_msat=None,
       +            max_accepted_htlcs=None,
       +            initial_msat=None,
       +            reserve_sat=None,
       +            funding_locked_received=False,
       +            was_announced=False,
       +            current_commitment_signature=None,
       +            current_htlc_signatures=b'',
       +            htlc_minimum_msat=1,
       +        )
       +        self.config[REMOTE] = RemoteConfig(
       +            payment_basepoint=OnlyPubkeyKeypair(cb.remote_payment_pubkey),
       +            revocation_basepoint=OnlyPubkeyKeypair(cb.remote_revocation_pubkey),
       +            to_self_delay=cb.remote_delay,
       +            # dummy values
       +            multisig_key=OnlyPubkeyKeypair(None),
       +            htlc_basepoint=OnlyPubkeyKeypair(None),
       +            delayed_basepoint=OnlyPubkeyKeypair(None),
       +            dust_limit_sat=None,
       +            max_htlc_value_in_flight_msat=None,
       +            max_accepted_htlcs=None,
       +            initial_msat = None,
       +            reserve_sat = None,
       +            htlc_minimum_msat=None,
       +            next_per_commitment_point=None,
       +            current_per_commitment_point=None)
       +
       +        self.node_id = cb.node_id
       +        self.channel_id = cb.channel_id()
       +        self.funding_outpoint = cb.funding_outpoint()
       +        self.lnworker = lnworker
       +        self.short_channel_id = None
       +
       +    def is_backup(self):
       +        return True
       +
       +    def create_sweeptxs_for_their_ctx(self, ctx):
       +        return {}
       +
       +    def get_funding_address(self):
       +        return self.cb.funding_address
       +
       +    def short_id_for_GUI(self) -> str:
       +        return 'BACKUP'
       +
       +    def is_initiator(self):
       +        return self.cb.is_initiator
       +
       +    def get_state_for_GUI(self):
       +        cs = self.get_state()
       +        return cs.name
       +
       +    def get_oldest_unrevoked_ctn(self, who):
       +        return -1
       +
       +    def included_htlcs(self, subject, direction, ctn):
       +        return []
       +
       +    def funding_txn_minimum_depth(self):
       +        return 1
       +
       +    def is_funding_tx_mined(self, funding_height):
       +        return funding_height.conf > 1
       +
       +    def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL, ctn: int = None):
       +        return 0
       +
       +    def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
       +        return 0
       +
       +    def is_frozen_for_sending(self) -> bool:
       +        return False
       +
       +    def is_frozen_for_receiving(self) -> bool:
       +        return False
       +
       +
       +class Channel(AbstractChannel):
            # note: try to avoid naming ctns/ctxs/etc as "current" and "pending".
            #       they are ambiguous. Use "oldest_unrevoked" or "latest" or "next".
            #       TODO enforce this ^
        
       -    def diagnostic_name(self):
       -        if self.name:
       -            return str(self.name)
       -        try:
       -            return f"lnchannel_{bh2u(self.channel_id[-4:])}"
       -        except:
       -            return super().diagnostic_name()
       -
            def __init__(self, state: 'StoredDict', *, sweep_address=None, name=None, lnworker=None, initial_feerate=None):
                self.name = name
                Logger.__init__(self)
       t@@ -162,11 +401,22 @@ class Channel(Logger):
                self._receive_fail_reasons = {}  # type: Dict[int, BarePaymentAttemptLog]
                self._ignore_max_htlc_value = False  # used in tests
        
       -    def get_id_for_log(self) -> str:
       -        scid = self.short_channel_id
       -        if scid:
       -            return str(scid)
       -        return self.channel_id.hex()
       +    def short_id_for_GUI(self) -> str:
       +        return format_short_channel_id(self.short_channel_id)
       +
       +    def is_initiator(self):
       +        return self.constraints.is_initiator
       +
       +    def funding_txn_minimum_depth(self):
       +        return self.constraints.funding_txn_minimum_depth
       +
       +    def diagnostic_name(self):
       +        if self.name:
       +            return str(self.name)
       +        try:
       +            return f"lnchannel_{bh2u(self.channel_id[-4:])}"
       +        except:
       +            return super().diagnostic_name()
        
            def set_onion_key(self, key: int, value: bytes):
                self.onion_keys[key] = value
       t@@ -269,10 +519,6 @@ class Channel(Logger):
            def is_static_remotekey_enabled(self) -> bool:
                return bool(self.storage.get('static_remotekey_enabled'))
        
       -    def set_short_channel_id(self, short_id: ShortChannelID) -> None:
       -        self.short_channel_id = short_id
       -        self.storage["short_channel_id"] = short_id
       -
            def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:
                # returns feerate in sat/kw
                return self.hm.get_feerate(subject, ctn)
       t@@ -322,21 +568,11 @@ class Channel(Logger):
                    self.peer_state = peer_states.GOOD
        
            def set_state(self, state: channel_states) -> None:
       -        """ set on-chain state """
       -        old_state = self._state
       -        if (old_state, state) not in state_transitions:
       -            raise Exception(f"Transition not allowed: {old_state.name} -> {state.name}")
       -        self.logger.debug(f'Setting channel state: {old_state.name} -> {state.name}')
       -        self._state = state
       -        self.storage['state'] = self._state.name
       -
       +        super().set_state(state)
                if self.lnworker:
                    self.lnworker.save_channel(self)
                    self.lnworker.network.trigger_callback('channel', self)
        
       -    def get_state(self) -> channel_states:
       -        return self._state
       -
            def get_state_for_GUI(self):
                # status displayed in the GUI
                cs = self.get_state()
       t@@ -347,16 +583,6 @@ class Channel(Logger):
                    return ps.name
                return cs.name
        
       -    def is_open(self):
       -        return self.get_state() == channel_states.OPEN
       -
       -    def is_closing(self):
       -        return self.get_state() in [channel_states.CLOSING, channel_states.FORCE_CLOSING]
       -
       -    def is_closed(self):
       -        # the closing txid has been saved
       -        return self.get_state() >= channel_states.CLOSED
       -
            def set_can_send_ctx_updates(self, b: bool) -> None:
                self._can_send_ctx_updates = b
        
       t@@ -373,27 +599,6 @@ class Channel(Logger):
            def can_send_update_add_htlc(self) -> bool:
                return self.can_send_ctx_updates() and not self.is_closing()
        
       -    def save_funding_height(self, txid, height, timestamp):
       -        self.storage['funding_height'] = txid, height, timestamp
       -
       -    def get_funding_height(self):
       -        return self.storage.get('funding_height')
       -
       -    def delete_funding_height(self):
       -        self.storage.pop('funding_height', None)
       -
       -    def save_closing_height(self, txid, height, timestamp):
       -        self.storage['closing_height'] = txid, height, timestamp
       -
       -    def get_closing_height(self):
       -        return self.storage.get('closing_height')
       -
       -    def delete_closing_height(self):
       -        self.storage.pop('closing_height', None)
       -
       -    def is_redeemed(self):
       -        return self.get_state() == channel_states.REDEEMED
       -
            def is_frozen_for_sending(self) -> bool:
                """Whether the user has marked this channel as frozen for sending.
                Frozen channels are not supposed to be used for new outgoing payments.
       t@@ -1039,21 +1244,6 @@ class Channel(Logger):
                assert tx.is_complete()
                return tx
        
       -    def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
       -        txid = ctx.txid()
       -        if self.sweep_info.get(txid) is None:
       -            our_sweep_info = create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
       -            their_sweep_info = create_sweeptxs_for_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
       -            if our_sweep_info is not None:
       -                self.sweep_info[txid] = our_sweep_info
       -                self.logger.info(f'we force closed.')
       -            elif their_sweep_info is not None:
       -                self.sweep_info[txid] = their_sweep_info
       -                self.logger.info(f'they force closed.')
       -            else:
       -                self.sweep_info[txid] = {}
       -        return self.sweep_info[txid]
       -
            def sweep_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]:
                # look at the output address, check if it matches
                return create_sweeptx_for_their_revoked_htlc(self, ctx, htlc_tx, self.sweep_address)
       t@@ -1095,16 +1285,6 @@ class Channel(Logger):
                                                               500_000)
                return total_value_sat > min_value_worth_closing_channel_over_sat
        
       -    def update_onchain_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching):
       -        # note: state transitions are irreversible, but
       -        # save_funding_height, save_closing_height are reversible
       -        if funding_height.height == TX_HEIGHT_LOCAL:
       -            self.update_unfunded_state()
       -        elif closing_height.height == TX_HEIGHT_LOCAL:
       -            self.update_funded_state(funding_txid, funding_height)
       -        else:
       -            self.update_closed_state(funding_txid, funding_height, closing_txid, closing_height, keep_watching)
       -
            def is_funding_tx_mined(self, funding_height):
                """
                Checks if Funding TX has been mined. If it has, save the short channel ID in chan;
       t@@ -1114,7 +1294,7 @@ class Channel(Logger):
                funding_txid = self.funding_outpoint.txid
                funding_idx = self.funding_outpoint.output_index
                conf = funding_height.conf
       -        if conf < self.constraints.funding_txn_minimum_depth:
       +        if conf < self.funding_txn_minimum_depth():
                    self.logger.info(f"funding tx is still not at sufficient depth. actual depth: {conf}")
                    return False
                assert conf > 0
       t@@ -1132,53 +1312,3 @@ class Channel(Logger):
                    return False
                return True
        
       -    def update_unfunded_state(self):
       -        self.delete_funding_height()
       -        self.delete_closing_height()
       -        if self.get_state() in [channel_states.PREOPENING, channel_states.OPENING, channel_states.FORCE_CLOSING] and self.lnworker:
       -            if self.constraints.is_initiator:
       -                # set channel state to REDEEMED so that it can be removed manually
       -                # to protect ourselves against a server lying by omission,
       -                # we check that funding_inputs have been double spent and deeply mined
       -                inputs = self.storage.get('funding_inputs', [])
       -                if not inputs:
       -                    self.logger.info(f'channel funding inputs are not provided')
       -                    self.set_state(channel_states.REDEEMED)
       -                for i in inputs:
       -                    spender_txid = self.lnworker.wallet.db.get_spent_outpoint(*i)
       -                    if spender_txid is None:
       -                        continue
       -                    if spender_txid != self.funding_outpoint.txid:
       -                        tx_mined_height = self.lnworker.wallet.get_tx_height(spender_txid)
       -                        if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY:
       -                            self.logger.info(f'channel is double spent {inputs}')
       -                            self.set_state(channel_states.REDEEMED)
       -                            break
       -            else:
       -                now = int(time.time())
       -                if now - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT:
       -                    self.lnworker.remove_channel(self.channel_id)
       -
       -    def update_funded_state(self, funding_txid, funding_height):
       -        self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp)
       -        self.delete_closing_height()
       -        if self.get_state() == channel_states.OPENING:
       -            if self.is_funding_tx_mined(funding_height):
       -                self.set_state(channel_states.FUNDED)
       -                self.set_short_channel_id(ShortChannelID.from_components(
       -                    funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
       -                self.logger.info(f"save_short_channel_id: {self.short_channel_id}")
       -
       -    def update_closed_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching):
       -        self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp)
       -        self.save_closing_height(closing_txid, closing_height.height, closing_height.timestamp)
       -        if self.get_state() < channel_states.CLOSED:
       -            conf = closing_height.conf
       -            if conf > 0:
       -                self.set_state(channel_states.CLOSED)
       -            else:
       -                # we must not trust the server with unconfirmed transactions
       -                # if the remote force closed, we remain OPEN until the closing tx is confirmed
       -                pass
       -        if self.get_state() == channel_states.CLOSED and not keep_watching:
       -            self.set_state(channel_states.REDEEMED)
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -44,7 +44,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
                             MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY,
                             NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID,
                             IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage)
       -from .lnutil import FeeUpdate
       +from .lnutil import FeeUpdate, channel_id_from_funding_tx
        from .lntransport import LNTransport, LNTransportBase
        from .lnmsg import encode_msg, decode_msg
        from .interface import GracefulDisconnect, NetworkException
       t@@ -60,10 +60,6 @@ if TYPE_CHECKING:
        
        LN_P2P_NETWORK_TIMEOUT = 20
        
       -def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]:
       -    funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
       -    i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index
       -    return i.to_bytes(32, 'big'), funding_txid_bytes
        
        class Peer(Logger):
        
       t@@ -222,7 +218,7 @@ class Peer(Logger):
                    if constants.net.rev_genesis_bytes() not in their_chains:
                        raise GracefulDisconnect(f"no common chain found with remote. (they sent: {their_chains})")
                # all checks passed
       -        if isinstance(self.transport, LNTransport):
       +        if self.channel_db and isinstance(self.transport, LNTransport):
                    self.channel_db.add_recent_peer(self.transport.peer_addr)
                    for chan in self.channels.values():
                        chan.add_or_update_peer_addr(self.transport.peer_addr)
       t@@ -728,6 +724,17 @@ class Peer(Logger):
                    raise Exception(f'reserve too high: {remote_reserve_sat}, funding_sat: {funding_sat}')
                return remote_reserve_sat
        
       +    async def trigger_force_close(self, channel_id):
       +        await self.initialized
       +        latest_point = 0
       +        self.send_message(
       +            "channel_reestablish",
       +            channel_id=channel_id,
       +            next_local_commitment_number=0,
       +            next_remote_revocation_number=0,
       +            your_last_per_commitment_secret=0,
       +            my_current_per_commitment_point=latest_point)
       +
            async def reestablish_channel(self, chan: Channel):
                await self.initialized
                chan_id = chan.channel_id
       t@@ -749,8 +756,7 @@ class Peer(Logger):
                next_remote_ctn = chan.get_next_ctn(REMOTE)
                assert self.features & LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
                # send message
       -        srk_enabled = chan.is_static_remotekey_enabled()
       -        if srk_enabled:
       +        if chan.is_static_remotekey_enabled():
                    latest_secret, latest_point = chan.get_secret_and_point(LOCAL, 0)
                else:
                    latest_secret, latest_point = chan.get_secret_and_point(LOCAL, latest_local_ctn)
       t@@ -878,10 +884,6 @@ class Peer(Logger):
                    self.logger.warning(f"channel_reestablish ({chan.get_id_for_log()}): we are ahead of remote! trying to force-close.")
                    await self.lnworker.try_force_closing(chan_id)
                    return
       -        elif self.lnworker.wallet.is_lightning_backup():
       -            self.logger.warning(f"channel_reestablish ({chan.get_id_for_log()}): force-closing because we are a recent backup")
       -            await self.lnworker.try_force_closing(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
   DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
       t@@ -18,7 +18,7 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
        from .transaction import (Transaction, TxOutput, construct_witness, PartialTransaction, PartialTxInput,
                                  PartialTxOutput, TxOutpoint)
        from .simple_config import SimpleConfig
       -from .logging import get_logger
       +from .logging import get_logger, Logger
        
        if TYPE_CHECKING:
            from .lnchannel import Channel
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -24,6 +24,7 @@ from . import segwit_addr
        from .i18n import _
        from .lnaddr import lndecode
        from .bip32 import BIP32Node, BIP32_PRIME
       +from .transaction import BCDataStream
        
        if TYPE_CHECKING:
            from .lnchannel import Channel
       t@@ -47,6 +48,11 @@ def ln_dummy_address():
        from .json_db import StoredObject
        
        
       +def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]:
       +    funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
       +    i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index
       +    return i.to_bytes(32, 'big'), funding_txid_bytes
       +
        hex_to_bytes = lambda v: v if isinstance(v, bytes) else bytes.fromhex(v) if v is not None else None
        json_to_keypair = lambda v: v if isinstance(v, OnlyPubkeyKeypair) else Keypair(**v) if len(v)==2 else OnlyPubkeyKeypair(**v)
        
       t@@ -116,6 +122,66 @@ class ChannelConstraints(StoredObject):
            is_initiator = attr.ib(type=bool)  # note: sometimes also called "funder"
            funding_txn_minimum_depth = attr.ib(type=int)
        
       +@attr.s
       +class ChannelBackupStorage(StoredObject):
       +    node_id = attr.ib(type=bytes, converter=hex_to_bytes)
       +    privkey = attr.ib(type=bytes, converter=hex_to_bytes)
       +    funding_txid = attr.ib(type=str)
       +    funding_index = attr.ib(type=int, converter=int)
       +    funding_address = attr.ib(type=str)
       +    host = attr.ib(type=str)
       +    port = attr.ib(type=int, converter=int)
       +    is_initiator = attr.ib(type=bool)
       +    channel_seed = attr.ib(type=bytes, converter=hex_to_bytes)
       +    local_delay = attr.ib(type=int, converter=int)
       +    remote_delay = attr.ib(type=int, converter=int)
       +    remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
       +    remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
       +
       +    def funding_outpoint(self):
       +        return Outpoint(self.funding_txid, self.funding_index)
       +
       +    def channel_id(self):
       +        chan_id, _ = channel_id_from_funding_tx(self.funding_txid, self.funding_index)
       +        return chan_id
       +
       +    def to_bytes(self):
       +        vds = BCDataStream()
       +        vds.write_boolean(self.is_initiator)
       +        vds.write_bytes(self.privkey, 32)
       +        vds.write_bytes(self.channel_seed, 32)
       +        vds.write_bytes(self.node_id, 33)
       +        vds.write_bytes(bfh(self.funding_txid), 32)
       +        vds.write_int16(self.funding_index)
       +        vds.write_string(self.funding_address)
       +        vds.write_bytes(self.remote_payment_pubkey, 33)
       +        vds.write_bytes(self.remote_revocation_pubkey, 33)
       +        vds.write_int16(self.local_delay)
       +        vds.write_int16(self.remote_delay)
       +        vds.write_string(self.host)
       +        vds.write_int16(self.port)
       +        return vds.input
       +
       +    @staticmethod
       +    def from_bytes(s):
       +        vds = BCDataStream()
       +        vds.write(s)
       +        return ChannelBackupStorage(
       +            is_initiator = bool(vds.read_bytes(1)),
       +            privkey = vds.read_bytes(32).hex(),
       +            channel_seed = vds.read_bytes(32).hex(),
       +            node_id = vds.read_bytes(33).hex(),
       +            funding_txid = vds.read_bytes(32).hex(),
       +            funding_index = vds.read_int16(),
       +            funding_address = vds.read_string(),
       +            remote_payment_pubkey = vds.read_bytes(33).hex(),
       +            remote_revocation_pubkey = vds.read_bytes(33).hex(),
       +            local_delay = vds.read_int16(),
       +            remote_delay = vds.read_int16(),
       +            host = vds.read_string(),
       +            port = vds.read_int16())
       +
       +
        
        class ScriptHtlc(NamedTuple):
            redeem_script: bytes
       t@@ -716,8 +782,8 @@ def extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoi
            return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
        
        def extract_ctn_from_tx_and_chan(tx: Transaction, chan: 'Channel') -> int:
       -    funder_conf = chan.config[LOCAL] if     chan.constraints.is_initiator else chan.config[REMOTE]
       -    fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE]
       +    funder_conf = chan.config[LOCAL] if     chan.is_initiator() else chan.config[REMOTE]
       +    fundee_conf = chan.config[LOCAL] if not chan.is_initiator() else chan.config[REMOTE]
            return extract_ctn_from_tx(tx, txin_index=0,
                                       funder_payment_basepoint=funder_conf.payment_basepoint.pubkey,
                                       fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey)
   DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -15,6 +15,7 @@ from typing import NamedTuple, Dict
        from .sql_db import SqlDB, sql
        from .wallet_db import WalletDB
        from .util import bh2u, bfh, log_exceptions, ignore_exceptions
       +from .lnutil import Outpoint
        from . import wallet
        from .storage import WalletStorage
        from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -64,6 +64,9 @@ from .lnrouter import RouteEdge, LNPaymentRoute, is_route_sane_to_use
        from .address_synchronizer import TX_HEIGHT_LOCAL
        from . import lnsweep
        from .lnwatcher import LNWalletWatcher
       +from .crypto import pw_encode_bytes, pw_decode_bytes, PW_HASH_VERSION_LATEST
       +from .lnutil import ChannelBackupStorage
       +from .lnchannel import ChannelBackup
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -219,7 +222,8 @@ class LNWorker(Logger):
                return peer
        
            def peer_closed(self, peer: Peer) -> None:
       -        self.peers.pop(peer.pubkey)
       +        if peer.pubkey in self.peers:
       +            self.peers.pop(peer.pubkey)
        
            def num_peers(self) -> int:
                return sum([p.is_initialized() for p in self.peers.values()])
       t@@ -492,7 +496,8 @@ class LNWallet(LNWorker):
                self.lnwatcher = LNWalletWatcher(self, network)
                self.lnwatcher.start_network(network)
                self.network = network
       -        for chan_id, chan in self.channels.items():
       +
       +        for chan in self.channels.values():
                    self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
        
                super().start_network(network)
       t@@ -763,8 +768,6 @@ class LNWallet(LNWorker):
            def open_channel(self, *, connect_str: str, funding_tx: PartialTransaction,
                             funding_sat: int, push_amt_sat: int, password: str = None,
                             timeout: Optional[int] = 20) -> Tuple[Channel, PartialTransaction]:
       -        if self.wallet.is_lightning_backup():
       -            raise Exception(_('Cannot create channel: this is a backup file'))
                if funding_sat > LN_MAX_FUNDING_SAT:
                    raise Exception(_("Requested channel capacity is over protocol allowed maximum."))
                coro = self._open_channel_coroutine(connect_str=connect_str, funding_tx=funding_tx, funding_sat=funding_sat,
       t@@ -1319,3 +1322,100 @@ class LNWallet(LNWorker):
                if feerate_per_kvbyte is None:
                    feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE
                return max(253, feerate_per_kvbyte // 4)
       +
       +    def create_channel_backup(self, channel_id):
       +        chan = self.channels[channel_id]
       +        peer_addresses = list(chan.get_peer_addresses())
       +        peer_addr = peer_addresses[0]
       +        return ChannelBackupStorage(
       +            node_id = chan.node_id,
       +            privkey = self.node_keypair.privkey,
       +            funding_txid = chan.funding_outpoint.txid,
       +            funding_index = chan.funding_outpoint.output_index,
       +            funding_address = chan.get_funding_address(),
       +            host = peer_addr.host,
       +            port = peer_addr.port,
       +            is_initiator = chan.constraints.is_initiator,
       +            channel_seed = chan.config[LOCAL].channel_seed,
       +            local_delay = chan.config[LOCAL].to_self_delay,
       +            remote_delay = chan.config[REMOTE].to_self_delay,
       +            remote_revocation_pubkey = chan.config[REMOTE].revocation_basepoint.pubkey,
       +            remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey)
       +
       +    def export_channel_backup(self, channel_id):
       +        xpub = self.wallet.get_fingerprint()
       +        backup_bytes = self.create_channel_backup(channel_id).to_bytes()
       +        assert backup_bytes == ChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), "roundtrip failed"
       +        encrypted = pw_encode_bytes(backup_bytes, xpub, version=PW_HASH_VERSION_LATEST)
       +        assert backup_bytes == pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST), "encrypt failed"
       +        return encrypted
       +
       +
       +class LNBackups(Logger):
       +
       +    def __init__(self, wallet: 'Abstract_Wallet'):
       +        Logger.__init__(self)
       +        self.features = LnFeatures(0)
       +        self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT
       +        self.taskgroup = SilentTaskGroup()
       +        self.lock = threading.RLock()
       +        self.wallet = wallet
       +        self.db = wallet.db
       +        self.sweep_address = wallet.get_receiving_address()
       +        self.channel_backups = {}
       +        for channel_id, cb in self.db.get_dict("channel_backups").items():
       +            self.channel_backups[bfh(channel_id)] = ChannelBackup(cb, sweep_address=self.sweep_address, lnworker=self)
       +
       +    def peer_closed(self, chan):
       +        pass
       +
       +    async def on_channel_update(self, chan):
       +        pass
       +
       +    def channel_by_txo(self, txo):
       +        with self.lock:
       +            channel_backups = list(self.channel_backups.values())
       +        for chan in channel_backups:
       +            if chan.funding_outpoint.to_str() == txo:
       +                return chan
       +
       +    def start_network(self, network: 'Network'):
       +        assert network
       +        self.lnwatcher = LNWalletWatcher(self, network)
       +        self.lnwatcher.start_network(network)
       +        self.network = network
       +        for cb in self.channel_backups.values():
       +            self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address())
       +
       +    def import_channel_backup(self, encrypted):
       +        xpub = self.wallet.get_fingerprint()
       +        x = pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST)
       +        cb = ChannelBackupStorage.from_bytes(x)
       +        channel_id = cb.channel_id().hex()
       +        d = self.db.get_dict("channel_backups")
       +        if channel_id in d:
       +            raise Exception('Channel already in wallet')
       +        d[channel_id] = cb
       +        self.channel_backups[bfh(channel_id)] = ChannelBackup(cb, sweep_address=self.sweep_address, lnworker=self)
       +        self.wallet.save_db()
       +        self.network.trigger_callback('channels_updated', self.wallet)
       +
       +    def remove_channel_backup(self, channel_id):
       +        d = self.db.get_dict("channel_backups")
       +        if channel_id.hex() not in d:
       +            raise Exception('Channel not found')
       +        d.pop(channel_id.hex())
       +        self.channel_backups.pop(channel_id)
       +        self.wallet.save_db()
       +        self.network.trigger_callback('channels_updated', self.wallet)
       +
       +    @log_exceptions
       +    async def request_force_close(self, channel_id):
       +        cb = self.channel_backups[channel_id].cb
       +        peer_addr = LNPeerAddr(cb.host, cb.port, cb.node_id)
       +        transport = LNTransport(cb.privkey, peer_addr)
       +        peer = Peer(self, cb.node_id, transport)
       +        await self.taskgroup.spawn(peer._message_loop())
       +        await peer.initialized
       +        await self.taskgroup.spawn(peer.trigger_force_close(channel_id))
   DIR diff --git a/electrum/transaction.py b/electrum/transaction.py
       t@@ -289,6 +289,10 @@ class BCDataStream(object):
                else:
                    raise SerializationError('attempt to read past end of buffer')
        
       +    def write_bytes(self, _bytes: Union[bytes, bytearray], length: int):
       +        assert len(_bytes) == length, len(_bytes)
       +        self.write(_bytes)
       +
            def can_read_more(self) -> bool:
                if not self.input:
                    return False
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -72,7 +72,7 @@ from .contacts import Contacts
        from .interface import NetworkException
        from .mnemonic import Mnemonic
        from .logging import get_logger
       -from .lnworker import LNWallet
       +from .lnworker import LNWallet, LNBackups
        from .paymentrequest import PaymentRequest
        
        if TYPE_CHECKING:
       t@@ -259,6 +259,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                # lightning
                ln_xprv = self.db.get('lightning_privkey2')
                self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
       +        self.lnbackups = LNBackups(self)
        
            def save_db(self):
                if self.storage:
       t@@ -269,7 +270,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                if backup_dir is None:
                    return
                new_db = WalletDB(self.db.dump(), manual_upgrades=False)
       -        new_db.put('is_backup', True)
       +
       +        if self.lnworker:
       +            channel_backups = new_db.get_dict('channel_backups')
       +            for chan_id, chan in self.lnworker.channels.items():
       +                channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id)
       +            new_db.put('channels', None)
       +            new_db.put('lightning_privkey2', None)
       +
                new_path = os.path.join(backup_dir, self.basename() + '.backup')
                new_storage = WalletStorage(new_path)
                new_storage._encryption_version = self.storage._encryption_version
       t@@ -305,9 +313,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self.db.put('lightning_privkey2', None)
                self.save_db()
        
       -    def is_lightning_backup(self):
       -        return self.has_lightning() and self.db.get('is_backup')
       -
            def stop_threads(self):
                super().stop_threads()
                if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
       t@@ -324,9 +329,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
        
            def start_network(self, network):
                AddressSynchronizer.start_network(self, network)
       -        if self.lnworker and network and not self.is_lightning_backup():
       -            network.maybe_init_lightning()
       -            self.lnworker.start_network(network)
       +        if network:
       +            if self.lnworker:
       +                network.maybe_init_lightning()
       +                self.lnworker.start_network(network)
       +            self.lnbackups.start_network(network)
        
            def load_and_cleanup(self):
                self.load_keystore()
   DIR diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
       t@@ -36,7 +36,7 @@ from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
        from .keystore import bip44_derivation
        from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
        from .logging import Logger
       -from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore
       +from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore, ChannelBackupStorage
        from .lnutil import ChannelConstraints, Outpoint, ShachainElement
        from .json_db import StoredDict, JsonDB, locked, modifier
        from .plugin import run_hook, plugin_loaders
       t@@ -1101,6 +1101,8 @@ class WalletDB(JsonDB):
                    v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
                elif key == 'fee_updates':
                    v = dict((k, FeeUpdate(**x)) for k, x in v.items())
       +        elif key == 'channel_backups':
       +            v = dict((k, ChannelBackupStorage(**x)) for k, x in v.items())
                elif key == 'tx_fees':
                    v = dict((k, TxFeesValue(*x)) for k, x in v.items())
                elif key == 'prevouts_by_scripthash':