Browse Source

Allow user to enable lightning in the GUI. Make it a per-wallet setting.

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 5 years ago
parent
commit
90ce9f195b
  1. 8
      electrum/commands.py
  2. 7
      electrum/gui/kivy/main_window.py
  3. 2
      electrum/gui/kivy/uix/ui_screens/receive.kv
  4. 5
      electrum/gui/qt/__init__.py
  5. 9
      electrum/gui/qt/channels_list.py
  6. 40
      electrum/gui/qt/main_window.py
  7. 11
      electrum/gui/qt/settings_dialog.py
  8. 14
      electrum/lnworker.py
  9. 21
      electrum/network.py
  10. 15
      electrum/tests/regtest/regtest.sh
  11. 26
      electrum/wallet.py
  12. 8
      run_electrum

8
electrum/commands.py

@ -622,6 +622,12 @@ class Commands:
kwargs['fx'] = fx
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 """
@ -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):

7
electrum/gui/kivy/main_window.py

@ -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)
@ -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'])
@ -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())

2
electrum/gui/kivy/uix/ui_screens/receive.kv

@ -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

5
electrum/gui/qt/__init__.py

@ -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)

9
electrum/gui/qt/channels_list.py

@ -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
@ -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):
@ -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():

40
electrum/gui/qt/main_window.py

@ -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")
@ -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
@ -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()
@ -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)
@ -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)
@ -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)
@ -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))
@ -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))
@ -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()
@ -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():

11
electrum/gui/qt/settings_dialog.py

@ -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

14
electrum/lnworker.py

@ -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
@ -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:
@ -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):

21
electrum/network.py

@ -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
@ -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'
@ -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()

15
electrum/tests/regtest/regtest.sh

@ -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"
@ -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."
@ -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."
@ -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."
@ -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

26
electrum/wallet.py

@ -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,
@ -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()
@ -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):

8
run_electrum

@ -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')

Loading…
Cancel
Save