diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index fdaa96933..3a22a0c80 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -545,7 +545,7 @@ 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': + if k.can_have_deterministic_lightning_xprv(): self.data['lightning_xprv'] = k.get_lightning_xprv(None) self.on_keystore(k) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 57c824361..ba5d668ec 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -1421,23 +1421,26 @@ class ElectrumWindow(App, Logger): "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() + elif self.wallet.can_have_lightning(): + root.dismiss() + if self.wallet.can_have_deterministic_lightning(): + msg = messages.MSG_LIGHTNING_SCB_WARNING + "\n" + _("Create lightning keys?") + else: 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 + d = Question(msg, self._enable_lightning, title=_('Enable Lightning?')) + d.open() def _enable_lightning(self, b): if not b: return + self.protected(_("Create lightning keys?"), self.__enable_lightning, ()) + + def __enable_lightning(self, password): wallet_path = self.get_wallet_path() - self.wallet.init_lightning() + self.wallet.init_lightning(password=password) self.show_info(_('Lightning keys have been initialized.')) self.stop_wallet() self.load_wallet_by_name(wallet_path) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2779b6d79..481cd5adf 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2386,12 +2386,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.set_contact(line2.text(), line1.text()) def init_lightning_dialog(self): - if self.question(_( + assert not self.wallet.has_lightning() + if self.wallet.can_have_deterministic_lightning(): + msg = messages.MSG_LIGHTNING_SCB_WARNING + "\n" + _("Create lightning keys?") + else: + 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?")): - self.wallet.init_lightning() - self.show_message("Lightning keys created. Please restart Electrum") + "Create lightning keys?") + if self.question(msg): + self._init_lightning_dialog() + + @protected + def _init_lightning_dialog(self, *, password): + self.wallet.init_lightning(password=password) + self.show_message("Lightning keys created. Please restart Electrum") def show_wallet_info(self): dialog = WindowModalDialog(self, _("Wallet Information")) @@ -2400,7 +2409,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): wallet_type = self.wallet.db.get('wallet_type', '') if self.wallet.is_watching_only(): wallet_type += ' [{}]'.format(_('watching-only')) - seed_available = _('True') if self.wallet.has_seed() else _('False') + seed_available = _('False') + if self.wallet.has_seed(): + seed_available = _('True') + ks = self.wallet.keystore + assert isinstance(ks, keystore.Deterministic_KeyStore) + seed_available += f" ({ks.get_seed_type()})" keystore_types = [k.get_type_text() for k in self.wallet.get_keystores()] grid = QGridLayout() basename = os.path.basename(self.wallet.storage.path) diff --git a/electrum/keystore.py b/electrum/keystore.py index 414b0da5f..4cb002e0a 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -160,6 +160,9 @@ class KeyStore(Logger, ABC): return pubkey, list(path) return None, None + def can_have_deterministic_lightning_xprv(self) -> bool: + return False + class Software_KeyStore(KeyStore): @@ -282,8 +285,9 @@ class Deterministic_KeyStore(Software_KeyStore): def __init__(self, d): Software_KeyStore.__init__(self, d) - self.seed = d.get('seed', '') + self.seed = d.get('seed', '') # only electrum seeds self.passphrase = d.get('passphrase', '') + self._seed_type = d.get('seed_type', None) # only electrum seeds def is_deterministic(self): return True @@ -297,11 +301,16 @@ class Deterministic_KeyStore(Software_KeyStore): d['seed'] = self.seed if self.passphrase: d['passphrase'] = self.passphrase + if self._seed_type: + d['seed_type'] = self._seed_type return d def has_seed(self): return bool(self.seed) + def get_seed_type(self) -> Optional[str]: + return self._seed_type + def is_watching_only(self): return not self.has_seed() @@ -313,6 +322,7 @@ class Deterministic_KeyStore(Software_KeyStore): if self.seed: raise Exception("a seed exists") self.seed = self.format_seed(seed) + self._seed_type = seed_type(seed) or None def get_seed(self, password): if not self.has_seed(): @@ -613,7 +623,14 @@ class BIP32_KeyStore(Xpub, Deterministic_KeyStore): cK = ecc.ECPrivkey(k).get_public_key_bytes() return cK, k - def get_lightning_xprv(self, password): + def can_have_deterministic_lightning_xprv(self): + if (self.get_seed_type() == 'segwit' + and self.get_bip32_node_for_xpub().xtype == 'p2wpkh'): + return True + return False + + def get_lightning_xprv(self, password) -> str: + assert self.can_have_deterministic_lightning_xprv() xprv = self.get_master_private_key(password) rootnode = BIP32Node.from_xkey(xprv) node = rootnode.subkey_at_private_derivation("m/67'/") diff --git a/electrum/wallet.py b/electrum/wallet.py index f65c94efe..3175601ed 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -333,24 +333,34 @@ class Abstract_Wallet(AddressSynchronizer, ABC): new_db.write(new_storage) return new_path - def has_lightning(self): + def has_lightning(self) -> bool: return bool(self.lnworker) - def can_have_lightning(self): + def can_have_lightning(self) -> bool: # we want static_remotekey to be a wallet address return self.txin_type == 'p2wpkh' - def init_lightning(self): + def can_have_deterministic_lightning(self) -> bool: + if not self.can_have_lightning(): + return False + if not self.keystore: + return False + return self.keystore.can_have_deterministic_lightning_xprv() + + def init_lightning(self, *, password) -> None: 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 - # 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.db.put('lightning_privkey2', ln_xprv) + assert self.db.get('lightning_privkey2') is None + + if self.can_have_deterministic_lightning(): + ks = self.keystore + assert isinstance(ks, keystore.BIP32_KeyStore) + self.db.put('lightning_xprv', ks.get_lightning_xprv(password)) + else: + seed = os.urandom(32) + node = BIP32Node.from_rootseed(seed, xtype='standard') + ln_xprv = node.to_xprv() + self.db.put('lightning_privkey2', ln_xprv) async def stop(self): """Stop all networking and save DB to disk.""" @@ -3185,7 +3195,7 @@ 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': + if k.can_have_deterministic_lightning_xprv(): db.put('lightning_xprv', k.get_lightning_xprv(None)) if gap_limit is not None: db.put('gap_limit', gap_limit) @@ -3229,7 +3239,7 @@ 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': + if k.can_have_deterministic_lightning_xprv(): db.put('lightning_xprv', k.get_lightning_xprv(None)) else: raise Exception("Seed or key not recognized") diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index f740f77c0..3a602c36d 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -53,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 = 39 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 40 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -188,6 +188,7 @@ class WalletDB(JsonDB): self._convert_version_37() self._convert_version_38() self._convert_version_39() + self._convert_version_40() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -787,6 +788,29 @@ class WalletDB(JsonDB): self.data['imported_channel_backups'] = self.data.pop('channel_backups', {}) self.data['seed_version'] = 39 + def _convert_version_40(self): + # put 'seed_type' into keystores + if not self._is_upgrade_method_needed(39, 39): + return + for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]): + ks = self.data.get(ks_name, None) + if ks is None: continue + seed = ks.get('seed') + if not seed: continue + seed_type = None + xpub = ks.get('xpub') or None + if xpub: + assert isinstance(xpub, str) + if xpub[0:4] in ('xpub', 'tpub'): + seed_type = 'standard' + elif xpub[0:4] in ('zpub', 'Zpub', 'vpub', 'Vpub'): + seed_type = 'segwit' + elif ks.get('type') == 'old': + seed_type = 'old' + if seed_type is not None: + ks['seed_type'] = seed_type + self.data['seed_version'] = 40 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return