1
0

Deterministic NodeID:

- use_recoverable_channel is a user setting, available
   only in standard wallets with a 'segwit' seed_type
 - if enabled, 'lightning_xprv' is derived from seed
 - otherwise, wallets use the existing 'lightning_privkey2'

Recoverable channels:
 - channel recovery data is added funding tx using an OP_RETURN
 - recovery data = 4 magic bytes + node id[0:16]
 - recovery data is chacha20 encrypted using funding_address as nonce.
   (this will allow to fund multiple channels in the same tx)

GUI:
  - whether channels are recoverable is shown in wallet info dialog.
  - if the wallet can have recoverable channels but has an old node_id,
    users are told to close their channels and restore from seed
    to have that feature.
This commit is contained in:
ThomasV
2021-03-09 09:55:55 +01:00
parent e3025b3d7b
commit 64a931f21e
23 changed files with 395 additions and 76 deletions

View File

@@ -98,6 +98,26 @@
id: lbl2
text: root.value
<BoxButton@BoxLayout>
text: ''
value: ''
size_hint_y: None
height: max(lbl1.height, lbl2.height)
TopLabel
id: lbl1
text: root.text
pos_hint: {'top':1}
Button
id: lbl2
text: root.value
background_color: (0,0,0,0)
bold: True
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
on_release:
root.callback()
<OutputItem>
address: ''
value: ''

View File

@@ -208,6 +208,10 @@ class ElectrumWindow(App, Logger):
def on_use_unconfirmed(self, instance, x):
self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)
use_recoverable_channels = BooleanProperty(True)
def on_use_recoverable_channels(self, instance, x):
self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, True)
def switch_to_send_screen(func):
# try until send_screen is available
def wrapper(self, *args):
@@ -1352,3 +1356,59 @@ class ElectrumWindow(App, Logger):
self.show_error("failed to import backup" + '\n' + str(e))
return
self.lightning_channels_dialog()
def lightning_status(self):
if self.wallet.has_lightning():
if self.wallet.lnworker.has_deterministic_node_id():
status = _('Enabled')
else:
status = _('Enabled, non-recoverable channels')
if self.wallet.db.get('seed_type') == 'segwit':
msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed")
else:
msg = _("Your channels cannot be recovered from seed. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want to have recoverable channels, you must create a new wallet with an Electrum seed")
else:
if self.wallet.can_have_lightning():
status = _('Not enabled')
else:
status = _("Not available for this wallet.")
return status
def on_lightning_status(self, root):
if self.wallet.has_lightning():
if self.wallet.lnworker.has_deterministic_node_id():
pass
else:
if self.wallet.db.get('seed_type') == 'segwit':
msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed")
else:
msg = _("Your channels cannot be recovered from seed. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want to have recoverable channels, you must create a new wallet with an Electrum seed")
self.show_info(msg)
else:
if self.wallet.can_have_lightning():
root.dismiss()
msg = _(
"Warning: this wallet type does not support channel recovery from seed. "
"You will need to backup your wallet everytime you create a new wallet. "
"Create lightning keys?")
d = Question(msg, self._enable_lightning, title=_('Enable Lightning?'))
d.open()
else:
pass
def _enable_lightning(self, b):
if not b:
return
wallet_path = self.get_wallet_path()
self.wallet.init_lightning()
self.show_info(_('Lightning keys have been initialized.'))
self.stop_wallet()
self.load_wallet_by_name(wallet_path)

View File

@@ -528,7 +528,7 @@ class ChannelDetailsPopup(Popup, Logger):
to_self_delay = self.chan.config[REMOTE].to_self_delay
help_text = ' '.join([
_('If you force-close this channel, the funds you have in it will not be available for {} blocks.').format(to_self_delay),
_('During that time, funds will not be recoverabe from your seed, and may be lost if you lose your device.'),
_('During that time, funds will not be recoverable from your seed, and may be lost if you lose your device.'),
_('To prevent that, please save this channel backup.'),
_('It may be imported in another wallet with the same seed.')
])

View File

