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:
@@ -545,6 +545,8 @@ class BaseWizard(Logger):
|
||||
|
||||
def create_keystore(self, seed, passphrase):
|
||||
k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig')
|
||||
if self.wallet_type == 'standard' and self.seed_type == 'segwit':
|
||||
self.data['lightning_xprv'] = k.get_lightning_xprv(None)
|
||||
self.on_keystore(k)
|
||||
|
||||
def on_bip43(self, seed, passphrase, derivation, script_type):
|
||||
|
||||
@@ -295,6 +295,10 @@ def push_script(data: str) -> str:
|
||||
return _op_push(data_len) + bh2u(data)
|
||||
|
||||
|
||||
def make_op_return(x:bytes) -> bytes:
|
||||
return bytes([opcodes.OP_RETURN]) + bytes.fromhex(push_script(x.hex()))
|
||||
|
||||
|
||||
def add_number_to_script(i: int) -> bytes:
|
||||
return bfh(push_script(script_num_to_hex(i)))
|
||||
|
||||
|
||||
@@ -867,6 +867,13 @@ class ChannelDB(SqlDB):
|
||||
with self.lock:
|
||||
return self._policies.copy()
|
||||
|
||||
def get_node_by_prefix(self, prefix):
|
||||
with self.lock:
|
||||
for k in self._addresses.keys():
|
||||
if k.startswith(prefix):
|
||||
return k
|
||||
raise Exception('node not found')
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
""" Generates a graph representation in terms of a dictionary.
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from .mnemonic import Mnemonic
|
||||
from .lnutil import SENT, RECEIVED
|
||||
from .lnutil import LnFeatures
|
||||
from .lnutil import extract_nodeid
|
||||
from .lnpeer import channel_id_from_funding_tx
|
||||
from .plugin import run_hook
|
||||
from .version import ELECTRUM_VERSION
|
||||
@@ -997,9 +998,11 @@ class Commands:
|
||||
funding_sat = satoshis(amount)
|
||||
push_sat = satoshis(push_amount)
|
||||
coins = wallet.get_spendable_coins(None)
|
||||
node_id, rest = extract_nodeid(connection_string)
|
||||
funding_tx = wallet.lnworker.mktx_for_open_channel(
|
||||
coins=coins,
|
||||
funding_sat=funding_sat,
|
||||
node_id=node_id,
|
||||
fee_est=None)
|
||||
chan, funding_tx = await wallet.lnworker._open_channel_coroutine(
|
||||
connect_str=connection_string,
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -613,6 +613,11 @@ class BIP32_KeyStore(Xpub, Deterministic_KeyStore):
|
||||
cK = ecc.ECPrivkey(k).get_public_key_bytes()
|
||||
return cK, k
|
||||
|
||||
def get_lightning_xprv(self, password):
|
||||
xprv = self.get_master_private_key(password)
|
||||
rootnode = BIP32Node.from_xkey(xprv)
|
||||
node = rootnode.subkey_at_private_derivation("m/67'/")
|
||||
return node.to_xprv()
|
||||
|
||||
class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore):
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ 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 ChannelBackupStorage, ImportedChannelBackupStorage
|
||||
from .lnutil import format_short_channel_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -301,9 +301,15 @@ class AbstractChannel(Logger, ABC):
|
||||
if conf > 0:
|
||||
self.set_state(ChannelState.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 not self.is_backup():
|
||||
# we must not trust the server with unconfirmed transactions,
|
||||
# because the state transition is irreversible. if the remote
|
||||
# force closed, we remain OPEN until the closing tx is confirmed
|
||||
pass
|
||||
else:
|
||||
# for a backup, that state change will only affect the GUI
|
||||
self.set_state(ChannelState.FORCE_CLOSING)
|
||||
|
||||
if self.get_state() == ChannelState.CLOSED and not keep_watching:
|
||||
self.set_state(ChannelState.REDEEMED)
|
||||
|
||||
@@ -400,11 +406,21 @@ class ChannelBackup(AbstractChannel):
|
||||
self.name = None
|
||||
Logger.__init__(self)
|
||||
self.cb = cb
|
||||
self.is_imported = isinstance(self.cb, ImportedChannelBackupStorage)
|
||||
self._sweep_info = {}
|
||||
self._fallback_sweep_address = sweep_address
|
||||
self.storage = {} # dummy storage
|
||||
self._state = ChannelState.OPENING
|
||||
self.node_id = cb.node_id if self.is_imported else cb.node_id_prefix
|
||||
self.channel_id = cb.channel_id()
|
||||
self.funding_outpoint = cb.funding_outpoint()
|
||||
self.lnworker = lnworker
|
||||
self.short_channel_id = None
|
||||
self.config = {}
|
||||
if self.is_imported:
|
||||
self.init_config(cb)
|
||||
|
||||
def init_config(self, cb):
|
||||
self.config[LOCAL] = LocalConfig.from_seed(
|
||||
channel_seed=cb.channel_seed,
|
||||
to_self_delay=cb.local_delay,
|
||||
@@ -440,11 +456,6 @@ class ChannelBackup(AbstractChannel):
|
||||
next_per_commitment_point=None,
|
||||
current_per_commitment_point=None,
|
||||
upfront_shutdown_script='')
|
||||
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 get_capacity(self):
|
||||
return self.lnworker.lnwatcher.get_tx_delta(self.funding_outpoint.txid, self.cb.funding_address)
|
||||
@@ -455,6 +466,13 @@ class ChannelBackup(AbstractChannel):
|
||||
def create_sweeptxs_for_their_ctx(self, ctx):
|
||||
return {}
|
||||
|
||||
def create_sweeptxs_for_our_ctx(self, ctx):
|
||||
if self.is_imported:
|
||||
return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
|
||||
else:
|
||||
# backup from op_return
|
||||
return {}
|
||||
|
||||
def get_funding_address(self):
|
||||
return self.cb.funding_address
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import constants
|
||||
from .util import (bh2u, bfh, log_exceptions, ignore_exceptions, chunks, SilentTaskGroup,
|
||||
UnrelatedTransactionException)
|
||||
from . import transaction
|
||||
from .bitcoin import make_op_return
|
||||
from .transaction import PartialTxOutput, match_script_against_template
|
||||
from .logging import Logger
|
||||
from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment,
|
||||
@@ -681,6 +682,19 @@ class Peer(Logger):
|
||||
funding_tx._outputs.remove(dummy_output)
|
||||
if dummy_output in funding_tx.outputs(): raise Exception("LN dummy output (err 2)")
|
||||
funding_tx.add_outputs([funding_output])
|
||||
# find and encrypt op_return data associated to funding_address
|
||||
if self.lnworker and self.lnworker.has_recoverable_channels():
|
||||
backup_data = self.lnworker.cb_data(self.pubkey)
|
||||
dummy_scriptpubkey = make_op_return(backup_data)
|
||||
for o in funding_tx.outputs():
|
||||
if o.scriptpubkey == dummy_scriptpubkey:
|
||||
encrypted_data = self.lnworker.encrypt_cb_data(backup_data, funding_address)
|
||||
assert len(encrypted_data) == len(backup_data)
|
||||
o.scriptpubkey = make_op_return(encrypted_data)
|
||||
break
|
||||
else:
|
||||
raise Exception('op_return output not found in funding tx')
|
||||
# must not be malleable
|
||||
funding_tx.set_rbf(False)
|
||||
if not funding_tx.is_segwit():
|
||||
raise Exception('Funding transaction is not segwit')
|
||||
@@ -755,6 +769,9 @@ class Peer(Logger):
|
||||
|
||||
Channel configurations are initialized in this method.
|
||||
"""
|
||||
if self.lnworker.has_recoverable_channels():
|
||||
# FIXME: we might want to keep the connection open
|
||||
raise Exception('not accepting channels')
|
||||
# <- open_channel
|
||||
if payload['chain_hash'] != constants.net.rev_genesis_bytes():
|
||||
raise Exception('wrong chain_hash')
|
||||
|
||||
@@ -169,21 +169,13 @@ class ChannelConstraints(StoredObject):
|
||||
|
||||
|
||||
CHANNEL_BACKUP_VERSION = 0
|
||||
|
||||
@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)
|
||||
@@ -192,6 +184,22 @@ class ChannelBackupStorage(StoredObject):
|
||||
chan_id, _ = channel_id_from_funding_tx(self.funding_txid, self.funding_index)
|
||||
return chan_id
|
||||
|
||||
@attr.s
|
||||
class OnchainChannelBackupStorage(ChannelBackupStorage):
|
||||
node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes)
|
||||
|
||||
@attr.s
|
||||
class ImportedChannelBackupStorage(ChannelBackupStorage):
|
||||
node_id = attr.ib(type=bytes, converter=hex_to_bytes)
|
||||
privkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
||||
host = attr.ib(type=str)
|
||||
port = attr.ib(type=int, converter=int)
|
||||
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 to_bytes(self) -> bytes:
|
||||
vds = BCDataStream()
|
||||
vds.write_int16(CHANNEL_BACKUP_VERSION)
|
||||
@@ -217,7 +225,7 @@ class ChannelBackupStorage(StoredObject):
|
||||
version = vds.read_int16()
|
||||
if version != CHANNEL_BACKUP_VERSION:
|
||||
raise Exception(f"unknown version for channel backup: {version}")
|
||||
return ChannelBackupStorage(
|
||||
return ImportedChannelBackupStorage(
|
||||
is_initiator = vds.read_boolean(),
|
||||
privkey = vds.read_bytes(32).hex(),
|
||||
channel_seed = vds.read_bytes(32).hex(),
|
||||
@@ -1245,6 +1253,7 @@ class LnKeyFamily(IntEnum):
|
||||
DELAY_BASE = 4 | BIP32_PRIME
|
||||
REVOCATION_ROOT = 5 | BIP32_PRIME
|
||||
NODE_KEY = 6
|
||||
BACKUP_CIPHER = 7 | BIP32_PRIME
|
||||
|
||||
|
||||
def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair:
|
||||
|
||||
@@ -32,10 +32,13 @@ from .util import NetworkRetryManager, JsonRPCClient
|
||||
from .lnutil import LN_MAX_FUNDING_SAT
|
||||
from .keystore import BIP32_KeyStore
|
||||
from .bitcoin import COIN
|
||||
from .bitcoin import opcodes, make_op_return, address_to_script
|
||||
from .transaction import Transaction
|
||||
from .transaction import get_script_type_from_output_script
|
||||
from .crypto import sha256
|
||||
from .bip32 import BIP32Node
|
||||
from .util import bh2u, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions
|
||||
from .crypto import chacha20_encrypt, chacha20_decrypt
|
||||
from .util import ignore_exceptions, make_aiohttp_session, SilentTaskGroup
|
||||
from .util import timestamp_to_datetime, random_shuffled_copy
|
||||
from .util import MyEncoder, is_private_netaddress
|
||||
@@ -71,7 +74,7 @@ from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from . import lnsweep
|
||||
from .lnwatcher import LNWalletWatcher
|
||||
from .crypto import pw_encode_with_version_and_mac, pw_decode_with_version_and_mac
|
||||
from .lnutil import ChannelBackupStorage
|
||||
from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage
|
||||
from .lnchannel import ChannelBackup
|
||||
from .channel_db import UpdateStatus
|
||||
from .channel_db import get_mychannel_info, get_mychannel_policy
|
||||
@@ -92,6 +95,10 @@ SAVED_PR_STATUS = [PR_PAID, PR_UNPAID] # status that are persisted
|
||||
|
||||
NUM_PEERS_TARGET = 4
|
||||
|
||||
# onchain channel backup data
|
||||
CB_VERSION = 0
|
||||
CB_MAGIC_BYTES = bytes([0, 0, 0, CB_VERSION])
|
||||
|
||||
|
||||
FALLBACK_NODE_LIST_TESTNET = (
|
||||
LNPeerAddr(host='203.132.95.10', port=9735, pubkey=bfh('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9')),
|
||||
@@ -189,6 +196,7 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
|
||||
)
|
||||
self.lock = threading.RLock()
|
||||
self.node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY)
|
||||
self.backup_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.BACKUP_CIPHER).privkey
|
||||
self._peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer # needs self.lock
|
||||
self.taskgroup = SilentTaskGroup()
|
||||
self.listen_server = None # type: Optional[asyncio.AbstractServer]
|
||||
@@ -612,9 +620,11 @@ class LNWallet(LNWorker):
|
||||
self._channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self)
|
||||
|
||||
self._channel_backups = {} # type: Dict[bytes, ChannelBackup]
|
||||
channel_backups = self.db.get_dict("channel_backups")
|
||||
for channel_id, cb in random_shuffled_copy(channel_backups.items()):
|
||||
self._channel_backups[bfh(channel_id)] = ChannelBackup(cb, sweep_address=self.sweep_address, lnworker=self)
|
||||
# order is important: imported should overwrite onchain
|
||||
for name in ["onchain_channel_backups", "imported_channel_backups"]:
|
||||
channel_backups = self.db.get_dict(name)
|
||||
for channel_id, storage in channel_backups.items():
|
||||
self._channel_backups[bfh(channel_id)] = ChannelBackup(storage, sweep_address=self.sweep_address, lnworker=self)
|
||||
|
||||
self.sent_htlcs = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]]
|
||||
self.sent_htlcs_routes = dict() # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat
|
||||
@@ -629,6 +639,15 @@ class LNWallet(LNWorker):
|
||||
|
||||
self.trampoline_forwarding_failures = {} # todo: should be persisted
|
||||
|
||||
def has_deterministic_node_id(self):
|
||||
return bool(self.db.get('lightning_xprv'))
|
||||
|
||||
def has_recoverable_channels(self):
|
||||
# TODO: expose use_recoverable_channels in preferences
|
||||
return self.has_deterministic_node_id() \
|
||||
and self.config.get('use_recoverable_channels', True) \
|
||||
and not (self.config.get('lightning_listen'))
|
||||
|
||||
@property
|
||||
def channels(self) -> Mapping[bytes, Channel]:
|
||||
"""Returns a read-only copy of channels."""
|
||||
@@ -990,13 +1009,29 @@ class LNWallet(LNWorker):
|
||||
self.remove_channel(chan.channel_id)
|
||||
raise
|
||||
|
||||
def cb_data(self, node_id):
|
||||
return CB_MAGIC_BYTES + node_id[0:16]
|
||||
|
||||
def decrypt_cb_data(self, encrypted_data, funding_address):
|
||||
funding_scriptpubkey = bytes.fromhex(address_to_script(funding_address))
|
||||
nonce = funding_scriptpubkey[0:12]
|
||||
return chacha20_decrypt(key=self.backup_key, data=encrypted_data, nonce=nonce)
|
||||
|
||||
def encrypt_cb_data(self, data, funding_address):
|
||||
funding_scriptpubkey = bytes.fromhex(address_to_script(funding_address))
|
||||
nonce = funding_scriptpubkey[0:12]
|
||||
return chacha20_encrypt(key=self.backup_key, data=data, nonce=nonce)
|
||||
|
||||
def mktx_for_open_channel(
|
||||
self, *,
|
||||
coins: Sequence[PartialTxInput],
|
||||
funding_sat: int,
|
||||
node_id: bytes,
|
||||
fee_est=None) -> PartialTransaction:
|
||||
dummy_address = ln_dummy_address()
|
||||
outputs = [PartialTxOutput.from_address_and_value(dummy_address, funding_sat)]
|
||||
outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat)]
|
||||
if self.has_recoverable_channels():
|
||||
dummy_scriptpubkey = make_op_return(self.cb_data(node_id))
|
||||
outputs.append(PartialTxOutput(scriptpubkey=dummy_scriptpubkey, value=0))
|
||||
tx = self.wallet.make_unsigned_transaction(
|
||||
coins=coins,
|
||||
outputs=outputs,
|
||||
@@ -1986,7 +2021,7 @@ class LNWallet(LNWorker):
|
||||
assert chan.is_static_remotekey_enabled()
|
||||
peer_addresses = list(chan.get_peer_addresses())
|
||||
peer_addr = peer_addresses[0]
|
||||
return ChannelBackupStorage(
|
||||
return ImportedChannelBackupStorage(
|
||||
node_id = chan.node_id,
|
||||
privkey = self.node_keypair.privkey,
|
||||
funding_txid = chan.funding_outpoint.txid,
|
||||
@@ -2004,7 +2039,7 @@ class LNWallet(LNWorker):
|
||||
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"
|
||||
assert backup_bytes == ImportedChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), "roundtrip failed"
|
||||
encrypted = pw_encode_with_version_and_mac(backup_bytes, xpub)
|
||||
assert backup_bytes == pw_decode_with_version_and_mac(encrypted, xpub), "encrypt failed"
|
||||
return 'channel_backup:' + encrypted
|
||||
@@ -2030,22 +2065,22 @@ class LNWallet(LNWorker):
|
||||
encrypted = data[15:]
|
||||
xpub = self.wallet.get_fingerprint()
|
||||
decrypted = pw_decode_with_version_and_mac(encrypted, xpub)
|
||||
cb_storage = ChannelBackupStorage.from_bytes(decrypted)
|
||||
cb_storage = ImportedChannelBackupStorage.from_bytes(decrypted)
|
||||
channel_id = cb_storage.channel_id()
|
||||
if channel_id.hex() in self.db.get_dict("channels"):
|
||||
raise Exception('Channel already in wallet')
|
||||
self.logger.info(f'importing channel backup: {channel_id.hex()}')
|
||||
cb = ChannelBackup(cb_storage, sweep_address=self.sweep_address, lnworker=self)
|
||||
d = self.db.get_dict("channel_backups")
|
||||
d = self.db.get_dict("imported_channel_backups")
|
||||
d[channel_id.hex()] = cb_storage
|
||||
with self.lock:
|
||||
cb = ChannelBackup(cb_storage, sweep_address=self.sweep_address, lnworker=self)
|
||||
self._channel_backups[channel_id] = cb
|
||||
self.wallet.save_db()
|
||||
util.trigger_callback('channels_updated', self.wallet)
|
||||
self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address())
|
||||
|
||||
def remove_channel_backup(self, channel_id):
|
||||
d = self.db.get_dict("channel_backups")
|
||||
d = self.db.get_dict("imported_channel_backups")
|
||||
if channel_id.hex() not in d:
|
||||
raise Exception('Channel not found')
|
||||
with self.lock:
|
||||
@@ -2061,11 +2096,65 @@ class LNWallet(LNWorker):
|
||||
raise Exception(f'channel backup not found {self.channel_backups}')
|
||||
cb = cb.cb # storage
|
||||
self.logger.info(f'requesting channel force close: {channel_id.hex()}')
|
||||
# TODO also try network addresses from gossip db (as it might have changed)
|
||||
peer_addr = LNPeerAddr(cb.host, cb.port, cb.node_id)
|
||||
transport = LNTransport(cb.privkey, peer_addr, proxy=self.network.proxy)
|
||||
peer = Peer(self, cb.node_id, transport, is_channel_backup=True)
|
||||
async with TaskGroup(wait=any) as group:
|
||||
await group.spawn(peer._message_loop())
|
||||
await group.spawn(peer.trigger_force_close(channel_id))
|
||||
return True
|
||||
if isinstance(cb, ImportedChannelBackupStorage):
|
||||
node_id = cb.node_id
|
||||
addresses = [(cb.host, cb.port, 0)]
|
||||
# TODO also try network addresses from gossip db (as it might have changed)
|
||||
else:
|
||||
assert isinstance(cb, OnchainChannelBackupStorage)
|
||||
if not self.channel_db:
|
||||
raise Exception('Enable gossip first')
|
||||
node_id = self.network.channel_db.get_node_by_prefix(cb.node_id_prefix)
|
||||
addresses = self.network.channel_db.get_node_addresses(node_id)
|
||||
if not addresses:
|
||||
raise Exception('Peer not found in gossip database')
|
||||
for host, port, timestamp in addresses:
|
||||
peer_addr = LNPeerAddr(host, port, node_id)
|
||||
transport = LNTransport(self.node_keypair.privkey, peer_addr, proxy=self.network.proxy)
|
||||
peer = Peer(self, node_id, transport, is_channel_backup=True)
|
||||
try:
|
||||
async with TaskGroup(wait=any) as group:
|
||||
await group.spawn(peer._message_loop())
|
||||
await group.spawn(peer.trigger_force_close(channel_id))
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.info(f'failed to connect {host} {e}')
|
||||
continue
|
||||
else:
|
||||
raise Exception('failed to connect')
|
||||
|
||||
def maybe_add_backup_from_tx(self, tx):
|
||||
funding_address = None
|
||||
node_id_prefix = None
|
||||
for i, o in enumerate(tx.outputs()):
|
||||
script_type = get_script_type_from_output_script(o.scriptpubkey)
|
||||
if script_type == 'p2wsh':
|
||||
funding_index = i
|
||||
funding_address = o.address
|
||||
for o2 in tx.outputs():
|
||||
if o2.scriptpubkey.startswith(bytes([opcodes.OP_RETURN])):
|
||||
encrypted_data = o2.scriptpubkey[2:]
|
||||
data = self.decrypt_cb_data(encrypted_data, funding_address)
|
||||
if data.startswith(CB_MAGIC_BYTES):
|
||||
node_id_prefix = data[4:]
|
||||
if node_id_prefix is None:
|
||||
return
|
||||
funding_txid = tx.txid()
|
||||
cb_storage = OnchainChannelBackupStorage(
|
||||
node_id_prefix = node_id_prefix,
|
||||
funding_txid = funding_txid,
|
||||
funding_index = funding_index,
|
||||
funding_address = funding_address,
|
||||
is_initiator = True)
|
||||
channel_id = cb_storage.channel_id().hex()
|
||||
if channel_id in self.db.get_dict("channels"):
|
||||
return
|
||||
self.logger.info(f"adding backup from tx")
|
||||
d = self.db.get_dict("onchain_channel_backups")
|
||||
d[channel_id] = cb_storage
|
||||
cb = ChannelBackup(cb_storage, sweep_address=self.sweep_address, lnworker=self)
|
||||
self.wallet.save_db()
|
||||
with self.lock:
|
||||
self._channel_backups[bfh(channel_id)] = cb
|
||||
util.trigger_callback('channels_updated', self.wallet)
|
||||
self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address())
|
||||
|
||||
@@ -141,7 +141,6 @@ if [[ $1 == "backup" ]]; then
|
||||
echo "channel point: $channel"
|
||||
new_blocks 3
|
||||
wait_until_channel_open alice
|
||||
backup=$($alice export_channel_backup $channel)
|
||||
request=$($bob add_lightning_request 0.01 -m "blah" | jq -r ".invoice")
|
||||
echo "alice pays"
|
||||
$alice lnpay $request
|
||||
@@ -151,7 +150,6 @@ if [[ $1 == "backup" ]]; then
|
||||
$alice -o restore "$seed"
|
||||
$alice daemon -d
|
||||
$alice load_wallet
|
||||
$alice import_channel_backup $backup
|
||||
$alice request_force_close $channel
|
||||
wait_for_balance alice 0.989
|
||||
fi
|
||||
|
||||
@@ -167,7 +167,7 @@ class TestCreateRestoreWallet(WalletTestCase):
|
||||
wallet = d['wallet'] # type: Standard_Wallet
|
||||
|
||||
# lightning initialization
|
||||
self.assertTrue(wallet.db.get('lightning_privkey2').startswith('xprv'))
|
||||
self.assertTrue(wallet.db.get('lightning_xprv').startswith('zprv'))
|
||||
|
||||
wallet.check_password(password)
|
||||
self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))
|
||||
|
||||
@@ -326,6 +326,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
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_xprv', None)
|
||||
new_db.put('lightning_privkey2', None)
|
||||
|
||||
new_path = os.path.join(backup_dir, self.basename() + '.backup')
|
||||
@@ -345,6 +346,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
|
||||
def init_lightning(self):
|
||||
assert self.can_have_lightning()
|
||||
assert self.db.get('lightning_xprv') is None
|
||||
if self.db.get('lightning_privkey2'):
|
||||
return
|
||||
# TODO derive this deterministically from wallet.keystore at keystore generation time
|
||||
@@ -887,9 +889,10 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
|
||||
def add_transaction(self, tx, *, allow_unrelated=False):
|
||||
tx_was_added = super().add_transaction(tx, allow_unrelated=allow_unrelated)
|
||||
|
||||
if tx_was_added:
|
||||
self._maybe_set_tx_label_based_on_invoices(tx)
|
||||
if self.lnworker:
|
||||
self.lnworker.maybe_add_backup_from_tx(tx)
|
||||
return tx_was_added
|
||||
|
||||
@profiler
|
||||
@@ -2758,11 +2761,8 @@ class Deterministic_Wallet(Abstract_Wallet):
|
||||
# generate addresses now. note that without libsecp this might block
|
||||
# for a few seconds!
|
||||
self.synchronize()
|
||||
|
||||
# create lightning keys
|
||||
if self.can_have_lightning():
|
||||
self.init_lightning()
|
||||
ln_xprv = self.db.get('lightning_privkey2')
|
||||
# lightning_privkey2 is not deterministic (legacy wallets, bip39)
|
||||
ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')
|
||||
# lnworker can only be initialized once receiving addresses are available
|
||||
# therefore we instantiate lnworker in DeterministicWallet
|
||||
self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
|
||||
@@ -3143,6 +3143,8 @@ def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=N
|
||||
k = keystore.from_seed(seed, passphrase)
|
||||
db.put('keystore', k.dump())
|
||||
db.put('wallet_type', 'standard')
|
||||
if keystore.seed_type(seed) == 'segwit':
|
||||
db.put('lightning_xprv', k.get_lightning_xprv(None))
|
||||
if gap_limit is not None:
|
||||
db.put('gap_limit', gap_limit)
|
||||
wallet = Wallet(db, storage, config=config)
|
||||
@@ -3185,6 +3187,8 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig,
|
||||
k = keystore.from_master_key(text)
|
||||
elif keystore.is_seed(text):
|
||||
k = keystore.from_seed(text, passphrase)
|
||||
if keystore.seed_type(text) == 'segwit':
|
||||
db.put('lightning_xprv', k.get_lightning_xprv(None))
|
||||
else:
|
||||
raise Exception("Seed or key not recognized")
|
||||
db.put('keystore', k.dump())
|
||||
|
||||
@@ -37,7 +37,8 @@ from .invoices import PR_TYPE_ONCHAIN, Invoice
|
||||
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, ChannelBackupStorage
|
||||
from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore
|
||||
from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage
|
||||
from .lnutil import ChannelConstraints, Outpoint, ShachainElement
|
||||
from .json_db import StoredDict, JsonDB, locked, modifier
|
||||
from .plugin import run_hook, plugin_loaders
|
||||
@@ -52,7 +53,7 @@ if TYPE_CHECKING:
|
||||
|
||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||
FINAL_SEED_VERSION = 38 # electrum >= 2.7 will set this to prevent
|
||||
FINAL_SEED_VERSION = 39 # electrum >= 2.7 will set this to prevent
|
||||
# old versions from overwriting new format
|
||||
|
||||
|
||||
@@ -186,6 +187,7 @@ class WalletDB(JsonDB):
|
||||
self._convert_version_36()
|
||||
self._convert_version_37()
|
||||
self._convert_version_38()
|
||||
self._convert_version_39()
|
||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||
|
||||
self._after_upgrade_tasks()
|
||||
@@ -778,6 +780,13 @@ class WalletDB(JsonDB):
|
||||
del d[key]
|
||||
self.data['seed_version'] = 38
|
||||
|
||||
def _convert_version_39(self):
|
||||
# this upgrade prevents initialization of lightning_privkey2 after lightning_xprv has been set
|
||||
if not self._is_upgrade_method_needed(38, 38):
|
||||
return
|
||||
self.data['imported_channel_backups'] = self.data.pop('channel_backups', {})
|
||||
self.data['seed_version'] = 39
|
||||
|
||||
def _convert_imported(self):
|
||||
if not self._is_upgrade_method_needed(0, 13):
|
||||
return
|
||||
@@ -1273,8 +1282,10 @@ class WalletDB(JsonDB):
|
||||
v = dict((k, FeeUpdate(**x)) for k, x in v.items())
|
||||
elif key == 'submarine_swaps':
|
||||
v = dict((k, SwapData(**x)) for k, x in v.items())
|
||||
elif key == 'channel_backups':
|
||||
v = dict((k, ChannelBackupStorage(**x)) for k, x in v.items())
|
||||
elif key == 'imported_channel_backups':
|
||||
v = dict((k, ImportedChannelBackupStorage(**x)) for k, x in v.items())
|
||||
elif key == 'onchain_channel_backups':
|
||||
v = dict((k, OnchainChannelBackupStorage(**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':
|
||||
|
||||
Reference in New Issue
Block a user