twallet: make importing thousands of addr/privkeys fast - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 34569d172ff797047d11ae6f6bb6f95a9189879b DIR parent 917b7fa898479a80d19633279ced4d539819b8c2 HTML Author: SomberNight <somber.night@protonmail.com> Date: Sat, 27 Oct 2018 17:36:10 +0200 wallet: make importing thousands of addr/privkeys fast fixes #3101 closes #3106 closes #3113 Diffstat: M electrum/base_wizard.py | 14 ++++++++++---- M electrum/commands.py | 16 +++++++++++----- M electrum/gui/qt/main_window.py | 29 ++++++++++++++--------------- M electrum/tests/test_wallet_vertica… | 6 +++--- M electrum/wallet.py | 73 +++++++++++++++++++------------ 5 files changed, 84 insertions(+), 54 deletions(-) --- DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py t@@ -189,17 +189,23 @@ class BaseWizard(object): # will be reflected on self.storage if keystore.is_address_list(text): w = Imported_Wallet(self.storage) - for x in text.split(): - w.import_address(x) + addresses = text.split() + good_inputs, bad_inputs = w.import_addresses(addresses) elif keystore.is_private_key_list(text): k = keystore.Imported_KeyStore({}) self.storage.put('keystore', k.dump()) w = Imported_Wallet(self.storage) - for x in keystore.get_private_keys(text): - w.import_private_key(x, None) + keys = keystore.get_private_keys(text) + good_inputs, bad_inputs = w.import_private_keys(keys, None) self.keystores.append(w.keystore) else: return self.terminate() + if bad_inputs: + msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) + if len(bad_inputs) > 10: msg += '\n...' + self.show_error(_("The following inputs could not be imported") + + f' ({len(bad_inputs)}):\n' + msg) + # FIXME what if len(good_inputs) == 0 ? return self.run('create_wallet') def restore_from_key(self): DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -166,14 +166,20 @@ class Commands: text = text.strip() if keystore.is_address_list(text): wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_address(x) + addresses = text.split() + good_inputs, bad_inputs = wallet.import_addresses(addresses) + # FIXME tell user about bad_inputs + if not good_inputs: + raise Exception("None of the given addresses can be imported") elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): k = keystore.Imported_KeyStore({}) storage.put('keystore', k.dump()) wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_private_key(x, password) + keys = keystore.get_private_keys(text) + good_inputs, bad_inputs = wallet.import_private_keys(keys, password) + # FIXME tell user about bad_inputs + if not good_inputs: + raise Exception("None of the given privkeys can be imported") else: if keystore.is_seed(text): k = keystore.from_seed(text, passphrase) t@@ -435,7 +441,7 @@ class Commands: try: addr = self.wallet.import_private_key(privkey, password) out = "Keypair imported: " + addr - except BaseException as e: + except Exception as e: out = "Error: " + str(e) return out DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py t@@ -2612,19 +2612,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True) if not text: return - bad = [] - good = [] - for key in str(text).split(): - try: - addr = func(key) - good.append(addr) - except BaseException as e: - bad.append(key) - continue - if good: - self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(good)) - if bad: - self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(bad)) + keys = str(text).split() + good_inputs, bad_inputs = func(keys) + if good_inputs: + msg = '\n'.join(good_inputs[:10]) + if len(good_inputs) > 10: msg += '\n...' + self.show_message(_("The following addresses were added") + + f' ({len(good_inputs)}):\n' + msg) + if bad_inputs: + msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) + if len(bad_inputs) > 10: msg += '\n...' + self.show_error(_("The following inputs could not be imported") + + f' ({len(bad_inputs)}):\n' + msg) self.address_list.update() self.history_list.update() t@@ -2632,7 +2631,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not self.wallet.can_import_address(): return title, msg = _('Import addresses'), _("Enter addresses")+':' - self._do_import(title, msg, self.wallet.import_address) + self._do_import(title, msg, self.wallet.import_addresses) @protected def do_import_privkey(self, password): t@@ -2642,7 +2641,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): header_layout = QHBoxLayout() header_layout.addWidget(QLabel(_("Enter private keys")+':')) header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) - self._do_import(title, header_layout, lambda x: self.wallet.import_private_key(x, password)) + self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password)) def update_fiat(self): b = self.fx and self.fx.is_enabled() DIR diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py t@@ -1158,7 +1158,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', pw=None) + wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') t@@ -1192,7 +1192,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', pw=None) + wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') t@@ -1226,7 +1226,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', pw=None) + wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') DIR diff --git a/electrum/wallet.py b/electrum/wallet.py t@@ -38,7 +38,7 @@ import traceback from functools import partial from numbers import Number from decimal import Decimal -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Tuple from .i18n import _ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, t@@ -1227,16 +1227,29 @@ class Imported_Wallet(Simple_Wallet): def get_change_addresses(self): return [] - def import_address(self, address): - if not bitcoin.is_address(address): - return '' - if address in self.addresses: - return '' - self.addresses[address] = {} - self.add_address(address) + def import_addresses(self, addresses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]: + good_addr = [] # type: List[str] + bad_addr = [] # type: List[Tuple[str, str]] + for address in addresses: + if not bitcoin.is_address(address): + bad_addr.append((address, _('invalid address'))) + continue + if address in self.addresses: + bad_addr.append((address, _('address already in wallet'))) + continue + good_addr.append(address) + self.addresses[address] = {} + self.add_address(address) self.save_addresses() self.save_transactions(write=True) - return address + return good_addr, bad_addr + + def import_address(self, address: str) -> str: + good_addr, bad_addr = self.import_addresses([address]) + if good_addr and good_addr[0] == address: + return address + else: + raise BitcoinException(str(bad_addr[0][1])) def delete_address(self, address): if address not in self.addresses: t@@ -1293,28 +1306,34 @@ class Imported_Wallet(Simple_Wallet): def get_public_key(self, address): return self.addresses[address].get('pubkey') - def import_private_key(self, sec, pw, redeem_script=None): - try: - txin_type, pubkey = self.keystore.import_privkey(sec, pw) - except Exception: - neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:] - raise BitcoinException('Invalid private key: {}'.format(neutered_privkey)) - if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - if redeem_script is not None: - raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type)) + def import_private_keys(self, keys: List[str], password: Optional[str]) -> Tuple[List[str], + List[Tuple[str, str]]]: + good_addr = [] # type: List[str] + bad_keys = [] # type: List[Tuple[str, str]] + for key in keys: + try: + txin_type, pubkey = self.keystore.import_privkey(key, password) + except Exception: + bad_keys.append((key, _('invalid private key'))) + continue + if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): + bad_keys.append((key, _('not implemented type') + f': {txin_type}')) + continue addr = bitcoin.pubkey_to_address(txin_type, pubkey) - elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - if redeem_script is None: - raise BitcoinException('Redeem script required for script type {}'.format(txin_type)) - addr = bitcoin.redeem_script_to_address(txin_type, redeem_script) - else: - raise NotImplementedError(txin_type) - self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script} + good_addr.append(addr) + self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None} + self.add_address(addr) self.save_keystore() - self.add_address(addr) self.save_addresses() self.save_transactions(write=True) - return addr + return good_addr, bad_keys + + def import_private_key(self, key: str, password: Optional[str]) -> str: + good_addr, bad_keys = self.import_private_keys([key], password=password) + if good_addr: + return good_addr[0] + else: + raise BitcoinException(str(bad_keys[0][1])) def get_redeem_script(self, address): d = self.addresses[address]