electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 21c35726009295412b0d5d51e8dff6c2ac064842
parent 53a5a21ee89b791c351ad39a47e69837ed347af8
Author: ghost43 <somber.night@protonmail.com>
Date:   Tue,  8 Sep 2020 15:52:53 +0000

hardware devices: run all device communication on dedicated thread (#6561)

hidapi/libusb etc are not thread-safe.

related: #6554
Diffstat:
Melectrum/plugin.py | 84+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Melectrum/plugins/bitbox02/bitbox02.py | 35+++++++++++++++++++++++------------
Melectrum/plugins/coldcard/coldcard.py | 30+++++++++++++++++++-----------
Melectrum/plugins/digitalbitbox/digitalbitbox.py | 36+++++++++++++++---------------------
Melectrum/plugins/hw_wallet/plugin.py | 6+++++-
Melectrum/plugins/keepkey/clientbase.py | 12++++++++++++
Melectrum/plugins/keepkey/keepkey.py | 14+++++++++++++-
Melectrum/plugins/ledger/ledger.py | 27++++++++++++++++++---------
Melectrum/plugins/safe_t/clientbase.py | 12++++++++++++
Melectrum/plugins/safe_t/safe_t.py | 10+++++++++-
Melectrum/plugins/trezor/clientbase.py | 17+++++++++++++++++
Melectrum/plugins/trezor/trezor.py | 9++++++++-
12 files changed, 195 insertions(+), 97 deletions(-)

diff --git a/electrum/plugin.py b/electrum/plugin.py @@ -29,9 +29,10 @@ import time import threading import sys from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, - Dict, Iterable, List, Sequence) + Dict, Iterable, List, Sequence, Callable, TypeVar) import concurrent from concurrent import futures +from functools import wraps, partial from .i18n import _ from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) @@ -334,11 +335,37 @@ PLACEHOLDER_HW_CLIENT_LABELS = {None, "", " "} # https://github.com/signal11/hidapi/pull/414#issuecomment-445164238 # It is not entirely clear to me, exactly what is safe and what isn't, when # using multiple threads... -# For now, we use a dedicated thread to enumerate devices (_hid_executor), -# and we synchronize all device opens/closes/enumeration (_hid_lock). -# FIXME there are still probably threading issues with how we use hidapi... -_hid_executor = None # type: Optional[concurrent.futures.Executor] -_hid_lock = threading.Lock() +# Hence, we use a single thread for all device communications, including +# enumeration. Everything that uses hidapi, libusb, etc, MUST run on +# the following thread: +_hwd_comms_executor = concurrent.futures.ThreadPoolExecutor( + max_workers=1, + thread_name_prefix='hwd_comms_thread' +) + + +T = TypeVar('T') + + +def run_in_hwd_thread(func: Callable[[], T]) -> T: + if threading.current_thread().name.startswith("hwd_comms_thread"): + return func() + else: + fut = _hwd_comms_executor.submit(func) + return fut.result() + #except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e: + + +def runs_in_hwd_thread(func): + @wraps(func) + def wrapper(*args, **kwargs): + return run_in_hwd_thread(partial(func, *args, **kwargs)) + return wrapper + + +def assert_runs_in_hwd_thread(): + if not threading.current_thread().name.startswith("hwd_comms_thread"): + raise Exception("must only be called from HWD communication thread") class DeviceMgr(ThreadJob): @@ -384,24 +411,11 @@ class DeviceMgr(ThreadJob): self._recognised_hardware = {} # type: Dict[Tuple[int, int], HW_PluginBase] # Custom enumerate functions for devices we don't know about. self._enumerate_func = set() # Needs self.lock. - # locks: if you need to take multiple ones, acquire them in the order they are defined here! - self._scan_lock = threading.RLock() + self.lock = threading.RLock() - self.hid_lock = _hid_lock self.config = config - global _hid_executor - if _hid_executor is None: - _hid_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1, - thread_name_prefix='hid_enumerate_thread') - - def with_scan_lock(func): - def func_wrapper(self: 'DeviceMgr', *args, **kwargs): - with self._scan_lock: - return func(self, *args, **kwargs) - return func_wrapper - def thread_jobs(self): # Thread job to handle device timeouts return [self] @@ -423,6 +437,7 @@ class DeviceMgr(ThreadJob): with self.lock: self._enumerate_func.add(func) + @runs_in_hwd_thread def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']: # Get from cache first @@ -452,7 +467,7 @@ class DeviceMgr(ThreadJob): if xpub not in self.xpub_ids: return _id = self.xpub_ids.pop(xpub) - self._close_client(_id) + self._close_client(_id) def unpair_id(self, id_): xpub = self.xpub_by_id(id_) @@ -462,8 +477,9 @@ class DeviceMgr(ThreadJob): self._close_client(id_) def _close_client(self, id_): - client = self._client_by_id(id_) - self.clients.pop(client, None) + with self.lock: + client = self._client_by_id(id_) + self.clients.pop(client, None) if client: client.close() @@ -486,7 +502,7 @@ class DeviceMgr(ThreadJob): self.scan_devices() return self._client_by_id(id_) - @with_scan_lock + @runs_in_hwd_thread def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'], keystore: 'Hardware_KeyStore', force_pair: bool, *, @@ -655,25 +671,15 @@ class DeviceMgr(ThreadJob): # note: updated label/soft_device_id will be saved after pairing succeeds return info - @with_scan_lock + @runs_in_hwd_thread def _scan_devices_with_hid(self) -> List['Device']: try: import hid except ImportError: return [] - def hid_enumerate(): - with self.hid_lock: - return hid.enumerate(0, 0) - - hid_list_fut = _hid_executor.submit(hid_enumerate) - try: - hid_list = hid_list_fut.result() - except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e: - return [] - devices = [] - for d in hid_list: + for d in hid.enumerate(0, 0): product_key = (d['vendor_id'], d['product_id']) if product_key in self._recognised_hardware: plugin = self._recognised_hardware[product_key] @@ -681,7 +687,7 @@ class DeviceMgr(ThreadJob): devices.append(device) return devices - @with_scan_lock + @runs_in_hwd_thread @profiler def scan_devices(self) -> Sequence['Device']: self.logger.info("scanning devices...") @@ -693,10 +699,8 @@ class DeviceMgr(ThreadJob): with self.lock: enumerate_funcs = list(self._enumerate_func) for f in enumerate_funcs: - # custom enumerate functions might use hidapi, so use hid thread to be safe - new_devices_fut = _hid_executor.submit(f) try: - new_devices = new_devices_fut.result() + new_devices = f() except BaseException as e: self.logger.error('custom device enum failed. func {}, error {}' .format(str(f), repr(e))) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py @@ -13,7 +13,7 @@ from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wall from electrum.util import bh2u, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard from electrum.logging import get_logger -from electrum.plugin import Device, DeviceInfo +from electrum.plugin import Device, DeviceInfo, runs_in_hwd_thread from electrum.simple_config import SimpleConfig from electrum.json_db import StoredDict from electrum.storage import get_derivation_used_for_hw_device_encryption @@ -73,18 +73,19 @@ class BitBox02Client(HardwareClientBase): def is_initialized(self) -> bool: return True + @runs_in_hwd_thread def close(self): - with self.device_manager().hid_lock: - try: - self.bitbox02_device.close() - except: - pass + try: + self.bitbox02_device.close() + except: + pass def has_usable_connection_with_device(self) -> bool: if self.bitbox_hid_info is None: return False return True + @runs_in_hwd_thread def get_soft_device_id(self) -> Optional[str]: if self.handler is None: # Can't do the pairing without the handler. This happens at wallet creation time, when @@ -94,6 +95,7 @@ class BitBox02Client(HardwareClientBase): self.pairing_dialog() return self.bitbox02_device.root_fingerprint().hex() + @runs_in_hwd_thread def pairing_dialog(self): def pairing_step(code: str, device_response: Callable[[], bool]) -> bool: msg = "Please compare and confirm the pairing code on your BitBox02:\n" + code @@ -102,8 +104,7 @@ class BitBox02Client(HardwareClientBase): res = device_response() except: # Close the hid device on exception - with self.device_manager().hid_lock: - hid_device.close() + hid_device.close() raise finally: self.handler.finished() @@ -167,10 +168,8 @@ class BitBox02Client(HardwareClientBase): return set_noise_privkey(privkey) if self.bitbox02_device is None: - with self.device_manager().hid_lock: - hid_device = hid.device() - hid_device.open_path(self.bitbox_hid_info["path"]) - + hid_device = hid.device() + hid_device.open_path(self.bitbox_hid_info["path"]) bitbox02_device = bitbox02.BitBox02( transport=u2fhid.U2FHid(hid_device), @@ -197,6 +196,7 @@ class BitBox02Client(HardwareClientBase): return bitbox02.btc.TBTC return bitbox02.btc.BTC + @runs_in_hwd_thread def get_password_for_storage_encryption(self) -> str: derivation = get_derivation_used_for_hw_device_encryption() derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation) @@ -204,6 +204,7 @@ class BitBox02Client(HardwareClientBase): node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(()) return node.eckey.get_public_key_bytes(compressed=True).hex() + @runs_in_hwd_thread def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str: if self.bitbox02_device is None: self.pairing_dialog() @@ -244,6 +245,7 @@ class BitBox02Client(HardwareClientBase): display=display, ) + @runs_in_hwd_thread def label(self) -> str: if self.handler is None: # Can't do the pairing without the handler. This happens at wallet creation time, when @@ -258,6 +260,7 @@ class BitBox02Client(HardwareClientBase): self.bitbox02_device.root_fingerprint().hex(), ) + @runs_in_hwd_thread def request_root_fingerprint_from_device(self) -> str: if self.bitbox02_device is None: raise Exception( @@ -271,6 +274,7 @@ class BitBox02Client(HardwareClientBase): return False return True + @runs_in_hwd_thread def btc_multisig_config( self, coin, bip32_path: List[int], wallet: Multisig_Wallet ): @@ -316,6 +320,7 @@ class BitBox02Client(HardwareClientBase): raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02") return multisig_config + @runs_in_hwd_thread def show_address( self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet ) -> str: @@ -357,6 +362,7 @@ class BitBox02Client(HardwareClientBase): display=True, ) + @runs_in_hwd_thread def sign_transaction( self, keystore: Hardware_KeyStore, @@ -553,6 +559,7 @@ class BitBox02_KeyStore(Hardware_KeyStore): ).format(self.device) ) + @runs_in_hwd_thread def sign_transaction(self, tx: PartialTransaction, password: str): if tx.is_complete(): return @@ -572,6 +579,7 @@ class BitBox02_KeyStore(Hardware_KeyStore): self.give_error(e, True) return + @runs_in_hwd_thread def show_address( self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet ): @@ -616,6 +624,7 @@ class BitBox02Plugin(HW_PluginBase): raise ImportError() # handler is a BitBox02_Handler + @runs_in_hwd_thread def create_client(self, device: Device, handler: Any) -> BitBox02Client: if not handler: self.handler = handler @@ -645,6 +654,7 @@ class BitBox02Plugin(HW_PluginBase): assert client.bitbox02_device is not None return client.get_xpub(derivation, xtype) + @runs_in_hwd_thread def show_address( self, wallet: Deterministic_Wallet, @@ -660,6 +670,7 @@ class BitBox02Plugin(HW_PluginBase): sequence = wallet.get_address_index(address) keystore.show_address(sequence, txin_type, wallet) + @runs_in_hwd_thread def show_xpub(self, keystore: BitBox02_KeyStore): client = keystore.get_client() assert isinstance(client, BitBox02Client) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py @@ -10,7 +10,7 @@ import struct from electrum import bip32 from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes from electrum.i18n import _ -from electrum.plugin import Device, hook +from electrum.plugin import Device, hook, runs_in_hwd_thread from electrum.keystore import Hardware_KeyStore, KeyStoreWithMPK from electrum.transaction import PartialTransaction from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet @@ -72,9 +72,8 @@ class CKCCClient(HardwareClientBase): self.dev = ElectrumColdcardDevice(dev_path, encrypt=True) else: # open the real HID device - with self.device_manager().hid_lock: - hd = hid.device(path=dev_path) - hd.open_path(dev_path) + hd = hid.device(path=dev_path) + hd.open_path(dev_path) self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True) @@ -85,6 +84,7 @@ class CKCCClient(HardwareClientBase): return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint), self.label()) + @runs_in_hwd_thread def verify_connection(self, expected_xfp: int, expected_xpub=None): ex = (expected_xfp, expected_xpub) @@ -121,14 +121,10 @@ class CKCCClient(HardwareClientBase): # can't do anything w/ devices that aren't setup (this code not normally reachable) return bool(self.dev.master_xpub) - def timeout(self, cutoff): - # nothing to do? - pass - + @runs_in_hwd_thread def close(self): # close the HID device (so can be reused) - with self.device_manager().hid_lock: - self.dev.close() + self.dev.close() self.dev = None def is_initialized(self): @@ -160,6 +156,7 @@ class CKCCClient(HardwareClientBase): return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub) + @runs_in_hwd_thread def has_usable_connection_with_device(self): # Do end-to-end ping test try: @@ -168,6 +165,7 @@ class CKCCClient(HardwareClientBase): except: return False + @runs_in_hwd_thread def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES _logger.info('Derive xtype = %r' % xtype) @@ -183,6 +181,7 @@ class CKCCClient(HardwareClientBase): xpub = node._replace(xtype=xtype).to_xpub() return xpub + @runs_in_hwd_thread def ping_check(self): # check connection is working assert self.dev.session_key, 'not encrypted?' @@ -193,26 +192,32 @@ class CKCCClient(HardwareClientBase): except: raise RuntimeError("Communication trouble with Coldcard") + @runs_in_hwd_thread def show_address(self, path, addr_fmt): # prompt user w/ address, also returns it immediately. return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None) + @runs_in_hwd_thread def show_p2sh_address(self, *args, **kws): # prompt user w/ p2sh address, also returns it immediately. return self.dev.send_recv(CCProtocolPacker.show_p2sh_address(*args, **kws), timeout=None) + @runs_in_hwd_thread def get_version(self): # gives list of strings return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n') + @runs_in_hwd_thread def sign_message_start(self, path, msg): # this starts the UX experience. self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None) + @runs_in_hwd_thread def sign_message_poll(self): # poll device... if user has approved, will get tuple: (addr, sig) else None return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None) + @runs_in_hwd_thread def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False): # Multiple steps to sign: # - upload binary @@ -228,10 +233,12 @@ class CKCCClient(HardwareClientBase): if resp != None: raise ValueError(resp) + @runs_in_hwd_thread def sign_transaction_poll(self): # poll device... if user has approved, will get tuple: (legnth, checksum) else None return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) + @runs_in_hwd_thread def download_file(self, length, checksum, file_number=1): # get a file return self.dev.download_file(length, checksum, file_number=file_number) @@ -317,7 +324,6 @@ class Coldcard_KeyStore(Hardware_KeyStore): % MSG_SIGNING_MAX_LENGTH) return b'' - client = self.get_client() path = self.get_derivation_prefix() + ("/%d/%d" % sequence) try: cl = self.get_client() @@ -508,6 +514,7 @@ class ColdcardPlugin(HW_PluginBase): return [] + @runs_in_hwd_thread def create_client(self, device, handler): if handler: self.handler = handler @@ -539,6 +546,7 @@ class ColdcardPlugin(HW_PluginBase): xpub = client.get_xpub(derivation, xtype) return xpub + @runs_in_hwd_thread def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True) -> Optional['CKCCClient']: # Acquire a connection to the hardware device (via USB) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -30,6 +30,7 @@ from electrum.util import to_string, UserCancelled, UserFacingException, bfh from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from electrum.network import Network from electrum.logging import get_logger +from electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread from ..hw_wallet import HW_PluginBase, HardwareClientBase @@ -74,21 +75,16 @@ class DigitalBitbox_Client(HardwareClientBase): self.setupRunning = False self.usbReportSize = 64 # firmware > v2.0.0 - + @runs_in_hwd_thread def close(self): if self.opened: - with self.device_manager().hid_lock: - try: - self.dbb_hid.close() - except: - pass + try: + self.dbb_hid.close() + except: + pass self.opened = False - def timeout(self, cutoff): - pass - - def is_pairable(self): return True @@ -111,7 +107,6 @@ class DigitalBitbox_Client(HardwareClientBase): if self.check_device_dialog(): return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8')) - def get_xpub(self, bip32_path, xtype): assert xtype in self.plugin.SUPPORTED_XTYPES reply = self._get_xpub(bip32_path) @@ -171,9 +166,9 @@ class DigitalBitbox_Client(HardwareClientBase): self.password = password.encode('utf8') return True - def check_device_dialog(self): - match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+', self.dbb_hid.get_serial_number_string()) + match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+', + run_in_hwd_thread(self.dbb_hid.get_serial_number_string)) if match is None: raise Exception("error detecting firmware version") major_version = int(match.group(1)) @@ -350,7 +345,7 @@ class DigitalBitbox_Client(HardwareClientBase): raise UserFacingException(hid_reply['error']['message']) return True - + @runs_in_hwd_thread def hid_send_frame(self, data): HWW_CID = 0xFF000000 HWW_CMD = 0x80 + 0x40 + 0x01 @@ -370,7 +365,7 @@ class DigitalBitbox_Client(HardwareClientBase): seq += 1 idx += len(write) - + @runs_in_hwd_thread def hid_read_frame(self): # INIT response read = bytearray(self.dbb_hid.read(self.usbReportSize)) @@ -386,7 +381,7 @@ class DigitalBitbox_Client(HardwareClientBase): idx += len(read) - 5 return data - + @runs_in_hwd_thread def hid_send_plain(self, msg): reply = "" try: @@ -408,7 +403,7 @@ class DigitalBitbox_Client(HardwareClientBase): _logger.info(f'Exception caught {repr(e)}') return reply - + @runs_in_hwd_thread def hid_send_encrypt(self, msg): sha256_byte_len = 32 reply = "" @@ -680,11 +675,10 @@ class DigitalBitboxPlugin(HW_PluginBase): self.digitalbitbox_config = self.config.get('digitalbitbox', {}) - + @runs_in_hwd_thread def get_dbb_device(self, device): - with self.device_manager().hid_lock: - dev = hid.device() - dev.open_path(device.path) + dev = hid.device() + dev.open_path(device.path) return dev diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py @@ -27,7 +27,8 @@ from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type from functools import partial -from electrum.plugin import BasePlugin, hook, Device, DeviceMgr, DeviceInfo +from electrum.plugin import (BasePlugin, hook, Device, DeviceMgr, DeviceInfo, + assert_runs_in_hwd_thread, runs_in_hwd_thread) from electrum.i18n import _ from electrum.bitcoin import is_address, opcodes from electrum.util import bfh, versiontuple, UserFacingException @@ -197,6 +198,7 @@ class HardwareClientBase: handler = None # type: Optional['HardwareHandlerBase'] def __init__(self, *, plugin: 'HW_PluginBase'): + assert_runs_in_hwd_thread() self.plugin = plugin def device_manager(self) -> 'DeviceMgr': @@ -242,6 +244,7 @@ class HardwareClientBase: def get_xpub(self, bip32_path: str, xtype) -> str: raise NotImplementedError() + @runs_in_hwd_thread def request_root_fingerprint_from_device(self) -> str: # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths # so ask for a direct child, and read out fingerprint from that: @@ -249,6 +252,7 @@ class HardwareClientBase: root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower() return root_fingerprint + @runs_in_hwd_thread def get_password_for_storage_encryption(self) -> str: # note: using a different password based on hw device type is highly undesirable! see #5993 derivation = get_derivation_used_for_hw_device_encryption() diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py @@ -8,6 +8,7 @@ from electrum.util import UserCancelled from electrum.keystore import bip39_normalize_passphrase from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 from electrum.logging import Logger +from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase @@ -129,6 +130,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): def is_pairable(self): return not self.features.bootloader_mode + @runs_in_hwd_thread def has_usable_connection_with_device(self): try: res = self.ping("electrum pinging device") @@ -143,6 +145,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): def prevent_timeouts(self): self.last_operation = float('inf') + @runs_in_hwd_thread def timeout(self, cutoff): '''Time out the client if the last operation was before cutoff.''' if self.last_operation < cutoff: @@ -153,6 +156,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): def expand_path(n): return convert_bip32_path_to_list_of_uint32(n) + @runs_in_hwd_thread def cancel(self): '''Provided here as in keepkeylib but not trezorlib.''' self.transport.write(self.proto.Cancel()) @@ -160,6 +164,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): def i4b(self, x): return pack('>I', x) + @runs_in_hwd_thread def get_xpub(self, bip32_path, xtype): address_n = self.expand_path(bip32_path) creating = False @@ -171,6 +176,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): fingerprint=self.i4b(node.fingerprint), child_number=self.i4b(node.child_num)).to_xpub() + @runs_in_hwd_thread def toggle_passphrase(self): if self.features.passphrase_protection: self.msg = _("Confirm on your {} device to disable passphrases") @@ -179,14 +185,17 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): enabled = not self.features.passphrase_protection self.apply_settings(use_passphrase=enabled) + @runs_in_hwd_thread def change_label(self, label): self.msg = _("Confirm the new label on your {} device") self.apply_settings(label=label) + @runs_in_hwd_thread def change_homescreen(self, homescreen): self.msg = _("Confirm on your {} device to change your home screen") self.apply_settings(homescreen=homescreen) + @runs_in_hwd_thread def set_pin(self, remove): if remove: self.msg = _("Confirm on your {} device to disable PIN protection") @@ -196,6 +205,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): self.msg = _("Confirm on your {} device to set a PIN") self.change_pin(remove) + @runs_in_hwd_thread def clear_session(self): '''Clear the session to force pin (and passphrase if enabled) re-entry. Does not leak exceptions.''' @@ -207,10 +217,12 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): # If the device was removed it has the same effect... self.logger.info(f"clear_session: ignoring error {e}") + @runs_in_hwd_thread def get_public_node(self, address_n, creating): self.creating_wallet = creating return super(KeepKeyClientBase, self).get_public_node(address_n) + @runs_in_hwd_thread def close(self): '''Called when Our wallet was closed or the device removed.''' self.logger.info("closing client") diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -9,7 +9,7 @@ from electrum import constants from electrum.i18n import _ from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from electrum.keystore import Hardware_KeyStore -from electrum.plugin import Device +from electrum.plugin import Device, runs_in_hwd_thread from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase @@ -37,6 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): def decrypt_message(self, sequence, message, password): raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) + @runs_in_hwd_thread def sign_message(self, sequence, message, password): client = self.get_client() address_path = self.get_derivation_prefix() + "/%d/%d"%sequence @@ -44,6 +45,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) return msg_sig.signature + @runs_in_hwd_thread def sign_transaction(self, tx, password): if tx.is_complete(): return @@ -95,6 +97,7 @@ class KeepKeyPlugin(HW_PluginBase): except ImportError: self.libraries_available = False + @runs_in_hwd_thread def enumerate(self): from keepkeylib.transport_webusb import WebUsbTransport results = [] @@ -112,16 +115,19 @@ class KeepKeyPlugin(HW_PluginBase): def _dev_to_str(dev: "usb1.USBDevice") -> str: return ":".join(str(x) for x in ["%03i" % (dev.getBusNumber(),)] + dev.getPortNumberList()) + @runs_in_hwd_thread def hid_transport(self, pair): from keepkeylib.transport_hid import HidTransport return HidTransport(pair) + @runs_in_hwd_thread def webusb_transport(self, device): from keepkeylib.transport_webusb import WebUsbTransport for dev in WebUsbTransport.enumerate(): if device.path == self._dev_to_str(dev): return WebUsbTransport(dev) + @runs_in_hwd_thread def _try_hid(self, device): self.logger.info("Trying to connect over USB...") if device.interface_number == 1: @@ -137,6 +143,7 @@ class KeepKeyPlugin(HW_PluginBase): self.logger.info(f"cannot connect at {device.path} {e}") return None + @runs_in_hwd_thread def _try_webusb(self, device): self.logger.info("Trying to connect over WebUSB...") try: @@ -145,6 +152,7 @@ class KeepKeyPlugin(HW_PluginBase): self.logger.info(f"cannot connect at {device.path} {e}") return None + @runs_in_hwd_thread def create_client(self, device, handler): if device.product_key[1] == 2: transport = self._try_webusb(device) @@ -179,6 +187,7 @@ class KeepKeyPlugin(HW_PluginBase): return client + @runs_in_hwd_thread def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True) -> Optional['KeepKeyClient']: client = super().get_client(keystore, force_pair, @@ -236,6 +245,7 @@ class KeepKeyPlugin(HW_PluginBase): finally: wizard.loop.exit(exit_code) + @runs_in_hwd_thread def _initialize_device(self, settings, method, device_id, wizard, handler): item, label, pin_protection, passphrase_protection = settings @@ -315,6 +325,7 @@ class KeepKeyPlugin(HW_PluginBase): return self.types.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) + @runs_in_hwd_thread def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): self.prev_tx = prev_tx client = self.get_client(keystore) @@ -325,6 +336,7 @@ class KeepKeyPlugin(HW_PluginBase): signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures) + @runs_in_hwd_thread def show_address(self, wallet, address, keystore=None): if keystore is None: keystore = wallet.get_keystore() diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -16,6 +16,7 @@ from electrum.wallet import Standard_Wallet from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from electrum.logging import get_logger +from electrum.plugin import runs_in_hwd_thread from ..hw_wallet import HW_PluginBase, HardwareClientBase from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable @@ -74,16 +75,14 @@ class Ledger_Client(HardwareClientBase): def is_pairable(self): return True + @runs_in_hwd_thread def close(self): - with self.device_manager().hid_lock: - self.dongleObject.dongle.close() - - def timeout(self, cutoff): - pass + self.dongleObject.dongle.close() def is_initialized(self): return True + @runs_in_hwd_thread def get_soft_device_id(self): if self._soft_device_id is None: # modern ledger can provide xpub without user interaction @@ -106,6 +105,7 @@ class Ledger_Client(HardwareClientBase): return "Ledger Nano X" return None + @runs_in_hwd_thread def has_usable_connection_with_device(self): try: self.dongleObject.getFirmwareVersion() @@ -113,6 +113,7 @@ class Ledger_Client(HardwareClientBase): return False return True + @runs_in_hwd_thread @test_pin_unlocked def get_xpub(self, bip32_path, xtype): self.checkDevice() @@ -180,6 +181,7 @@ class Ledger_Client(HardwareClientBase): def supports_segwit_trustedInputs(self): return self.segwitTrustedInputs + @runs_in_hwd_thread def perform_hw1_preflight(self): try: firmwareInfo = self.dongleObject.getFirmwareVersion() @@ -224,6 +226,7 @@ class Ledger_Client(HardwareClientBase): "Please make sure that 'Browser support' is disabled on your device.") raise e + @runs_in_hwd_thread def checkDevice(self): if not self.preflightDone: try: @@ -290,6 +293,7 @@ class Ledger_KeyStore(Hardware_KeyStore): def decrypt_message(self, pubkey, message, password): raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) + @runs_in_hwd_thread @test_pin_unlocked @set_and_unset_signing def sign_message(self, sequence, message, password): @@ -336,6 +340,7 @@ class Ledger_KeyStore(Hardware_KeyStore): # And convert it return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s + @runs_in_hwd_thread @test_pin_unlocked @set_and_unset_signing def sign_transaction(self, tx, password): @@ -536,6 +541,7 @@ class Ledger_KeyStore(Hardware_KeyStore): finally: self.handler.finished() + @runs_in_hwd_thread @test_pin_unlocked @set_and_unset_signing def show_address(self, sequence, txin_type): @@ -607,6 +613,7 @@ class LedgerPlugin(HW_PluginBase): else: raise LibraryFoundButUnusable(library_version=version) + @runs_in_hwd_thread def get_btchip_device(self, device): ledger = False if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c: @@ -618,12 +625,12 @@ class LedgerPlugin(HW_PluginBase): ledger = True else: return None # non-compatible interface of a Nano S or Blue - with self.device_manager().hid_lock: - dev = hid.device() - dev.open_path(device.path) - dev.set_nonblocking(True) + dev = hid.device() + dev.open_path(device.path) + dev.set_nonblocking(True) return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) + @runs_in_hwd_thread def create_client(self, device, handler): if handler: self.handler = handler @@ -648,6 +655,7 @@ class LedgerPlugin(HW_PluginBase): xpub = client.get_xpub(derivation, xtype) return xpub + @runs_in_hwd_thread def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True): # All client interaction should not be in the main GUI thread @@ -661,6 +669,7 @@ class LedgerPlugin(HW_PluginBase): client.checkDevice() return client + @runs_in_hwd_thread def show_address(self, wallet, address, keystore=None): if keystore is None: keystore = wallet.get_keystore() diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py @@ -8,6 +8,7 @@ from electrum.util import UserCancelled from electrum.keystore import bip39_normalize_passphrase from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 from electrum.logging import Logger +from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase @@ -131,6 +132,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): def is_pairable(self): return not self.features.bootloader_mode + @runs_in_hwd_thread def has_usable_connection_with_device(self): try: res = self.ping("electrum pinging device") @@ -145,6 +147,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): def prevent_timeouts(self): self.last_operation = float('inf') + @runs_in_hwd_thread def timeout(self, cutoff): '''Time out the client if the last operation was before cutoff.''' if self.last_operation < cutoff: @@ -155,6 +158,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): def expand_path(n): return convert_bip32_path_to_list_of_uint32(n) + @runs_in_hwd_thread def cancel(self): '''Provided here as in keepkeylib but not safetlib.''' self.transport.write(self.proto.Cancel()) @@ -162,6 +166,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): def i4b(self, x): return pack('>I', x) + @runs_in_hwd_thread def get_xpub(self, bip32_path, xtype): address_n = self.expand_path(bip32_path) creating = False @@ -173,6 +178,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): fingerprint=self.i4b(node.fingerprint), child_number=self.i4b(node.child_num)).to_xpub() + @runs_in_hwd_thread def toggle_passphrase(self): if self.features.passphrase_protection: self.msg = _("Confirm on your {} device to disable passphrases") @@ -181,14 +187,17 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): enabled = not self.features.passphrase_protection self.apply_settings(use_passphrase=enabled) + @runs_in_hwd_thread def change_label(self, label): self.msg = _("Confirm the new label on your {} device") self.apply_settings(label=label) + @runs_in_hwd_thread def change_homescreen(self, homescreen): self.msg = _("Confirm on your {} device to change your home screen") self.apply_settings(homescreen=homescreen) + @runs_in_hwd_thread def set_pin(self, remove): if remove: self.msg = _("Confirm on your {} device to disable PIN protection") @@ -198,6 +207,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): self.msg = _("Confirm on your {} device to set a PIN") self.change_pin(remove) + @runs_in_hwd_thread def clear_session(self): '''Clear the session to force pin (and passphrase if enabled) re-entry. Does not leak exceptions.''' @@ -209,10 +219,12 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): # If the device was removed it has the same effect... self.logger.info(f"clear_session: ignoring error {e}") + @runs_in_hwd_thread def get_public_node(self, address_n, creating): self.creating_wallet = creating return super(SafeTClientBase, self).get_public_node(address_n) + @runs_in_hwd_thread def close(self): '''Called when Our wallet was closed or the device removed.''' self.logger.info("closing client") diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py @@ -7,7 +7,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ -from electrum.plugin import Device +from electrum.plugin import Device, runs_in_hwd_thread from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported @@ -35,6 +35,7 @@ class SafeTKeyStore(Hardware_KeyStore): def decrypt_message(self, sequence, message, password): raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) + @runs_in_hwd_thread def sign_message(self, sequence, message, password): client = self.get_client() address_path = self.get_derivation_prefix() + "/%d/%d"%sequence @@ -42,6 +43,7 @@ class SafeTKeyStore(Hardware_KeyStore): msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) return msg_sig.signature + @runs_in_hwd_thread def sign_transaction(self, tx, password): if tx.is_complete(): return @@ -96,6 +98,7 @@ class SafeTPlugin(HW_PluginBase): except AttributeError: return 'unknown' + @runs_in_hwd_thread def enumerate(self): devices = self.transport_handler.enumerate_devices() return [Device(path=d.get_path(), @@ -106,6 +109,7 @@ class SafeTPlugin(HW_PluginBase): transport_ui_string=d.get_path()) for d in devices] + @runs_in_hwd_thread def create_client(self, device, handler): try: self.logger.info(f"connecting to device at {device.path}") @@ -141,6 +145,7 @@ class SafeTPlugin(HW_PluginBase): return client + @runs_in_hwd_thread def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True) -> Optional['SafeTClient']: client = super().get_client(keystore, force_pair, @@ -198,6 +203,7 @@ class SafeTPlugin(HW_PluginBase): finally: wizard.loop.exit(exit_code) + @runs_in_hwd_thread def _initialize_device(self, settings, method, device_id, wizard, handler): item, label, pin_protection, passphrase_protection = settings @@ -289,6 +295,7 @@ class SafeTPlugin(HW_PluginBase): return self.types.OutputScriptType.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) + @runs_in_hwd_thread def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): self.prev_tx = prev_tx client = self.get_client(keystore) @@ -299,6 +306,7 @@ class SafeTPlugin(HW_PluginBase): signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures) + @runs_in_hwd_thread def show_address(self, wallet, address, keystore=None): if keystore is None: keystore = wallet.get_keystore() diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py @@ -7,6 +7,7 @@ from electrum.util import UserCancelled, UserFacingException from electrum.keystore import bip39_normalize_passphrase from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path from electrum.logging import Logger +from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE @@ -107,6 +108,7 @@ class TrezorClientBase(HardwareClientBase, Logger): def is_pairable(self): return not self.features.bootloader_mode + @runs_in_hwd_thread def has_usable_connection_with_device(self): if self.in_flow: return True @@ -123,6 +125,7 @@ class TrezorClientBase(HardwareClientBase, Logger): def prevent_timeouts(self): self.last_operation = float('inf') + @runs_in_hwd_thread def timeout(self, cutoff): '''Time out the client if the last operation was before cutoff.''' if self.last_operation < cutoff: @@ -132,6 +135,7 @@ class TrezorClientBase(HardwareClientBase, Logger): def i4b(self, x): return pack('>I', x) + @runs_in_hwd_thread def get_xpub(self, bip32_path, xtype, creating=False): address_n = parse_path(bip32_path) with self.run_flow(creating_wallet=creating): @@ -143,6 +147,7 @@ class TrezorClientBase(HardwareClientBase, Logger): fingerprint=self.i4b(node.fingerprint), child_number=self.i4b(node.child_num)).to_xpub() + @runs_in_hwd_thread def toggle_passphrase(self): if self.features.passphrase_protection: msg = _("Confirm on your {} device to disable passphrases") @@ -152,14 +157,17 @@ class TrezorClientBase(HardwareClientBase, Logger): with self.run_flow(msg): trezorlib.device.apply_settings(self.client, use_passphrase=enabled) + @runs_in_hwd_thread def change_label(self, label): with self.run_flow(_("Confirm the new label on your {} device")): trezorlib.device.apply_settings(self.client, label=label) + @runs_in_hwd_thread def change_homescreen(self, homescreen): with self.run_flow(_("Confirm on your {} device to change your home screen")): trezorlib.device.apply_settings(self.client, homescreen=homescreen) + @runs_in_hwd_thread def set_pin(self, remove): if remove: msg = _("Confirm on your {} device to disable PIN protection") @@ -170,6 +178,7 @@ class TrezorClientBase(HardwareClientBase, Logger): with self.run_flow(msg): trezorlib.device.change_pin(self.client, remove) + @runs_in_hwd_thread def clear_session(self): '''Clear the session to force pin (and passphrase if enabled) re-entry. Does not leak exceptions.''' @@ -181,11 +190,13 @@ class TrezorClientBase(HardwareClientBase, Logger): # If the device was removed it has the same effect... self.logger.info(f"clear_session: ignoring error {e}") + @runs_in_hwd_thread def close(self): '''Called when Our wallet was closed or the device removed.''' self.logger.info("closing client") self.clear_session() + @runs_in_hwd_thread def is_uptodate(self): if self.client.is_outdated(): return False @@ -203,6 +214,7 @@ class TrezorClientBase(HardwareClientBase, Logger): return "Trezor T" return None + @runs_in_hwd_thread def show_address(self, address_str, script_type, multisig=None): coin_name = self.plugin.get_coin_name() address_n = parse_path(address_str) @@ -215,6 +227,7 @@ class TrezorClientBase(HardwareClientBase, Logger): script_type=script_type, multisig=multisig) + @runs_in_hwd_thread def sign_message(self, address_str, message): coin_name = self.plugin.get_coin_name() address_n = parse_path(address_str) @@ -225,6 +238,7 @@ class TrezorClientBase(HardwareClientBase, Logger): address_n, message) + @runs_in_hwd_thread def recover_device(self, recovery_type, *args, **kwargs): input_callback = self.mnemonic_callback(recovery_type) with self.run_flow(): @@ -237,14 +251,17 @@ class TrezorClientBase(HardwareClientBase, Logger): # ========= Unmodified trezorlib methods ========= + @runs_in_hwd_thread def sign_tx(self, *args, **kwargs): with self.run_flow(): return trezorlib.btc.sign_tx(self.client, *args, **kwargs) + @runs_in_hwd_thread def reset_device(self, *args, **kwargs): with self.run_flow(): return trezorlib.device.reset(self.client, *args, **kwargs) + @runs_in_hwd_thread def wipe_device(self, *args, **kwargs): with self.run_flow(): return trezorlib.device.wipe(self.client, *args, **kwargs) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py @@ -6,7 +6,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path from electrum import constants from electrum.i18n import _ -from electrum.plugin import Device +from electrum.plugin import Device, runs_in_hwd_thread from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET @@ -143,6 +143,7 @@ class TrezorPlugin(HW_PluginBase): else: raise LibraryFoundButUnusable(library_version=version) + @runs_in_hwd_thread def is_bridge_available(self) -> bool: # Testing whether the Bridge is available can take several seconds # (when it is not), as it is slow to timeout, hence we cache it. @@ -157,6 +158,7 @@ class TrezorPlugin(HW_PluginBase): self._is_bridge_available = True return self._is_bridge_available + @runs_in_hwd_thread def enumerate(self): # If there is a bridge, prefer that. # On Windows, the bridge runs as Admin (and Electrum usually does not), @@ -174,6 +176,7 @@ class TrezorPlugin(HW_PluginBase): transport_ui_string=d.get_path()) for d in devices] + @runs_in_hwd_thread def create_client(self, device, handler): try: self.logger.info(f"connecting to device at {device.path}") @@ -190,6 +193,7 @@ class TrezorPlugin(HW_PluginBase): # note that this call can still raise! return TrezorClientBase(transport, handler, self) + @runs_in_hwd_thread def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True) -> Optional['TrezorClientBase']: client = super().get_client(keystore, force_pair, @@ -238,6 +242,7 @@ class TrezorPlugin(HW_PluginBase): finally: wizard.loop.exit(exit_code) + @runs_in_hwd_thread def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler): if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceType.ScrambledWords: handler.show_error(_( @@ -333,6 +338,7 @@ class TrezorPlugin(HW_PluginBase): return OutputScriptType.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) + @runs_in_hwd_thread def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() } client = self.get_client(keystore) @@ -343,6 +349,7 @@ class TrezorPlugin(HW_PluginBase): signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures) + @runs_in_hwd_thread def show_address(self, wallet, address, keystore=None): if keystore is None: keystore = wallet.get_keystore()