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:
@@ -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: ''
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.')
|
||||
])
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
14
electrum/gui/messages.py
Normal 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.
|
||||
"""
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user