@@ -9,7 +9,7 @@ from electrum.util import bh2u
from electrum.bitcoin import COIN
import electrum.simple_config as config
from electrum.logging import Logger
from electrum.lnutil import ln_dummy_address
from electrum.lnutil import ln_dummy_address, extract_nodeid
from .label_dialog import LabelDialog
from .confirm_tx_dialog import ConfirmTxDialog
@@ -178,9 +178,11 @@ class LightningOpenChannelDialog(Factory.Popup, Logger):
self.dismiss()
lnworker = self.app.wallet.lnworker
coins = self.app.wallet.get_spendable_coins(None, nonlocal_only=True)
node_id, rest = extract_nodeid(conn_str)
make_tx = lambda rbf: lnworker.mktx_for_open_channel(
coins=coins,
funding_sat=amount,
node_id=node_id,
fee_est=None)
on_pay = lambda tx: self.app.protected('Create a new channel?', self.do_open_channel, (tx, conn_str))
d = ConfirmTxDialog(

View File

@@ -16,6 +16,7 @@ from .choice_dialog import ChoiceDialog
Builder.load_string('''
#:import partial functools.partial
#:import _ electrum.gui.kivy.i18n._
#:import messages electrum.gui.messages
<SettingsDialog@Popup>
id: settings
@@ -80,6 +81,13 @@ Builder.load_string('''
description: _('Change your password') if app._use_single_password else _("Change your password for this wallet.")
action: root.change_password
CardSeparator
SettingsItem:
status: _('Yes') if app.use_recoverable_channels else _('No')
title: _('Use recoverable channels') + ': ' + self.status
description: _("Add channel recovery data to funding transaction.")
message: _(messages.MSG_RECOVERABLE_CHANNELS)
action: partial(root.boolean_dialog, 'use_recoverable_channels', _('Use recoverable_channels'), self.message)
CardSeparator
SettingsItem:
status: _('Trampoline') if not app.use_gossip else _('Gossip')
title: _('Lightning Routing') + ': ' + self.status

View File

@@ -31,22 +31,21 @@ Popup:
BoxLabel:
text: _("Wallet type:")
value: app.wallet.wallet_type
BoxLabel:
BoxButton:
text: _("Lightning:")
value: (_('Enabled') if app.wallet.has_lightning() else _('Disabled')) if app.wallet.can_have_lightning() else _('Not available')
value: app.lightning_status()
callback: lambda: app.on_lightning_status(root)
BoxLabel:
text: _("Balance") + ':'
value: app.format_amount_and_units(root.confirmed + root.unconfirmed + root.unmatured + root.lightning)
BoxLabel:
text: _("Onchain") + ':'
text: ' - ' + _("Onchain") + ':'
value: app.format_amount_and_units(root.confirmed + root.unconfirmed + root.unmatured)
opacity: 1 if root.lightning else 0
BoxLabel:
text: _("Lightning") + ':'
text: ' - ' + _("Lightning") + ':'
opacity: 1 if root.lightning else 0
value: app.format_amount_and_units(root.lightning)
GridLayout:
cols: 1
height: self.minimum_height

14
electrum/gui/messages.py Normal file
View File

@@ -0,0 +1,14 @@
# note: qt and kivy use different i18n methods
MSG_RECOVERABLE_CHANNELS = """
Add extra data to your channel funding transactions, so that a static backup can be
recovered from your seed.
Note that static backups only allow you to request a force-close with the remote node.
This assumes that the remote node is still online, did not lose its data, and accepts
to force close the channel.
If this is enabled, other nodes cannot open a channel to you. Channel recovery data
is encrypted, so that only your wallet can decrypt it. However, blockchain analysis
will be able to tell that the transaction was probably created by Electrum.
"""

View File

@@ -345,13 +345,14 @@ class ChannelsList(MyTreeView):
def new_channel_with_warning(self):
if not self.parent.wallet.lnworker.channels:
warning1 = _("Lightning support in Electrum is experimental. "
warning = _("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 create a new channel.")
if not self.parent.wallet.lnworker.has_recoverable_channels():
warning += _("Funds stored in lightning channels are not recoverable from your seed. "
"You must backup your wallet file everytime you create a new channel.")
answer = self.parent.question(
_('Do you want to create your first channel?') + '\n\n' +
_('WARNINGS') + ': ' + '\n\n' + warning1 + '\n\n' + warning2)
_('WARNING') + ': ' + '\n\n' + warning)
if answer:
self.new_channel_dialog()
else:

View File

@@ -1796,23 +1796,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
WaitingDialog(self, _('Broadcasting transaction...'),
broadcast_thread, broadcast_done, self.on_error)
def mktx_for_open_channel(self, funding_sat):
def mktx_for_open_channel(self, funding_sat, node_id):
coins = self.get_coins(nonlocal_only=True)
make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(
coins=coins,
funding_sat=funding_sat,
node_id=node_id,
fee_est=fee_est)
return make_tx
def open_channel(self, connect_str, funding_sat, push_amt):
try:
extract_nodeid(connect_str)
node_id, rest = extract_nodeid(connect_str)
except ConnStringFormatError as e:
self.show_error(str(e))
return
# use ConfirmTxDialog
# we need to know the fee before we broadcast, because the txid is required
make_tx = self.mktx_for_open_channel(funding_sat)
make_tx = self.mktx_for_open_channel(funding_sat, node_id)
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, is_sweep=False)
# disable preview button because the user must not broadcast tx before establishment_flow
d.preview_button.setEnabled(False)
@@ -2365,9 +2366,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if d.exec_():
self.set_contact(line2.text(), line1.text())
def init_lightning_dialog(self):
if self.question(_(
"Warning: this wallet type does not support channel recovery from seed. "
"You will need to backup your wallet everytime you create a new wallet. "
"Create lightning keys?")):
self.wallet.init_lightning()
self.show_message("Lightning keys created. Please restart Electrum")
def show_wallet_info(self):
dialog = WindowModalDialog(self, _("Wallet Information"))
dialog.setMinimumSize(500, 100)
dialog.setMinimumSize(800, 100)
vbox = QVBoxLayout()
wallet_type = self.wallet.db.get('wallet_type', '')
if self.wallet.is_watching_only():
@@ -2390,15 +2399,42 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
grid.addWidget(QLabel(ks_type), 4, 1)
# lightning
grid.addWidget(QLabel(_('Lightning') + ':'), 5, 0)
if self.wallet.can_have_lightning():
grid.addWidget(QLabel(_('Enabled')), 5, 1)
local_nodeid = QLabel(bh2u(self.wallet.lnworker.node_keypair.pubkey))
local_nodeid.setTextInteractionFlags(Qt.TextSelectableByMouse)
grid.addWidget(QLabel(_('Lightning Node ID:')), 6, 0)
grid.addWidget(local_nodeid, 6, 1, 1, 3)
from .util import IconLabel
if self.wallet.has_lightning():
if self.wallet.lnworker.has_deterministic_node_id():
grid.addWidget(QLabel(_('Enabled')), 5, 1)
else:
label = IconLabel(text='Enabled, non-recoverable channels')
label.setIcon(read_QIcon('warning.png'))
grid.addWidget(label, 5, 1)
if self.wallet.db.get('seed_type') == 'segwit':
msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed")
else:
msg = _("Your channels cannot be recovered from seed. "
"This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
"If you want to have recoverable channels, you must create a new wallet with an Electrum seed")
grid.addWidget(HelpButton(msg), 5, 3)
grid.addWidget(QLabel(_('Lightning Node ID:')), 7, 0)
# TODO: ButtonsLineEdit should have a addQrButton method
nodeid_text = self.wallet.lnworker.node_keypair.pubkey.hex()
nodeid_e = ButtonsLineEdit(nodeid_text)
qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
nodeid_e.addButton(qr_icon, lambda: self.show_qrcode(nodeid_text, _("Node ID")), _("Show QR Code"))
nodeid_e.addCopyButton(self.app)
nodeid_e.setReadOnly(True)
nodeid_e.setFont(QFont(MONOSPACE_FONT))
grid.addWidget(nodeid_e, 8, 0, 1, 4)
else:
grid.addWidget(QLabel(_("Not available for this wallet.")), 5, 1)
grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), 5, 2)
if self.wallet.can_have_lightning():
grid.addWidget(QLabel('Not enabled'), 5, 1)
button = QPushButton(_("Enable"))
button.pressed.connect(self.init_lightning_dialog)
grid.addWidget(button, 5, 3)
else:
grid.addWidget(QLabel(_("Not available for this wallet.")), 5, 1)
grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), 5, 2)
vbox.addLayout(grid)
labels_clayout = None

View File

@@ -41,6 +41,7 @@ from .util import (ColorScheme, WindowModalDialog, HelpLabel, Buttons,
from electrum.i18n import languages
from electrum import qrscanner
from electrum.gui import messages
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@@ -130,6 +131,17 @@ class SettingsDialog(WindowModalDialog):
# lightning
lightning_widgets = []
if self.wallet.lnworker and self.wallet.lnworker.has_deterministic_node_id():
help_recov = _(messages.MSG_RECOVERABLE_CHANNELS)
recov_cb = QCheckBox(_("Create recoverable channels"))
recov_cb.setToolTip(help_recov)
recov_cb.setChecked(bool(self.config.get('use_recoverable_channels', True)))
def on_recov_checked(x):
self.config.set_key('use_recoverable_channels', bool(x))
recov_cb.stateChanged.connect(on_recov_checked)
recov_cb.setEnabled(not bool(self.config.get('lightning_listen')))
lightning_widgets.append((recov_cb, None))
help_gossip = _("""If this option is enabled, Electrum will download the network
channels graph and compute payment path locally, instead of using trampoline payments. """)
gossip_cb = QCheckBox(_("Download network graph"))