tAllow user to enable lightning in the GUI. Make it a per-wallet setting. - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 90ce9f195b97978becdabd62e618dda9b8daba42 DIR parent a201ed44df7c6c530b27709eb0e73b36237ae766 HTML Author: ThomasV <thomasv@electrum.org> Date: Sun, 13 Oct 2019 20:34:38 +0200 Allow user to enable lightning in the GUI. Make it a per-wallet setting. Diffstat: M electrum/commands.py | 8 ++++++-- M electrum/gui/kivy/main_window.py | 7 ++----- M electrum/gui/kivy/uix/ui_screens/r… | 2 +- M electrum/gui/qt/__init__.py | 5 ++--- M electrum/gui/qt/channels_list.py | 9 ++++++--- M electrum/gui/qt/main_window.py | 40 +++++++++++++++++++++++-------- M electrum/gui/qt/settings_dialog.py | 11 ----------- M electrum/lnworker.py | 14 +++----------- M electrum/network.py | 21 ++++++++++----------- M electrum/tests/regtest/regtest.sh | 15 +++++++++------ M electrum/wallet.py | 26 +++++++++++++++++++++----- M run_electrum | 8 -------- 12 files changed, 90 insertions(+), 76 deletions(-) --- DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -623,6 +623,12 @@ class Commands: return json_encode(wallet.get_detailed_history(**kwargs)) @command('w') + async def init_lightning(self, wallet: Abstract_Wallet = None): + """Enable lightning payments""" + wallet.init_lightning() + return "Lightning keys have been created." + + @command('w') async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None): """ lightning history """ lightning_history = wallet.lnworker.get_history() if wallet.lnworker else [] t@@ -1127,8 +1133,6 @@ def add_global_options(parser): group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet") group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest") group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet") - group.add_argument("--lightning", action="store_true", dest="lightning", default=False, help="Enable lightning") - group.add_argument("--reckless", action="store_true", dest="reckless", default=False, help="Allow to enable lightning on mainnet") group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") def add_wallet_option(parser): DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py t@@ -341,9 +341,6 @@ class ElectrumWindow(App): self.gui_object = kwargs.get('gui_object', None) # type: ElectrumGui self.daemon = self.gui_object.daemon self.fx = self.daemon.fx - - self.is_lightning_enabled = bool(config.get('lightning')) - self.use_rbf = config.get('use_rbf', True) self.use_unconfirmed = not config.get('confirmed_only', False) t@@ -558,7 +555,7 @@ class ElectrumWindow(App): self.network.register_callback(self.on_fee_histogram, ['fee_histogram']) self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) - self.network.register_callback(self.on_channels, ['channels']) + self.network.register_callback(self.on_channels, ['channels_updated']) self.network.register_callback(self.on_channel, ['channel']) self.network.register_callback(self.on_invoice_status, ['invoice_status']) self.network.register_callback(self.on_request_status, ['request_status']) t@@ -687,7 +684,7 @@ class ElectrumWindow(App): if self._channels_dialog: Clock.schedule_once(lambda dt: self._channels_dialog.update()) - def on_channels(self, evt): + def on_channels(self, evt, wallet): if self._channels_dialog: Clock.schedule_once(lambda dt: self._channels_dialog.update()) DIR diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv t@@ -91,7 +91,7 @@ ReceiveScreen: id: address_label text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address')) shorten: True - on_release: root.is_lightning = not root.is_lightning if app.is_lightning_enabled else False + on_release: root.is_lightning = not root.is_lightning if app.wallet.has_lightning() else False CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color DIR diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py t@@ -155,9 +155,8 @@ class ElectrumGui(Logger): else: m = self.tray.contextMenu() m.clear() - if self.config.get('lightning'): - m.addAction(_("Lightning"), self.show_lightning_dialog) - m.addAction(_("Watchtower"), self.show_watchtower_dialog) + m.addAction(_("Lightning"), self.show_lightning_dialog) + m.addAction(_("Watchtower"), self.show_watchtower_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py t@@ -10,6 +10,7 @@ from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout from electrum.util import inv_dict, bh2u, bfh from electrum.i18n import _ from electrum.lnchannel import Channel +from electrum.wallet import Abstract_Wallet from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError, format_short_channel_id from .util import MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WWLabel, WaitingDialog t@@ -21,7 +22,7 @@ ROLE_CHANNEL_ID = Qt.UserRole class ChannelsList(MyTreeView): - update_rows = QtCore.pyqtSignal() + update_rows = QtCore.pyqtSignal(Abstract_Wallet) update_single_row = QtCore.pyqtSignal(Channel) class Columns(IntEnum): t@@ -121,8 +122,10 @@ class ChannelsList(MyTreeView): for column, v in enumerate(self.format_fields(chan)): self.model().item(row, column).setData(v, QtCore.Qt.DisplayRole) - @QtCore.pyqtSlot() - def do_update_rows(self): + @QtCore.pyqtSlot(Abstract_Wallet) + def do_update_rows(self, wallet): + if wallet != self.parent.wallet: + return self.model().clear() self.update_headers(self.headers) for chan in self.parent.wallet.lnworker.channels.values(): DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py t@@ -196,7 +196,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.config.get('lightning'): + if self.wallet.has_lightning(): 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") t@@ -232,7 +232,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): interests = ['wallet_updated', 'network_updated', 'blockchain_updated', 'new_transaction', 'status', 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes', - 'on_history', 'channel', 'channels', + 'on_history', 'channel', 'channels_updated', 'invoice_status', 'request_status'] # To avoid leaking references to "self" that prevent the # window from being GC-ed when closed, callbacks should be t@@ -377,8 +377,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.on_fx_quotes() elif event == 'on_history': self.on_fx_history() - elif event == 'channels': - self.channels_list.update_rows.emit() + elif event == 'channels_updated': + self.channels_list.update_rows.emit(*args) elif event == 'channel': self.channels_list.update_single_row.emit(*args) self.update_status() t@@ -583,7 +583,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): file_menu.addAction(_("&Quit"), self.close) wallet_menu = menubar.addMenu(_("&Wallet")) - wallet_menu.addAction(_("&Information"), self.show_master_public_keys) + wallet_menu.addAction(_("&Information"), self.show_wallet_info) wallet_menu.addSeparator() self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog) self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog) t@@ -623,7 +623,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): view_menu = menubar.addMenu(_("&View")) add_toggle_action(view_menu, self.addresses_tab) add_toggle_action(view_menu, self.utxo_tab) - if self.config.get('lightning'): + if self.wallet.has_lightning(): add_toggle_action(view_menu, self.channels_tab) add_toggle_action(view_menu, self.contacts_tab) add_toggle_action(view_menu, self.console_tab) t@@ -633,7 +633,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): # Settings / Preferences are all reserved keywords in macOS using this as work around tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog) tools_menu.addAction(_("&Network"), lambda: self.gui_object.show_network_dialog(self)) - if self.config.get('lightning'): + if self.wallet.has_lightning(): tools_menu.addAction(_("&Lightning"), self.gui_object.show_lightning_dialog) tools_menu.addAction(_("&Watchtower"), self.gui_object.show_watchtower_dialog) tools_menu.addAction(_("&Plugins"), self.plugins_dialog) t@@ -985,7 +985,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): buttons.addStretch(1) buttons.addWidget(self.clear_invoice_button) buttons.addWidget(self.create_invoice_button) - if self.config.get('lightning'): + if self.wallet.has_lightning(): self.create_lightning_invoice_button = QPushButton(_('Lightning')) self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png")) self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True)) t@@ -2300,7 +2300,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog ) ) self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog ) sb.addPermanentWidget(self.seed_button) - if self.config.get('lightning'): + if self.wallet.has_lightning(): self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog) sb.addPermanentWidget(self.lightning_button) self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), lambda: self.gui_object.show_network_dialog(self)) t@@ -2390,7 +2390,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if d.exec_(): self.set_contact(line2.text(), line1.text()) - def show_master_public_keys(self): + def enable_lightning(self): + warning1 = _("Lightning support in Electrum is experimental. Do not put large amounts in lightning channels.") + warning2 = _("Funds stored in lightning channels are not recoverable from your seed. You must backup your wallet file everytime you crate a new channel.") + r = self.question(_('Enable Lightning payments?') + '\n\n' + _('WARNINGS') + ': ' + '\n\n' + warning1 + '\n\n' + warning2) + if not r: + return + self.wallet.init_lightning() + self.show_warning(_('Lightning keys have been initialized. Please restart Electrum')) + + def show_wallet_info(self): dialog = WindowModalDialog(self, _("Wallet Information")) dialog.setMinimumSize(500, 100) mpk_list = self.wallet.get_master_public_keys() t@@ -2414,6 +2423,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): grid.addWidget(QLabel(_("Keystore type") + ':'), 4, 0) ks_type = str(keystore_types[0]) if keystore_types else _('No keystore') grid.addWidget(QLabel(ks_type), 4, 1) + # lightning + if self.wallet.has_lightning(): + lightning_b = None + lightning_label = QLabel(_('Enabled')) + else: + lightning_b = QPushButton(_('Enable')) + lightning_b.clicked.connect(self.enable_lightning) + lightning_label = QLabel(_('Disabled')) + grid.addWidget(QLabel(_('Lightning')), 5, 0) + grid.addWidget(lightning_label, 5, 1) + grid.addWidget(lightning_b, 5, 2) vbox.addLayout(grid) if self.wallet.is_deterministic(): DIR diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py t@@ -171,18 +171,7 @@ class SettingsDialog(WindowModalDialog): fee_widgets.append((batch_rbf_cb, None)) # lightning - help_lightning = _("""Enable Lightning Network payments. Note that funds stored in -lightning channels are not recoverable from your seed. You must backup -your wallet file after every channel creation.""") lightning_widgets = [] - lightning_cb = QCheckBox(_("Enable Lightning")) - lightning_cb.setToolTip(help_lightning) - lightning_cb.setChecked(bool(self.config.get('lightning', False))) - def on_lightning_checked(x): - self.config.set_key('lightning', bool(x)) - lightning_cb.stateChanged.connect(on_lightning_checked) - lightning_widgets.append((lightning_cb, None)) - 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 DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -307,19 +307,11 @@ class LNGossip(LNWorker): class LNWallet(LNWorker): - def __init__(self, wallet: 'Abstract_Wallet'): + def __init__(self, wallet: 'Abstract_Wallet', xprv): Logger.__init__(self) self.wallet = wallet self.storage = wallet.storage self.config = wallet.config - xprv = self.storage.get('lightning_privkey2') - if xprv is None: - # TODO derive this deterministically from wallet.keystore at keystore generation time - # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ ) - seed = os.urandom(32) - node = BIP32Node.from_rootseed(seed, xtype='standard') - xprv = node.to_xprv() - self.storage.put('lightning_privkey2', xprv) LNWorker.__init__(self, xprv) self.ln_keystore = keystore.from_xprv(xprv) self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ t@@ -789,7 +781,7 @@ class LNWallet(LNWorker): return chan def on_channels_updated(self): - self.network.trigger_callback('channels') + self.network.trigger_callback('channels_updated', self.wallet) @log_exceptions async def add_peer(self, connect_str: str) -> Peer: t@@ -1211,7 +1203,7 @@ class LNWallet(LNWorker): with self.lock: self.channels.pop(chan_id) self.save_channels() - self.network.trigger_callback('channels', self.wallet) + self.network.trigger_callback('channels_updated', self.wallet) self.network.trigger_callback('wallet_updated', self.wallet) async def reestablish_peer_for_given_channel(self, chan): DIR diff --git a/electrum/network.py b/electrum/network.py t@@ -302,7 +302,12 @@ class Network(Logger): self._set_status('disconnected') # lightning network - if self.config.get('lightning'): + self.channel_db = None # type: Optional[ChannelDB] + self.lngossip = None # type: Optional[LNGossip] + self.local_watchtower = None # type: Optional[WatchTower] + + def maybe_init_lightning(self): + if self.channel_db is None: from . import lnwatcher from . import lnworker from . import lnrouter t@@ -311,10 +316,10 @@ class Network(Logger): self.path_finder = lnrouter.LNPathFinder(self.channel_db) self.lngossip = lnworker.LNGossip(self) self.local_watchtower = lnwatcher.WatchTower(self) if self.config.get('local_watchtower', False) else None - else: - self.channel_db = None # type: Optional[ChannelDB] - self.lngossip = None # type: Optional[LNGossip] - self.local_watchtower = None # type: Optional[WatchTower] + self.lngossip.start_network(self) + if self.local_watchtower: + self.local_watchtower.start_network(self) + asyncio.ensure_future(self.local_watchtower.start_watching) def run_from_another_thread(self, coro, *, timeout=None): assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' t@@ -1158,12 +1163,6 @@ class Network(Logger): self._set_oneserver(self.config.get('oneserver', False)) self._start_interface(self.default_server) - if self.lngossip: - self.lngossip.start_network(self) - if self.local_watchtower: - self.local_watchtower.start_network(self) - await self.local_watchtower.start_watching() - async def main(): try: await self._init_headers_file() DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh t@@ -4,9 +4,9 @@ set -eu # alice -> bob -> carol -alice="./run_electrum --regtest --lightning -D /tmp/alice" -bob="./run_electrum --regtest --lightning -D /tmp/bob" -carol="./run_electrum --regtest --lightning -D /tmp/carol" +alice="./run_electrum --regtest -D /tmp/alice" +bob="./run_electrum --regtest -D /tmp/bob" +carol="./run_electrum --regtest -D /tmp/carol" bitcoin_cli="bitcoin-cli -rpcuser=doggman -rpcpassword=donkey -rpcport=18554 -regtest" t@@ -18,7 +18,7 @@ function new_blocks() function wait_for_balance() { msg="wait until $1's balance reaches $2" - cmd="./run_electrum --regtest --lightning -D /tmp/$1" + cmd="./run_electrum --regtest -D /tmp/$1" while balance=$($cmd getbalance | jq '[.confirmed, .unconfirmed] | to_entries | map(select(.value != null).value) | map(tonumber) | add ') && (( $(echo "$balance < $2" | bc -l) )); do sleep 1 msg="$msg." t@@ -30,7 +30,7 @@ function wait_for_balance() function wait_until_channel_open() { msg="wait until $1 sees channel open" - cmd="./run_electrum --regtest --lightning -D /tmp/$1" + cmd="./run_electrum --regtest -D /tmp/$1" while channel_state=$($cmd list_channels | jq '.[0] | .state' | tr -d '"') && [ $channel_state != "OPEN" ]; do sleep 1 msg="$msg." t@@ -42,7 +42,7 @@ function wait_until_channel_open() function wait_until_channel_closed() { msg="wait until $1 sees channel closed" - cmd="./run_electrum --regtest --lightning -D /tmp/$1" + cmd="./run_electrum --regtest -D /tmp/$1" while [[ $($cmd list_channels | jq '.[0].state' | tr -d '"') != "CLOSED" ]]; do sleep 1 msg="$msg." t@@ -73,6 +73,9 @@ if [[ $1 == "init" ]]; then $alice create --offline > /dev/null $bob create --offline > /dev/null $carol create --offline > /dev/null + $alice -o init_lightning + $bob -o init_lightning + $carol -o init_lightning $alice setconfig --offline log_to_file True $bob setconfig --offline log_to_file True $carol setconfig --offline log_to_file True DIR diff --git a/electrum/wallet.py b/electrum/wallet.py t@@ -41,6 +41,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence from .i18n import _ +from .bip32 import BIP32Node from .crypto import sha256 from .util import (NotEnoughFunds, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, t@@ -229,22 +230,36 @@ class Abstract_Wallet(AddressSynchronizer): self.fiat_value = storage.get('fiat_value', {}) self.receive_requests = storage.get('payment_requests', {}) self.invoices = storage.get('invoices', {}) - # convert invoices for invoice_key, invoice in self.invoices.items(): if invoice.get('type') == PR_TYPE_ONCHAIN: outputs = [TxOutput(*output) for output in invoice.get('outputs')] invoice['outputs'] = outputs - self.calc_unused_change_addresses() - # save wallet type the first time if self.storage.get('wallet_type') is None: self.storage.put('wallet_type', self.wallet_type) - self.contacts = Contacts(self.storage) self._coin_price_cache = {} - self.lnworker = LNWallet(self) if self.config.get('lightning') else None + # lightning + ln_xprv = self.storage.get('lightning_privkey2') + self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None + + def has_lightning(self): + return bool(self.lnworker) + + def init_lightning(self): + if self.storage.get('lightning_privkey2'): + return + if not is_using_fast_ecc(): + raise Exception('libsecp256k1 library not available. ' + 'Verifying Lightning channels is too computationally expensive without libsecp256k1, aborting.') + # TODO derive this deterministically from wallet.keystore at keystore generation time + # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ ) + seed = os.urandom(32) + node = BIP32Node.from_rootseed(seed, xtype='standard') + ln_xprv = node.to_xprv() + self.storage.put('lightning_privkey2', ln_xprv) def stop_threads(self): super().stop_threads() t@@ -261,6 +276,7 @@ class Abstract_Wallet(AddressSynchronizer): def start_network(self, network): AddressSynchronizer.start_network(self, network) if self.lnworker: + network.maybe_init_lightning() self.lnworker.start_network(network) def load_and_cleanup(self): DIR diff --git a/run_electrum b/run_electrum t@@ -333,14 +333,6 @@ if __name__ == '__main__': constants.set_regtest() elif config.get('simnet'): constants.set_simnet() - elif config.get('lightning') and not config.get('reckless'): - raise Exception('lightning option not available on mainnet') - - if config.get('lightning'): - from electrum.ecc_fast import is_using_fast_ecc - if not is_using_fast_ecc(): - raise Exception('libsecp256k1 library not available. ' - 'Verifying Lightning channels is too computationally expensive without libsecp256k1, aborting.') cmdname = config.get('cmd')