From 830834305bd093e6ec39c23f37349e077bd89ec9 Mon Sep 17 00:00:00 2001 From: harzo Date: Tue, 8 Dec 2020 13:24:42 +0100 Subject: [PATCH 01/28] Update btchip dependency --- contrib/requirements/requirements-hw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index dc1f233a3f55..4902c57a76f5 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,6 +1,6 @@ trezor[hidapi]>=0.11.5 safet>=0.1.5 keepkey>=6.0.3 -btchip-python>=0.1.26 +git+ssh://git@gitlab.minebest.com/r-and-d/ledger-integration/btchip-python.git@feature/btcv ckcc-protocol>=0.7.7 hidapi From 37dc6f404c605479ace303b2db6eab70c0b6e2e5 Mon Sep 17 00:00:00 2001 From: harzo Date: Wed, 16 Dec 2020 11:47:49 +0100 Subject: [PATCH 02/28] Add Ledger 2 Keys wallet creation flow --- electrum/base_wizard.py | 39 ++++++++++++++++++-- electrum/gui/qt/installwizard.py | 15 +++++++- electrum/gui/qt/three_keys_dialogs.py | 25 +++++++++++++ electrum/plugins/hw_wallet/plugin.py | 6 ++++ electrum/plugins/ledger/ledger.py | 51 +++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index ecbaace339aa..7c1e6b2d2166 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -49,7 +49,9 @@ from .plugin import DeviceInfo # hardware device setup purpose -HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) +HWD_SETUP_NEW_WALLET = 0 +HWD_SETUP_DECRYPT_WALLET = 1 +HWD_SETUP_NEW_BTCV_WALLET = 2 class ScriptTypeNotSupported(Exception): pass @@ -215,6 +217,15 @@ def process_choice(choice): action = 'three_keys_2fa' + sub_action else: raise Exception('Invalid multikey wallet type: ' + self.wallet_type) + elif choice[:11] == 'multikey_hw': + self.data['multikey_type'] = 'hw' + sub_action = choice[-7:] + if self.wallet_type == '2-key': + action = 'two_keys_hw' + sub_action + elif self.wallet_type == '3-key': + action = 'three_keys_hw' + sub_action + else: + raise Exception('Invalid multikey wallet type: ' + self.wallet_type) else: raise Exception('Invalid choice: ' + choice) self.run(action) @@ -228,7 +239,9 @@ def process_choice(choice): choices = [ ('multikey_2fa_create', _('Use Gold Wallet and create a new wallet')), ('multikey_2fa_import', _('Use Gold Wallet and import an existing wallet')), - ('multikey_standalone', _('Do not use Gold Wallet')), + ('multikey_hw_create', _('Use Ledger device and create a new wallet')), + ('multikey_hw_import', _('Use Ledger device and import an existing wallet')), + ('multikey_standalone', _('Do not use Gold Wallet nor Ledger device')), ] self.choice_dialog(title=title, message=message, choices=choices, run_next=process_choice) @@ -242,6 +255,12 @@ def two_keys_2fa_create(self): def two_keys_2fa_import(self): self.get_authenticator_pubkey(run_next=self.on_two_keys_import) + def two_keys_hw_create(self): + self.get_hw_password(run_next=self.on_two_keys_hw_create, title=_('Cancel password')) + + def two_keys_hw_import(self): + self.run('restore_from_seed') + def on_two_keys_create(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey self.run('choose_keystore') @@ -250,6 +269,10 @@ def on_two_keys_import(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey self.run('restore_from_seed') + def on_two_keys_hw_create(self, recovery_password: str): + self.data['recovery_password'] = recovery_password + self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET) + def three_keys_standalone(self): def collect_instant_pubkey(instant_pubkey: str): self.data['instant_pubkey'] = instant_pubkey @@ -489,6 +512,16 @@ def f(derivation, script_type): if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this client.clear_session() raise + elif purpose == HWD_SETUP_NEW_BTCV_WALLET: + if 'recovery_password' not in self.data: + raise Exception('Recovery password not set') + + self.plugin.set_recovery_password(device_info.device.id_, self.data['recovery_password'], self) + + script_type = 'p2wsh-p2sh' + derivation = normalize_bip32_derivation(purpose48_derivation(0, xtype=script_type)) + self.run('on_hw_derivation', name, device_info, derivation, script_type) + else: raise Exception('unknown purpose: %s' % purpose) @@ -614,7 +647,7 @@ def on_keystore(self, k): if has_xpub: t1 = xpub_type(k.xpub) if self.wallet_type in ['standard', '2-key', '3-key']: - if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: + if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: self.show_error(_('Wrong key type') + ' %s' % t1) self.run('choose_keystore') return diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 2aea4408a5b2..db148812035a 100755 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -21,7 +21,7 @@ from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW from .seed_dialog import SeedLayout, KeysLayout from .terms_and_conditions_mixin import TermsAndConditionsMixin, PushedButton -from .three_keys_dialogs import InsertPubKeyDialog, Qr2FaDialog +from .three_keys_dialogs import InsertPubKeyDialog, InsertHWPasswordDialog, Qr2FaDialog from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel, InfoButton, char_width_in_lineedit, get_default_language) @@ -651,6 +651,19 @@ def get_authenticator_pubkey(self, run_next, disallowed_key=None): self.exec_layout(layout, _('Gold Wallet authenticator public key'), next_enabled=False) return layout.get_compressed_pubkey() + @wizard_dialog + def get_hw_password(self, run_next, title): + label = QLabel() + message = _('Please provide password related to your Ledger device') + label.setText(message) + label.setOpenExternalLinks(True) + label.setTextInteractionFlags(Qt.TextBrowserInteraction) + label.setWordWrap(True) + + layout = InsertHWPasswordDialog(self, message_label=label) + self.exec_layout(layout, title, next_enabled=False) + return layout.get_password() + @wizard_dialog def display_2fa_pairing_qr(self, run_next, entropy: bytes): title_label = QLabel() diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py index 3b759dcf986f..976ec4e20124 100644 --- a/electrum/gui/qt/three_keys_dialogs.py +++ b/electrum/gui/qt/three_keys_dialogs.py @@ -138,6 +138,31 @@ def get_compressed_pubkey(self): return pubkey.get_public_key_hex(compressed=True) +class InsertHWPasswordDialog(QVBoxLayout): + def __init__(self, parent, message_label): + super().__init__() + self.parent = parent + label = message_label + edit = QLineEdit() + edit.setEchoMode(QLineEdit.Password) + error_label = ErrorLabel() + + edit.textChanged.connect(self._on_change) + self.addWidget(label) + self.addWidget(edit) + self.addWidget(error_label) + self.edit = edit + + def _on_change(self): + self.parent.next_button.setEnabled(self._get_str().strip() != '') + + def _get_str(self) -> str: + return self.edit.text().replace('\n', '') + + def get_password(self): + return self._get_str() + + class Qr2FaDialog(QVBoxLayout): def __init__(self, parent, title_label: str, pin_label: str, qr_data: dict): diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index ee68dd7945c7..492b526ea58b 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -167,6 +167,12 @@ def can_recognize_device(self, device: Device) -> bool: """ return device.product_key in self.DEVICE_IDS + def set_instant_password(self, device_id, password: str, wizard) -> bool: + raise NotImplementedError() + + def set_recovery_password(self, device_id, password: str, wizard) -> bool: + raise NotImplementedError() + class HardwareClientBase: diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 9b9d0baf55ac..e4f5f5dd0380 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -124,6 +124,14 @@ def get_xpub(self, bip32_path, xtype): fingerprint=fingerprint_bytes, child_number=childnum_bytes).to_xpub() + @test_pin_unlocked + def set_instant_password(self, password): + self.dongleObject.setBTCVPassword(password, btchip.BTCV_PASSWORD_INSTANT) + + @test_pin_unlocked + def set_recovery_password(self, password): + self.dongleObject.setBTCVPassword(password, btchip.BTCV_PASSWORD_RECOVERY) + def has_detached_pin_support(self, client): try: client.getVerifyPinRemainingAttempts() @@ -494,6 +502,38 @@ def sign_transaction(self, tx, password): finally: self.handler.finished() + @test_pin_unlocked + @set_and_unset_signing + def get_address(self, sequence, txin_type, btcvAddr=False): + client = self.get_client() + address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence + segwit = is_segwit_script_type(txin_type) + segwitNative = txin_type == 'p2wpkh' + + result = None + try: + result = client.getWalletPublicKey(address_path, showOnScreen=False, segwit=segwit, segwitNative=segwitNative, btcvAddr=btcvAddr) + except Exception as e: + self.logger.exception(e) + + return result['address'].decode("utf-8") + + @test_pin_unlocked + @set_and_unset_signing + def get_addresses_batch(self, sequences, txin_type, btcvAddr=False): + client = self.get_client() + address_paths = [self.get_derivation_prefix()[2:] + "/%d/%d"%sequence for sequence in sequences] + segwit = is_segwit_script_type(txin_type) + segwitNative = txin_type == 'p2wpkh' + + resultBatch = None + try: + resultBatch = client.getWalletPublicKeyBatch(address_paths, segwit=segwit, segwitNative=segwitNative, btcvAddr=btcvAddr) + except Exception as e: + self.logger.exception(e) + + return [result['address'].decode("utf-8") for result in resultBatch] + @test_pin_unlocked @set_and_unset_signing def show_address(self, sequence, txin_type): @@ -593,6 +633,17 @@ def setup_device(self, device_info, wizard, purpose): client.handler = self.create_handler(wizard) client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 + def set_recovery_password(self, device_id, password, wizard): + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = self.create_handler(wizard) + client.checkDevice() + try: + client.set_recovery_password(password) + return True + except: + return False + def get_xpub(self, device_id, derivation, xtype, wizard): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported(_('This type of script is not supported with {device}.').format(device=self.device)) From 47d83e129174a9a4847b491f8e097ec88f36e1d8 Mon Sep 17 00:00:00 2001 From: harzo Date: Wed, 16 Dec 2020 12:35:54 +0100 Subject: [PATCH 03/28] Update synchronization flow for hw --- electrum/wallet.py | 63 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index ab6fee7c7dcb..7aaa5fa0505c 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2306,10 +2306,71 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenera self.multikey_type = storage.get('multikey_type') self.multisig_script_generator = scriptGenerator self.set_alert() + self.postponed_synchronization = False + if self.multikey_type == 'hw': + self.postponed_synchronization = True # super has to be at the end otherwise wallet breaks super().__init__(storage=storage, config=config) self.multiple_change = storage.get('multiple_change', True) + def create_new_hw_address(self, for_change=False): + assert type(for_change) is bool + with self.lock: + n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + address = self.keystore.get_address((for_change, n), self.txin_type, True) + self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) + self.add_address(address) + if for_change: + # note: if it's actually used, it will get filtered later + self._unused_change_addresses.append(address) + return address + + def create_new_hw_addresses(self, sequences, for_change=False): + assert type(for_change) is bool + with self.lock: + addresses = self.keystore.get_addresses_batch(sequences, self.txin_type, True) + for address in addresses: + self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) + self.add_address(address) + if for_change: + # note: if it's actually used, it will get filtered later + self._unused_change_addresses.append(address) + + return addresses + + def synchronize_sequence(self, for_change): + limit = self.gap_limit_for_change if for_change else self.gap_limit + num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + + if self.multikey_type == 'hw': + if num_addr < limit: + sequences = [(for_change, num_addr + sequence) for sequence in range(limit-num_addr)] + self.create_new_hw_addresses(sequences, for_change) + else: + while num_addr < limit: + self.create_new_address(for_change) + num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + + while True: + if for_change: + last_few_addresses = self.get_change_addresses(slice_start=-limit) + else: + last_few_addresses = self.get_receiving_addresses(slice_start=-limit) + if any(map(self.address_is_old, last_few_addresses)): + if self.multikey_type == 'hw': + self.create_new_hw_address(for_change) + else: + self.create_new_address(for_change) + else: + break + + @AddressSynchronizer.with_local_height_cached + def synchronize(self): + if not self.postponed_synchronization: + with self.lock: + self.synchronize_sequence(False) + self.synchronize_sequence(True) + def set_alert(self): self.multisig_script_generator.set_alert() @@ -2341,7 +2402,7 @@ def derive_txin_type_from_keystore(keystore): if txin_type == 'standard': return 'p2sh' - if 'p2wpkh' in txin_type: + if 'p2wpkh' in txin_type or 'p2wsh-p2sh' == txin_type: return 'p2wsh-p2sh' raise UnknownTxinType(f'Cannot derive txin_type from {txin_type}') From f3064ef944d9b10e7eba0a38e2a9442249c8d043 Mon Sep 17 00:00:00 2001 From: harzo Date: Wed, 16 Dec 2020 14:13:11 +0100 Subject: [PATCH 04/28] Create 2 and 3 keys HW wallets classes --- electrum/base_wizard.py | 9 +- electrum/gui/qt/__init__.py | 12 + electrum/gui/qt/three_keys_windows.py | 8 + electrum/wallet.py | 331 +++++++++++++++++++++----- 4 files changed, 293 insertions(+), 67 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 7c1e6b2d2166..2f7038539a9a 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -219,6 +219,7 @@ def process_choice(choice): raise Exception('Invalid multikey wallet type: ' + self.wallet_type) elif choice[:11] == 'multikey_hw': self.data['multikey_type'] = 'hw' + self.data['wallet_type'] += '-hw' sub_action = choice[-7:] if self.wallet_type == '2-key': action = 'two_keys_hw' + sub_action @@ -303,7 +304,7 @@ def on_three_keys_import(self, recovery_pubkey: str): self.run('restore_from_seed') def choose_keystore(self): - assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key'] + assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key', '2-key-hw', '3-key-hw'] i = len(self.keystores) title = _('Add cosigner') + ' (%d of %d)' % (i + 1, self.n) if self.wallet_type == 'multisig' else _('Keystore') if self.wallet_type == 'multisig' and i > 0: @@ -327,7 +328,7 @@ def choose_keystore(self): advanced_choices = [ ('restore_from_key', _('Use a master key')), ] - if not self.is_kivy and self.wallet_type not in ['2-key', '3-key']: + if not self.is_kivy and self.wallet_type not in ['2-key', '3-key', '2-key-hw', '3-key-hw']: advanced_choices.append(('choose_hw_device', _('Use a hardware device'))) if self.wallet_type == 'multisig': self.choice_dialog(title=title, message=message, choices=base_choices + advanced_choices, run_next=self.run) @@ -646,7 +647,7 @@ def on_keystore(self, k): has_xpub = isinstance(k, keystore.Xpub) if has_xpub: t1 = xpub_type(k.xpub) - if self.wallet_type in ['standard', '2-key', '3-key']: + if self.wallet_type in ['standard', '2-key', '3-key', '2-key-hw', '3-key-hw']: if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: self.show_error(_('Wrong key type') + ' %s' % t1) self.run('choose_keystore') @@ -723,7 +724,7 @@ def on_password(self, password, *, encrypt_storage: bool, for k in self.keystores: if k.may_have_password(): k.update_password(None, password) - if self.wallet_type in ['standard', '2-key', '3-key']: + if self.wallet_type in ['standard', '2-key', '3-key', '2-key-hw', '3-key-hw']: self.data['seed_type'] = self.seed_type keys = self.keystores[0].dump() self.data['keystore'] = keys diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 13e691be05f9..818f345234a3 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -211,6 +211,10 @@ def _create_window_for_wallet(self, wallet): w = ElectrumARWindow(self, wallet) elif wallet_type == '3-key': w = ElectrumAIRWindow(self, wallet) + elif wallet_type == '2-key-hw': + w = ElectrumARWindow(self, wallet) + elif wallet_type == '3-key-hw': + w = ElectrumAIRWindow(self, wallet) else: w = ElectrumWindow(self, wallet) self.windows.append(w) @@ -340,6 +344,14 @@ def accept_terms_and_conditions(self): self.stop() raise TermsNotAccepted + def wait_for_task(self, task, msg): + wizard = InstallWizard(self.config, self.app, self.plugins) + + def on_finished(): + wizard.terminate() + + wizard.waiting_dialog(task, msg, on_finished) + def choose_language(self): if self.config.get('language', None) is None: wizard = InstallWizard(self.config, self.app, self.plugins) diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index c3a342e6c162..1000bf93a691 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -34,6 +34,14 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): self.READY_TO_UPDATE = True + if self.wallet.multikey_type == 'hw': + self.synchronize_wallet() + + def synchronize_wallet(self): + if self.wallet.postponed_synchronization: + self.wallet.postponed_synchronization = False + self.gui_object.wait_for_task(self.wallet.synchronize(), _('Synchonizing wallet...')) + def timer_actions(self): # synchronizing the timer thread with end of the __init__ call if self.READY_TO_UPDATE: diff --git a/electrum/wallet.py b/electrum/wallet.py index 7aaa5fa0505c..32e5036d0286 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2306,71 +2306,11 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenera self.multikey_type = storage.get('multikey_type') self.multisig_script_generator = scriptGenerator self.set_alert() - self.postponed_synchronization = False - if self.multikey_type == 'hw': - self.postponed_synchronization = True + # super has to be at the end otherwise wallet breaks super().__init__(storage=storage, config=config) self.multiple_change = storage.get('multiple_change', True) - def create_new_hw_address(self, for_change=False): - assert type(for_change) is bool - with self.lock: - n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - address = self.keystore.get_address((for_change, n), self.txin_type, True) - self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) - self.add_address(address) - if for_change: - # note: if it's actually used, it will get filtered later - self._unused_change_addresses.append(address) - return address - - def create_new_hw_addresses(self, sequences, for_change=False): - assert type(for_change) is bool - with self.lock: - addresses = self.keystore.get_addresses_batch(sequences, self.txin_type, True) - for address in addresses: - self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) - self.add_address(address) - if for_change: - # note: if it's actually used, it will get filtered later - self._unused_change_addresses.append(address) - - return addresses - - def synchronize_sequence(self, for_change): - limit = self.gap_limit_for_change if for_change else self.gap_limit - num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - - if self.multikey_type == 'hw': - if num_addr < limit: - sequences = [(for_change, num_addr + sequence) for sequence in range(limit-num_addr)] - self.create_new_hw_addresses(sequences, for_change) - else: - while num_addr < limit: - self.create_new_address(for_change) - num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - - while True: - if for_change: - last_few_addresses = self.get_change_addresses(slice_start=-limit) - else: - last_few_addresses = self.get_receiving_addresses(slice_start=-limit) - if any(map(self.address_is_old, last_few_addresses)): - if self.multikey_type == 'hw': - self.create_new_hw_address(for_change) - else: - self.create_new_address(for_change) - else: - break - - @AddressSynchronizer.with_local_height_cached - def synchronize(self): - if not self.postponed_synchronization: - with self.lock: - self.synchronize_sequence(False) - self.synchronize_sequence(True) - def set_alert(self): self.multisig_script_generator.set_alert() @@ -2569,7 +2509,7 @@ def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_k tx.finalize_psbt() return tx - + def get_wallet_label(self): return '2-Key Vault' @@ -2635,6 +2575,267 @@ def get_wallet_label(self): return '3-Key Vault' +class MultikeyHWWallet(Simple_Deterministic_Wallet): + + def __init__(self, storage: WalletStorage, *, config: SimpleConfig): + self.wallet_type = storage.get('wallet_type') + self.multikey_type = storage.get('multikey_type') + self.postponed_synchronization = True + + # super has to be at the end otherwise wallet breaks + super().__init__(storage=storage, config=config) + self.multiple_change = storage.get('multiple_change', True) + + def create_new_hw_address(self, for_change=False): + assert type(for_change) is bool + with self.lock: + n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + address = self.keystore.get_address((for_change, n), self.txin_type, True) + self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) + self.add_address(address) + if for_change: + # note: if it's actually used, it will get filtered later + self._unused_change_addresses.append(address) + return address + + def create_new_hw_addresses(self, sequences, for_change=False): + assert type(for_change) is bool + with self.lock: + addresses = self.keystore.get_addresses_batch(sequences, self.txin_type, True) + for address in addresses: + self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) + self.add_address(address) + if for_change: + # note: if it's actually used, it will get filtered later + self._unused_change_addresses.append(address) + + return addresses + + def synchronize_sequence(self, for_change): + limit = self.gap_limit_for_change if for_change else self.gap_limit + num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + + if self.multikey_type == 'hw': + if num_addr < limit: + sequences = [(for_change, num_addr + sequence) for sequence in range(limit-num_addr)] + self.create_new_hw_addresses(sequences, for_change) + else: + while num_addr < limit: + self.create_new_address(for_change) + num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + + while True: + if for_change: + last_few_addresses = self.get_change_addresses(slice_start=-limit) + else: + last_few_addresses = self.get_receiving_addresses(slice_start=-limit) + if any(map(self.address_is_old, last_few_addresses)): + if self.multikey_type == 'hw': + self.create_new_hw_address(for_change) + else: + self.create_new_address(for_change) + else: + break + + @AddressSynchronizer.with_local_height_cached + def synchronize(self): + if not self.postponed_synchronization: + with self.lock: + self.synchronize_sequence(False) + self.synchronize_sequence(True) + + def set_alert(self): + self.multisig_script_generator.set_alert() + + def set_recovery(self): + self.multisig_script_generator.set_recovery() + + def set_instant(self): + self.multisig_script_generator.set_instant() + + def load_keystore(self): + self.keystore = load_keystore(self.storage, 'keystore') + self.txin_type = self.derive_txin_type_from_keystore(self.keystore) + + @staticmethod + def derive_txin_type_from_keystore(keystore): + try: + txin_type = bip32.xpub_type(keystore.xpub) + except: + txin_type = 'standard' + + if txin_type == 'standard': + return 'p2sh' + if 'p2wpkh' in txin_type or 'p2wsh-p2sh' == txin_type: + return 'p2wsh-p2sh' + raise UnknownTxinType(f'Cannot derive txin_type from {txin_type}') + + def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], + outputs: List[PartialTxOutput], fee=None, + change_addr: str = None, is_sweep=False) -> PartialTransaction: + self.update_tx_input_multisig_generator(coins) + tx = super().make_unsigned_transaction( + coins=coins, + outputs=outputs, + fee=fee, + change_addr=change_addr, + is_sweep=is_sweep, + ) + self.update_transaction_multisig_generator(tx) + return tx + + def get_atxs_to_recovery(self): + txi_list = self.db.list_txi() + recovery_mempool_transactions = { + history_item.txid: self.db.get_transaction(history_item.txid) + for history_item in self.get_history() if history_item.tx_mined_status.conf == 0 and history_item.tx_mined_status.txtype == TxType.RECOVERY.name + } + with self.transaction_lock: + conflicting_alert_inputs = set([ + txin.prevout.txid.hex() + for tx in recovery_mempool_transactions.values() for txin in tx.inputs() + ]) + for tx_hash, tx in self.db.transactions.items(): + mined_info = self.get_tx_height(tx_hash) + # skip incoming alerts, mempool alerts and alerts conflicted with recovery mempool + if tx.tx_type == TxType.ALERT_PENDING and mined_info.conf > 0 and tx_hash in txi_list: + if not set([txin.prevout.txid.hex() for txin in tx.inputs()]).issubset(conflicting_alert_inputs): + yield tx + + def get_inputs_and_output_for_recovery(self, alert_transactions: ThreeKeysTransaction, destination_address: str): + inputs = [PartialTxInput.from_txin(txin) for atx in alert_transactions for txin in atx.inputs()] + scriptpubkey = bfh(bitcoin.address_to_script(destination_address)) + # ! sign sets max value to output + output = PartialTxOutput(scriptpubkey=scriptpubkey, value='!') + return inputs, output + + def prepare_inputs_for_recovery(self, inputs: list): + """Methods for modification tx inputs coming from alert transaction to work with recovery tx. + Method adds missing address, satoshi and height value from db storage""" + updated_inputs = copy.deepcopy(inputs) + # cache for not doubling fetching data for repeating address + db_address_satoshi_height_cache = {} + for input in updated_inputs: + tx_hash = input.prevout.txid.hex() + prevout_index = input.prevout.out_idx + key = (tx_hash, prevout_index) + if key not in db_address_satoshi_height_cache: + fetched_data = self.db.get_address_satoshi_height_for_tx(tx_hash) + db_address_satoshi_height_cache.update(fetched_data) + fetched_data = fetched_data[key] + else: + fetched_data = db_address_satoshi_height_cache[key] + + input._trusted_address = fetched_data['address'] + input._trusted_value_sats = fetched_data['satoshi'] + input.block_height = fetched_data['height'] + return updated_inputs + + def sign_transaction(self, tx: PartialTransaction, password, external_keypairs=None, update_pubkeys_fn=None, skip_finalize=False) -> Optional[PartialTransaction]: + if self.is_watching_only(): + return + if not isinstance(tx, PartialTransaction): + return + # add info to a temporary tx copy; including xpubs + # and full derivation paths as hw keystores might want them + tmp_tx = copy.deepcopy(tx) + # update tmp tx + self.update_transaction_multisig_generator(tmp_tx) + tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True) + if update_pubkeys_fn: + update_pubkeys_fn(tx) + update_pubkeys_fn(tmp_tx) + # sign. start with ready keystores. + for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): + try: + if k.can_sign(tmp_tx): + k.sign_transaction(tmp_tx, password) + except UserCancelled: + continue + + if external_keypairs: + tmp_tx.sign(external_keypairs) + # remove sensitive info; then copy back details from temporary tx + tmp_tx.remove_xpubs_and_bip32_paths() + # update tx + self.update_transaction_multisig_generator(tx) + tx.combine_with_other_psbt(tmp_tx, skip_finalize) + tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False) + + if update_pubkeys_fn: + update_pubkeys_fn(tx) + + return tx + + +class TwoKeysHWWallet(MultikeyHWWallet): + + def _add_recovery_pubkey_to_transaction(self, tx): + for input in tx.inputs(): + recovery_pubkey = bytes.fromhex(self.multisig_script_generator.recovery_pubkey) + if recovery_pubkey not in input.pubkeys: + input.pubkeys.append(recovery_pubkey) + input.num_sig = 2 + assert len(input.pubkeys) == 2, 'Wrong number of pubkeys for performing recovery tx' + return tx + + def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs) -> Optional[ + PartialTransaction]: + if not isinstance(tx, PartialTransaction): + return + + # Skip inputs finalization + skip_finalize = self.multikey_type == '2fa' + tx = self.sign_transaction(tx, password, recovery_keypairs, self._add_recovery_pubkey_to_transaction, + skip_finalize) + + if not skip_finalize: + if not tx.is_complete(): + _logger.error(f'Recovery transaction not completed') + tx.finalize_psbt() + + return tx + + def get_wallet_label(self): + return '2-Key Vault' + + +class ThreeKeysHWWallet(MultikeyHWWallet): + + def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs) -> Optional[PartialTransaction]: + if not isinstance(tx, PartialTransaction): + return + + # Skip tx finalization when tx should be authenticated + skip_finalize = self.multikey_type == '2fa' + tx = self.sign_transaction(tx, password, instant_keypairs, self._add_instant_pubkey_to_transaction, skip_finalize) + + if not skip_finalize: + if not tx.is_complete(): + _logger.error(f'Instant transaction not completed') + tx.finalize_psbt() + + return tx + + def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs) -> Optional[PartialTransaction]: + if not isinstance(tx, PartialTransaction): + return + + # Skip tx finalization when tx should be authenticated + skip_finalize = self.multikey_type == '2fa' + tx = self.sign_transaction(tx, password, recovery_keypairs, self._add_recovery_pubkey_to_transaction, skip_finalize) + + if not skip_finalize: + if not tx.is_complete(): + _logger.error(f'Recovery transaction not completed') + tx.finalize_psbt() + + return tx + + def get_wallet_label(self): + return '3-Key Vault' + + wallet_types = [ '2-key', '3-key', @@ -2654,12 +2855,16 @@ def register_wallet_type(category): 'xpub': Standard_Wallet, 'imported': Imported_Wallet, '2-key': TwoKeysWallet, - '3-key': ThreeKeysWallet + '3-key': ThreeKeysWallet, + '2-key-hw': TwoKeysHWWallet, + '3-key-hw': ThreeKeysHWWallet } + def register_constructor(wallet_type, constructor): wallet_constructors[wallet_type] = constructor + # former WalletFactory class Wallet(object): """The main wallet "entry point". From f09c05a4cbd2150b53b3c5d312e47b82827d977f Mon Sep 17 00:00:00 2001 From: harzo Date: Tue, 22 Dec 2020 09:35:42 +0100 Subject: [PATCH 05/28] Use multiple keystores --- electrum/base_wizard.py | 60 ++++++++++----- electrum/gui/qt/__init__.py | 8 -- electrum/gui/qt/three_keys_windows.py | 13 ---- electrum/three_keys/script.py | 82 +++++++++++--------- electrum/wallet.py | 104 +++++--------------------- 5 files changed, 106 insertions(+), 161 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 2f7038539a9a..b0306a56266a 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -220,10 +220,11 @@ def process_choice(choice): elif choice[:11] == 'multikey_hw': self.data['multikey_type'] = 'hw' self.data['wallet_type'] += '-hw' + self.wallet_type += '-hw' sub_action = choice[-7:] - if self.wallet_type == '2-key': + if self.wallet_type == '2-key-hw': action = 'two_keys_hw' + sub_action - elif self.wallet_type == '3-key': + elif self.wallet_type == '3-key-hw': action = 'three_keys_hw' + sub_action else: raise Exception('Invalid multikey wallet type: ' + self.wallet_type) @@ -518,6 +519,8 @@ def f(derivation, script_type): raise Exception('Recovery password not set') self.plugin.set_recovery_password(device_info.device.id_, self.data['recovery_password'], self) + if 'instant_password' in self.data: + self.plugin.set_instant_password(device_info.device.id_, self.data['instant_password'], self) script_type = 'p2wsh-p2sh' derivation = normalize_bip32_derivation(purpose48_derivation(0, xtype=script_type)) @@ -560,7 +563,7 @@ def derivation_and_script_type_dialog(self, f): self.show_error(e) # let the user choose again - def on_hw_derivation(self, name, device_info, derivation, xtype): + def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False): from .keystore import hardware_keystore try: xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) @@ -572,16 +575,19 @@ def on_hw_derivation(self, name, device_info, derivation, xtype): self.show_error(e) return xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower() - d = { - 'type': 'hardware', - 'hw_type': name, - 'derivation': derivation, - 'root_fingerprint': xfp, - 'xpub': xpub, - 'label': device_info.label, - } - k = hardware_keystore(d) - self.on_keystore(k) + if xpub_keystore: + k = keystore.from_master_key(xpub) + else: + d = { + 'type': 'hardware', + 'hw_type': name, + 'derivation': derivation, + 'root_fingerprint': xfp, + 'xpub': xpub, + 'label': device_info.label, + } + k = hardware_keystore(d) + self.on_keystore(k, name, device_info) def passphrase_dialog(self, run_next, is_restoring=False): title = _('Seed extension') @@ -643,17 +649,35 @@ def on_bip43(self, seed, passphrase, derivation, script_type): k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type) self.on_keystore(k) - def on_keystore(self, k): + def on_keystore(self, k, name=None, device_info=None): has_xpub = isinstance(k, keystore.Xpub) if has_xpub: t1 = xpub_type(k.xpub) - if self.wallet_type in ['standard', '2-key', '3-key', '2-key-hw', '3-key-hw']: - if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: + if self.wallet_type in ['standard', '2-key', '3-key']: + if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: self.show_error(_('Wrong key type') + ' %s' % t1) self.run('choose_keystore') return self.keystores.append(k) self.run('create_wallet') + elif self.wallet_type in ['2-key-hw', '3-key-hw']: + if has_xpub and t1 != 'p2wsh-p2sh': + self.show_error(_('Wrong key type') + ' %s' % t1) + self.run('choose_keystore') + return + self.keystores.append(k) + + keystores_needed = 2 if self.wallet_type == '2-key-hw' else 3 + if len(self.keystores) < keystores_needed: + if not name or not device_info: + self.show_error(_('Missing device info')) + self.run('choose_keystore') + return + script_type = 'p2wsh-p2sh' + derivation = normalize_bip32_derivation(purpose48_derivation(len(self.keystores), xtype=script_type)) + self.run('on_hw_derivation', name, device_info, derivation, script_type, True) + else: + self.run('create_wallet') elif self.wallet_type == 'multisig': assert has_xpub if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: @@ -724,11 +748,11 @@ def on_password(self, password, *, encrypt_storage: bool, for k in self.keystores: if k.may_have_password(): k.update_password(None, password) - if self.wallet_type in ['standard', '2-key', '3-key', '2-key-hw', '3-key-hw']: + if self.wallet_type in ['standard', '2-key', '3-key']: self.data['seed_type'] = self.seed_type keys = self.keystores[0].dump() self.data['keystore'] = keys - elif self.wallet_type == 'multisig': + elif self.wallet_type in ['multisig', '2-key-hw', '3-key-hw']: for i, k in enumerate(self.keystores): self.data['x%d/' % (i + 1)] = k.dump() elif self.wallet_type == 'imported': diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 818f345234a3..f54c84909cc1 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -344,14 +344,6 @@ def accept_terms_and_conditions(self): self.stop() raise TermsNotAccepted - def wait_for_task(self, task, msg): - wizard = InstallWizard(self.config, self.app, self.plugins) - - def on_finished(): - wizard.terminate() - - wizard.waiting_dialog(task, msg, on_finished) - def choose_language(self): if self.config.get('language', None) is None: wizard = InstallWizard(self.config, self.app, self.plugins) diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 1000bf93a691..09ecbae5bd01 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -34,19 +34,6 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): self.READY_TO_UPDATE = True - if self.wallet.multikey_type == 'hw': - self.synchronize_wallet() - - def synchronize_wallet(self): - if self.wallet.postponed_synchronization: - self.wallet.postponed_synchronization = False - self.gui_object.wait_for_task(self.wallet.synchronize(), _('Synchonizing wallet...')) - - def timer_actions(self): - # synchronizing the timer thread with end of the __init__ call - if self.READY_TO_UPDATE: - super().timer_actions() - def create_recovery_tab(self, wallet: 'Abstract_Wallet', config): raise NotImplementedError() diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index d7436d5b0a36..a16e210fbdff 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -14,39 +14,45 @@ def __init__(self, recovery_pubkey: str): self._recovery_alert_flag = None self.witness_flags = [] - def get_redeem_script(self, public_keys: List[str]) -> str: - if not isinstance(public_keys, list) or len(public_keys) not in [1, 2]: - raise ThreeKeysError(f"Wrong input type! Expected 1 or 2 elements list not '{public_keys}'") - # filter out recovery pubkey - filtered_keys = list(filter(lambda item: item != self.recovery_pubkey, public_keys)) - if len(filtered_keys) != 1: - raise ThreeKeysError(f'Cannot deduce pubkey from {public_keys}') - - pub_key = filtered_keys[0] + @staticmethod + def create_redeem_script(alert_pubkey, recovery_pubkey): return ( opcodes.OP_IF.hex() + opcodes.OP_1.hex() + opcodes.OP_ELSE.hex() + opcodes.OP_2.hex() + opcodes.OP_ENDIF.hex() + - - push_script(pub_key) + - push_script(self.recovery_pubkey) + - + push_script(alert_pubkey) + + push_script(recovery_pubkey) + opcodes.OP_2.hex() + opcodes.OP_CHECKMULTISIG.hex() ) + @staticmethod + def _create_script_sig(signatures, flag, redeem_script): + return ( + opcodes.OP_0.hex() + + signatures + + flag + + push_script(redeem_script) + ) + + def get_redeem_script(self, public_keys: List[str]) -> str: + if not isinstance(public_keys, list) or len(public_keys) not in [1, 2]: + raise ThreeKeysError(f"Wrong input type! Expected 1 or 2 elements list not '{public_keys}'") + # filter out recovery pubkey + filtered_keys = list(filter(lambda item: item != self.recovery_pubkey, public_keys)) + if len(filtered_keys) != 1: + raise ThreeKeysError(f'Cannot deduce pubkey from {public_keys}') + + pub_key = filtered_keys[0] + return self.create_redeem_script(pub_key, self.recovery_pubkey) + def get_script_sig(self, signatures: List[str], public_keys: List[str]) -> str: if self._recovery_alert_flag is None: raise ThreeKeysError('Recovery/alert flag not set!') sigs = ''.join(push_script(sig) for sig in signatures) - return ( - opcodes.OP_0.hex() + - sigs + - self._recovery_alert_flag + - push_script(self.get_redeem_script(public_keys)) - ) + return self._create_script_sig(sigs, self._recovery_alert_flag, self.get_redeem_script(public_keys)) def set_alert(self): # 1 of 2 @@ -72,11 +78,8 @@ def __init__(self, recovery_pubkey: str, instant_pubkey: str): self._instant_recovery_alert_flag = None self.witness_flags = [] - def get_redeem_script(self, public_keys: List[str]) -> str: - if not isinstance(public_keys, list) or len(public_keys) > 3: - raise ThreeKeysError(f"Wrong input type! Expected list not '{public_keys}'") - - pub_key = public_keys[0] + @staticmethod + def create_redeem_script(alert_pubkey, instant_pubkey, recovery_pubkey): return ( opcodes.OP_IF.hex() + opcodes.OP_1.hex() + @@ -87,25 +90,34 @@ def get_redeem_script(self, public_keys: List[str]) -> str: opcodes.OP_3.hex() + opcodes.OP_ENDIF.hex() + opcodes.OP_ENDIF.hex() + - - push_script(pub_key) + - push_script(self.instant_pubkey) + - push_script(self.recovery_pubkey) + - + push_script(alert_pubkey) + + push_script(instant_pubkey) + + push_script(recovery_pubkey) + opcodes.OP_3.hex() + opcodes.OP_CHECKMULTISIG.hex() ) + @staticmethod + def _create_script_sig(signatures, flags, redeem_script): + return ( + opcodes.OP_0.hex() + + signatures + + flags + + push_script(redeem_script) + ) + + def get_redeem_script(self, public_keys: List[str]) -> str: + if not isinstance(public_keys, list) or len(public_keys) > 3: + raise ThreeKeysError(f"Wrong input type! Expected list not '{public_keys}'") + + pub_key = public_keys[0] + return self.create_redeem_script(pub_key, self.instant_pubkey, self.recovery_pubkey) + def get_script_sig(self, signatures: List[str], public_keys: List[str]) -> str: if self._instant_recovery_alert_flag is None: raise ThreeKeysError('Recovery/alert/instant flag not set!') sigs = ''.join(push_script(sig) for sig in signatures) - return ( - opcodes.OP_0.hex() + - sigs + - self._instant_recovery_alert_flag + - push_script(self.get_redeem_script(public_keys)) - ) + return self._create_script_sig(sigs, self._instant_recovery_alert_flag, self.get_redeem_script(public_keys)) def set_alert(self): # 1 of 3 diff --git a/electrum/wallet.py b/electrum/wallet.py index 32e5036d0286..09bc80e80d3e 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -59,7 +59,7 @@ from .plugin import run_hook from .simple_config import SimpleConfig from .storage import StorageEncryptionVersion, WalletStorage -from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator +from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator, ThreeKeysError from .three_keys.transaction import TxType, ThreeKeysTransaction from .three_keys.utils import filter_spendable_coins, update_tx_status from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, @@ -2575,100 +2575,24 @@ def get_wallet_label(self): return '3-Key Vault' -class MultikeyHWWallet(Simple_Deterministic_Wallet): +class MultikeyHWWallet(Multisig_Wallet): def __init__(self, storage: WalletStorage, *, config: SimpleConfig): self.wallet_type = storage.get('wallet_type') self.multikey_type = storage.get('multikey_type') - self.postponed_synchronization = True - + # self.postponed_synchronization = True + self.n = 2 + Deterministic_Wallet.__init__(self, storage, config=config) # super has to be at the end otherwise wallet breaks - super().__init__(storage=storage, config=config) self.multiple_change = storage.get('multiple_change', True) - def create_new_hw_address(self, for_change=False): - assert type(for_change) is bool - with self.lock: - n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - address = self.keystore.get_address((for_change, n), self.txin_type, True) - self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) - self.add_address(address) - if for_change: - # note: if it's actually used, it will get filtered later - self._unused_change_addresses.append(address) - return address - - def create_new_hw_addresses(self, sequences, for_change=False): - assert type(for_change) is bool - with self.lock: - addresses = self.keystore.get_addresses_batch(sequences, self.txin_type, True) - for address in addresses: - self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) - self.add_address(address) - if for_change: - # note: if it's actually used, it will get filtered later - self._unused_change_addresses.append(address) - - return addresses - - def synchronize_sequence(self, for_change): - limit = self.gap_limit_for_change if for_change else self.gap_limit - num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - - if self.multikey_type == 'hw': - if num_addr < limit: - sequences = [(for_change, num_addr + sequence) for sequence in range(limit-num_addr)] - self.create_new_hw_addresses(sequences, for_change) - else: - while num_addr < limit: - self.create_new_address(for_change) - num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() - - while True: - if for_change: - last_few_addresses = self.get_change_addresses(slice_start=-limit) - else: - last_few_addresses = self.get_receiving_addresses(slice_start=-limit) - if any(map(self.address_is_old, last_few_addresses)): - if self.multikey_type == 'hw': - self.create_new_hw_address(for_change) - else: - self.create_new_address(for_change) - else: - break - - @AddressSynchronizer.with_local_height_cached - def synchronize(self): - if not self.postponed_synchronization: - with self.lock: - self.synchronize_sequence(False) - self.synchronize_sequence(True) - - def set_alert(self): - self.multisig_script_generator.set_alert() - - def set_recovery(self): - self.multisig_script_generator.set_recovery() - - def set_instant(self): - self.multisig_script_generator.set_instant() - def load_keystore(self): - self.keystore = load_keystore(self.storage, 'keystore') - self.txin_type = self.derive_txin_type_from_keystore(self.keystore) - - @staticmethod - def derive_txin_type_from_keystore(keystore): - try: - txin_type = bip32.xpub_type(keystore.xpub) - except: - txin_type = 'standard' - - if txin_type == 'standard': - return 'p2sh' - if 'p2wpkh' in txin_type or 'p2wsh-p2sh' == txin_type: - return 'p2wsh-p2sh' - raise UnknownTxinType(f'Cannot derive txin_type from {txin_type}') + self.keystores = {} + for i in range(self.n): + name = 'x%d/'%(i+1) + self.keystores[name] = load_keystore(self.storage, name) + self.keystore = self.keystores['x1/'] + self.txin_type = 'p2wsh-p2sh' def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], outputs: List[PartialTxOutput], fee=None, @@ -2770,6 +2694,12 @@ def sign_transaction(self, tx: PartialTransaction, password, external_keypairs=N class TwoKeysHWWallet(MultikeyHWWallet): + def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: + if not isinstance(pubkeys, list) or len(pubkeys) != 2: + raise ThreeKeysError(f"Wrong input type! Expected list not '{pubkeys}'") + + return TwoKeysScriptGenerator.create_redeem_script(pubkeys[0], pubkeys[1]) + def _add_recovery_pubkey_to_transaction(self, tx): for input in tx.inputs(): recovery_pubkey = bytes.fromhex(self.multisig_script_generator.recovery_pubkey) From d89c5c348b535501f076bd096169c3874b666491 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Wed, 30 Dec 2020 16:09:44 +0100 Subject: [PATCH 06/28] Use alert/instant/recovery pubkey derivation parameter in getWalletPublicKey for keystore creation --- electrum/base_wizard.py | 14 +++++++++----- electrum/plugins/hw_wallet/plugin.py | 3 ++- electrum/plugins/ledger/ledger.py | 9 +++++---- electrum/three_keys/pubkey_type.py | 6 ++++++ 4 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 electrum/three_keys/pubkey_type.py diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index b0306a56266a..9e5c721ce390 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -41,7 +41,7 @@ from .simple_config import SimpleConfig from .storage import (WalletStorage, StorageEncryptionVersion, get_derivation_used_for_hw_device_encryption) -from .three_keys import short_mnemonic +from .three_keys.pubkey_type import PubkeyType from .util import UserCancelled, InvalidPassword from .wallet import (wallet_types) @@ -563,10 +563,10 @@ def derivation_and_script_type_dialog(self, f): self.show_error(e) # let the user choose again - def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False): + def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT): from .keystore import hardware_keystore try: - xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) + xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self, pubkey_type) root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self) except ScriptTypeNotSupported: raise # this is handled in derivation_dialog @@ -674,8 +674,12 @@ def on_keystore(self, k, name=None, device_info=None): self.run('choose_keystore') return script_type = 'p2wsh-p2sh' - derivation = normalize_bip32_derivation(purpose48_derivation(len(self.keystores), xtype=script_type)) - self.run('on_hw_derivation', name, device_info, derivation, script_type, True) + derivation = purpose48_derivation(0, xtype=script_type) + if keystores_needed == 3: + pubkey_type = len(self.keystores) + elif keystores_needed == 2: + pubkey_type = 2 * len(self.keystores) + self.run('on_hw_derivation', name, device_info, derivation, script_type, True, pubkey_type) else: self.run('create_wallet') elif self.wallet_type == 'multisig': diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 492b526ea58b..f61c0c660ed4 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -32,6 +32,7 @@ from electrum.util import bfh, versiontuple, UserFacingException from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from electrum.bip32 import BIP32Node +from electrum.three_keys.pubkey_type import PubkeyType if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet @@ -201,7 +202,7 @@ def label(self) -> str: def has_usable_connection_with_device(self) -> bool: raise NotImplementedError() - def get_xpub(self, bip32_path: str, xtype) -> str: + def get_xpub(self, bip32_path: str, xtype, pubkey_type=PubkeyType.PUBKEY_ALERT) -> str: raise NotImplementedError() diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index e4f5f5dd0380..96ca22ba753d 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -16,6 +16,7 @@ from electrum.base_wizard import ScriptTypeNotSupported from electrum.logging import get_logger from electrum.plugin import Device +from electrum.three_keys.pubkey_type import PubkeyType from typing import Tuple, Optional from ..hw_wallet import HW_PluginBase, HardwareClientBase @@ -90,7 +91,7 @@ def has_usable_connection_with_device(self): return True @test_pin_unlocked - def get_xpub(self, bip32_path, xtype): + def get_xpub(self, bip32_path, xtype, pubkey_type=PubkeyType.PUBKEY_ALERT): self.checkDevice() # bip32_path is of the form 44'/0'/1' # S-L-O-W - we don't handle the fingerprint directly, so compute @@ -114,7 +115,7 @@ def get_xpub(self, bip32_path, xtype): else: fingerprint_bytes = bytes(4) childnum_bytes = bytes(4) - nodeData = self.dongleObject.getWalletPublicKey(bip32_path) + nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvPubkeyTree=pubkey_type) publicKey = compress_public_key(nodeData['publicKey']) depth = len(bip32_intpath) return BIP32Node(xtype=xtype, @@ -644,14 +645,14 @@ def set_recovery_password(self, device_id, password, wizard): except: return False - def get_xpub(self, device_id, derivation, xtype, wizard): + def get_xpub(self, device_id, derivation, xtype, wizard, pubkey_type=PubkeyType.PUBKEY_ALERT): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported(_('This type of script is not supported with {device}.').format(device=self.device)) devmgr = self.device_manager() client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) client.checkDevice() - xpub = client.get_xpub(derivation, xtype) + xpub = client.get_xpub(derivation, xtype, pubkey_type) return xpub def get_client(self, keystore, force_pair=True): diff --git a/electrum/three_keys/pubkey_type.py b/electrum/three_keys/pubkey_type.py new file mode 100644 index 000000000000..1863d5faed90 --- /dev/null +++ b/electrum/three_keys/pubkey_type.py @@ -0,0 +1,6 @@ +from enum import IntEnum + +class PubkeyType(IntEnum): + PUBKEY_ALERT = 0 + PUBKEY_INSTANT = 1 + PUBKEY_RECOVERY = 2 \ No newline at end of file From d6e237bc55c6d0c0b5bdb241d6378c2f37d58e81 Mon Sep 17 00:00:00 2001 From: harzo Date: Mon, 4 Jan 2021 11:50:44 +0100 Subject: [PATCH 07/28] Use p2sh for ledger --- electrum/base_wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 9e5c721ce390..423b2dd8f243 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -661,7 +661,7 @@ def on_keystore(self, k, name=None, device_info=None): self.keystores.append(k) self.run('create_wallet') elif self.wallet_type in ['2-key-hw', '3-key-hw']: - if has_xpub and t1 != 'p2wsh-p2sh': + if has_xpub and t1 not in ['p2sh', 'p2wsh-p2sh']: self.show_error(_('Wrong key type') + ' %s' % t1) self.run('choose_keystore') return @@ -673,7 +673,7 @@ def on_keystore(self, k, name=None, device_info=None): self.show_error(_('Missing device info')) self.run('choose_keystore') return - script_type = 'p2wsh-p2sh' + script_type = 'p2sh' derivation = purpose48_derivation(0, xtype=script_type) if keystores_needed == 3: pubkey_type = len(self.keystores) From 9e754b207ab9ab9a9bc714d3bfbad2fc74cfcfb3 Mon Sep 17 00:00:00 2001 From: harzo Date: Mon, 4 Jan 2021 12:04:25 +0100 Subject: [PATCH 08/28] Revert "Use p2sh for ledger" This reverts commit d6e237bc55c6d0c0b5bdb241d6378c2f37d58e81. --- electrum/base_wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 423b2dd8f243..9e5c721ce390 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -661,7 +661,7 @@ def on_keystore(self, k, name=None, device_info=None): self.keystores.append(k) self.run('create_wallet') elif self.wallet_type in ['2-key-hw', '3-key-hw']: - if has_xpub and t1 not in ['p2sh', 'p2wsh-p2sh']: + if has_xpub and t1 != 'p2wsh-p2sh': self.show_error(_('Wrong key type') + ' %s' % t1) self.run('choose_keystore') return @@ -673,7 +673,7 @@ def on_keystore(self, k, name=None, device_info=None): self.show_error(_('Missing device info')) self.run('choose_keystore') return - script_type = 'p2sh' + script_type = 'p2wsh-p2sh' derivation = purpose48_derivation(0, xtype=script_type) if keystores_needed == 3: pubkey_type = len(self.keystores) From 2bd8eebe87b97a51483d5ff709c0941c81efbcec Mon Sep 17 00:00:00 2001 From: harzo Date: Mon, 4 Jan 2021 17:02:06 +0100 Subject: [PATCH 09/28] Sign alert tx with ledger --- electrum/gui/qt/__init__.py | 4 +- electrum/gui/qt/three_keys_windows.py | 57 ++++++++++++++++++++- electrum/plugins/ledger/ledger.py | 18 ++++++- electrum/three_keys/script.py | 11 ++++ electrum/wallet.py | 74 ++++++++++++++++++--------- 5 files changed, 136 insertions(+), 28 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index f54c84909cc1..63f907ee3b52 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -31,7 +31,7 @@ from typing import Optional, TYPE_CHECKING from .terms_and_conditions_mixin import TermsNotAccepted -from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow +from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow, ElectrumARHWWindow try: import PyQt5 @@ -212,7 +212,7 @@ def _create_window_for_wallet(self, wallet): elif wallet_type == '3-key': w = ElectrumAIRWindow(self, wallet) elif wallet_type == '2-key-hw': - w = ElectrumARWindow(self, wallet) + w = ElectrumARHWWindow(self, wallet) elif wallet_type == '3-key-hw': w = ElectrumAIRWindow(self, wallet) else: diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 09ecbae5bd01..b6a92d628c6d 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -46,8 +46,9 @@ def show_recovery_tab(self): def update_tabs(self, wallet=None): super().update_tabs(wallet=wallet) - self.recovery_tab.update_view() - self.recovery_tab.update_recovery_button() + if hasattr(self, 'recovery_tab'): + self.recovery_tab.update_view() + self.recovery_tab.update_recovery_button() class ElectrumARWindow(ElectrumMultikeyWalletWindow): @@ -538,3 +539,55 @@ def preview_tx_dialog(self, make_tx, outputs, external_keypairs=None, invoice=No else PreviewTxDialog d = dialog_class(make_tx, outputs, external_keypairs, window=self, invoice=invoice) d.show() + + +class ElectrumARHWWindow(ElectrumARWindow): + + def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): + super().__init__(gui_object=gui_object, wallet=wallet) + + def do_pay(self): + invoice = self.read_invoice() + if not invoice: + return + invoice['txtype'] = TxType.ALERT_PENDING.name + self.wallet.save_invoice(invoice) + self.invoice_list.update() + self.do_clear() + self.do_pay_invoice(invoice) + + def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None): + # trustedcoin requires this + if run_hook('abort_send', self): + return + is_sweep = False # Was bool(external_keypairs). Should be good to keep it false, since we do not use trustedcoin + make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( + coins=inputs, + outputs=outputs, + fee=fee_est, + is_sweep=is_sweep) + if self.config.get('advanced_preview'): + self.preview_tx_dialog(make_tx, outputs, external_keypairs=external_keypairs, invoice=invoice) + return + + output_values = [x.value for x in outputs] + output_value = '!' if '!' in output_values else sum(output_values) + d = ConfirmTxDialog(self, make_tx, output_value, is_sweep) + d.update_tx() + if d.not_enough_funds: + self.show_message(_('Not Enough Funds')) + return + cancelled, is_send, password, tx = d.run() + if cancelled: + return + + if invoice['txtype'] == TxType.ALERT_PENDING.name: + self.wallet.set_alert() + + if is_send: + def sign_done(success): + if success: + self.broadcast_or_show(tx, invoice=invoice) + self.sign_tx_with_password(tx, sign_done, password, external_keypairs) + else: + self.preview_tx_dialog(make_tx, outputs, external_keypairs=external_keypairs, invoice=invoice) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 96ca22ba753d..a60eef43ad0f 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1,3 +1,4 @@ +import enum from struct import pack, unpack import hashlib import sys @@ -5,7 +6,7 @@ from electrum import ecc from electrum import bip32 -from electrum.crypto import hash_160 +from electrum.crypto import hash_160, sha256 from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath from electrum.i18n import _ @@ -63,6 +64,12 @@ def catch_exception(self, *args, **kwargs): return catch_exception +class LedgerBtcvTxType(enum.IntEnum): + ALERT = 0x00 + INSTANT = 0x01 + RECOVERY = 0x02 + + class Ledger_Client(HardwareClientBase): def __init__(self, hidDevice): self.dongleObject = btchip(hidDevice) @@ -316,6 +323,15 @@ def sign_message(self, sequence, message, password): # And convert it return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s + @test_pin_unlocked + def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None): + client = self.get_client() + passwordHash = sha256(bytearray(password.encode('utf-8')).hex().ljust(64, '0')) if password else bytearray(32) + try: + client.setBTCVPasswordUse(passwordHash, tx_type) + except Exception as e: + self.logger.exception(e) + @test_pin_unlocked @set_and_unset_signing def sign_transaction(self, tx, password): diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index a16e210fbdff..893c90370d4f 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -142,3 +142,14 @@ def is_instant_mode(self): def is_alert_mode(self): return self.witness_flags == [1] + + +class TwoKeysHWScriptGenerator(TwoKeysScriptGenerator): + def __init__(self): + self._recovery_alert_flag = None + self.witness_flags = [] + + def get_redeem_script(self, public_keys: List[str]) -> str: + if not isinstance(public_keys, list) or len(public_keys) != 2: + raise ThreeKeysError(f"Wrong input type! Expected 2 elements list not '{public_keys}'") + return self.create_redeem_script(public_keys[0], public_keys[1]) diff --git a/electrum/wallet.py b/electrum/wallet.py index 09bc80e80d3e..7fc69c5a5580 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -59,7 +59,8 @@ from .plugin import run_hook from .simple_config import SimpleConfig from .storage import StorageEncryptionVersion, WalletStorage -from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator, ThreeKeysError +from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator, ThreeKeysError, \ + TwoKeysHWScriptGenerator from .three_keys.transaction import TxType, ThreeKeysTransaction from .three_keys.utils import filter_spendable_coins, update_tx_status from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, @@ -2577,11 +2578,12 @@ def get_wallet_label(self): class MultikeyHWWallet(Multisig_Wallet): - def __init__(self, storage: WalletStorage, *, config: SimpleConfig): + def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenerator: MultiKeyScriptGenerator): self.wallet_type = storage.get('wallet_type') self.multikey_type = storage.get('multikey_type') - # self.postponed_synchronization = True + self.multisig_script_generator = scriptGenerator self.n = 2 + self.m = 1 Deterministic_Wallet.__init__(self, storage, config=config) # super has to be at the end otherwise wallet breaks self.multiple_change = storage.get('multiple_change', True) @@ -2608,6 +2610,14 @@ def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], self.update_transaction_multisig_generator(tx) return tx + def update_transaction_multisig_generator(self, tx: Transaction): + tx.multisig_script_generator = self.multisig_script_generator + tx.update_inputs() + + def update_tx_input_multisig_generator(self, inputs: Sequence[PartialTxInput]): + for txin in inputs: + txin.multisig_script_generator = self.multisig_script_generator + def get_atxs_to_recovery(self): txi_list = self.db.list_txi() recovery_mempool_transactions = { @@ -2655,7 +2665,22 @@ def prepare_inputs_for_recovery(self, inputs: list): input.block_height = fetched_data['height'] return updated_inputs - def sign_transaction(self, tx: PartialTransaction, password, external_keypairs=None, update_pubkeys_fn=None, skip_finalize=False) -> Optional[PartialTransaction]: + def set_alert(self): + from .plugins.ledger.ledger import LedgerBtcvTxType + self.multisig_script_generator.set_alert() + self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.ALERT) + + def set_recovery(self): + from .plugins.ledger.ledger import LedgerBtcvTxType + self.multisig_script_generator.set_recovery() + self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.RECOVERY) + + def set_instant(self): + from .plugins.ledger.ledger import LedgerBtcvTxType + self.multisig_script_generator.set_instant() + self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.INSTANT) + + def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: if self.is_watching_only(): return if not isinstance(tx, PartialTransaction): @@ -2663,37 +2688,40 @@ def sign_transaction(self, tx: PartialTransaction, password, external_keypairs=N # add info to a temporary tx copy; including xpubs # and full derivation paths as hw keystores might want them tmp_tx = copy.deepcopy(tx) - # update tmp tx - self.update_transaction_multisig_generator(tmp_tx) tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True) - if update_pubkeys_fn: - update_pubkeys_fn(tx) - update_pubkeys_fn(tmp_tx) # sign. start with ready keystores. - for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): - try: - if k.can_sign(tmp_tx): - k.sign_transaction(tmp_tx, password) - except UserCancelled: - continue + keystore = self._get_hw_keystore() - if external_keypairs: - tmp_tx.sign(external_keypairs) + if keystore.can_sign(tmp_tx): + keystore.sign_transaction(tmp_tx, password) + else: + # todo: ledger keystore needs to be ready for signing + pass # remove sensitive info; then copy back details from temporary tx tmp_tx.remove_xpubs_and_bip32_paths() - # update tx - self.update_transaction_multisig_generator(tx) - tx.combine_with_other_psbt(tmp_tx, skip_finalize) + tx.combine_with_other_psbt(tmp_tx) tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False) + return tx - if update_pubkeys_fn: - update_pubkeys_fn(tx) + def get_coin_chooser(self): + if self.multisig_script_generator.is_alert_mode(): + return coinchooser.get_coin_chooser_alert(self.config) + else: + return coinchooser.get_coin_chooser(self.config) - return tx + def _get_hw_keystore(self): + for k in self.get_keystores(): + if isinstance(k, Hardware_KeyStore): + return k + assert False class TwoKeysHWWallet(MultikeyHWWallet): + def __init__(self, storage: WalletStorage, *, config: SimpleConfig): + script_generator = TwoKeysHWScriptGenerator() + super().__init__(storage, config=config, scriptGenerator=script_generator) + def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: if not isinstance(pubkeys, list) or len(pubkeys) != 2: raise ThreeKeysError(f"Wrong input type! Expected list not '{pubkeys}'") From 3462d03c6dac07bb5df8719e4a1ad4b299311c3a Mon Sep 17 00:00:00 2001 From: harzo Date: Mon, 4 Jan 2021 17:26:33 +0100 Subject: [PATCH 10/28] Change default derivation path for ledger --- electrum/base_wizard.py | 4 ++-- electrum/gui/qt/three_keys_windows.py | 5 +++++ electrum/wallet.py | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 9e5c721ce390..d691419650c2 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -522,8 +522,8 @@ def f(derivation, script_type): if 'instant_password' in self.data: self.plugin.set_instant_password(device_info.device.id_, self.data['instant_password'], self) + derivation = bip44_derivation(0) script_type = 'p2wsh-p2sh' - derivation = normalize_bip32_derivation(purpose48_derivation(0, xtype=script_type)) self.run('on_hw_derivation', name, device_info, derivation, script_type) else: @@ -674,7 +674,7 @@ def on_keystore(self, k, name=None, device_info=None): self.run('choose_keystore') return script_type = 'p2wsh-p2sh' - derivation = purpose48_derivation(0, xtype=script_type) + derivation = bip44_derivation(0) if keystores_needed == 3: pubkey_type = len(self.keystores) elif keystores_needed == 2: diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index b6a92d628c6d..0fe7e9e6789d 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -583,10 +583,15 @@ def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=No if invoice['txtype'] == TxType.ALERT_PENDING.name: self.wallet.set_alert() + elif invoice['txtype'] == TxType.INSTANT.name: + self.wallet.set_recovery() + elif invoice['txtype'] == TxType.RECOVERY.name: + self.wallet.set_instant() if is_send: def sign_done(success): if success: + self.wallet.multisig_script_generator.set_alert() self.broadcast_or_show(tx, invoice=invoice) self.sign_tx_with_password(tx, sign_done, password, external_keypairs) else: diff --git a/electrum/wallet.py b/electrum/wallet.py index 7fc69c5a5580..7cee4999976b 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2668,17 +2668,17 @@ def prepare_inputs_for_recovery(self, inputs: list): def set_alert(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_alert() - self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.ALERT) + self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) def set_recovery(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_recovery() - self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.RECOVERY) + self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY) def set_instant(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_instant() - self._get_hw_keystore().set_btcv_password_use(LedgerBtcvTxType.INSTANT) + self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT) def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: if self.is_watching_only(): From 9f5ae4501c6578c8fa9f467e5309b276ee584a48 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Tue, 5 Jan 2021 00:04:02 +0100 Subject: [PATCH 11/28] Ask for password confirmation in ledger btcv wallet creation --- electrum/gui/qt/three_keys_dialogs.py | 38 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py index 976ec4e20124..81884135fd43 100644 --- a/electrum/gui/qt/three_keys_dialogs.py +++ b/electrum/gui/qt/three_keys_dialogs.py @@ -142,25 +142,35 @@ class InsertHWPasswordDialog(QVBoxLayout): def __init__(self, parent, message_label): super().__init__() self.parent = parent - label = message_label - edit = QLineEdit() - edit.setEchoMode(QLineEdit.Password) - error_label = ErrorLabel() - - edit.textChanged.connect(self._on_change) - self.addWidget(label) - self.addWidget(edit) - self.addWidget(error_label) - self.edit = edit + label1 = message_label + self.edit1 = QLineEdit() + self.edit1.setEchoMode(QLineEdit.Password) + label2 = QLabel("Repeat password: ") + self.edit2 = QLineEdit() + self.edit2.setEchoMode(QLineEdit.Password) + self.error_label = ErrorLabel() + + self.edit1.textChanged.connect(self._on_change) + self.edit2.textChanged.connect(self._on_change) + self.addWidget(label1) + self.addWidget(self.edit1) + self.addWidget(label2) + self.addWidget(self.edit2) + self.addWidget(self.error_label) def _on_change(self): - self.parent.next_button.setEnabled(self._get_str().strip() != '') + if(self._get_str(self.edit1) != self._get_str(self.edit2)): + self.error_label.setText("Password confirmation does not match") + self.parent.next_button.setEnabled(False) + else: + self.error_label.setText("") + self.parent.next_button.setEnabled(self._get_str(self.edit1).strip() != '') - def _get_str(self) -> str: - return self.edit.text().replace('\n', '') + def _get_str(self, line) -> str: + return line.text().replace('\n', '') def get_password(self): - return self._get_str() + return self._get_str(self.edit1) class Qr2FaDialog(QVBoxLayout): From 1fa81440c6e8ece72d9275564ecaac2ebc7d7660 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Tue, 5 Jan 2021 11:47:03 +0100 Subject: [PATCH 12/28] Ledger 3-keys HW wallet creation --- electrum/base_wizard.py | 8 ++++++++ electrum/gui/qt/installwizard.py | 26 ++++++++++++++++++++++++++ electrum/plugins/ledger/ledger.py | 11 +++++++++++ electrum/three_keys/script.py | 10 ++++++++++ electrum/wallet.py | 6 +++++- 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index d691419650c2..11840d76e170 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -296,6 +296,9 @@ def collect_instant_pubkey(instant_pubkey: str): self.get_authenticator_pubkey(run_next=collect_instant_pubkey) + def three_keys_hw_create(self): + self.get_hw_passwords(run_next=self.on_three_keys_hw_create, title=_('Instant and cancel passwords')) + def on_three_keys_create(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey self.run('choose_keystore') @@ -304,6 +307,11 @@ def on_three_keys_import(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey self.run('restore_from_seed') + def on_three_keys_hw_create(self, *passwords): + self.data['instant_password'] = passwords[0] + self.data['recovery_password'] = passwords[1] + self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET) + def choose_keystore(self): assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key', '2-key-hw', '3-key-hw'] i = len(self.keystores) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index db148812035a..98bb5b1255a1 100755 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -664,6 +664,32 @@ def get_hw_password(self, run_next, title): self.exec_layout(layout, title, next_enabled=False) return layout.get_password() + @wizard_dialog + def get_hw_passwords(self, run_next, title): + label1 = QLabel() + message1 = _('Please provide instant password related to your Ledger device') + label1.setText(message1) + label1.setOpenExternalLinks(True) + label1.setTextInteractionFlags(Qt.TextBrowserInteraction) + label1.setWordWrap(True) + + layout1 = InsertHWPasswordDialog(self, message_label=label1) + self.exec_layout(layout1, title, next_enabled=False) + instant_password = layout1.get_password() + + label2 = QLabel() + message2 = _('Please provide cancel password related to your Ledger device') + label2.setText(message2) + label2.setOpenExternalLinks(True) + label2.setTextInteractionFlags(Qt.TextBrowserInteraction) + label2.setWordWrap(True) + + layout2 = InsertHWPasswordDialog(self, message_label=label2) + self.exec_layout(layout2, title, next_enabled=False) + recovery_password = layout2.get_password() + + return (instant_password, recovery_password) + @wizard_dialog def display_2fa_pairing_qr(self, run_next, entropy: bytes): title_label = QLabel() diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index a60eef43ad0f..9e25056bb873 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -661,6 +661,17 @@ def set_recovery_password(self, device_id, password, wizard): except: return False + def set_instant_password(self, device_id, password, wizard): + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = self.create_handler(wizard) + client.checkDevice() + try: + client.set_instant_password(password) + return True + except: + return False + def get_xpub(self, device_id, derivation, xtype, wizard, pubkey_type=PubkeyType.PUBKEY_ALERT): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported(_('This type of script is not supported with {device}.').format(device=self.device)) diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index 893c90370d4f..11397db22adc 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -153,3 +153,13 @@ def get_redeem_script(self, public_keys: List[str]) -> str: if not isinstance(public_keys, list) or len(public_keys) != 2: raise ThreeKeysError(f"Wrong input type! Expected 2 elements list not '{public_keys}'") return self.create_redeem_script(public_keys[0], public_keys[1]) + +class ThreeKeysHWScriptGenerator(ThreeKeysScriptGenerator): + def __init__(self): + self._instant_recovery_alert_flag = None + self.witness_flags = [] + + def get_redeem_script(self, public_keys: List[str]) -> str: + if not isinstance(public_keys, list) or len(public_keys) != 3: + raise ThreeKeysError(f"Wrong input type! Expected 3 elements list not '{public_keys}'") + return self.create_redeem_script(public_keys[0], public_keys[1], public_keys[1]) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7cee4999976b..12150f5e4d49 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -60,7 +60,7 @@ from .simple_config import SimpleConfig from .storage import StorageEncryptionVersion, WalletStorage from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator, ThreeKeysError, \ - TwoKeysHWScriptGenerator + TwoKeysHWScriptGenerator, ThreeKeysHWScriptGenerator from .three_keys.transaction import TxType, ThreeKeysTransaction from .three_keys.utils import filter_spendable_coins, update_tx_status from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, @@ -2760,6 +2760,10 @@ def get_wallet_label(self): class ThreeKeysHWWallet(MultikeyHWWallet): + def __init__(self, storage: WalletStorage, *, config: SimpleConfig): + script_generator = ThreeKeysHWScriptGenerator() + super().__init__(storage, config=config, scriptGenerator=script_generator) + def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs) -> Optional[PartialTransaction]: if not isinstance(tx, PartialTransaction): return From 9330ef9927794552bfc8efc182cd3e7b682c1a87 Mon Sep 17 00:00:00 2001 From: harzo Date: Tue, 5 Jan 2021 14:04:22 +0100 Subject: [PATCH 13/28] Fix witness checksum missmatch --- electrum/gui/qt/three_keys_windows.py | 15 ++++++------ electrum/wallet.py | 34 +++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 0fe7e9e6789d..c0484caa716d 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -561,6 +561,14 @@ def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=No if run_hook('abort_send', self): return is_sweep = False # Was bool(external_keypairs). Should be good to keep it false, since we do not use trustedcoin + + if invoice['txtype'] == TxType.ALERT_PENDING.name: + self.wallet.set_alert() + elif invoice['txtype'] == TxType.INSTANT.name: + self.wallet.set_recovery() + elif invoice['txtype'] == TxType.RECOVERY.name: + self.wallet.set_instant() + make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( coins=inputs, outputs=outputs, @@ -581,13 +589,6 @@ def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=No if cancelled: return - if invoice['txtype'] == TxType.ALERT_PENDING.name: - self.wallet.set_alert() - elif invoice['txtype'] == TxType.INSTANT.name: - self.wallet.set_recovery() - elif invoice['txtype'] == TxType.RECOVERY.name: - self.wallet.set_instant() - if is_send: def sign_done(success): if success: diff --git a/electrum/wallet.py b/electrum/wallet.py index 12150f5e4d49..4249fe2331e7 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2079,14 +2079,16 @@ def get_public_keys_with_deriv_info(self, address: str): return {k.derive_pubkey(*der_suffix): (k, der_suffix) for k in self.get_keystores()} - def _add_input_sig_info(self, txin, address, *, only_der_suffix=True): - self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) + def _add_input_sig_info(self, txin, address, *, only_der_suffix=True, sort_pubkeys=True): + self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix, sort_pubkeys=sort_pubkeys) - def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True): + def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True, sort_pubkeys=True): if not self.is_mine(address): return pubkey_deriv_info = self.get_public_keys_with_deriv_info(address) - txinout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)]) + txinout.pubkeys = [bfh(pk) for pk in list(pubkey_deriv_info)] + if sort_pubkeys: + txinout.pubkeys = sorted(txinout.pubkeys) for pubkey_hex in pubkey_deriv_info: ks, der_suffix = pubkey_deriv_info[pubkey_hex] fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, @@ -2709,6 +2711,30 @@ def get_coin_chooser(self): else: return coinchooser.get_coin_chooser(self.config) + def add_input_info(self, txin: PartialTxInput, *, only_der_suffix: bool = True) -> None: + address = self.get_txin_address(txin) + if not self.is_mine(address): + is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address) + if not is_mine: + return + # set script_type first, as later checks might rely on it: + txin.script_type = self.get_txin_type(address) + self._add_input_utxo_info(txin, address) + txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1 + if txin.redeem_script is None: + try: + redeem_script_hex = self.get_redeem_script(address) + txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None + except UnknownTxinType: + pass + if txin.witness_script is None: + try: + witness_script_hex = self.get_witness_script(address) + txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None + except UnknownTxinType: + pass + self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix, sort_pubkeys=False) + def _get_hw_keystore(self): for k in self.get_keystores(): if isinstance(k, Hardware_KeyStore): From 3b1477a4532c0a12e1a491a96756f5621ff84de1 Mon Sep 17 00:00:00 2001 From: harzo Date: Thu, 7 Jan 2021 13:28:15 +0100 Subject: [PATCH 14/28] Update set_password_use method --- electrum/plugins/ledger/ledger.py | 18 +++++++++++++----- electrum/three_keys/script.py | 3 ++- electrum/wallet.py | 5 ++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 9e25056bb873..b20c1b06251c 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -6,7 +6,7 @@ from electrum import ecc from electrum import bip32 -from electrum.crypto import hash_160, sha256 +from electrum.crypto import hash_160 from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath from electrum.i18n import _ @@ -326,15 +326,19 @@ def sign_message(self, sequence, message, password): @test_pin_unlocked def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None): client = self.get_client() - passwordHash = sha256(bytearray(password.encode('utf-8')).hex().ljust(64, '0')) if password else bytearray(32) + password_hash = bytearray(32) + if password: + m = hashlib.sha256() + m.update(bytearray.fromhex(bytearray(password.encode('utf-8')).hex().ljust(64, '0'))) + password_hash = m.digest() try: - client.setBTCVPasswordUse(passwordHash, tx_type) + client.setBTCVPasswordUse(password_hash, tx_type) except Exception as e: self.logger.exception(e) @test_pin_unlocked @set_and_unset_signing - def sign_transaction(self, tx, password): + def sign_transaction(self, tx, password, pubkey_index=None): if tx.is_complete(): return inputs = [] @@ -366,7 +370,11 @@ def sign_transaction(self, tx, password): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True - my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) + if pubkey_index: + my_pubkey, = txin.bip32_paths.keys()[pubkey_index] + full_path = self.get_pubkey_derivation(my_pubkey, txin, only_der_suffix=False) + else: + my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) if not full_path: self.give_error("No matching pubkey for sign_transaction") # should never happen full_path = convert_bip32_intpath_to_strpath(full_path)[2:] diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index 11397db22adc..ecd15d0f16db 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -154,6 +154,7 @@ def get_redeem_script(self, public_keys: List[str]) -> str: raise ThreeKeysError(f"Wrong input type! Expected 2 elements list not '{public_keys}'") return self.create_redeem_script(public_keys[0], public_keys[1]) + class ThreeKeysHWScriptGenerator(ThreeKeysScriptGenerator): def __init__(self): self._instant_recovery_alert_flag = None @@ -162,4 +163,4 @@ def __init__(self): def get_redeem_script(self, public_keys: List[str]) -> str: if not isinstance(public_keys, list) or len(public_keys) != 3: raise ThreeKeysError(f"Wrong input type! Expected 3 elements list not '{public_keys}'") - return self.create_redeem_script(public_keys[0], public_keys[1], public_keys[1]) + return self.create_redeem_script(public_keys[0], public_keys[1], public_keys[2]) diff --git a/electrum/wallet.py b/electrum/wallet.py index 4249fe2331e7..da0fe0aecd6e 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2695,7 +2695,8 @@ def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransac keystore = self._get_hw_keystore() if keystore.can_sign(tmp_tx): - keystore.sign_transaction(tmp_tx, password) + pubkey_index = 0 + keystore.sign_transaction(tmp_tx, password, pubkey_index) else: # todo: ledger keystore needs to be ready for signing pass @@ -2789,6 +2790,8 @@ class ThreeKeysHWWallet(MultikeyHWWallet): def __init__(self, storage: WalletStorage, *, config: SimpleConfig): script_generator = ThreeKeysHWScriptGenerator() super().__init__(storage, config=config, scriptGenerator=script_generator) + self.n = 3 + self.m = 1 def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs) -> Optional[PartialTransaction]: if not isinstance(tx, PartialTransaction): From bece11480100d4ef52254c1fdca73283e5f0f5a0 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Mon, 11 Jan 2021 11:36:36 +0100 Subject: [PATCH 15/28] Importing 2-keys and 3-keys wallets from Ledger - asking user for password(s) --- electrum/base_wizard.py | 24 ++++++++++++++-- electrum/gui/qt/installwizard.py | 41 ++++++++++++++++++++++++++- electrum/gui/qt/three_keys_dialogs.py | 23 +++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 11840d76e170..6d23ba7745d5 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -52,6 +52,7 @@ HWD_SETUP_NEW_WALLET = 0 HWD_SETUP_DECRYPT_WALLET = 1 HWD_SETUP_NEW_BTCV_WALLET = 2 +HWD_SETUP_DECRYPT_BTCV_WALLET = 3 class ScriptTypeNotSupported(Exception): pass @@ -261,7 +262,8 @@ def two_keys_hw_create(self): self.get_hw_password(run_next=self.on_two_keys_hw_create, title=_('Cancel password')) def two_keys_hw_import(self): - self.run('restore_from_seed') + self.check_hw_password(run_next=self.on_two_keys_hw_import, title=_('Cancel password')) + def on_two_keys_create(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey @@ -275,6 +277,10 @@ def on_two_keys_hw_create(self, recovery_password: str): self.data['recovery_password'] = recovery_password self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET) + def on_two_keys_hw_import(self, recovery_password: str): + self.data['recovery_password'] = recovery_password + self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET) + def three_keys_standalone(self): def collect_instant_pubkey(instant_pubkey: str): self.data['instant_pubkey'] = instant_pubkey @@ -299,6 +305,9 @@ def collect_instant_pubkey(instant_pubkey: str): def three_keys_hw_create(self): self.get_hw_passwords(run_next=self.on_three_keys_hw_create, title=_('Instant and cancel passwords')) + def three_keys_hw_import(self): + self.check_hw_passwords(run_next=self.on_three_keys_hw_import, title=_('Instant and cancel passwords')) + def on_three_keys_create(self, recovery_pubkey: str): self.data['recovery_pubkey'] = recovery_pubkey self.run('choose_keystore') @@ -312,6 +321,11 @@ def on_three_keys_hw_create(self, *passwords): self.data['recovery_password'] = passwords[1] self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET) + def on_three_keys_hw_import(self, *passwords): + self.data['instant_password'] = passwords[0] + self.data['recovery_password'] = passwords[1] + self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET) + def choose_keystore(self): assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key', '2-key-hw', '3-key-hw'] i = len(self.keystores) @@ -533,7 +547,13 @@ def f(derivation, script_type): derivation = bip44_derivation(0) script_type = 'p2wsh-p2sh' self.run('on_hw_derivation', name, device_info, derivation, script_type) - + elif purpose == HWD_SETUP_DECRYPT_BTCV_WALLET: + if 'recovery_password' not in self.data: + raise Exception('Invalid recovery password') + if 'instant_password' in self.data: + raise Exception('MG1 3-key wallet import not implemented yet') + else: + raise Exception('MG1 2-key wallet import not implemented yet') else: raise Exception('unknown purpose: %s' % purpose) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 98bb5b1255a1..ac1e864c7c70 100755 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -21,7 +21,7 @@ from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW from .seed_dialog import SeedLayout, KeysLayout from .terms_and_conditions_mixin import TermsAndConditionsMixin, PushedButton -from .three_keys_dialogs import InsertPubKeyDialog, InsertHWPasswordDialog, Qr2FaDialog +from .three_keys_dialogs import InsertPubKeyDialog, InsertHWPasswordDialog, Qr2FaDialog, CheckHWPasswordDialog from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel, InfoButton, char_width_in_lineedit, get_default_language) @@ -690,6 +690,45 @@ def get_hw_passwords(self, run_next, title): return (instant_password, recovery_password) + @wizard_dialog + def check_hw_password(self, run_next, title): + label = QLabel() + message = _('Please provide password related to your Ledger device') + label.setText(message) + label.setOpenExternalLinks(True) + label.setTextInteractionFlags(Qt.TextBrowserInteraction) + label.setWordWrap(True) + + layout = CheckHWPasswordDialog(self, message_label=label) + self.exec_layout(layout, title, next_enabled=False) + return layout.get_password() + + @wizard_dialog + def check_hw_passwords(self, run_next, title): + label1 = QLabel() + message1 = _('Please provide instant password related to your Ledger device') + label1.setText(message1) + label1.setOpenExternalLinks(True) + label1.setTextInteractionFlags(Qt.TextBrowserInteraction) + label1.setWordWrap(True) + + layout1 = CheckHWPasswordDialog(self, message_label=label1) + self.exec_layout(layout1, title, next_enabled=False) + instant_password = layout1.get_password() + + label2 = QLabel() + message2 = _('Please provide cancel password related to your Ledger device') + label2.setText(message2) + label2.setOpenExternalLinks(True) + label2.setTextInteractionFlags(Qt.TextBrowserInteraction) + label2.setWordWrap(True) + + layout2 = CheckHWPasswordDialog(self, message_label=label2) + self.exec_layout(layout2, title, next_enabled=False) + recovery_password = layout2.get_password() + + return (instant_password, recovery_password) + @wizard_dialog def display_2fa_pairing_qr(self, run_next, entropy: bytes): title_label = QLabel() diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py index 81884135fd43..120a30d73ebe 100644 --- a/electrum/gui/qt/three_keys_dialogs.py +++ b/electrum/gui/qt/three_keys_dialogs.py @@ -173,6 +173,29 @@ def get_password(self): return self._get_str(self.edit1) +class CheckHWPasswordDialog(QVBoxLayout): + def __init__(self, parent, message_label): + super().__init__() + # self.parent.next_button.setEnabled(False) + self.parent = parent + label = message_label + self.edit = QLineEdit() + self.edit.setEchoMode(QLineEdit.Password) + + self.edit.textChanged.connect(self._on_change) + self.addWidget(label) + self.addWidget(self.edit) + + def _on_change(self): + self.parent.next_button.setEnabled(self._get_str(self.edit).strip() != '') + + def _get_str(self, line) -> str: + return line.text().replace('\n', '') + + def get_password(self): + return self._get_str(self.edit) + + class Qr2FaDialog(QVBoxLayout): def __init__(self, parent, title_label: str, pin_label: str, qr_data: dict): From bb809f359e55f26b2a9ce1abf0e06b1266e9c4a8 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Fri, 29 Jan 2021 12:51:06 +0100 Subject: [PATCH 16/28] Check recovery password saved on stick on 2-keys wallet import --- electrum/base_wizard.py | 22 ++++++++++++++++++++-- electrum/plugins/ledger/ledger.py | 9 ++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 6d23ba7745d5..a9e0abadfaca 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -28,6 +28,8 @@ import sys from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional +from btchip.btchipException import BTChipException + from . import bitcoin from . import keystore from . import mnemonic @@ -553,7 +555,9 @@ def f(derivation, script_type): if 'instant_password' in self.data: raise Exception('MG1 3-key wallet import not implemented yet') else: - raise Exception('MG1 2-key wallet import not implemented yet') + derivation = bip44_derivation(0) + script_type = 'p2wsh-p2sh' + self.run('on_hw_derivation', name, device_info, derivation, script_type, btcv_recovery_password_check=self.data['recovery_password']) else: raise Exception('unknown purpose: %s' % purpose) @@ -591,7 +595,8 @@ def derivation_and_script_type_dialog(self, f): self.show_error(e) # let the user choose again - def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT): + def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT, + btcv_recovery_password_check = None): from .keystore import hardware_keystore try: xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self, pubkey_type) @@ -615,6 +620,19 @@ def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=F 'label': device_info.label, } k = hardware_keystore(d) + from electrum.plugins.ledger.ledger import Ledger_KeyStore, LedgerBtcvTxType + if(btcv_recovery_password_check and isinstance(k, Ledger_KeyStore)): + try: + k.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=btcv_recovery_password_check, wizard=self) + k.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self) + except BTChipException as e: + title = _('Hardware wallet import fail') + if e.sw == 0x6a80: # cancelled by user + msg = title + "\n" + str(e) + "\n" + ('Password invalid') + else: + msg = title + "\n" + str(e) + self.show_error(msg) + self.terminate() self.on_keystore(k, name, device_info) def passphrase_dialog(self, run_next, is_restoring=False): diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index b20c1b06251c..ad6ae3a55381 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -324,17 +324,16 @@ def sign_message(self, sequence, message, password): return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s @test_pin_unlocked - def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None): + def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None, wizard=None): + if self.handler == None: + self.handler = self.plugin.create_handler(wizard) client = self.get_client() password_hash = bytearray(32) if password: m = hashlib.sha256() m.update(bytearray.fromhex(bytearray(password.encode('utf-8')).hex().ljust(64, '0'))) password_hash = m.digest() - try: - client.setBTCVPasswordUse(password_hash, tx_type) - except Exception as e: - self.logger.exception(e) + client.setBTCVPasswordUse(password_hash, tx_type) @test_pin_unlocked @set_and_unset_signing From 95992491dd1ad7ea2556cdd495f14334eaeb6b0b Mon Sep 17 00:00:00 2001 From: mgrychow Date: Fri, 29 Jan 2021 14:10:00 +0100 Subject: [PATCH 17/28] Check recovery and instant passwords saved on stick on 3-keys wallet import --- electrum/base_wizard.py | 25 ++++++++++--------------- electrum/plugins/ledger/ledger.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index a9e0abadfaca..bde2309cdbf7 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -553,7 +553,11 @@ def f(derivation, script_type): if 'recovery_password' not in self.data: raise Exception('Invalid recovery password') if 'instant_password' in self.data: - raise Exception('MG1 3-key wallet import not implemented yet') + derivation = bip44_derivation(0) + script_type = 'p2wsh-p2sh' + self.run('on_hw_derivation', name, device_info, derivation, script_type, + btcv_instant_password_check=self.data['instant_password'], + btcv_recovery_password_check=self.data['recovery_password']) else: derivation = bip44_derivation(0) script_type = 'p2wsh-p2sh' @@ -596,7 +600,7 @@ def derivation_and_script_type_dialog(self, f): # let the user choose again def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT, - btcv_recovery_password_check = None): + btcv_instant_password_check=None, btcv_recovery_password_check=None): from .keystore import hardware_keystore try: xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self, pubkey_type) @@ -620,19 +624,10 @@ def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=F 'label': device_info.label, } k = hardware_keystore(d) - from electrum.plugins.ledger.ledger import Ledger_KeyStore, LedgerBtcvTxType - if(btcv_recovery_password_check and isinstance(k, Ledger_KeyStore)): - try: - k.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=btcv_recovery_password_check, wizard=self) - k.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self) - except BTChipException as e: - title = _('Hardware wallet import fail') - if e.sw == 0x6a80: # cancelled by user - msg = title + "\n" + str(e) + "\n" + ('Password invalid') - else: - msg = title + "\n" + str(e) - self.show_error(msg) - self.terminate() + from electrum.plugins.ledger.ledger import Ledger_KeyStore + if isinstance(k, Ledger_KeyStore) \ + and not k.are_3keys_ledger_passwords_correct(self, btcv_instant_password_check, btcv_recovery_password_check): + return self.terminate() self.on_keystore(k, name, device_info) def passphrase_dialog(self, run_next, is_restoring=False): diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index ad6ae3a55381..a1e763c88298 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -335,6 +335,21 @@ def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None, w password_hash = m.digest() client.setBTCVPasswordUse(password_hash, tx_type) + def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check=None, btcv_recovery_password_check=None): + try: + if btcv_recovery_password_check: + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=btcv_recovery_password_check, wizard=wizard) + if btcv_instant_password_check: + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT, password=btcv_instant_password_check, wizard=wizard) + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self) + except BTChipException as e: + msg = _('Hardware wallet import fail') + "\n" + str(e) + if e.sw == 0x6a80: + msg = msg + "\n" + ('Invalid password(s)') + wizard.show_error(msg) + return False + return True + @test_pin_unlocked @set_and_unset_signing def sign_transaction(self, tx, password, pubkey_index=None): From 915979e050d64dab612f5540a7e9f0e1ba8eaebd Mon Sep 17 00:00:00 2001 From: mgrychow Date: Mon, 1 Feb 2021 17:20:39 +0100 Subject: [PATCH 18/28] Btcv wallet import cornercases: setting and checking wallet type info on stick + password len constraint in gui --- electrum/base_wizard.py | 6 +++++- electrum/gui/qt/three_keys_dialogs.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index bde2309cdbf7..9f04205d3a2a 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -545,6 +545,8 @@ def f(derivation, script_type): self.plugin.set_recovery_password(device_info.device.id_, self.data['recovery_password'], self) if 'instant_password' in self.data: self.plugin.set_instant_password(device_info.device.id_, self.data['instant_password'], self) + else: + self.plugin.set_instant_password(device_info.device.id_, str(0x00), self) derivation = bip44_derivation(0) script_type = 'p2wsh-p2sh' @@ -561,7 +563,9 @@ def f(derivation, script_type): else: derivation = bip44_derivation(0) script_type = 'p2wsh-p2sh' - self.run('on_hw_derivation', name, device_info, derivation, script_type, btcv_recovery_password_check=self.data['recovery_password']) + self.run('on_hw_derivation', name, device_info, derivation, script_type, + btcv_recovery_password_check=self.data['recovery_password'], + btcv_instant_password_check=str(0x00)) else: raise Exception('unknown purpose: %s' % purpose) diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py index 120a30d73ebe..82e85e6b1601 100644 --- a/electrum/gui/qt/three_keys_dialogs.py +++ b/electrum/gui/qt/three_keys_dialogs.py @@ -23,6 +23,7 @@ class ValidationState(IntEnum): INTERMEDIATE = 2 CROPPED = 3 +MAX_3KEYS_PASSWD_LEN = 32 class PubKeyValidator: COMPRESSED_PREFIXES = ('02', '03') @@ -145,9 +146,11 @@ def __init__(self, parent, message_label): label1 = message_label self.edit1 = QLineEdit() self.edit1.setEchoMode(QLineEdit.Password) + self.edit1.setMaxLength(MAX_3KEYS_PASSWD_LEN) label2 = QLabel("Repeat password: ") self.edit2 = QLineEdit() self.edit2.setEchoMode(QLineEdit.Password) + self.edit2.setMaxLength(MAX_3KEYS_PASSWD_LEN) self.error_label = ErrorLabel() self.edit1.textChanged.connect(self._on_change) @@ -181,6 +184,7 @@ def __init__(self, parent, message_label): label = message_label self.edit = QLineEdit() self.edit.setEchoMode(QLineEdit.Password) + self.edit.setMaxLength(MAX_3KEYS_PASSWD_LEN) self.edit.textChanged.connect(self._on_change) self.addWidget(label) From d5482b124c44c2f197d00ca6904655e757973bb1 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Sun, 7 Feb 2021 02:09:41 +0100 Subject: [PATCH 19/28] signing 3-keys alert transaction on ledger --- electrum/gui/qt/__init__.py | 4 ++-- electrum/gui/qt/three_keys_windows.py | 11 +++++++++++ electrum/wallet.py | 11 ++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 63f907ee3b52..936d493d93f1 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -31,7 +31,7 @@ from typing import Optional, TYPE_CHECKING from .terms_and_conditions_mixin import TermsNotAccepted -from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow, ElectrumARHWWindow +from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow, ElectrumARHWWindow, ElectrumAIRHWWindow try: import PyQt5 @@ -214,7 +214,7 @@ def _create_window_for_wallet(self, wallet): elif wallet_type == '2-key-hw': w = ElectrumARHWWindow(self, wallet) elif wallet_type == '3-key-hw': - w = ElectrumAIRWindow(self, wallet) + w = ElectrumARHWWindow(self, wallet) else: w = ElectrumWindow(self, wallet) self.windows.append(w) diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index c0484caa716d..01e28bcf5650 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -597,3 +597,14 @@ def sign_done(success): self.sign_tx_with_password(tx, sign_done, password, external_keypairs) else: self.preview_tx_dialog(make_tx, outputs, external_keypairs=external_keypairs, invoice=invoice) + + +class ElectrumAIRHWWindow(ElectrumAIRWindow): + def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): + super().__init__(gui_object=gui_object, wallet=wallet) + + def do_pay(self): + self.show_message(_('do_pay')) + + def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None): + self.show_message(_('pay_onchain_dialog')) diff --git a/electrum/wallet.py b/electrum/wallet.py index da0fe0aecd6e..79320fa7c3ce 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2584,7 +2584,10 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenera self.wallet_type = storage.get('wallet_type') self.multikey_type = storage.get('multikey_type') self.multisig_script_generator = scriptGenerator - self.n = 2 + if isinstance(self.multisig_script_generator, ThreeKeysHWScriptGenerator): + self.n = 3 + else: + self.n = 2 self.m = 1 Deterministic_Wallet.__init__(self, storage, config=config) # super has to be at the end otherwise wallet breaks @@ -2793,6 +2796,12 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig): self.n = 3 self.m = 1 + def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: + if not isinstance(pubkeys, list) or len(pubkeys) != 3: + raise ThreeKeysError(f"Wrong input type! Expected list not '{pubkeys}'") + + return ThreeKeysScriptGenerator.create_redeem_script(pubkeys[0], pubkeys[1], pubkeys[2]) + def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs) -> Optional[PartialTransaction]: if not isinstance(tx, PartialTransaction): return From 158c9a0e6fddee865a313540e74c44a692a17068 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Tue, 23 Feb 2021 18:51:05 +0100 Subject: [PATCH 20/28] Ledger signing 2-keys recovery transaction with hardcoded recovery password --- electrum/gui/qt/recovery_list.py | 10 +++-- electrum/keystore.py | 3 ++ electrum/plugins/ledger/ledger.py | 33 +++++++++++++++- electrum/three_keys/script.py | 3 +- electrum/transaction.py | 3 ++ electrum/wallet.py | 62 ++++++++++++++++++------------- 6 files changed, 82 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py index e6d588328903..d44903ff0ec0 100755 --- a/electrum/gui/qt/recovery_list.py +++ b/electrum/gui/qt/recovery_list.py @@ -183,6 +183,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): self.config = config self.wallet = wallet self.is_2fa = self.wallet.storage.get('multikey_type', '') == '2fa' + self.is_hw = self.wallet.storage.get('multikey_type', '') == 'hw' QWidget.__init__(self) self.invoice_list = RecoveryView(self.electrum_main_window, self) @@ -352,7 +353,7 @@ def recover_action(self): atxs = self.invoice_list.selected() address = self.recovery_address_line.currentText() recovery_keypair = None - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): recovery_keypair = self._get_recovery_keypair() if not is_address_valid(address): @@ -371,7 +372,8 @@ def recover_action(self): recovery_keypairs=recovery_keypair, ) - if not self.is_2fa: + # if not (self.is_2fa or self.is_hw): + if not (self.is_2fa): self.recovery_privkey_line.setText('') def _create_privkey_line(self, on_edit): @@ -416,7 +418,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): grid_layout.addWidget(self.recovery_address_line, 0, 1) # Row 2 - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 1, 0) # complete line edit with suggestions self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit) @@ -434,7 +436,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): self.setLayout(self.main_layout) def update_recovery_button(self): - if self.is_2fa: + if self.is_2fa or self.is_hw: enabled = self.is_address_valid and len(self.invoice_list.selected()) else: enabled = self.is_address_valid \ diff --git a/electrum/keystore.py b/electrum/keystore.py index 859e74989f8d..472cc36e23af 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -745,6 +745,9 @@ def opportunistically_fill_in_missing_info_from_device(self, client: 'HardwareCl self.label = client.label() self.is_requesting_to_be_rewritten_to_wallet_file = True + def set_btcv_password_use(self, tx_type, password=None): + raise NotImplementedError + def bip39_normalize_passphrase(passphrase): return normalize('NFKD', passphrase or '') diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index a1e763c88298..830d1e522bda 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -352,7 +352,7 @@ def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check @test_pin_unlocked @set_and_unset_signing - def sign_transaction(self, tx, password, pubkey_index=None): + def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False): if tx.is_complete(): return inputs = [] @@ -499,6 +499,37 @@ def sign_transaction(self, tx, password, pubkey_index=None): signing_pubkey=my_pubkey.hex(), sig=inputSignature.hex()) inputIndex = inputIndex + 1 + + inputIndex = 0 + if is_recovery: + # self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password="aaa") + self.get_client().startUntrustedTransaction(True, inputIndex, + chipInputs, redeemScripts[inputIndex], version=tx.version) + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + outputData['outputData'] = txOutput + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + self.handler.show_message(_("Confirmed. Signing Transaction...")) + while inputIndex < len(inputs): + singleInput = [ chipInputs[inputIndex] ] + self.get_client().startUntrustedTransaction(False, 0, + singleInput, redeemScripts[inputIndex], version=tx.version) + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password="aaa") + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + + my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[1] + + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) + inputIndex = inputIndex + 1 else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index ecd15d0f16db..402d70b01264 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -145,8 +145,9 @@ def is_alert_mode(self): class TwoKeysHWScriptGenerator(TwoKeysScriptGenerator): - def __init__(self): + def __init__(self, recovery_pubkey: str): self._recovery_alert_flag = None + self.recovery_pubkey = recovery_pubkey self.witness_flags = [] def get_redeem_script(self, public_keys: List[str]) -> str: diff --git a/electrum/transaction.py b/electrum/transaction.py index 3a0caeb6ae2a..3616ca9b04dc 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -521,6 +521,9 @@ def update_inputs(self): for input in self._inputs: input.multisig_script_generator = self.multisig_script_generator + def update_input_multisig_generator(self, input, multisig_script_generator): + input.multisig_script_generator = multisig_script_generator + def to_json(self) -> dict: d = { 'version': self.version, diff --git a/electrum/wallet.py b/electrum/wallet.py index 79320fa7c3ce..b855449b6ad9 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2678,34 +2678,45 @@ def set_alert(self): def set_recovery(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_recovery() - self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY) def set_instant(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_instant() - self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT) - def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: + def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None) -> Optional[PartialTransaction]: if self.is_watching_only(): return if not isinstance(tx, PartialTransaction): return - # add info to a temporary tx copy; including xpubs - # and full derivation paths as hw keystores might want them tmp_tx = copy.deepcopy(tx) + for input in tmp_tx.inputs(): + tmp_pubkeys = self.get_public_keys_with_deriv_info(input.address) + self.multisig_script_generator.recovery_pubkey = list(tmp_pubkeys)[1] + tmp_tx.multisig_script_generator = self.multisig_script_generator + tmp_tx.update_input_multisig_generator(input, copy.deepcopy(self.multisig_script_generator)) + tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True) - # sign. start with ready keystores. - keystore = self._get_hw_keystore() + if update_pubkeys_fn: + update_pubkeys_fn(tmp_tx) + + for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): + try: + if k.can_sign(tmp_tx): + from electrum.plugins.ledger.ledger import Ledger_KeyStore + if isinstance(k, Ledger_KeyStore): + from electrum.plugins.ledger.ledger import LedgerBtcvTxType + self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) + if update_pubkeys_fn: + k.sign_transaction(tmp_tx, password, None, True) + else: + k.sign_transaction(tmp_tx, password) + else: + raise NotImplementedError + except UserCancelled: + continue - if keystore.can_sign(tmp_tx): - pubkey_index = 0 - keystore.sign_transaction(tmp_tx, password, pubkey_index) - else: - # todo: ledger keystore needs to be ready for signing - pass - # remove sensitive info; then copy back details from temporary tx tmp_tx.remove_xpubs_and_bip32_paths() - tx.combine_with_other_psbt(tmp_tx) + tx.combine_with_other_psbt(tmp_tx, True) tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False) return tx @@ -2745,11 +2756,13 @@ def _get_hw_keystore(self): return k assert False + def is_recovery_mode(self): + return self.multisig_script_generator.is_recovery_mode() class TwoKeysHWWallet(MultikeyHWWallet): def __init__(self, storage: WalletStorage, *, config: SimpleConfig): - script_generator = TwoKeysHWScriptGenerator() + script_generator = TwoKeysHWScriptGenerator(recovery_pubkey=storage.get('recovery_pubkey')) super().__init__(storage, config=config, scriptGenerator=script_generator) def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: @@ -2760,27 +2773,24 @@ def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: def _add_recovery_pubkey_to_transaction(self, tx): for input in tx.inputs(): - recovery_pubkey = bytes.fromhex(self.multisig_script_generator.recovery_pubkey) + recovery_pubkey = bytes.fromhex(input.multisig_script_generator.recovery_pubkey) if recovery_pubkey not in input.pubkeys: input.pubkeys.append(recovery_pubkey) input.num_sig = 2 assert len(input.pubkeys) == 2, 'Wrong number of pubkeys for performing recovery tx' return tx - def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs) -> Optional[ + def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs)-> Optional[ PartialTransaction]: + if not isinstance(tx, PartialTransaction): return - # Skip inputs finalization - skip_finalize = self.multikey_type == '2fa' - tx = self.sign_transaction(tx, password, recovery_keypairs, self._add_recovery_pubkey_to_transaction, - skip_finalize) + tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction) - if not skip_finalize: - if not tx.is_complete(): - _logger.error(f'Recovery transaction not completed') - tx.finalize_psbt() + if not tx.is_complete(): + _logger.error(f'Recovery transaction not completed') + tx.finalize_psbt() return tx From 879da000779f07dc3ee098832ae9803fbe433ede Mon Sep 17 00:00:00 2001 From: mgrychow Date: Wed, 24 Feb 2021 11:31:29 +0100 Subject: [PATCH 21/28] User provided recovery password check for 2-keys recovery tx signed by ledger --- electrum/gui/qt/recovery_list.py | 42 +++++++++++++++++++++++++------ electrum/plugins/ledger/ledger.py | 25 +++++++++++++----- electrum/wallet.py | 8 +++--- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py index d44903ff0ec0..3392b4846cc4 100755 --- a/electrum/gui/qt/recovery_list.py +++ b/electrum/gui/qt/recovery_list.py @@ -7,16 +7,17 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QStandardItemModel, QStandardItem, QMouseEvent, QValidator, QKeySequence -from PyQt5.QtWidgets import QPushButton, QLabel, QWidget, QComboBox,\ - QTreeView, QHeaderView, QStyledItemDelegate,\ - QVBoxLayout, QGridLayout,\ - QCompleter, QShortcut +from PyQt5.QtWidgets import QPushButton, QLabel, QWidget, QComboBox, \ + QTreeView, QHeaderView, QStyledItemDelegate, \ + QVBoxLayout, QGridLayout, \ + QCompleter, QShortcut, QLineEdit from electrum.i18n import _ from electrum.logging import get_logger from electrum.wallet import Abstract_Wallet from electrum.util import get_request_status, PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum import bitcoin +from .three_keys_dialogs import MAX_3KEYS_PASSWD_LEN from .util import read_QIcon, pr_icons, WaitingDialog, filter_non_printable, ColorScheme from .confirm_tx_dialog import ConfirmTxDialog @@ -82,6 +83,7 @@ def clear_cache(self): def onItemChecked(self): self.selected() self.tab.update_recovery_button() + self.tab.on_recovery_password_line_edit() def onSelectAll(self): model = self.model() @@ -273,12 +275,23 @@ def validate_words(words): validity_fn(is_valid and len(seed) == 12) self.update_recovery_button() + def on_recovery_password_line_edit(self): + password = self._get_recovery_password() + is_valid = (len(password) > 0) or len(self.invoice_list.selected()) == 0 + if is_valid: + self.recovery_password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True)) + else: + self.recovery_password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + def get_recovery_seed(self): text = self.recovery_privkey_line.text() words = text.split() del text return words + def _get_recovery_password(self): + return self.recovery_password_line.text() + def _get_recovery_keypair(self): stored_recovery_pubkey = self.wallet.storage.get('recovery_pubkey') seed = self.get_recovery_seed() @@ -342,7 +355,10 @@ def on_failure(exc_info): on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success if self.wallet.is_recovery_mode(): - task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs) + if not self.is_hw: + task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs) + else: + task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs, self._get_recovery_password()) else: task = partial(self.wallet.sign_transaction, tx, password, external_keypairs) msg = _('Signing transaction...') @@ -353,8 +369,11 @@ def recover_action(self): atxs = self.invoice_list.selected() address = self.recovery_address_line.currentText() recovery_keypair = None + recovery_password = None if not (self.is_2fa or self.is_hw): recovery_keypair = self._get_recovery_keypair() + if self.is_hw: + recovery_password = self._get_recovery_password() if not is_address_valid(address): raise Exception(_('Invalid cancelation address')) @@ -372,10 +391,12 @@ def recover_action(self): recovery_keypairs=recovery_keypair, ) - # if not (self.is_2fa or self.is_hw): - if not (self.is_2fa): + if not (self.is_2fa or self.is_hw): self.recovery_privkey_line.setText('') + if self.is_hw: + self.recovery_password_line.setText('') + def _create_privkey_line(self, on_edit): class CompleterDelegate(QStyledItemDelegate): def initStyleOption(self, option, index): @@ -423,6 +444,13 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): # complete line edit with suggestions self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit) grid_layout.addWidget(self.recovery_privkey_line, 1, 1) + if self.is_hw: + grid_layout.addWidget(QLabel(_('Cancel password')), 1, 0) + self.recovery_password_line = QLineEdit() + self.recovery_password_line.setEchoMode(QLineEdit.Password) + self.recovery_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN) + self.recovery_password_line.textChanged.connect(self.on_recovery_password_line_edit) + grid_layout.addWidget(self.recovery_password_line, 1, 1) # Row 3 button = self.recover_button diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 830d1e522bda..1c8a42da55bc 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -4,6 +4,8 @@ import sys import traceback +from PyQt5.QtWidgets import QMessageBox + from electrum import ecc from electrum import bip32 from electrum.crypto import hash_160 @@ -333,7 +335,11 @@ def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None, w m = hashlib.sha256() m.update(bytearray.fromhex(bytearray(password.encode('utf-8')).hex().ljust(64, '0'))) password_hash = m.digest() - client.setBTCVPasswordUse(password_hash, tx_type) + try: + client.setBTCVPasswordUse(password_hash, tx_type) + except BTChipException as e: + if e.sw == 0x6a80: + raise BTChipException(e.message + "\n(Password may be incorrect)") def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check=None, btcv_recovery_password_check=None): try: @@ -341,20 +347,26 @@ def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=btcv_recovery_password_check, wizard=wizard) if btcv_instant_password_check: self.set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT, password=btcv_instant_password_check, wizard=wizard) - self.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self) except BTChipException as e: msg = _('Hardware wallet import fail') + "\n" + str(e) if e.sw == 0x6a80: msg = msg + "\n" + ('Invalid password(s)') - wizard.show_error(msg) + if wizard: + wizard.show_error(msg) return False + finally: + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self) return True @test_pin_unlocked @set_and_unset_signing - def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False): + def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False, recovery_password=None): if tx.is_complete(): return + if is_recovery and recovery_password == None: + raise RuntimeError("No recovery_password for recovery transaction provided") + if recovery_password and not self.are_3keys_ledger_passwords_correct(None, btcv_recovery_password_check=recovery_password): + raise RuntimeError("Invalid recovery password provided") inputs = [] inputsPaths = [] chipInputs = [] @@ -502,7 +514,7 @@ def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False): inputIndex = 0 if is_recovery: - # self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password="aaa") + #TODO: check recovery_password corectness self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees @@ -520,7 +532,8 @@ def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, singleInput, redeemScripts[inputIndex], version=tx.version) - self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password="aaa") + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=recovery_password) + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ diff --git a/electrum/wallet.py b/electrum/wallet.py index b855449b6ad9..9824e009817b 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2683,7 +2683,7 @@ def set_instant(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_instant() - def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None) -> Optional[PartialTransaction]: + def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, recovery_password=None) -> Optional[PartialTransaction]: if self.is_watching_only(): return if not isinstance(tx, PartialTransaction): @@ -2707,7 +2707,7 @@ def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None) -> from electrum.plugins.ledger.ledger import LedgerBtcvTxType self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) if update_pubkeys_fn: - k.sign_transaction(tmp_tx, password, None, True) + k.sign_transaction(tmp_tx, password, None, True, recovery_password) else: k.sign_transaction(tmp_tx, password) else: @@ -2780,13 +2780,13 @@ def _add_recovery_pubkey_to_transaction(self, tx): assert len(input.pubkeys) == 2, 'Wrong number of pubkeys for performing recovery tx' return tx - def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs)-> Optional[ + def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs, recovery_password=None)-> Optional[ PartialTransaction]: if not isinstance(tx, PartialTransaction): return - tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction) + tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction, recovery_password) if not tx.is_complete(): _logger.error(f'Recovery transaction not completed') From 6437aa5c8400d8d408c926f021af5eafd18c8ca2 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Fri, 26 Feb 2021 14:02:05 +0100 Subject: [PATCH 22/28] 3-keys recovery transaction signed by ledger --- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/recovery_list.py | 79 +++++++++++++++++++++++---- electrum/gui/qt/three_keys_windows.py | 17 ++++-- electrum/plugins/ledger/ledger.py | 53 +++++++++++++++--- electrum/three_keys/script.py | 4 +- electrum/wallet.py | 42 ++++++++++---- 6 files changed, 157 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 936d493d93f1..e66279e5d92f 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -214,7 +214,7 @@ def _create_window_for_wallet(self, wallet): elif wallet_type == '2-key-hw': w = ElectrumARHWWindow(self, wallet) elif wallet_type == '3-key-hw': - w = ElectrumARHWWindow(self, wallet) + w = ElectrumAIRHWWindow(self, wallet) else: w = ElectrumWindow(self, wallet) self.windows.append(w) diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py index 3392b4846cc4..fed7998f47fa 100755 --- a/electrum/gui/qt/recovery_list.py +++ b/electrum/gui/qt/recovery_list.py @@ -14,7 +14,7 @@ from electrum.i18n import _ from electrum.logging import get_logger -from electrum.wallet import Abstract_Wallet +from electrum.wallet import Abstract_Wallet, TwoKeysHWWallet, ThreeKeysHWWallet from electrum.util import get_request_status, PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum import bitcoin from .three_keys_dialogs import MAX_3KEYS_PASSWD_LEN @@ -83,7 +83,10 @@ def clear_cache(self): def onItemChecked(self): self.selected() self.tab.update_recovery_button() - self.tab.on_recovery_password_line_edit() + if (isinstance(self.tab, RecoveryTabAIR) and self.tab.is_hw): + self.tab.on_3k_password_line_edit() + elif self.tab.is_hw: + self.tab.on_recovery_password_line_edit() def onSelectAll(self): model = self.model() @@ -357,8 +360,10 @@ def on_failure(exc_info): if self.wallet.is_recovery_mode(): if not self.is_hw: task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs) - else: + elif isinstance(self.wallet, TwoKeysHWWallet): task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs, self._get_recovery_password()) + elif isinstance(self.wallet, ThreeKeysHWWallet): + task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs, self._get_recovery_password(), self._get_instant_password()) else: task = partial(self.wallet.sign_transaction, tx, password, external_keypairs) msg = _('Signing transaction...') @@ -369,11 +374,8 @@ def recover_action(self): atxs = self.invoice_list.selected() address = self.recovery_address_line.currentText() recovery_keypair = None - recovery_password = None if not (self.is_2fa or self.is_hw): recovery_keypair = self._get_recovery_keypair() - if self.is_hw: - recovery_password = self._get_recovery_password() if not is_address_valid(address): raise Exception(_('Invalid cancelation address')) @@ -395,7 +397,7 @@ def recover_action(self): self.recovery_privkey_line.setText('') if self.is_hw: - self.recovery_password_line.setText('') + self._clean_password_lines() def _create_privkey_line(self, on_edit): class CompleterDelegate(QStyledItemDelegate): @@ -422,6 +424,8 @@ def clear_cache(self): def update_view(self): self.invoice_list.update_data() + def _clean_password_lines(self): + pass class RecoveryTabAR(RecoveryTab): @@ -477,6 +481,9 @@ def update_recovery_button(self): else: self.recover_button.setText('Send cancel transaction') + def _clean_password_lines(self): + self.recovery_password_line.setText('') + class RecoveryTabAIR(RecoveryTab): @@ -497,19 +504,34 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): grid_layout.addWidget(self.recovery_address_line, 0, 1) # Row 2 - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): grid_layout.addWidget(QLabel(_('Secure Fast seedphrase')), 1, 0) # complete line edit with suggestions self.instant_privkey_line = self._create_privkey_line(self.on_instant_seed_line_edit) self.instant_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu) grid_layout.addWidget(self.instant_privkey_line, 1, 1) + elif self.is_hw: + grid_layout.addWidget(QLabel(_('Instant password')), 1, 0) + self.instant_password_line = QLineEdit() + self.instant_password_line.setEchoMode(QLineEdit.Password) + self.instant_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN) + self.instant_password_line.textChanged.connect(self.on_3k_password_line_edit) + grid_layout.addWidget(self.instant_password_line, 1, 1) # Row 3 - grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 2, 0) # complete line edit with suggestions - self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit) - self.recovery_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu) - grid_layout.addWidget(self.recovery_privkey_line, 2, 1) + if self.is_hw: + grid_layout.addWidget(QLabel(_('Cancel password')), 2, 0) + self.recovery_password_line = QLineEdit() + self.recovery_password_line.setEchoMode(QLineEdit.Password) + self.recovery_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN) + self.recovery_password_line.textChanged.connect(self.on_3k_password_line_edit) + grid_layout.addWidget(self.recovery_password_line, 2, 1) + else: + grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 2, 0) + self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit) + self.recovery_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu) + grid_layout.addWidget(self.recovery_privkey_line, 2, 1) # Row 4 button = self.recover_button @@ -538,6 +560,10 @@ def _get_instant_keypair(self): return {pubkey: (privkey, True)} def recover_action(self): + if self.is_hw: + RecoveryTab.recover_action(self) + return + #TODO: design pattern for this(!) try: address = self.recovery_address_line.currentText() instant_keypair = None @@ -576,6 +602,11 @@ def update_recovery_button(self): enabled = self.is_address_valid \ and self.is_recovery_seed_valid \ and len(self.invoice_list.selected()) + elif self.is_hw: + enabled = self.is_address_valid \ + and len(self.invoice_list.selected()) \ + and len(self.instant_password_line.text()) \ + and len(self.recovery_password_line.text()) else: enabled = self.is_address_valid \ and self.is_instant_seed_valid \ @@ -592,3 +623,27 @@ def on_instant_seed_line_edit(self): return self.on_seed_line_edit(self.instant_privkey_line, self.get_instant_seed(), self.set_instant_seed_validity) + + def _get_instant_password(self): + return self.instant_password_line.text() + + def set_password_line_style(self, password, password_line): + is_valid = (len(password) > 0) or len(self.invoice_list.selected()) == 0 + if is_valid: + password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True)) + else: + password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + + def on_3k_password_line_edit(self): + password = self._get_recovery_password() + self.set_password_line_style(password, self.recovery_password_line) + password = self._get_instant_password() + self.set_password_line_style(password, self.instant_password_line) + self.update_recovery_button() + + def _get_instant_password(self): + return self.instant_password_line.text() + + def _clean_password_lines(self): + self.recovery_password_line.setText('') + self.instant_password_line.setText('') diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 01e28bcf5650..8c1c50d260e0 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -603,8 +603,15 @@ class ElectrumAIRHWWindow(ElectrumAIRWindow): def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): super().__init__(gui_object=gui_object, wallet=wallet) - def do_pay(self): - self.show_message(_('do_pay')) - - def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None): - self.show_message(_('pay_onchain_dialog')) + # def do_pay(self): + # invoice = self.read_invoice() + # if not invoice: + # return + # invoice['txtype'] = TxType.ALERT_PENDING.name + # self.wallet.save_invoice(invoice) + # self.invoice_list.update() + # self.do_clear() + # self.do_pay_invoice(invoice) + # + # def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None): + # self.show_message(_('pay_onchain_dialog')) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 1c8a42da55bc..0aa9ad951970 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -360,13 +360,14 @@ def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check @test_pin_unlocked @set_and_unset_signing - def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False, recovery_password=None): + def sign_transaction(self, tx, password, pubkey_index=None, recovery_password=None, instant_password=False): + if tx.is_complete(): return - if is_recovery and recovery_password == None: - raise RuntimeError("No recovery_password for recovery transaction provided") if recovery_password and not self.are_3keys_ledger_passwords_correct(None, btcv_recovery_password_check=recovery_password): raise RuntimeError("Invalid recovery password provided") + if instant_password and not self.are_3keys_ledger_passwords_correct(None, btcv_instant_password_check=instant_password): + raise RuntimeError("Invalid instant password provided") inputs = [] inputsPaths = [] chipInputs = [] @@ -503,7 +504,7 @@ def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False, r while inputIndex < len(inputs): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex], version=tx.version) + singleInput, redeemScripts[inputIndex], version=tx.version) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ my_pubkey = inputs[inputIndex][4] @@ -512,11 +513,12 @@ def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False, r sig=inputSignature.hex()) inputIndex = inputIndex + 1 - inputIndex = 0 - if is_recovery: - #TODO: check recovery_password corectness + + #TODO: strategy design pattern? + if recovery_password: + inputIndex = 0 self.get_client().startUntrustedTransaction(True, inputIndex, - chipInputs, redeemScripts[inputIndex], version=tx.version) + chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) @@ -531,12 +533,45 @@ def sign_transaction(self, tx, password, pubkey_index=None, is_recovery=False, r while inputIndex < len(inputs): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex], version=tx.version) + singleInput, redeemScripts[inputIndex], version=tx.version) self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=recovery_password) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ + if instant_password: + my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[2] + else: + my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[1] + + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) + inputIndex = inputIndex + 1 + if instant_password: + inputIndex = 0 + self.get_client().startUntrustedTransaction(True, inputIndex, + chipInputs, redeemScripts[inputIndex], version=tx.version) + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + outputData['outputData'] = txOutput + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + self.handler.show_message(_("Confirmed. Signing Transaction...")) + while inputIndex < len(inputs): + singleInput = [ chipInputs[inputIndex] ] + self.get_client().startUntrustedTransaction(False, 0, + singleInput, redeemScripts[inputIndex], version=tx.version) + self.set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT, password=instant_password) + + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[1] tx.add_signature_to_txin(txin_idx=inputIndex, diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py index 402d70b01264..1684d80a1ef5 100644 --- a/electrum/three_keys/script.py +++ b/electrum/three_keys/script.py @@ -157,8 +157,10 @@ def get_redeem_script(self, public_keys: List[str]) -> str: class ThreeKeysHWScriptGenerator(ThreeKeysScriptGenerator): - def __init__(self): + def __init__(self, instant_pubkey: str, recovery_pubkey: str): self._instant_recovery_alert_flag = None + self.instant_pubkey = instant_pubkey + self.recovery_pubkey = recovery_pubkey self.witness_flags = [] def get_redeem_script(self, public_keys: List[str]) -> str: diff --git a/electrum/wallet.py b/electrum/wallet.py index 9824e009817b..5b473aef00dc 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2683,7 +2683,7 @@ def set_instant(self): from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_instant() - def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, recovery_password=None) -> Optional[PartialTransaction]: + def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, recovery_password=None, instant_password=None) -> Optional[PartialTransaction]: if self.is_watching_only(): return if not isinstance(tx, PartialTransaction): @@ -2692,6 +2692,8 @@ def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, re for input in tmp_tx.inputs(): tmp_pubkeys = self.get_public_keys_with_deriv_info(input.address) self.multisig_script_generator.recovery_pubkey = list(tmp_pubkeys)[1] + if isinstance(self.multisig_script_generator, ThreeKeysHWScriptGenerator): + self.multisig_script_generator.instant_pubkey = list(tmp_pubkeys)[2] tmp_tx.multisig_script_generator = self.multisig_script_generator tmp_tx.update_input_multisig_generator(input, copy.deepcopy(self.multisig_script_generator)) @@ -2706,8 +2708,10 @@ def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, re if isinstance(k, Ledger_KeyStore): from electrum.plugins.ledger.ledger import LedgerBtcvTxType self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) - if update_pubkeys_fn: - k.sign_transaction(tmp_tx, password, None, True, recovery_password) + if recovery_password and instant_password: + k.sign_transaction(tmp_tx, password, None, recovery_password, instant_password) + elif recovery_password: + k.sign_transaction(tmp_tx, password, None, recovery_password) else: k.sign_transaction(tmp_tx, password) else: @@ -2801,7 +2805,9 @@ def get_wallet_label(self): class ThreeKeysHWWallet(MultikeyHWWallet): def __init__(self, storage: WalletStorage, *, config: SimpleConfig): - script_generator = ThreeKeysHWScriptGenerator() + + script_generator = ThreeKeysHWScriptGenerator(instant_pubkey=storage.get('instant_pubkey'), + recovery_pubkey=storage.get('recovery_pubkey')) super().__init__(storage, config=config, scriptGenerator=script_generator) self.n = 3 self.m = 1 @@ -2827,18 +2833,30 @@ def sign_instant_transaction(self, tx: PartialTransaction, password, instant_key return tx - def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs) -> Optional[PartialTransaction]: + def _add_recovery_pubkey_to_transaction(self, tx): + for input in tx.inputs(): + instant_pubkey = bytes.fromhex(input.multisig_script_generator.instant_pubkey) + if instant_pubkey not in input.pubkeys: + input.pubkeys.append(instant_pubkey) + recovery_pubkey = bytes.fromhex(input.multisig_script_generator.recovery_pubkey) + if recovery_pubkey not in input.pubkeys: + input.pubkeys.append(recovery_pubkey) + input.num_sig = 3 + assert len(input.pubkeys) == 3, 'Wrong number of pubkeys for performing recovery tx' + return tx + + + def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs, recovery_password, instant_password)-> Optional[ + PartialTransaction]: + if not isinstance(tx, PartialTransaction): return - # Skip tx finalization when tx should be authenticated - skip_finalize = self.multikey_type == '2fa' - tx = self.sign_transaction(tx, password, recovery_keypairs, self._add_recovery_pubkey_to_transaction, skip_finalize) + tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction, recovery_password, instant_password) - if not skip_finalize: - if not tx.is_complete(): - _logger.error(f'Recovery transaction not completed') - tx.finalize_psbt() + if not tx.is_complete(): + _logger.error(f'Recovery transaction not completed') + tx.finalize_psbt() return tx From d271049e83e618e5d7c9d159c055d87fc5cbbd8d Mon Sep 17 00:00:00 2001 From: mgrychow Date: Mon, 1 Mar 2021 11:58:37 +0100 Subject: [PATCH 23/28] 3-keys instant transaction signed by ledger --- electrum/gui/qt/main_window.py | 5 +- electrum/gui/qt/recovery_list.py | 3 -- electrum/gui/qt/three_keys_windows.py | 70 ++++++++++++++++++++------- electrum/wallet.py | 35 ++++++++------ 4 files changed, 77 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 914aad3fee41..32ded03eb3d1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -72,7 +72,7 @@ from electrum.version import ELECTRUM_VERSION from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, sweep_preparations, InternalAddressCorruption, - ThreeKeysWallet) + ThreeKeysWallet, ThreeKeysHWWallet) from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog @@ -1541,6 +1541,9 @@ def on_failure(exc_info): elif external_keypairs: # can sign directly task = partial(tx.sign, external_keypairs) + elif isinstance(self.wallet, ThreeKeysHWWallet) and self.wallet.is_instant_mode(): + instant_password = self._get_instant_password() + task = partial(self.wallet.sign_instant_transaction, tx, password, None, instant_password) else: task = partial(self.wallet.sign_transaction, tx, password) msg = _('Signing transaction...') diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py index fed7998f47fa..1de9faf917b1 100755 --- a/electrum/gui/qt/recovery_list.py +++ b/electrum/gui/qt/recovery_list.py @@ -641,9 +641,6 @@ def on_3k_password_line_edit(self): self.set_password_line_style(password, self.instant_password_line) self.update_recovery_button() - def _get_instant_password(self): - return self.instant_password_line.text() - def _clean_password_lines(self): self.recovery_password_line.setText('') self.instant_password_line.setText('') diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 8c1c50d260e0..7f19ec7e8042 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QVBoxLayout, QLabel, QWidget, QHBoxLayout, \ QGridLayout, QCompleter, QComboBox, \ - QStyledItemDelegate + QStyledItemDelegate, QLineEdit from electrum.i18n import _ from .amountedit import BTCAmountEdit, MyLineEdit, AmountEdit @@ -11,7 +11,7 @@ from .confirm_tx_dialog import ConfirmTxDialog from .main_window import ElectrumWindow from .recovery_list import RecoveryTabAR, RecoveryTabAIR -from .three_keys_dialogs import PreviewPsbtTxDialog +from .three_keys_dialogs import PreviewPsbtTxDialog, MAX_3KEYS_PASSWD_LEN from .transaction_dialog import PreviewTxDialog from .util import read_QIcon, HelpLabel, EnterButton, ColorScheme from ...mnemonic import load_wordlist @@ -26,6 +26,7 @@ class ElectrumMultikeyWalletWindow(ElectrumWindow): def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): self.is_2fa = wallet.storage.get('multikey_type', '') == '2fa' + self.is_hw = wallet.storage.get('multikey_type', '') == 'hw' super().__init__(gui_object=gui_object, wallet=wallet) self.recovery_tab = self.create_recovery_tab(wallet, self.config) self.tabs.addTab(self.recovery_tab, read_QIcon('recovery.png'), _('Cancel')) @@ -301,7 +302,7 @@ def create_send_tab(self): grid.addWidget(self.max_button, 3, 3) def on_tx_type(index): - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure: self.instant_privkey_line.setEnabled(False) self.instant_privkey_line.clear() @@ -309,6 +310,21 @@ def on_tx_type(index): elif self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast: self.instant_privkey_line.setEnabled(True) self.label_transaction_limitations.hide() + elif self.is_hw: + if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure: + self.instant_password_line.setEnabled(False) + self.instant_password_line.clear() + self.label_transaction_limitations.show() + elif self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast: + self.instant_password_line.setEnabled(True) + self.label_transaction_limitations.hide() + password = self.instant_password_line.text() + is_valid = (len(password) > 0) or not self.instant_password_line.isEnabled() + if is_valid: + self.instant_password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True)) + else: + self.instant_password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + self.instant_password_line.textChanged.connect(on_tx_type) else: if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure: description_label.setEnabled(True) @@ -335,7 +351,7 @@ def on_tx_type(index): grid.addWidget(tx_type_label, 4, 0) grid.addWidget(self.tx_type_combo, 4, 1, 1, -1) - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): instant_privkey_label = HelpLabel(_('Secure Fast Tx seed'), msg) self.instant_privkey_line = CompletionTextEdit() self.instant_privkey_line.setTabChangesFocus(False) @@ -358,6 +374,16 @@ def initStyleOption(self, option, index): grid.addWidget(instant_privkey_label, 5, 0) grid.addWidget(self.instant_privkey_line, 5, 1, 1, -1) + elif self.is_hw: + instant_password_label = HelpLabel(_('Secure Fast Tx password'), msg) + self.instant_password_line = QLineEdit() + self.instant_password_line.setEchoMode(QLineEdit.Password) + self.instant_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN) + self.instant_password_line.setEnabled(False) + grid.addWidget(instant_password_label, 5, 0) + grid.addWidget(self.instant_password_line, 5, 1, 1, -1) + + self.save_button = EnterButton(_("Save"), self.do_save_invoice) self.send_button = EnterButton(_("Pay"), self.do_pay) self.clear_button = EnterButton(_("Clear"), self.do_clear) @@ -457,7 +483,7 @@ def do_pay_invoice(self, invoice, external_keypairs=None): self.wallet.set_alert() if invoice['txtype'] == TxType.INSTANT.name: try: - if not self.is_2fa and external_keypairs == None: + if not (self.is_2fa or self.is_hw) and external_keypairs == None: external_keypairs = self.get_instant_keypair() self.wallet.set_instant() except Exception as e: @@ -491,7 +517,7 @@ def do_clear(self): for e in [self.payto_e, self.message_e, self.amount_e]: e.setText('') e.setFrozen(False) - if not self.is_2fa: + if not (self.is_2fa or self.is_hw): self.instant_privkey_line.clear() self.tx_type_combo.setCurrentIndex(self.TX_TYPES.Secure) self.update_status() @@ -603,15 +629,23 @@ class ElectrumAIRHWWindow(ElectrumAIRWindow): def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): super().__init__(gui_object=gui_object, wallet=wallet) - # def do_pay(self): - # invoice = self.read_invoice() - # if not invoice: - # return - # invoice['txtype'] = TxType.ALERT_PENDING.name - # self.wallet.save_invoice(invoice) - # self.invoice_list.update() - # self.do_clear() - # self.do_pay_invoice(invoice) - # - # def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None): - # self.show_message(_('pay_onchain_dialog')) + def do_pay(self): + invoice = self.read_invoice() + if not invoice: + return + + # keypair = None + if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast: + invoice['txtype'] = TxType.INSTANT.name + password = self.instant_password_line.text() + self.wallet.set_instant() + # TODO: try-catch? + else: + invoice['txtype'] = TxType.ALERT_PENDING.name + self.wallet.set_alert() + self.wallet.save_invoice(invoice) + self.invoice_list.update() + self.do_pay_invoice(invoice) + + def _get_instant_password(self): + return self.instant_password_line.text() diff --git a/electrum/wallet.py b/electrum/wallet.py index 5b473aef00dc..0b9da6c0df19 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2708,12 +2708,9 @@ def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, re if isinstance(k, Ledger_KeyStore): from electrum.plugins.ledger.ledger import LedgerBtcvTxType self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) - if recovery_password and instant_password: - k.sign_transaction(tmp_tx, password, None, recovery_password, instant_password) - elif recovery_password: - k.sign_transaction(tmp_tx, password, None, recovery_password) - else: - k.sign_transaction(tmp_tx, password) + + k.sign_transaction(tmp_tx, password, None, recovery_password, instant_password) + else: raise NotImplementedError except UserCancelled: @@ -2818,21 +2815,28 @@ def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: return ThreeKeysScriptGenerator.create_redeem_script(pubkeys[0], pubkeys[1], pubkeys[2]) - def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs) -> Optional[PartialTransaction]: + def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs, instant_password) -> Optional[PartialTransaction]: if not isinstance(tx, PartialTransaction): return - # Skip tx finalization when tx should be authenticated - skip_finalize = self.multikey_type == '2fa' - tx = self.sign_transaction(tx, password, instant_keypairs, self._add_instant_pubkey_to_transaction, skip_finalize) + tx = self.sign_transaction(tx, password, self._add_instant_pubkey_to_transaction, None, instant_password) - if not skip_finalize: - if not tx.is_complete(): - _logger.error(f'Instant transaction not completed') - tx.finalize_psbt() + if not tx.is_complete(): + _logger.error(f'Recovery transaction not completed') + tx.finalize_psbt() return tx + def _add_instant_pubkey_to_transaction(self, tx): + for input in tx.inputs(): + # instant_pubkey = bytes.fromhex(input.multisig_script_generator.instant_pubkey) + instant_pubkey = bytes.fromhex(self.multisig_script_generator.instant_pubkey) + if instant_pubkey not in input.pubkeys: + input.pubkeys.append(instant_pubkey) + input.num_sig = 2 + assert len(input.pubkeys) == 3, 'Wrong number of pubkeys for performing recovery tx' + return tx + def _add_recovery_pubkey_to_transaction(self, tx): for input in tx.inputs(): instant_pubkey = bytes.fromhex(input.multisig_script_generator.instant_pubkey) @@ -2863,6 +2867,9 @@ def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_k def get_wallet_label(self): return '3-Key Vault' + def is_instant_mode(self): + return self.multisig_script_generator.is_instant_mode() + wallet_types = [ '2-key', From 0d5bbec439b88f53ae0f5a596c293ca31fd66c1d Mon Sep 17 00:00:00 2001 From: mgrychow Date: Tue, 2 Mar 2021 11:40:39 +0100 Subject: [PATCH 24/28] Fix for >2 output btcv multi-key tx signing on ledger device --- electrum/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 0b9da6c0df19..72cd59085905 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2591,7 +2591,7 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenera self.m = 1 Deterministic_Wallet.__init__(self, storage, config=config) # super has to be at the end otherwise wallet breaks - self.multiple_change = storage.get('multiple_change', True) + self.multiple_change = storage.get('multiple_change', False) def load_keystore(self): self.keystores = {} From 6094a861c47c33bbf81599c72527f47ca3756281 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Wed, 10 Mar 2021 15:42:31 +0100 Subject: [PATCH 25/28] Rework 1 for feature/ledger-integration merge --- contrib/requirements/requirements-hw.txt | 2 +- electrum/base_wizard.py | 13 +-- electrum/gui/qt/recovery_list.py | 8 +- electrum/gui/qt/three_keys_windows.py | 29 +++--- electrum/plugins/ledger/ledger.py | 120 ++++++++--------------- electrum/three_keys/pubkey_type.py | 2 +- 6 files changed, 68 insertions(+), 106 deletions(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 4902c57a76f5..47d15bc9a356 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,6 +1,6 @@ trezor[hidapi]>=0.11.5 safet>=0.1.5 keepkey>=6.0.3 -git+ssh://git@gitlab.minebest.com/r-and-d/ledger-integration/btchip-python.git@feature/btcv +btchip-python@git://github.com/bitcoinvault/btchip-python.git@feature/btcv ckcc-protocol>=0.7.7 hidapi diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 9f04205d3a2a..8f27c75b44a7 100755 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -548,22 +548,16 @@ def f(derivation, script_type): else: self.plugin.set_instant_password(device_info.device.id_, str(0x00), self) - derivation = bip44_derivation(0) - script_type = 'p2wsh-p2sh' - self.run('on_hw_derivation', name, device_info, derivation, script_type) + self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh') elif purpose == HWD_SETUP_DECRYPT_BTCV_WALLET: if 'recovery_password' not in self.data: raise Exception('Invalid recovery password') if 'instant_password' in self.data: - derivation = bip44_derivation(0) - script_type = 'p2wsh-p2sh' - self.run('on_hw_derivation', name, device_info, derivation, script_type, + self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh', btcv_instant_password_check=self.data['instant_password'], btcv_recovery_password_check=self.data['recovery_password']) else: - derivation = bip44_derivation(0) - script_type = 'p2wsh-p2sh' - self.run('on_hw_derivation', name, device_info, derivation, script_type, + self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh', btcv_recovery_password_check=self.data['recovery_password'], btcv_instant_password_check=str(0x00)) else: @@ -631,6 +625,7 @@ def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=F from electrum.plugins.ledger.ledger import Ledger_KeyStore if isinstance(k, Ledger_KeyStore) \ and not k.are_3keys_ledger_passwords_correct(self, btcv_instant_password_check, btcv_recovery_password_check): + # user already notified about invalid password(s) return self.terminate() self.on_keystore(k, name, device_info) diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py index 1de9faf917b1..00aa8538d58f 100755 --- a/electrum/gui/qt/recovery_list.py +++ b/electrum/gui/qt/recovery_list.py @@ -374,7 +374,7 @@ def recover_action(self): atxs = self.invoice_list.selected() address = self.recovery_address_line.currentText() recovery_keypair = None - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: recovery_keypair = self._get_recovery_keypair() if not is_address_valid(address): @@ -393,7 +393,7 @@ def recover_action(self): recovery_keypairs=recovery_keypair, ) - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: self.recovery_privkey_line.setText('') if self.is_hw: @@ -443,7 +443,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): grid_layout.addWidget(self.recovery_address_line, 0, 1) # Row 2 - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 1, 0) # complete line edit with suggestions self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit) @@ -504,7 +504,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config): grid_layout.addWidget(self.recovery_address_line, 0, 1) # Row 2 - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: grid_layout.addWidget(QLabel(_('Secure Fast seedphrase')), 1, 0) # complete line edit with suggestions self.instant_privkey_line = self._create_privkey_line(self.on_instant_seed_line_edit) diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index 7f19ec7e8042..bf72ade4c13c 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -302,7 +302,7 @@ def create_send_tab(self): grid.addWidget(self.max_button, 3, 3) def on_tx_type(index): - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure: self.instant_privkey_line.setEnabled(False) self.instant_privkey_line.clear() @@ -351,7 +351,7 @@ def on_tx_type(index): grid.addWidget(tx_type_label, 4, 0) grid.addWidget(self.tx_type_combo, 4, 1, 1, -1) - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: instant_privkey_label = HelpLabel(_('Secure Fast Tx seed'), msg) self.instant_privkey_line = CompletionTextEdit() self.instant_privkey_line.setTabChangesFocus(False) @@ -483,7 +483,7 @@ def do_pay_invoice(self, invoice, external_keypairs=None): self.wallet.set_alert() if invoice['txtype'] == TxType.INSTANT.name: try: - if not (self.is_2fa or self.is_hw) and external_keypairs == None: + if not self.is_2fa and not self.is_hw and external_keypairs == None: external_keypairs = self.get_instant_keypair() self.wallet.set_instant() except Exception as e: @@ -517,7 +517,7 @@ def do_clear(self): for e in [self.payto_e, self.message_e, self.amount_e]: e.setText('') e.setFrozen(False) - if not (self.is_2fa or self.is_hw): + if not self.is_2fa and not self.is_hw: self.instant_privkey_line.clear() self.tx_type_combo.setCurrentIndex(self.TX_TYPES.Secure) self.update_status() @@ -634,15 +634,18 @@ def do_pay(self): if not invoice: return - # keypair = None - if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast: - invoice['txtype'] = TxType.INSTANT.name - password = self.instant_password_line.text() - self.wallet.set_instant() - # TODO: try-catch? - else: - invoice['txtype'] = TxType.ALERT_PENDING.name - self.wallet.set_alert() + try: + if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast: + invoice['txtype'] = TxType.INSTANT.name + self.instant_password_line.text() + self.wallet.set_instant() + else: + invoice['txtype'] = TxType.ALERT_PENDING.name + self.wallet.set_alert() + except Exception as e: + self.on_error([0, str(e)]) + return + self.wallet.save_invoice(invoice) self.invoice_list.update() self.do_pay_invoice(invoice) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 0aa9ad951970..44b640ec1c4a 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -513,71 +513,19 @@ def sign_transaction(self, tx, password, pubkey_index=None, recovery_password=No sig=inputSignature.hex()) inputIndex = inputIndex + 1 - - #TODO: strategy design pattern? if recovery_password: - inputIndex = 0 - self.get_client().startUntrustedTransaction(True, inputIndex, - chipInputs, redeemScripts[inputIndex], version=tx.version) - # we don't set meaningful outputAddress, amount and fees - # as we only care about the alternateEncoding==True branch - outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - outputData['outputData'] = txOutput - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin - if not pin: - raise UserWarning() - self.handler.show_message(_("Confirmed. Signing Transaction...")) - while inputIndex < len(inputs): - singleInput = [ chipInputs[inputIndex] ] - self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex], version=tx.version) - self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=recovery_password) - - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ - - if instant_password: - my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[2] - else: - my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[1] - - tx.add_signature_to_txin(txin_idx=inputIndex, - signing_pubkey=my_pubkey.hex(), - sig=inputSignature.hex()) - inputIndex = inputIndex + 1 + three_keys_pubkey_index = 1 + if instant_password: + three_keys_pubkey_index = 2 + self.add_3k_signatures_to_tx(changePath, chipInputs, inputs, inputsPaths, + LedgerBtcvTxType.RECOVERY, output, pin, rawTx, recovery_password, + redeemScripts, tx, txOutput, three_keys_pubkey_index) if instant_password: - inputIndex = 0 - self.get_client().startUntrustedTransaction(True, inputIndex, - chipInputs, redeemScripts[inputIndex], version=tx.version) - # we don't set meaningful outputAddress, amount and fees - # as we only care about the alternateEncoding==True branch - outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - outputData['outputData'] = txOutput - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin - if not pin: - raise UserWarning() - self.handler.show_message(_("Confirmed. Signing Transaction...")) - while inputIndex < len(inputs): - singleInput = [ chipInputs[inputIndex] ] - self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex], version=tx.version) - self.set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT, password=instant_password) - - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ + three_keys_pubkey_index = 1 + self.add_3k_signatures_to_tx(changePath, chipInputs, inputs, inputsPaths, + LedgerBtcvTxType.INSTANT, output, pin, rawTx, instant_password, + redeemScripts, tx, txOutput, three_keys_pubkey_index) - my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[1] - - tx.add_signature_to_txin(txin_idx=inputIndex, - signing_pubkey=my_pubkey.hex(), - sig=inputSignature.hex()) - inputIndex = inputIndex + 1 else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, @@ -620,6 +568,38 @@ def sign_transaction(self, tx, password, pubkey_index=None, recovery_password=No finally: self.handler.finished() + def add_3k_signatures_to_tx(self, changePath, chipInputs, inputs, inputsPaths, tx_type, output, + pin, rawTx, three_keys_password, redeemScripts, tx, txOutput, three_keys_pubkey_index): + inputIndex = 0 + self.get_client().startUntrustedTransaction(True, inputIndex, + chipInputs, redeemScripts[inputIndex], version=tx.version) + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + outputData['outputData'] = txOutput + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth(outputData) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + self.handler.show_message(_("Confirmed. Signing Transaction...")) + while inputIndex < len(inputs): + singleInput = [chipInputs[inputIndex]] + self.get_client().startUntrustedTransaction(False, 0, + singleInput, redeemScripts[inputIndex], version=tx.version) + self.set_btcv_password_use(tx_type=tx_type, password=three_keys_password) + + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + + my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[three_keys_pubkey_index] + + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) + inputIndex = inputIndex + 1 + @test_pin_unlocked @set_and_unset_signing def get_address(self, sequence, txin_type, btcvAddr=False): @@ -636,22 +616,6 @@ def get_address(self, sequence, txin_type, btcvAddr=False): return result['address'].decode("utf-8") - @test_pin_unlocked - @set_and_unset_signing - def get_addresses_batch(self, sequences, txin_type, btcvAddr=False): - client = self.get_client() - address_paths = [self.get_derivation_prefix()[2:] + "/%d/%d"%sequence for sequence in sequences] - segwit = is_segwit_script_type(txin_type) - segwitNative = txin_type == 'p2wpkh' - - resultBatch = None - try: - resultBatch = client.getWalletPublicKeyBatch(address_paths, segwit=segwit, segwitNative=segwitNative, btcvAddr=btcvAddr) - except Exception as e: - self.logger.exception(e) - - return [result['address'].decode("utf-8") for result in resultBatch] - @test_pin_unlocked @set_and_unset_signing def show_address(self, sequence, txin_type): diff --git a/electrum/three_keys/pubkey_type.py b/electrum/three_keys/pubkey_type.py index 1863d5faed90..0bf96854b94a 100644 --- a/electrum/three_keys/pubkey_type.py +++ b/electrum/three_keys/pubkey_type.py @@ -3,4 +3,4 @@ class PubkeyType(IntEnum): PUBKEY_ALERT = 0 PUBKEY_INSTANT = 1 - PUBKEY_RECOVERY = 2 \ No newline at end of file + PUBKEY_RECOVERY = 2 From 49d619e77083476ce4829c82b60663da42f1b3e7 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Thu, 11 Mar 2021 10:15:17 +0100 Subject: [PATCH 26/28] update btchip-python dependency version tag --- contrib/requirements/requirements-hw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 47d15bc9a356..a1bf4386376f 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,6 +1,6 @@ trezor[hidapi]>=0.11.5 safet>=0.1.5 keepkey>=6.0.3 -btchip-python@git://github.com/bitcoinvault/btchip-python.git@feature/btcv +btchip-python@git://github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv ckcc-protocol>=0.7.7 hidapi From 8cb7db9b02c34fec30eca7abdcafa11ba64253a5 Mon Sep 17 00:00:00 2001 From: mgrychow Date: Mon, 15 Mar 2021 16:23:38 +0100 Subject: [PATCH 27/28] Rework 2 for feature/ledger-integration merge --- README.rst | 3 +++ electrum/gui/qt/three_keys_dialogs.py | 1 - electrum/gui/qt/three_keys_windows.py | 5 +++++ electrum/keystore.py | 2 +- electrum/plugins/ledger/ledger.py | 5 ++--- electrum/wallet.py | 5 +---- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index eb5f90669eca..5e85c4f8f5cd 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,9 @@ Run install (this should install dependencies):: python3 -m pip install .[fast] +In case of problems with btchip library:: + + pip install git+https://git@github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv Compile the protobuf description file:: diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py index 82e85e6b1601..c32d8d2a5799 100644 --- a/electrum/gui/qt/three_keys_dialogs.py +++ b/electrum/gui/qt/three_keys_dialogs.py @@ -179,7 +179,6 @@ def get_password(self): class CheckHWPasswordDialog(QVBoxLayout): def __init__(self, parent, message_label): super().__init__() - # self.parent.next_button.setEnabled(False) self.parent = parent label = message_label self.edit = QLineEdit() diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py index bf72ade4c13c..d0558d80a2cb 100644 --- a/electrum/gui/qt/three_keys_windows.py +++ b/electrum/gui/qt/three_keys_windows.py @@ -35,6 +35,11 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'): self.READY_TO_UPDATE = True + def timer_actions(self): + # synchronizing the timer thread with end of the __init__ call + if self.READY_TO_UPDATE: + super().timer_actions() + def create_recovery_tab(self, wallet: 'Abstract_Wallet', config): raise NotImplementedError() diff --git a/electrum/keystore.py b/electrum/keystore.py index 472cc36e23af..2989c944d674 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -746,7 +746,7 @@ def opportunistically_fill_in_missing_info_from_device(self, client: 'HardwareCl self.is_requesting_to_be_rewritten_to_wallet_file = True def set_btcv_password_use(self, tx_type, password=None): - raise NotImplementedError + raise NotImplementedError() def bip39_normalize_passphrase(passphrase): diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 44b640ec1c4a..2fbf3fa73a3d 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -598,7 +598,7 @@ def add_3k_signatures_to_tx(self, changePath, chipInputs, inputs, inputsPaths, t tx.add_signature_to_txin(txin_idx=inputIndex, signing_pubkey=my_pubkey.hex(), sig=inputSignature.hex()) - inputIndex = inputIndex + 1 + inputIndex += 1 @test_pin_unlocked @set_and_unset_signing @@ -608,13 +608,12 @@ def get_address(self, sequence, txin_type, btcvAddr=False): segwit = is_segwit_script_type(txin_type) segwitNative = txin_type == 'p2wpkh' - result = None try: result = client.getWalletPublicKey(address_path, showOnScreen=False, segwit=segwit, segwitNative=segwitNative, btcvAddr=btcvAddr) + return result['address'].decode("utf-8") except Exception as e: self.logger.exception(e) - return result['address'].decode("utf-8") @test_pin_unlocked @set_and_unset_signing diff --git a/electrum/wallet.py b/electrum/wallet.py index 72cd59085905..aebe7accf01b 100755 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2676,11 +2676,9 @@ def set_alert(self): self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT) def set_recovery(self): - from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_recovery() def set_instant(self): - from .plugins.ledger.ledger import LedgerBtcvTxType self.multisig_script_generator.set_instant() def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, recovery_password=None, instant_password=None) -> Optional[PartialTransaction]: @@ -2755,7 +2753,7 @@ def _get_hw_keystore(self): for k in self.get_keystores(): if isinstance(k, Hardware_KeyStore): return k - assert False + raise AssertionError("Hardware keystore not found") def is_recovery_mode(self): return self.multisig_script_generator.is_recovery_mode() @@ -2829,7 +2827,6 @@ def sign_instant_transaction(self, tx: PartialTransaction, password, instant_key def _add_instant_pubkey_to_transaction(self, tx): for input in tx.inputs(): - # instant_pubkey = bytes.fromhex(input.multisig_script_generator.instant_pubkey) instant_pubkey = bytes.fromhex(self.multisig_script_generator.instant_pubkey) if instant_pubkey not in input.pubkeys: input.pubkeys.append(instant_pubkey) From abef57cfd00e81c6b9652b07c415c835a94fac1b Mon Sep 17 00:00:00 2001 From: mgrychow Date: Wed, 17 Mar 2021 13:07:02 +0100 Subject: [PATCH 28/28] Backward compatibility with standard wallet on bitcoin ledger app --- electrum/plugins/ledger/ledger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 2fbf3fa73a3d..fbbe91946ae7 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -117,14 +117,17 @@ def get_xpub(self, bip32_path, xtype, pubkey_type=PubkeyType.PUBKEY_ALERT): bip32_path = bip32_path[2:] # cut off "m/" if len(bip32_intpath) >= 1: prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:] - nodeData = self.dongleObject.getWalletPublicKey(prevPath) + nodeData = self.dongleObject.getWalletPublicKey(prevPath, btcvAddr=False) publicKey = compress_public_key(nodeData['publicKey']) fingerprint_bytes = hash_160(publicKey)[0:4] childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big") else: fingerprint_bytes = bytes(4) childnum_bytes = bytes(4) - nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvPubkeyTree=pubkey_type) + if(pubkey_type == LedgerBtcvTxType.ALERT): + nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvAddr=False) + else: + nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvPubkeyTree=pubkey_type) publicKey = compress_public_key(nodeData['publicKey']) depth = len(bip32_intpath) return BIP32Node(xtype=xtype,