From de7419cc7965b83f6be666bcd0bef4197566fda3 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 2 Jan 2025 15:43:02 +0100 Subject: [PATCH 01/14] feat(python): introduce expect argument to client.call --- docs/developers/hello_world_feature_TT.md | 6 +++--- python/.changelog.d/4464.added.1 | 1 + python/src/trezorlib/client.py | 25 ++++++++++++++--------- python/src/trezorlib/exceptions.py | 12 ++++++++++- 4 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 python/.changelog.d/4464.added.1 diff --git a/docs/developers/hello_world_feature_TT.md b/docs/developers/hello_world_feature_TT.md index fffa7e0f60d..cd120fbb5db 100644 --- a/docs/developers/hello_world_feature_TT.md +++ b/docs/developers/hello_world_feature_TT.md @@ -154,7 +154,6 @@ if TYPE_CHECKING: from .protobuf import MessageType -@expect(messages.HelloWorldResponse, field="text", ret_type=str) def say_hello( client: "TrezorClient", name: str, @@ -166,8 +165,9 @@ def say_hello( name=name, amount=amount, show_display=show_display, - ) - ) + ), + expect=messages.HelloWorldResponse, + ).text ``` Code above is sending `HelloWorldRequest` into Trezor and is expecting to get `HelloWorldResponse` back (from which it extracts the `text` string as a response). diff --git a/python/.changelog.d/4464.added.1 b/python/.changelog.d/4464.added.1 new file mode 100644 index 00000000000..2bca4ba58bd --- /dev/null +++ b/python/.changelog.d/4464.added.1 @@ -0,0 +1 @@ +Added an `expect` argument to `TrezorClient.call()`, to enforce the returned message type. diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index c912ba00edf..fa7992ab0e4 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -14,6 +14,8 @@ # You should have received a copy of the License along with this library. # If not, see . +from __future__ import annotations + import logging import os import warnings @@ -24,14 +26,15 @@ from . import exceptions, mapping, messages, models from .log import DUMP_BYTES from .messages import Capability +from .protobuf import MessageType from .tools import expect, parse_path, session if TYPE_CHECKING: - from .protobuf import MessageType from .transport import Transport from .ui import TrezorClientUI UI = TypeVar("UI", bound="TrezorClientUI") +MT = TypeVar("MT", bound=MessageType) LOG = logging.getLogger(__name__) @@ -149,12 +152,12 @@ def close(self) -> None: def cancel(self) -> None: self._raw_write(messages.Cancel()) - def call_raw(self, msg: "MessageType") -> "MessageType": + def call_raw(self, msg: MessageType) -> MessageType: __tracebackhide__ = True # for pytest # pylint: disable=W0612 self._raw_write(msg) return self._raw_read() - def _raw_write(self, msg: "MessageType") -> None: + def _raw_write(self, msg: MessageType) -> None: __tracebackhide__ = True # for pytest # pylint: disable=W0612 LOG.debug( f"sending message: {msg.__class__.__name__}", @@ -167,7 +170,7 @@ def _raw_write(self, msg: "MessageType") -> None: ) self.transport.write(msg_type, msg_bytes) - def _raw_read(self) -> "MessageType": + def _raw_read(self) -> MessageType: __tracebackhide__ = True # for pytest # pylint: disable=W0612 msg_type, msg_bytes = self.transport.read() LOG.log( @@ -181,7 +184,7 @@ def _raw_read(self) -> "MessageType": ) return msg - def _callback_pin(self, msg: messages.PinMatrixRequest) -> "MessageType": + def _callback_pin(self, msg: messages.PinMatrixRequest) -> MessageType: try: pin = self.ui.get_pin(msg.type) except exceptions.Cancelled: @@ -204,12 +207,12 @@ def _callback_pin(self, msg: messages.PinMatrixRequest) -> "MessageType": else: return resp - def _callback_passphrase(self, msg: messages.PassphraseRequest) -> "MessageType": + def _callback_passphrase(self, msg: messages.PassphraseRequest) -> MessageType: available_on_device = Capability.PassphraseEntry in self.features.capabilities def send_passphrase( passphrase: Optional[str] = None, on_device: Optional[bool] = None - ) -> "MessageType": + ) -> MessageType: msg = messages.PassphraseAck(passphrase=passphrase, on_device=on_device) resp = self.call_raw(msg) if isinstance(resp, messages.Deprecated_PassphraseStateRequest): @@ -244,7 +247,7 @@ def send_passphrase( return send_passphrase(passphrase, on_device=False) - def _callback_button(self, msg: messages.ButtonRequest) -> "MessageType": + def _callback_button(self, msg: messages.ButtonRequest) -> MessageType: __tracebackhide__ = True # for pytest # pylint: disable=W0612 # do this raw - send ButtonAck first, notify UI later self._raw_write(messages.ButtonAck()) @@ -252,7 +255,7 @@ def _callback_button(self, msg: messages.ButtonRequest) -> "MessageType": return self._raw_read() @session - def call(self, msg: "MessageType") -> "MessageType": + def call(self, msg: MessageType, expect: type[MT] = MessageType) -> MT: self.check_firmware_version() resp = self.call_raw(msg) while True: @@ -266,6 +269,8 @@ def call(self, msg: "MessageType") -> "MessageType": if resp.code == messages.FailureType.ActionCancelled: raise exceptions.Cancelled raise exceptions.TrezorFailure(resp) + elif not isinstance(resp, expect): + raise exceptions.UnexpectedMessageError(expect, resp) else: return resp @@ -397,7 +402,7 @@ def ping( self, msg: str, button_protection: bool = False, - ) -> "MessageType": + ) -> MessageType: # We would like ping to work on any valid TrezorClient instance, but # due to the protection modes, we need to go through self.call, and that will # raise an exception if the firmware is too old. diff --git a/python/src/trezorlib/exceptions.py b/python/src/trezorlib/exceptions.py index fd7133d12f0..99f0048dd36 100644 --- a/python/src/trezorlib/exceptions.py +++ b/python/src/trezorlib/exceptions.py @@ -14,10 +14,13 @@ # You should have received a copy of the License along with this library. # If not, see . +from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: from .messages import Failure + from .protobuf import MessageType class TrezorException(Exception): @@ -25,7 +28,7 @@ class TrezorException(Exception): class TrezorFailure(TrezorException): - def __init__(self, failure: "Failure") -> None: + def __init__(self, failure: Failure) -> None: self.failure = failure self.code = failure.code self.message = failure.message @@ -55,3 +58,10 @@ class Cancelled(TrezorException): class OutdatedFirmwareError(TrezorException): pass + + +class UnexpectedMessageError(TrezorException): + def __init__(self, expected: type[MessageType], actual: MessageType) -> None: + self.expected = expected + self.actual = actual + super().__init__(f"Expected {expected.__name__} but Trezor sent {actual}") From f78b833b971342f210cf1ccfca3fc2a8f55ef2f3 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2025 11:57:42 +0100 Subject: [PATCH 02/14] fix(all): make more protobuf fields required [no changelog] --- common/protob/messages-management.proto | 2 +- common/protob/messages-monero.proto | 6 +++--- core/src/trezor/messages.py | 16 +++++++------- legacy/firmware/fsm_msg_coin.h | 1 - python/src/trezorlib/messages.py | 16 +++++++------- .../protos/generated/messages_management.rs | 7 +++++-- .../src/protos/generated/messages_monero.rs | 21 +++++++++++++------ 7 files changed, 40 insertions(+), 29 deletions(-) diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 170c02a8415..1533c9c21c6 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -651,7 +651,7 @@ message UnlockPath { * @next GetAddress */ message UnlockedPathRequest { - optional bytes mac = 1; // authentication code for future UnlockPath calls + required bytes mac = 1; // authentication code for future UnlockPath calls } /** diff --git a/common/protob/messages-monero.proto b/common/protob/messages-monero.proto index 30f13e05d4d..96661539d8f 100644 --- a/common/protob/messages-monero.proto +++ b/common/protob/messages-monero.proto @@ -98,7 +98,7 @@ message MoneroGetAddress { * @end */ message MoneroAddress { - optional bytes address = 1; + required bytes address = 1; } /** @@ -117,8 +117,8 @@ message MoneroGetWatchKey { * @end */ message MoneroWatchKey { - optional bytes watch_key = 1; - optional bytes address = 2; + required bytes watch_key = 1; + required bytes address = 2; } /** diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 2a3ce051256..e529707f4b5 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -2785,12 +2785,12 @@ def is_type_of(cls, msg: Any) -> TypeGuard["UnlockPath"]: return isinstance(msg, cls) class UnlockedPathRequest(protobuf.MessageType): - mac: "bytes | None" + mac: "bytes" def __init__( self, *, - mac: "bytes | None" = None, + mac: "bytes", ) -> None: pass @@ -4141,12 +4141,12 @@ def is_type_of(cls, msg: Any) -> TypeGuard["MoneroGetAddress"]: return isinstance(msg, cls) class MoneroAddress(protobuf.MessageType): - address: "bytes | None" + address: "bytes" def __init__( self, *, - address: "bytes | None" = None, + address: "bytes", ) -> None: pass @@ -4171,14 +4171,14 @@ def is_type_of(cls, msg: Any) -> TypeGuard["MoneroGetWatchKey"]: return isinstance(msg, cls) class MoneroWatchKey(protobuf.MessageType): - watch_key: "bytes | None" - address: "bytes | None" + watch_key: "bytes" + address: "bytes" def __init__( self, *, - watch_key: "bytes | None" = None, - address: "bytes | None" = None, + watch_key: "bytes", + address: "bytes", ) -> None: pass diff --git a/legacy/firmware/fsm_msg_coin.h b/legacy/firmware/fsm_msg_coin.h index 0f6bf7c2004..aae443a4233 100644 --- a/legacy/firmware/fsm_msg_coin.h +++ b/legacy/firmware/fsm_msg_coin.h @@ -862,7 +862,6 @@ void fsm_msgUnlockPath(const UnlockPath *msg) { unlock_path = msg->address_n[0]; resp->mac.size = SHA256_DIGEST_LENGTH; - resp->has_mac = true; msg_write(MessageType_MessageType_UnlockedPathRequest, resp); layoutHome(); } diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 06a36af68b3..6d28305f697 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -4000,13 +4000,13 @@ def __init__( class UnlockedPathRequest(protobuf.MessageType): MESSAGE_WIRE_TYPE = 94 FIELDS = { - 1: protobuf.Field("mac", "bytes", repeated=False, required=False, default=None), + 1: protobuf.Field("mac", "bytes", repeated=False, required=True), } def __init__( self, *, - mac: Optional["bytes"] = None, + mac: "bytes", ) -> None: self.mac = mac @@ -5568,13 +5568,13 @@ def __init__( class MoneroAddress(protobuf.MessageType): MESSAGE_WIRE_TYPE = 541 FIELDS = { - 1: protobuf.Field("address", "bytes", repeated=False, required=False, default=None), + 1: protobuf.Field("address", "bytes", repeated=False, required=True), } def __init__( self, *, - address: Optional["bytes"] = None, + address: "bytes", ) -> None: self.address = address @@ -5599,15 +5599,15 @@ def __init__( class MoneroWatchKey(protobuf.MessageType): MESSAGE_WIRE_TYPE = 543 FIELDS = { - 1: protobuf.Field("watch_key", "bytes", repeated=False, required=False, default=None), - 2: protobuf.Field("address", "bytes", repeated=False, required=False, default=None), + 1: protobuf.Field("watch_key", "bytes", repeated=False, required=True), + 2: protobuf.Field("address", "bytes", repeated=False, required=True), } def __init__( self, *, - watch_key: Optional["bytes"] = None, - address: Optional["bytes"] = None, + watch_key: "bytes", + address: "bytes", ) -> None: self.watch_key = watch_key self.address = address diff --git a/rust/trezor-client/src/protos/generated/messages_management.rs b/rust/trezor-client/src/protos/generated/messages_management.rs index 5fa5e2b3288..c070930f1a4 100644 --- a/rust/trezor-client/src/protos/generated/messages_management.rs +++ b/rust/trezor-client/src/protos/generated/messages_management.rs @@ -10709,7 +10709,7 @@ impl UnlockedPathRequest { ::std::default::Default::default() } - // optional bytes mac = 1; + // required bytes mac = 1; pub fn mac(&self) -> &[u8] { match self.mac.as_ref() { @@ -10765,6 +10765,9 @@ impl ::protobuf::Message for UnlockedPathRequest { const NAME: &'static str = "UnlockedPathRequest"; fn is_initialized(&self) -> bool { + if self.mac.is_none() { + return false; + } true } @@ -11724,7 +11727,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \n\x05Nonce\x12\x14\n\x05nonce\x18\x01\x20\x02(\x0cR\x05nonce:\x04\x88\ \xb2\x19\x01\";\n\nUnlockPath\x12\x1b\n\taddress_n\x18\x01\x20\x03(\rR\ \x08addressN\x12\x10\n\x03mac\x18\x02\x20\x01(\x0cR\x03mac\"'\n\x13Unloc\ - kedPathRequest\x12\x10\n\x03mac\x18\x01\x20\x01(\x0cR\x03mac\"\x14\n\x12\ + kedPathRequest\x12\x10\n\x03mac\x18\x01\x20\x02(\x0cR\x03mac\"\x14\n\x12\ ShowDeviceTutorial\"\x12\n\x10UnlockBootloader\"%\n\rSetBrightness\x12\ \x14\n\x05value\x18\x01\x20\x01(\rR\x05value*\x99\x01\n\nBackupType\x12\ \t\n\x05Bip39\x10\0\x12\x10\n\x0cSlip39_Basic\x10\x01\x12\x13\n\x0fSlip3\ diff --git a/rust/trezor-client/src/protos/generated/messages_monero.rs b/rust/trezor-client/src/protos/generated/messages_monero.rs index daf182a1415..ce7167305f2 100644 --- a/rust/trezor-client/src/protos/generated/messages_monero.rs +++ b/rust/trezor-client/src/protos/generated/messages_monero.rs @@ -2451,7 +2451,7 @@ impl MoneroAddress { ::std::default::Default::default() } - // optional bytes address = 1; + // required bytes address = 1; pub fn address(&self) -> &[u8] { match self.address.as_ref() { @@ -2507,6 +2507,9 @@ impl ::protobuf::Message for MoneroAddress { const NAME: &'static str = "MoneroAddress"; fn is_initialized(&self) -> bool { + if self.address.is_none() { + return false; + } true } @@ -2776,7 +2779,7 @@ impl MoneroWatchKey { ::std::default::Default::default() } - // optional bytes watch_key = 1; + // required bytes watch_key = 1; pub fn watch_key(&self) -> &[u8] { match self.watch_key.as_ref() { @@ -2812,7 +2815,7 @@ impl MoneroWatchKey { self.watch_key.take().unwrap_or_else(|| ::std::vec::Vec::new()) } - // optional bytes address = 2; + // required bytes address = 2; pub fn address(&self) -> &[u8] { match self.address.as_ref() { @@ -2873,6 +2876,12 @@ impl ::protobuf::Message for MoneroWatchKey { const NAME: &'static str = "MoneroWatchKey"; fn is_initialized(&self) -> bool { + if self.watch_key.is_none() { + return false; + } + if self.address.is_none() { + return false; + } true } @@ -11845,11 +11854,11 @@ static file_descriptor_proto_data: &'static [u8] = b"\ unt\x12\x14\n\x05minor\x18\x05\x20\x01(\rR\x05minor\x12\x1d\n\npayment_i\ d\x18\x06\x20\x01(\x0cR\tpaymentId\x12\x1a\n\x08chunkify\x18\x07\x20\x01\ (\x08R\x08chunkify\")\n\rMoneroAddress\x12\x18\n\x07address\x18\x01\x20\ - \x01(\x0cR\x07address\"\x8a\x01\n\x11MoneroGetWatchKey\x12\x1b\n\taddres\ + \x02(\x0cR\x07address\"\x8a\x01\n\x11MoneroGetWatchKey\x12\x1b\n\taddres\ s_n\x18\x01\x20\x03(\rR\x08addressN\x12X\n\x0cnetwork_type\x18\x02\x20\ \x01(\x0e2,.hw.trezor.messages.monero.MoneroNetworkType:\x07MAINNETR\x0b\ - networkType\"G\n\x0eMoneroWatchKey\x12\x1b\n\twatch_key\x18\x01\x20\x01(\ - \x0cR\x08watchKey\x12\x18\n\x07address\x18\x02\x20\x01(\x0cR\x07address\ + networkType\"G\n\x0eMoneroWatchKey\x12\x1b\n\twatch_key\x18\x01\x20\x02(\ + \x0cR\x08watchKey\x12\x18\n\x07address\x18\x02\x20\x02(\x0cR\x07address\ \"\xd1\x07\n\x1cMoneroTransactionInitRequest\x12\x18\n\x07version\x18\ \x01\x20\x01(\rR\x07version\x12\x1b\n\taddress_n\x18\x02\x20\x03(\rR\x08\ addressN\x12X\n\x0cnetwork_type\x18\x03\x20\x01(\x0e2,.hw.trezor.message\ From 86579646035cafaeb163af4b3ba6810fb1b34d7c Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2025 12:28:54 +0100 Subject: [PATCH 03/14] deprecation(python): deprecate @expect --- python/.changelog.d/4464.deprecated.2 | 1 + python/src/trezorlib/tools.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 python/.changelog.d/4464.deprecated.2 diff --git a/python/.changelog.d/4464.deprecated.2 b/python/.changelog.d/4464.deprecated.2 new file mode 100644 index 00000000000..1843512b8bd --- /dev/null +++ b/python/.changelog.d/4464.deprecated.2 @@ -0,0 +1 @@ +`@expect` decorator is deprecated -- use `TrezorClient.call(expect=...)` instead. diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py index 18f62061cfe..13a4d6d5cb5 100644 --- a/python/src/trezorlib/tools.py +++ b/python/src/trezorlib/tools.py @@ -14,11 +14,14 @@ # You should have received a copy of the License along with this library. # If not, see . +from __future__ import annotations + import functools import hashlib import re import struct import unicodedata +import warnings from typing import ( TYPE_CHECKING, Any, @@ -279,10 +282,16 @@ def expect( ret_type: "Optional[Type[R]]" = None, ) -> "Callable[[Callable[P, MessageType]], Callable[P, Union[MT, R]]]": """ - Decorator checks if the method - returned one of expected protobuf messages - or raises an exception + Decorator checks if the method returned one of expected protobuf messages or raises + an exception. + + Deprecated. Use `client.call(msg, expect=expected)` instead. """ + warnings.warn( + "Use `client.call(msg, expect=expected)` instead", + DeprecationWarning, + stacklevel=2, + ) def decorator(f: "Callable[P, MessageType]") -> "Callable[P, Union[MT, R]]": @functools.wraps(f) From 77e2505a7a7daf79b06c0c6212b77910175d1d50 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 9 Jan 2025 15:23:33 +0100 Subject: [PATCH 04/14] feat(python): introduce a deprecation helper it's a Python class that emits a DeprecationWarning if you try to use it for almost anything (two exceptions that can't be overriden from the wrapper: * isinstance(depr, SomeOtherClass) * depr is None) we will return instances of this class to indicate that a return value of something will be going away --- python/src/trezorlib/tools.py | 71 +++++++++++++++++++++++++++++++++++ python/tests/test_tools.py | 49 +++++++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py index 13a4d6d5cb5..5c5262f37dc 100644 --- a/python/src/trezorlib/tools.py +++ b/python/src/trezorlib/tools.py @@ -18,6 +18,7 @@ import functools import hashlib +import inspect import re import struct import unicodedata @@ -46,6 +47,7 @@ from typing_extensions import Concatenate, ParamSpec from . import client + from .messages import Success from .protobuf import MessageType MT = TypeVar("MT", bound=MessageType) @@ -310,6 +312,75 @@ def wrapped_f(*args: "P.args", **kwargs: "P.kwargs") -> "Union[MT, R]": return decorator +def _deprecation_retval_helper(value: Any, stacklevel: int = 0) -> Any: + stack = inspect.stack() + func_name = stack[stacklevel + 1].function + + warning_text = ( + f"The return value {value!r} of function {func_name}() " + "is deprecated and it will be removed in a future version." + ) + + # start with warnings disabled, otherwise we emit a lot of warnings while still + # constructing the deprecation warnings helper + warning_enabled = False + + def deprecation_warning_wrapper(orig_value: Callable[P, R]) -> Callable[P, R]: + def emit(*args: P.args, **kwargs: P.kwargs) -> R: + nonlocal warning_enabled + + if warning_enabled: + warnings.warn(warning_text, DeprecationWarning, stacklevel=2) + # only warn once per use + warning_enabled = False + return orig_value(*args, **kwargs) + + return emit + + # Deprecation wrapper class. + # Defined as empty at start. + class Deprecated(value.__class__): + pass + + # Here we install the deprecation_warning_wrapper for all dunder methods. + # This implicitly includes __getattribute__, which causes all non-dunder attribute + # accesses to also raise the warning. + for key in dir(value.__class__): + if not key.startswith("__"): + # skip non-dunder methods + continue + if key in ("__new__", "__init__", "__class__"): + # skip some problematic items + continue + orig_value = getattr(value.__class__, key) + if not callable(orig_value): + # skip non-functions + continue + # replace the method with a wrapper that emits a warning + setattr(Deprecated, key, deprecation_warning_wrapper(orig_value)) + + # construct an instance: + if isinstance(value, str): + # - for str, do it naively: + ret = Deprecated(value) + # (this branch should cover all builtin types, but we don't use the wrapper + # for anything other than str) + else: + # - for other types (we assume it's MessageType so a user-defined class) + # assign __class__ directly + value.__class__ = Deprecated + ret = value + + # enable warnings + warning_enabled = True + + return ret + + +def _return_success(msg: "Success") -> str | None: + return _deprecation_retval_helper(msg.message, stacklevel=1) + + def session( f: "Callable[Concatenate[TrezorClient, P], R]", ) -> "Callable[Concatenate[TrezorClient, P], R]": diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index 3bdda1fe976..a89d5a91f71 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -16,7 +16,7 @@ import pytest -from trezorlib import tools +from trezorlib import messages, tools VECTORS = ( # descriptor, checksum ( @@ -87,3 +87,50 @@ def test_b58encode(data_hex, encoding_b58): @pytest.mark.parametrize("data_hex,encoding_b58", BASE58_VECTORS) def test_b58decode(data_hex, encoding_b58): assert tools.b58decode(encoding_b58).hex() == data_hex + + +def test_return_success_deprecation(recwarn): + def mkfoo() -> str: + ret = tools._return_success(messages.Success(message="foo")) + assert ret is not None # too bad we can't hook "is None" check + return ret + + # check that just returning success will not cause a warning + mkfoo() + assert len(recwarn) == 0 + + with pytest.deprecated_call(): + # equality is deprecated + assert mkfoo() == "foo" + with pytest.deprecated_call(): + # comparison is deprecated + assert mkfoo() < "fooa" + with pytest.deprecated_call(): + # truthiness is deprecated + assert mkfoo() + with pytest.deprecated_call(): + # addition is deprecated (and hopefully all other operators) + assert mkfoo() + "a" == "fooa" + with pytest.deprecated_call(): + # indexing is deprecated + assert mkfoo()[0] == "f" + with pytest.deprecated_call(): + # methods are deprecated + assert mkfoo().startswith("f") + + +def test_deprecation_helper(recwarn): + def mkfoo() -> messages.Success: + return tools._deprecation_retval_helper(messages.Success(message="foo")) + + # check that just returning success will not cause a warning + mkfoo() + assert len(recwarn) == 0 + + with pytest.deprecated_call(): + # attributes are deprecated + assert mkfoo().message == "foo" + + with pytest.deprecated_call(): + # equality is deprecated (along with other operators hopefully) + assert mkfoo() != "foo" From 7b00942a6fcce790cba2dfca84c8f93ef7b57020 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 10 Jan 2025 13:43:49 +0100 Subject: [PATCH 05/14] feat(python): introduce `device.setup()` for entropy check this deprecates `device.reset()`, and moves the new arguments to `device.setup()`. Also it changes default backup type on core devices to SLIP39-single, same as Suite, and randomizes the number of entropy check rounds, if not provided from outside. --- python/.changelog.d/4155.added | 2 +- python/.changelog.d/4464.added | 1 + python/.changelog.d/4464.deprecated | 1 + python/src/trezorlib/device.py | 269 +++++++++++++++++++++++----- 4 files changed, 231 insertions(+), 42 deletions(-) create mode 100644 python/.changelog.d/4464.added create mode 100644 python/.changelog.d/4464.deprecated diff --git a/python/.changelog.d/4155.added b/python/.changelog.d/4155.added index da3f55e58f6..66d62bfe342 100644 --- a/python/.changelog.d/4155.added +++ b/python/.changelog.d/4155.added @@ -1 +1 @@ -Added support for entropy check workflow in device.reset(). +Added support for entropy check workflow in `device.reset()`. diff --git a/python/.changelog.d/4464.added b/python/.changelog.d/4464.added new file mode 100644 index 00000000000..3cea8e55ac5 --- /dev/null +++ b/python/.changelog.d/4464.added @@ -0,0 +1 @@ +Introduced `device.setup()` as a cleaner upgrade to `device.reset()`. diff --git a/python/.changelog.d/4464.deprecated b/python/.changelog.d/4464.deprecated new file mode 100644 index 00000000000..5135394328b --- /dev/null +++ b/python/.changelog.d/4464.deprecated @@ -0,0 +1 @@ +`device.reset()` is deprecated, migrate to `device.setup()` diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index 6b464282d3b..fcf948f5233 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -19,9 +19,11 @@ import hashlib import hmac import os +import random +import secrets import time import warnings -from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple from slip10 import SLIP10 @@ -36,6 +38,9 @@ RECOVERY_BACK = "\x08" # backspace character, sent literally +SLIP39_EXTENDABLE_MIN_VERSION = (2, 7, 1) +ENTROPY_CHECK_MIN_VERSION = (2, 8, 7) + @expect(messages.Success, field="message", ret_type=str) @session @@ -292,21 +297,103 @@ def reset_entropy_check( skip_backup: bool = False, no_backup: bool = False, backup_type: messages.BackupType = messages.BackupType.Bip39, - entropy_check_count: Optional[int] = None, - paths: List[Address] = [], -) -> Tuple["MessageType", Iterable[Tuple[Address, str]]]: +) -> str | None: + warnings.warn( + "reset() is deprecated. Use setup() instead.", + DeprecationWarning, + stacklevel=2, + ) + if display_random: warnings.warn( "display_random ignored. The feature is deprecated.", DeprecationWarning, + stacklevel=2, ) if language is not None: warnings.warn( "language ignored. Use change_language() to set device language.", DeprecationWarning, + stacklevel=2, ) + setup( + client, + strength=strength, + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label, + u2f_counter=u2f_counter, + skip_backup=skip_backup, + no_backup=no_backup, + backup_type=backup_type, + ) + + return _return_success(messages.Success(message="Initialized")) + + +def _get_external_entropy() -> bytes: + return secrets.token_bytes(32) + + +@session +def setup( + client: "TrezorClient", + *, + strength: Optional[int] = None, + passphrase_protection: bool = True, + pin_protection: bool = False, + label: Optional[str] = None, + u2f_counter: int = 0, + skip_backup: bool = False, + no_backup: bool = False, + backup_type: Optional[messages.BackupType] = None, + entropy_check_count: Optional[int] = None, + paths: Iterable[Address] = [], + _get_entropy: Callable[[], bytes] = _get_external_entropy, +) -> Iterable[Tuple[Address, str]]: + """Create a new wallet on device. + + On supporting devices, automatically performs the entropy check: for N rounds, ask + the device to generate a new seed and provide XPUBs derived from that seed. In the + next round, the previous round's seed is revealed and verified that it was generated + with the appropriate entropy and that it matches the provided XPUBs. + + On round N+1, instead of revealing, the final seed is stored on device. + + This function returns the XPUBs from the last round. Caller SHOULD store these XPUBs + and periodically check that the device still generates the same ones, to ensure that + the device has not maliciously switched to a pre-generated seed. + + The caller can provide a list of interesting derivation paths to be used in the + entropy check. If an empty list is provided, the function will use the first BTC + SegWit v0 account and the first ETH account. + + Returned XPUBs are in the form of tuples (derivation path, xpub). + + Specifying an entropy check count other than 0 on devices that don't support it, + such as Trezor Model One, will result in an error. If not specified, a random value + between 2 and 8 is chosen on supporting devices. + + Args: + * client: TrezorClient instance. + * strength: Entropy strength in bits. Default is 128 for the core family, and 256 + for Trezor Model One. + * passphrase_protection: Enable passphrase feature. Defaults to True. + * pin_protection: Enable and set up device PIN as part of the setup flow. Defaults + to False. + * label: Device label. + * u2f_counter: U2F counter value. + * skip_backup: Skip the backup step. Defaults to False. + * no_backup: Do not create backup (seedless mode). Defaults to False. + * entropy_check_count: Number of rounds for the entropy check. + + Returns: + Sequence of tuples (derivation path, xpub) from the last round of the entropy + check. + """ + if client.features.initialized: raise RuntimeError( "Device is initialized already. Call wipe_device() and try again." @@ -318,10 +405,24 @@ def reset_entropy_check( else: strength = 128 + if backup_type is None: + if client.version < SLIP39_EXTENDABLE_MIN_VERSION: + # includes Trezor One 1.x.x + backup_type = messages.BackupType.Bip39 + else: + backup_type = messages.BackupType.Slip39_Single_Extendable + if not paths: # Get XPUBs for the first BTC SegWit v0 account and first ETH account. paths = [parse_path("m/84h/0h/0h"), parse_path("m/44h/60h/0h")] + if entropy_check_count is None: + if client.version < ENTROPY_CHECK_MIN_VERSION: + # includes Trezor One 1.x.x + entropy_check_count = 0 + else: + entropy_check_count = random.randint(2, 8) + # Begin with device reset workflow msg = messages.ResetDevice( strength=strength, @@ -332,61 +433,147 @@ def reset_entropy_check( skip_backup=bool(skip_backup), no_backup=bool(no_backup), backup_type=backup_type, - entropy_check=entropy_check_count is not None, + entropy_check=entropy_check_count > 0, ) + if entropy_check_count > 0: + xpubs = _reset_with_entropycheck( + client, msg, entropy_check_count, paths, _get_entropy + ) + else: + _reset_no_entropycheck(client, msg, _get_entropy) + xpubs = [] - resp = client.call(msg) - if not isinstance(resp, messages.EntropyRequest): - raise RuntimeError("Invalid response, expected EntropyRequest") + client.init_device() + return xpubs - while True: - xpubs = [] - external_entropy = os.urandom(32) - entropy_commitment = resp.entropy_commitment - resp = client.call(messages.EntropyAck(entropy=external_entropy)) +def _reset_no_entropycheck( + client: "TrezorClient", + msg: messages.ResetDevice, + get_entropy: Callable[[], bytes], +) -> None: + """Simple reset workflow without entropy checks: + + >> ResetDevice + << EntropyRequest + >> EntropyAck(entropy=...) + << Success + """ + assert msg.entropy_check is False + client.call(msg, expect=messages.EntropyRequest) + client.call(messages.EntropyAck(entropy=get_entropy()), expect=messages.Success) - if entropy_check_count is None: - break - if not isinstance(resp, messages.EntropyCheckReady): - return resp, [] +def _reset_with_entropycheck( + client: "TrezorClient", + reset_msg: messages.ResetDevice, + entropy_check_count: int, + paths: Iterable[Address], + get_entropy: Callable[[], bytes], +) -> list[tuple[Address, str]]: + """Reset workflow with entropy checks: + + >> ResetDevice + repeat n times: + << EntropyRequest(entropy_commitment=..., prev_entropy=...) + >> EntropyAck(entropy=...) + << EntropyCheckReady + >> GetPublicKey(...) + << PublicKey(...) + >> EntropyCheckContinue(finish=False) + last round: + >> EntropyCheckContinue(finish=True) + << Success + + After each round, the device reveals its internal entropy via the prev_entropy + field. This function verifies that the entropy matches the respective commitment, + then recalculate the seed for the previous round, and verifies that the public keys + generated by the device match that seed. + + Returns the list of XPUBs from the last round. Caller is responsible for storing + those XPUBs and later verifying that these are still valid. + """ + assert reset_msg.strength is not None + assert reset_msg.backup_type is not None + strength = reset_msg.strength + backup_type = reset_msg.backup_type + def get_xpubs() -> list[tuple[Address, str]]: + xpubs = [] for path in paths: - resp = client.call(messages.GetPublicKey(address_n=path)) - if not isinstance(resp, messages.PublicKey): - return resp, [] - xpubs.append(resp.xpub) + resp = client.call( + messages.GetPublicKey(address_n=path), expect=messages.PublicKey + ) + xpubs.append((path, resp.xpub)) + return xpubs + + def verify_entropy_commitment( + internal_entropy: bytes | None, + external_entropy: bytes, + entropy_commitment: bytes | None, + xpubs: list[tuple[Address, str]], + ) -> None: + if internal_entropy is None or entropy_commitment is None: + raise TrezorException("Invalid entropy check response.") + calculated_commitment = hmac.HMAC( + key=internal_entropy, msg=b"", digestmod=hashlib.sha256 + ).digest() + if calculated_commitment != entropy_commitment: + raise TrezorException("Invalid entropy commitment.") + + seed = _seed_from_entropy( + internal_entropy, external_entropy, strength, backup_type + ) + slip10 = SLIP10.from_seed(seed) + for path, xpub in xpubs: + if slip10.get_xpub_from_path(path) != xpub: + raise TrezorException("Invalid XPUB in entropy check") + + xpubs = [] + resp = client.call(reset_msg, expect=messages.EntropyRequest) + entropy_commitment = resp.entropy_commitment + + while True: + # provide external entropy for this round + external_entropy = get_entropy() + client.call( + messages.EntropyAck(entropy=external_entropy), + expect=messages.EntropyCheckReady, + ) + + # fetch xpubs for the current round + xpubs = get_xpubs() if entropy_check_count <= 0: - resp = client.call(messages.EntropyCheckContinue(finish=True)) + # last round, wait for a Success and exit the loop + client.call( + messages.EntropyCheckContinue(finish=True), + expect=messages.Success, + ) break entropy_check_count -= 1 - resp = client.call(messages.EntropyCheckContinue(finish=False)) - if not isinstance(resp, messages.EntropyRequest): - raise RuntimeError("Invalid response, expected EntropyRequest") + # Next round starts. + resp = client.call( + messages.EntropyCheckContinue(finish=False), + expect=messages.EntropyRequest, + ) # Check the entropy commitment from the previous round. - assert resp.prev_entropy - if ( - hmac.HMAC(key=resp.prev_entropy, msg=b"", digestmod=hashlib.sha256).digest() - != entropy_commitment - ): - raise RuntimeError("Invalid entropy commitment.") - - # Derive the seed and check that XPUBs match. - seed = _seed_from_entropy( - resp.prev_entropy, external_entropy, strength, backup_type + verify_entropy_commitment( + resp.prev_entropy, external_entropy, entropy_commitment, xpubs ) - slip10 = SLIP10.from_seed(seed) - for path, xpub in zip(paths, xpubs): - if slip10.get_xpub_from_path(path) != xpub: - raise RuntimeError("Invalid XPUB in entropy check") + # Update the entropy commitment for the next round. + entropy_commitment = resp.entropy_commitment - client.init_device() - return resp, zip(paths, xpubs) + # TODO when we grow an API for auto-opening an empty passphrase session, + # we should run the following piece: + # xpubs_verify = get_xpubs() + # if xpubs != xpubs_verify: + # raise TrezorException("Invalid XPUBs after entropy check phase") + + return xpubs @expect(messages.Success, field="message", ret_type=str) From ab63f307ec881733a5ae157caeb01246e28655bf Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2025 16:36:40 +0100 Subject: [PATCH 06/14] refactor(python): replace usages of @expect --- python/.changelog.d/4464.deprecated.1 | 1 + python/.changelog.d/4464.deprecated.3 | 1 + python/.changelog.d/4464.incompatible | 1 + python/.changelog.d/4464.incompatible.1 | 1 + python/src/trezorlib/benchmark.py | 14 +- python/src/trezorlib/binance.py | 37 +-- python/src/trezorlib/btc.py | 83 +++--- python/src/trezorlib/cardano.py | 297 ++++++++++------------ python/src/trezorlib/cli/debug.py | 8 +- python/src/trezorlib/cli/device.py | 62 ++--- python/src/trezorlib/cli/fido.py | 12 +- python/src/trezorlib/cli/settings.py | 64 ++--- python/src/trezorlib/cli/solana.py | 6 +- python/src/trezorlib/client.py | 20 +- python/src/trezorlib/debuglink.py | 38 +-- python/src/trezorlib/device.py | 151 ++++++----- python/src/trezorlib/eos.py | 12 +- python/src/trezorlib/ethereum.py | 43 ++-- python/src/trezorlib/fido.py | 49 ++-- python/src/trezorlib/firmware/__init__.py | 9 +- python/src/trezorlib/misc.py | 40 ++- python/src/trezorlib/monero.py | 16 +- python/src/trezorlib/nem.py | 15 +- python/src/trezorlib/protobuf.py | 23 ++ python/src/trezorlib/ripple.py | 16 +- python/src/trezorlib/solana.py | 26 +- python/src/trezorlib/stellar.py | 10 +- python/src/trezorlib/tezos.py | 23 +- 28 files changed, 507 insertions(+), 571 deletions(-) create mode 100644 python/.changelog.d/4464.deprecated.1 create mode 100644 python/.changelog.d/4464.deprecated.3 create mode 100644 python/.changelog.d/4464.incompatible create mode 100644 python/.changelog.d/4464.incompatible.1 diff --git a/python/.changelog.d/4464.deprecated.1 b/python/.changelog.d/4464.deprecated.1 new file mode 100644 index 00000000000..12dda3d95fe --- /dev/null +++ b/python/.changelog.d/4464.deprecated.1 @@ -0,0 +1 @@ +String return values are deprecated in functions where the semantic result is a success (specifically those that were returning the message from Trezor's `Success` response). Type annotations are updated to `str | None`, and in a future release those functions will be returning `None` on success, or raise an exception on a failure. diff --git a/python/.changelog.d/4464.deprecated.3 b/python/.changelog.d/4464.deprecated.3 new file mode 100644 index 00000000000..a7dcbca4a0c --- /dev/null +++ b/python/.changelog.d/4464.deprecated.3 @@ -0,0 +1 @@ +Return value of `device.recover()` is deprecated. In the future, this function will return `None`. diff --git a/python/.changelog.d/4464.incompatible b/python/.changelog.d/4464.incompatible new file mode 100644 index 00000000000..2c8d4b814b2 --- /dev/null +++ b/python/.changelog.d/4464.incompatible @@ -0,0 +1 @@ +Return values in `solana` module were changed from the wrapping protobuf messages to the raw inner values (`str` for address, `bytes` for pubkey / signature). diff --git a/python/.changelog.d/4464.incompatible.1 b/python/.changelog.d/4464.incompatible.1 new file mode 100644 index 00000000000..f96056584f5 --- /dev/null +++ b/python/.changelog.d/4464.incompatible.1 @@ -0,0 +1 @@ +`trezorctl device` commands whose default result is a success will not print anything to stdout anymore, in line with Unix philosophy. diff --git a/python/src/trezorlib/benchmark.py b/python/src/trezorlib/benchmark.py index f96ef7970ea..6587e2a3abe 100644 --- a/python/src/trezorlib/benchmark.py +++ b/python/src/trezorlib/benchmark.py @@ -17,20 +17,18 @@ from typing import TYPE_CHECKING from . import messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType -@expect(messages.BenchmarkNames) def list_names( client: "TrezorClient", -) -> "MessageType": - return client.call(messages.BenchmarkListNames()) +) -> messages.BenchmarkNames: + return client.call(messages.BenchmarkListNames(), expect=messages.BenchmarkNames) -@expect(messages.BenchmarkResult) -def run(client: "TrezorClient", name: str) -> "MessageType": - return client.call(messages.BenchmarkRun(name=name)) +def run(client: "TrezorClient", name: str) -> messages.BenchmarkResult: + return client.call( + messages.BenchmarkRun(name=name), expect=messages.BenchmarkResult + ) diff --git a/python/src/trezorlib/binance.py b/python/src/trezorlib/binance.py index d2e4b97912c..938092a2dfc 100644 --- a/python/src/trezorlib/binance.py +++ b/python/src/trezorlib/binance.py @@ -18,35 +18,34 @@ from . import messages from .protobuf import dict_to_proto -from .tools import expect, session +from .tools import session if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address -@expect(messages.BinanceAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", address_n: "Address", show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.BinanceGetAddress( address_n=address_n, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.BinanceAddress, + ).address -@expect(messages.BinancePublicKey, field="public_key", ret_type=bytes) def get_public_key( client: "TrezorClient", address_n: "Address", show_display: bool = False -) -> "MessageType": +) -> bytes: return client.call( - messages.BinanceGetPublicKey(address_n=address_n, show_display=show_display) - ) + messages.BinanceGetPublicKey(address_n=address_n, show_display=show_display), + expect=messages.BinancePublicKey, + ).public_key @session @@ -60,13 +59,7 @@ def sign_tx( tx_msg["chunkify"] = chunkify envelope = dict_to_proto(messages.BinanceSignTx, tx_msg) - response = client.call(envelope) - - if not isinstance(response, messages.BinanceTxRequest): - raise RuntimeError( - "Invalid response, expected BinanceTxRequest, received " - + type(response).__name__ - ) + client.call(envelope, expect=messages.BinanceTxRequest) if "refid" in msg: msg = dict_to_proto(messages.BinanceCancelMsg, msg) @@ -77,12 +70,4 @@ def sign_tx( else: raise ValueError("can not determine msg type") - response = client.call(msg) - - if not isinstance(response, messages.BinanceSignedTx): - raise RuntimeError( - "Invalid response, expected BinanceSignedTx, received " - + type(response).__name__ - ) - - return response + return client.call(msg, expect=messages.BinanceSignedTx) diff --git a/python/src/trezorlib/btc.py b/python/src/trezorlib/btc.py index a71ead2adc2..d273a97ff5b 100644 --- a/python/src/trezorlib/btc.py +++ b/python/src/trezorlib/btc.py @@ -14,6 +14,8 @@ # You should have received a copy of the License along with this library. # If not, see . +from __future__ import annotations + import warnings from copy import copy from decimal import Decimal @@ -23,11 +25,10 @@ from typing_extensions import Protocol, TypedDict from . import exceptions, messages -from .tools import expect, prepare_message_bytes, session +from .tools import _return_success, prepare_message_bytes, session if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address class ScriptSig(TypedDict): @@ -103,7 +104,6 @@ def make_bin_output(vout: "Vout") -> messages.TxOutputBinType: ) -@expect(messages.PublicKey) def get_public_node( client: "TrezorClient", n: "Address", @@ -114,7 +114,7 @@ def get_public_node( ignore_xpub_magic: bool = False, unlock_path: Optional[List[int]] = None, unlock_path_mac: Optional[bytes] = None, -) -> "MessageType": +) -> messages.PublicKey: if unlock_path: res = client.call( messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac) @@ -130,16 +130,15 @@ def get_public_node( coin_name=coin_name, script_type=script_type, ignore_xpub_magic=ignore_xpub_magic, - ) + ), + expect=messages.PublicKey, ) -@expect(messages.Address, field="address", ret_type=str) def get_address(*args: Any, **kwargs: Any): - return get_authenticated_address(*args, **kwargs) + return get_authenticated_address(*args, **kwargs).address -@expect(messages.Address) def get_authenticated_address( client: "TrezorClient", coin_name: str, @@ -151,13 +150,12 @@ def get_authenticated_address( unlock_path: Optional[List[int]] = None, unlock_path_mac: Optional[bytes] = None, chunkify: bool = False, -) -> "MessageType": +) -> messages.Address: if unlock_path: - res = client.call( - messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac) + client.call( + messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac), + expect=messages.UnlockedPathRequest, ) - if not isinstance(res, messages.UnlockedPathRequest): - raise exceptions.TrezorException("Unexpected message") return client.call( messages.GetAddress( @@ -168,26 +166,27 @@ def get_authenticated_address( script_type=script_type, ignore_xpub_magic=ignore_xpub_magic, chunkify=chunkify, - ) + ), + expect=messages.Address, ) -@expect(messages.OwnershipId, field="ownership_id", ret_type=bytes) def get_ownership_id( client: "TrezorClient", coin_name: str, n: "Address", multisig: Optional[messages.MultisigRedeemScriptType] = None, script_type: messages.InputScriptType = messages.InputScriptType.SPENDADDRESS, -) -> "MessageType": +) -> bytes: return client.call( messages.GetOwnershipId( address_n=n, coin_name=coin_name, multisig=multisig, script_type=script_type, - ) - ) + ), + expect=messages.OwnershipId, + ).ownership_id def get_ownership_proof( @@ -202,9 +201,7 @@ def get_ownership_proof( preauthorized: bool = False, ) -> Tuple[bytes, bytes]: if preauthorized: - res = client.call(messages.DoPreauthorized()) - if not isinstance(res, messages.PreauthorizedRequest): - raise exceptions.TrezorException("Unexpected message") + client.call(messages.DoPreauthorized(), expect=messages.PreauthorizedRequest) res = client.call( messages.GetOwnershipProof( @@ -215,16 +212,13 @@ def get_ownership_proof( user_confirmation=user_confirmation, ownership_ids=ownership_ids, commitment_data=commitment_data, - ) + ), + expect=messages.OwnershipProof, ) - if not isinstance(res, messages.OwnershipProof): - raise exceptions.TrezorException("Unexpected message") - return res.ownership_proof, res.signature -@expect(messages.MessageSignature) def sign_message( client: "TrezorClient", coin_name: str, @@ -233,7 +227,7 @@ def sign_message( script_type: messages.InputScriptType = messages.InputScriptType.SPENDADDRESS, no_script_type: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> messages.MessageSignature: return client.call( messages.SignMessage( coin_name=coin_name, @@ -242,7 +236,8 @@ def sign_message( script_type=script_type, no_script_type=no_script_type, chunkify=chunkify, - ) + ), + expect=messages.MessageSignature, ) @@ -255,18 +250,19 @@ def verify_message( chunkify: bool = False, ) -> bool: try: - resp = client.call( + client.call( messages.VerifyMessage( address=address, signature=signature, message=prepare_message_bytes(message), coin_name=coin_name, chunkify=chunkify, - ) + ), + expect=messages.Success, ) + return True except exceptions.TrezorFailure: return False - return isinstance(resp, messages.Success) @session @@ -319,15 +315,12 @@ def sign_tx( setattr(signtx, name, value) if unlock_path: - res = client.call( - messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac) + client.call( + messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac), + expect=messages.UnlockedPathRequest, ) - if not isinstance(res, messages.UnlockedPathRequest): - raise exceptions.TrezorException("Unexpected message") elif preauthorized: - res = client.call(messages.DoPreauthorized()) - if not isinstance(res, messages.PreauthorizedRequest): - raise exceptions.TrezorException("Unexpected message") + client.call(messages.DoPreauthorized(), expect=messages.PreauthorizedRequest) res = client.call(signtx) @@ -418,10 +411,7 @@ def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: f"Unknown request type - {res.request_type}." ) - res = client.call(messages.TxAck(tx=msg)) - - if not isinstance(res, messages.TxRequest): - raise exceptions.TrezorException("Unexpected message") + res = client.call(messages.TxAck(tx=msg), expect=messages.TxRequest) for i, sig in zip(inputs, signatures): if i.script_type != messages.InputScriptType.EXTERNAL and sig is None: @@ -430,7 +420,6 @@ def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: return signatures, serialized_tx -@expect(messages.Success, field="message", ret_type=str) def authorize_coinjoin( client: "TrezorClient", coordinator: str, @@ -440,8 +429,8 @@ def authorize_coinjoin( n: "Address", coin_name: str, script_type: messages.InputScriptType = messages.InputScriptType.SPENDADDRESS, -) -> "MessageType": - return client.call( +) -> str | None: + resp = client.call( messages.AuthorizeCoinJoin( coordinator=coordinator, max_rounds=max_rounds, @@ -450,5 +439,7 @@ def authorize_coinjoin( address_n=n, coin_name=coin_name, script_type=script_type, - ) + ), + expect=messages.Success, ) + return _return_success(resp) diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index 49d2c6463f8..4cbc635f1ff 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -31,12 +31,11 @@ Union, ) -from . import exceptions, messages, tools -from .tools import expect +from . import messages as m +from . import tools if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType PROTOCOL_MAGICS = { "mainnet": 764824073, @@ -72,35 +71,33 @@ INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY = "The output's token_bundle entry is invalid" INVALID_MINT_TOKEN_BUNDLE_ENTRY = "The mint token_bundle entry is invalid" -InputWithPath = Tuple[messages.CardanoTxInput, List[int]] -CollateralInputWithPath = Tuple[messages.CardanoTxCollateralInput, List[int]] -AssetGroupWithTokens = Tuple[messages.CardanoAssetGroup, List[messages.CardanoToken]] +InputWithPath = Tuple[m.CardanoTxInput, List[int]] +CollateralInputWithPath = Tuple[m.CardanoTxCollateralInput, List[int]] +AssetGroupWithTokens = Tuple[m.CardanoAssetGroup, List[m.CardanoToken]] OutputWithData = Tuple[ - messages.CardanoTxOutput, + m.CardanoTxOutput, List[AssetGroupWithTokens], - List[messages.CardanoTxInlineDatumChunk], - List[messages.CardanoTxReferenceScriptChunk], + List[m.CardanoTxInlineDatumChunk], + List[m.CardanoTxReferenceScriptChunk], ] OutputItem = Union[ - messages.CardanoTxOutput, - messages.CardanoAssetGroup, - messages.CardanoToken, - messages.CardanoTxInlineDatumChunk, - messages.CardanoTxReferenceScriptChunk, + m.CardanoTxOutput, + m.CardanoAssetGroup, + m.CardanoToken, + m.CardanoTxInlineDatumChunk, + m.CardanoTxReferenceScriptChunk, ] CertificateItem = Union[ - messages.CardanoTxCertificate, - messages.CardanoPoolOwner, - messages.CardanoPoolRelayParameters, -] -MintItem = Union[ - messages.CardanoTxMint, messages.CardanoAssetGroup, messages.CardanoToken + m.CardanoTxCertificate, + m.CardanoPoolOwner, + m.CardanoPoolRelayParameters, ] +MintItem = Union[m.CardanoTxMint, m.CardanoAssetGroup, m.CardanoToken] PoolOwnersAndRelays = Tuple[ - List[messages.CardanoPoolOwner], List[messages.CardanoPoolRelayParameters] + List[m.CardanoPoolOwner], List[m.CardanoPoolRelayParameters] ] CertificateWithPoolOwnersAndRelays = Tuple[ - messages.CardanoTxCertificate, Optional[PoolOwnersAndRelays] + m.CardanoTxCertificate, Optional[PoolOwnersAndRelays] ] Path = List[int] Witness = Tuple[Path, bytes] @@ -108,9 +105,7 @@ SignTxResponse = Dict[str, Union[bytes, List[Witness], AuxiliaryDataSupplement]] Chunk = TypeVar( "Chunk", - bound=Union[ - messages.CardanoTxInlineDatumChunk, messages.CardanoTxReferenceScriptChunk - ], + bound=Union[m.CardanoTxInlineDatumChunk, m.CardanoTxReferenceScriptChunk], ) @@ -123,7 +118,7 @@ def parse_optional_int(value: Optional[str]) -> Optional[int]: def create_address_parameters( - address_type: messages.CardanoAddressType, + address_type: m.CardanoAddressType, address_n: List[int], address_n_staking: Optional[List[int]] = None, staking_key_hash: Optional[bytes] = None, @@ -132,18 +127,18 @@ def create_address_parameters( certificate_index: Optional[int] = None, script_payment_hash: Optional[bytes] = None, script_staking_hash: Optional[bytes] = None, -) -> messages.CardanoAddressParametersType: +) -> m.CardanoAddressParametersType: certificate_pointer = None if address_type in ( - messages.CardanoAddressType.POINTER, - messages.CardanoAddressType.POINTER_SCRIPT, + m.CardanoAddressType.POINTER, + m.CardanoAddressType.POINTER_SCRIPT, ): certificate_pointer = _create_certificate_pointer( block_index, tx_index, certificate_index ) - return messages.CardanoAddressParametersType( + return m.CardanoAddressParametersType( address_type=address_type, address_n=address_n, address_n_staking=address_n_staking, @@ -158,11 +153,11 @@ def _create_certificate_pointer( block_index: Optional[int], tx_index: Optional[int], certificate_index: Optional[int], -) -> messages.CardanoBlockchainPointerType: +) -> m.CardanoBlockchainPointerType: if block_index is None or tx_index is None or certificate_index is None: raise ValueError("Invalid pointer parameters") - return messages.CardanoBlockchainPointerType( + return m.CardanoBlockchainPointerType( block_index=block_index, tx_index=tx_index, certificate_index=certificate_index ) @@ -173,7 +168,7 @@ def parse_input(tx_input: dict) -> InputWithPath: path = tools.parse_path(tx_input.get("path", "")) return ( - messages.CardanoTxInput( + m.CardanoTxInput( prev_hash=bytes.fromhex(tx_input["prev_hash"]), prev_index=tx_input["prev_index"], ), @@ -204,22 +199,22 @@ def parse_output(output: dict) -> OutputWithData: datum_hash = parse_optional_bytes(output.get("datum_hash")) - serialization_format = messages.CardanoTxOutputSerializationFormat.ARRAY_LEGACY + serialization_format = m.CardanoTxOutputSerializationFormat.ARRAY_LEGACY if "format" in output: serialization_format = output["format"] inline_datum_size, inline_datum_chunks = _parse_chunkable_data( parse_optional_bytes(output.get("inline_datum")), - messages.CardanoTxInlineDatumChunk, + m.CardanoTxInlineDatumChunk, ) reference_script_size, reference_script_chunks = _parse_chunkable_data( parse_optional_bytes(output.get("reference_script")), - messages.CardanoTxReferenceScriptChunk, + m.CardanoTxReferenceScriptChunk, ) return ( - messages.CardanoTxOutput( + m.CardanoTxOutput( address=address, address_parameters=address_parameters, amount=int(output["amount"]), @@ -253,7 +248,7 @@ def _parse_token_bundle( result.append( ( - messages.CardanoAssetGroup( + m.CardanoAssetGroup( policy_id=bytes.fromhex(token_group["policy_id"]), tokens_count=len(tokens), ), @@ -264,7 +259,7 @@ def _parse_token_bundle( return result -def _parse_tokens(tokens: Iterable[dict], is_mint: bool) -> List[messages.CardanoToken]: +def _parse_tokens(tokens: Iterable[dict], is_mint: bool) -> List[m.CardanoToken]: error_message: str if is_mint: error_message = INVALID_MINT_TOKEN_BUNDLE_ENTRY @@ -288,7 +283,7 @@ def _parse_tokens(tokens: Iterable[dict], is_mint: bool) -> List[messages.Cardan amount = int(token["amount"]) result.append( - messages.CardanoToken( + m.CardanoToken( asset_name_bytes=bytes.fromhex(token["asset_name_bytes"]), amount=amount, mint_amount=mint_amount, @@ -300,7 +295,7 @@ def _parse_tokens(tokens: Iterable[dict], is_mint: bool) -> List[messages.Cardan def _parse_address_parameters( address_parameters: dict, error_message: str -) -> messages.CardanoAddressParametersType: +) -> m.CardanoAddressParametersType: if "addressType" not in address_parameters: raise ValueError(error_message) @@ -317,7 +312,7 @@ def _parse_address_parameters( ) return create_address_parameters( - messages.CardanoAddressType(address_parameters["addressType"]), + m.CardanoAddressType(address_parameters["addressType"]), payment_path, staking_path, staking_key_hash_bytes, @@ -346,7 +341,7 @@ def _create_data_chunks(data: bytes) -> Iterator[bytes]: processed_size += MAX_CHUNK_SIZE -def parse_native_script(native_script: dict) -> messages.CardanoNativeScript: +def parse_native_script(native_script: dict) -> m.CardanoNativeScript: if "type" not in native_script: raise ValueError("Script is missing some fields") @@ -364,7 +359,7 @@ def parse_native_script(native_script: dict) -> messages.CardanoNativeScript: invalid_before = parse_optional_int(native_script.get("invalid_before")) invalid_hereafter = parse_optional_int(native_script.get("invalid_hereafter")) - return messages.CardanoNativeScript( + return m.CardanoNativeScript( type=type, scripts=scripts, key_hash=key_hash, @@ -385,7 +380,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: certificate_type = certificate["type"] - if certificate_type == messages.CardanoCertificateType.STAKE_DELEGATION: + if certificate_type == m.CardanoCertificateType.STAKE_DELEGATION: if "pool" not in certificate: raise CERTIFICATE_MISSING_FIELDS_ERROR @@ -394,7 +389,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ) return ( - messages.CardanoTxCertificate( + m.CardanoTxCertificate( type=certificate_type, path=path, pool=bytes.fromhex(certificate["pool"]), @@ -404,15 +399,15 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: None, ) elif certificate_type in ( - messages.CardanoCertificateType.STAKE_REGISTRATION, - messages.CardanoCertificateType.STAKE_DEREGISTRATION, + m.CardanoCertificateType.STAKE_REGISTRATION, + m.CardanoCertificateType.STAKE_DEREGISTRATION, ): path, script_hash, key_hash = _parse_credential( certificate, CERTIFICATE_MISSING_FIELDS_ERROR ) return ( - messages.CardanoTxCertificate( + m.CardanoTxCertificate( type=certificate_type, path=path, script_hash=script_hash, @@ -421,8 +416,8 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: None, ) elif certificate_type in ( - messages.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, - messages.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + m.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + m.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, ): if "deposit" not in certificate: raise CERTIFICATE_MISSING_FIELDS_ERROR @@ -432,7 +427,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ) return ( - messages.CardanoTxCertificate( + m.CardanoTxCertificate( type=certificate_type, path=path, script_hash=script_hash, @@ -441,7 +436,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ), None, ) - elif certificate_type == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION: + elif certificate_type == m.CardanoCertificateType.STAKE_POOL_REGISTRATION: pool_parameters = certificate["pool_parameters"] if any( @@ -450,9 +445,9 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ): raise CERTIFICATE_MISSING_FIELDS_ERROR - pool_metadata: Optional[messages.CardanoPoolMetadataType] + pool_metadata: Optional[m.CardanoPoolMetadataType] if pool_parameters.get("metadata") is not None: - pool_metadata = messages.CardanoPoolMetadataType( + pool_metadata = m.CardanoPoolMetadataType( url=pool_parameters["metadata"]["url"], hash=bytes.fromhex(pool_parameters["metadata"]["hash"]), ) @@ -469,9 +464,9 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ] return ( - messages.CardanoTxCertificate( + m.CardanoTxCertificate( type=certificate_type, - pool_parameters=messages.CardanoPoolParametersType( + pool_parameters=m.CardanoPoolParametersType( pool_id=bytes.fromhex(pool_parameters["pool_id"]), vrf_key_hash=bytes.fromhex(pool_parameters["vrf_key_hash"]), pledge=int(pool_parameters["pledge"]), @@ -486,7 +481,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ), (owners, relays), ) - if certificate_type == messages.CardanoCertificateType.VOTE_DELEGATION: + if certificate_type == m.CardanoCertificateType.VOTE_DELEGATION: if "drep" not in certificate: raise CERTIFICATE_MISSING_FIELDS_ERROR @@ -495,13 +490,13 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ) return ( - messages.CardanoTxCertificate( + m.CardanoTxCertificate( type=certificate_type, path=path, script_hash=script_hash, key_hash=key_hash, - drep=messages.CardanoDRep( - type=messages.CardanoDRepType(certificate["drep"]["type"]), + drep=m.CardanoDRep( + type=m.CardanoDRepType(certificate["drep"]["type"]), key_hash=parse_optional_bytes(certificate["drep"].get("key_hash")), script_hash=parse_optional_bytes( certificate["drep"].get("script_hash") @@ -527,21 +522,21 @@ def _parse_credential( return path, script_hash, key_hash -def _parse_pool_owner(pool_owner: dict) -> messages.CardanoPoolOwner: +def _parse_pool_owner(pool_owner: dict) -> m.CardanoPoolOwner: if "staking_key_path" in pool_owner: - return messages.CardanoPoolOwner( + return m.CardanoPoolOwner( staking_key_path=tools.parse_path(pool_owner["staking_key_path"]) ) - return messages.CardanoPoolOwner( + return m.CardanoPoolOwner( staking_key_hash=bytes.fromhex(pool_owner["staking_key_hash"]) ) -def _parse_pool_relay(pool_relay: dict) -> messages.CardanoPoolRelayParameters: - pool_relay_type = messages.CardanoPoolRelayType(pool_relay["type"]) +def _parse_pool_relay(pool_relay: dict) -> m.CardanoPoolRelayParameters: + pool_relay_type = m.CardanoPoolRelayType(pool_relay["type"]) - if pool_relay_type == messages.CardanoPoolRelayType.SINGLE_HOST_IP: + if pool_relay_type == m.CardanoPoolRelayType.SINGLE_HOST_IP: ipv4_address_packed = ( ip_address(pool_relay["ipv4_address"]).packed if "ipv4_address" in pool_relay @@ -553,20 +548,20 @@ def _parse_pool_relay(pool_relay: dict) -> messages.CardanoPoolRelayParameters: else None ) - return messages.CardanoPoolRelayParameters( + return m.CardanoPoolRelayParameters( type=pool_relay_type, port=int(pool_relay["port"]), ipv4_address=ipv4_address_packed, ipv6_address=ipv6_address_packed, ) - elif pool_relay_type == messages.CardanoPoolRelayType.SINGLE_HOST_NAME: - return messages.CardanoPoolRelayParameters( + elif pool_relay_type == m.CardanoPoolRelayType.SINGLE_HOST_NAME: + return m.CardanoPoolRelayParameters( type=pool_relay_type, port=int(pool_relay["port"]), host_name=pool_relay["host_name"], ) - elif pool_relay_type == messages.CardanoPoolRelayType.MULTIPLE_HOST_NAME: - return messages.CardanoPoolRelayParameters( + elif pool_relay_type == m.CardanoPoolRelayType.MULTIPLE_HOST_NAME: + return m.CardanoPoolRelayParameters( type=pool_relay_type, host_name=pool_relay["host_name"], ) @@ -574,7 +569,7 @@ def _parse_pool_relay(pool_relay: dict) -> messages.CardanoPoolRelayParameters: raise ValueError("Unknown pool relay type") -def parse_withdrawal(withdrawal: dict) -> messages.CardanoTxWithdrawal: +def parse_withdrawal(withdrawal: dict) -> m.CardanoTxWithdrawal: WITHDRAWAL_MISSING_FIELDS_ERROR = ValueError( "The withdrawal is missing some fields" ) @@ -586,7 +581,7 @@ def parse_withdrawal(withdrawal: dict) -> messages.CardanoTxWithdrawal: withdrawal, WITHDRAWAL_MISSING_FIELDS_ERROR ) - return messages.CardanoTxWithdrawal( + return m.CardanoTxWithdrawal( path=path, amount=int(withdrawal["amount"]), script_hash=script_hash, @@ -596,7 +591,7 @@ def parse_withdrawal(withdrawal: dict) -> messages.CardanoTxWithdrawal: def parse_auxiliary_data( auxiliary_data: Optional[dict], -) -> Optional[messages.CardanoTxAuxiliaryData]: +) -> Optional[m.CardanoTxAuxiliaryData]: if auxiliary_data is None: return None @@ -620,17 +615,17 @@ def parse_auxiliary_data( if not all(k in delegation for k in REQUIRED_FIELDS_CVOTE_DELEGATION): raise AUXILIARY_DATA_MISSING_FIELDS_ERROR delegations.append( - messages.CardanoCVoteRegistrationDelegation( + m.CardanoCVoteRegistrationDelegation( vote_public_key=bytes.fromhex(delegation["vote_public_key"]), weight=int(delegation["weight"]), ) ) voting_purpose = None - if serialization_format == messages.CardanoCVoteRegistrationFormat.CIP36: + if serialization_format == m.CardanoCVoteRegistrationFormat.CIP36: voting_purpose = cvote_registration.get("voting_purpose") - cvote_registration_parameters = messages.CardanoCVoteRegistrationParametersType( + cvote_registration_parameters = m.CardanoCVoteRegistrationParametersType( vote_public_key=parse_optional_bytes( cvote_registration.get("vote_public_key") ), @@ -653,7 +648,7 @@ def parse_auxiliary_data( if hash is None and cvote_registration_parameters is None: raise AUXILIARY_DATA_MISSING_FIELDS_ERROR - return messages.CardanoTxAuxiliaryData( + return m.CardanoTxAuxiliaryData( hash=hash, cvote_registration_parameters=cvote_registration_parameters, ) @@ -673,7 +668,7 @@ def parse_collateral_input(collateral_input: dict) -> CollateralInputWithPath: path = tools.parse_path(collateral_input.get("path", "")) return ( - messages.CardanoTxCollateralInput( + m.CardanoTxCollateralInput( prev_hash=bytes.fromhex(collateral_input["prev_hash"]), prev_index=collateral_input["prev_index"], ), @@ -681,20 +676,20 @@ def parse_collateral_input(collateral_input: dict) -> CollateralInputWithPath: ) -def parse_required_signer(required_signer: dict) -> messages.CardanoTxRequiredSigner: +def parse_required_signer(required_signer: dict) -> m.CardanoTxRequiredSigner: key_hash = parse_optional_bytes(required_signer.get("key_hash")) key_path = tools.parse_path(required_signer.get("key_path", "")) - return messages.CardanoTxRequiredSigner( + return m.CardanoTxRequiredSigner( key_hash=key_hash, key_path=key_path, ) -def parse_reference_input(reference_input: dict) -> messages.CardanoTxReferenceInput: +def parse_reference_input(reference_input: dict) -> m.CardanoTxReferenceInput: if not all(k in reference_input for k in REQUIRED_FIELDS_INPUT): raise ValueError("The reference input is missing some fields") - return messages.CardanoTxReferenceInput( + return m.CardanoTxReferenceInput( prev_hash=bytes.fromhex(reference_input["prev_hash"]), prev_index=reference_input["prev_index"], ) @@ -712,16 +707,16 @@ def parse_additional_witness_request( def _get_witness_requests( inputs: Sequence[InputWithPath], certificates: Sequence[CertificateWithPoolOwnersAndRelays], - withdrawals: Sequence[messages.CardanoTxWithdrawal], + withdrawals: Sequence[m.CardanoTxWithdrawal], collateral_inputs: Sequence[CollateralInputWithPath], - required_signers: Sequence[messages.CardanoTxRequiredSigner], + required_signers: Sequence[m.CardanoTxRequiredSigner], additional_witness_requests: Sequence[Path], - signing_mode: messages.CardanoTxSigningMode, -) -> List[messages.CardanoTxWitnessRequest]: + signing_mode: m.CardanoTxSigningMode, +) -> List[m.CardanoTxWitnessRequest]: paths = set() # don't gather paths from tx elements in MULTISIG_TRANSACTION signing mode - if signing_mode != messages.CardanoTxSigningMode.MULTISIG_TRANSACTION: + if signing_mode != m.CardanoTxSigningMode.MULTISIG_TRANSACTION: for _, path in inputs: if path: paths.add(tuple(path)) @@ -729,18 +724,17 @@ def _get_witness_requests( if ( certificate.type in ( - messages.CardanoCertificateType.STAKE_DEREGISTRATION, - messages.CardanoCertificateType.STAKE_DELEGATION, - messages.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, - messages.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, - messages.CardanoCertificateType.VOTE_DELEGATION, + m.CardanoCertificateType.STAKE_DEREGISTRATION, + m.CardanoCertificateType.STAKE_DELEGATION, + m.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + m.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + m.CardanoCertificateType.VOTE_DELEGATION, ) and certificate.path ): paths.add(tuple(certificate.path)) elif ( - certificate.type - == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION + certificate.type == m.CardanoCertificateType.STAKE_POOL_REGISTRATION and pool_owners_and_relays is not None ): owners, _ = pool_owners_and_relays @@ -752,7 +746,7 @@ def _get_witness_requests( paths.add(tuple(withdrawal.path)) # gather Plutus-related paths - if signing_mode == messages.CardanoTxSigningMode.PLUTUS_TRANSACTION: + if signing_mode == m.CardanoTxSigningMode.PLUTUS_TRANSACTION: for _, path in collateral_inputs: if path: paths.add(tuple(path)) @@ -765,10 +759,10 @@ def _get_witness_requests( paths.add(tuple(additional_witness_request)) sorted_paths = sorted([list(path) for path in paths]) - return [messages.CardanoTxWitnessRequest(path=path) for path in sorted_paths] + return [m.CardanoTxWitnessRequest(path=path) for path in sorted_paths] -def _get_inputs_items(inputs: List[InputWithPath]) -> Iterator[messages.CardanoTxInput]: +def _get_inputs_items(inputs: List[InputWithPath]) -> Iterator[m.CardanoTxInput]: for input, _ in inputs: yield input @@ -807,7 +801,7 @@ def _get_certificates_items( def _get_mint_items(mint: Sequence[AssetGroupWithTokens]) -> Iterator[MintItem]: if not mint: return - yield messages.CardanoTxMint(asset_groups_count=len(mint)) + yield m.CardanoTxMint(asset_groups_count=len(mint)) for asset_group, tokens in mint: yield asset_group yield from tokens @@ -815,7 +809,7 @@ def _get_mint_items(mint: Sequence[AssetGroupWithTokens]) -> Iterator[MintItem]: def _get_collateral_inputs_items( collateral_inputs: Sequence[CollateralInputWithPath], -) -> Iterator[messages.CardanoTxCollateralInput]: +) -> Iterator[m.CardanoTxCollateralInput]: for collateral_input, _ in collateral_inputs: yield collateral_input @@ -823,88 +817,86 @@ def _get_collateral_inputs_items( # ====== Client functions ====== # -@expect(messages.CardanoAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", - address_parameters: messages.CardanoAddressParametersType, + address_parameters: m.CardanoAddressParametersType, protocol_magic: int = PROTOCOL_MAGICS["mainnet"], network_id: int = NETWORK_IDS["mainnet"], show_display: bool = False, - derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, + derivation_type: m.CardanoDerivationType = m.CardanoDerivationType.ICARUS, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( - messages.CardanoGetAddress( + m.CardanoGetAddress( address_parameters=address_parameters, protocol_magic=protocol_magic, network_id=network_id, show_display=show_display, derivation_type=derivation_type, chunkify=chunkify, - ) - ) + ), + expect=m.CardanoAddress, + ).address -@expect(messages.CardanoPublicKey) def get_public_key( client: "TrezorClient", address_n: List[int], - derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, + derivation_type: m.CardanoDerivationType = m.CardanoDerivationType.ICARUS, show_display: bool = False, -) -> "MessageType": +) -> m.CardanoPublicKey: return client.call( - messages.CardanoGetPublicKey( + m.CardanoGetPublicKey( address_n=address_n, derivation_type=derivation_type, show_display=show_display, - ) + ), + expect=m.CardanoPublicKey, ) -@expect(messages.CardanoNativeScriptHash) def get_native_script_hash( client: "TrezorClient", - native_script: messages.CardanoNativeScript, - display_format: messages.CardanoNativeScriptHashDisplayFormat = messages.CardanoNativeScriptHashDisplayFormat.HIDE, - derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, -) -> "MessageType": + native_script: m.CardanoNativeScript, + display_format: m.CardanoNativeScriptHashDisplayFormat = m.CardanoNativeScriptHashDisplayFormat.HIDE, + derivation_type: m.CardanoDerivationType = m.CardanoDerivationType.ICARUS, +) -> m.CardanoNativeScriptHash: return client.call( - messages.CardanoGetNativeScriptHash( + m.CardanoGetNativeScriptHash( script=native_script, display_format=display_format, derivation_type=derivation_type, - ) + ), + expect=m.CardanoNativeScriptHash, ) def sign_tx( client: "TrezorClient", - signing_mode: messages.CardanoTxSigningMode, + signing_mode: m.CardanoTxSigningMode, inputs: List[InputWithPath], outputs: List[OutputWithData], fee: int, ttl: Optional[int], validity_interval_start: Optional[int], certificates: Sequence[CertificateWithPoolOwnersAndRelays] = (), - withdrawals: Sequence[messages.CardanoTxWithdrawal] = (), + withdrawals: Sequence[m.CardanoTxWithdrawal] = (), protocol_magic: int = PROTOCOL_MAGICS["mainnet"], network_id: int = NETWORK_IDS["mainnet"], - auxiliary_data: Optional[messages.CardanoTxAuxiliaryData] = None, + auxiliary_data: Optional[m.CardanoTxAuxiliaryData] = None, mint: Sequence[AssetGroupWithTokens] = (), script_data_hash: Optional[bytes] = None, collateral_inputs: Sequence[CollateralInputWithPath] = (), - required_signers: Sequence[messages.CardanoTxRequiredSigner] = (), + required_signers: Sequence[m.CardanoTxRequiredSigner] = (), collateral_return: Optional[OutputWithData] = None, total_collateral: Optional[int] = None, - reference_inputs: Sequence[messages.CardanoTxReferenceInput] = (), + reference_inputs: Sequence[m.CardanoTxReferenceInput] = (), additional_witness_requests: Sequence[Path] = (), - derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, + derivation_type: m.CardanoDerivationType = m.CardanoDerivationType.ICARUS, include_network_id: bool = False, chunkify: bool = False, tag_cbor_sets: bool = False, ) -> Dict[str, Any]: - UNEXPECTED_RESPONSE_ERROR = exceptions.TrezorException("Unexpected response") - witness_requests = _get_witness_requests( inputs, certificates, @@ -916,7 +908,7 @@ def sign_tx( ) response = client.call( - messages.CardanoSignTxInit( + m.CardanoSignTxInit( signing_mode=signing_mode, inputs_count=len(inputs), outputs_count=len(outputs), @@ -940,10 +932,9 @@ def sign_tx( include_network_id=include_network_id, chunkify=chunkify, tag_cbor_sets=tag_cbor_sets, - ) + ), + expect=m.CardanoTxItemAck, ) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR for tx_item in chain( _get_inputs_items(inputs), @@ -951,55 +942,41 @@ def sign_tx( _get_certificates_items(certificates), withdrawals, ): - response = client.call(tx_item) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(tx_item, expect=m.CardanoTxItemAck) sign_tx_response: Dict[str, Any] = {} if auxiliary_data is not None: - auxiliary_data_supplement = client.call(auxiliary_data) - if not isinstance( - auxiliary_data_supplement, messages.CardanoTxAuxiliaryDataSupplement - ): - raise UNEXPECTED_RESPONSE_ERROR + auxiliary_data_supplement = client.call( + auxiliary_data, expect=m.CardanoTxAuxiliaryDataSupplement + ) if ( auxiliary_data_supplement.type - != messages.CardanoTxAuxiliaryDataSupplementType.NONE + != m.CardanoTxAuxiliaryDataSupplementType.NONE ): sign_tx_response["auxiliary_data_supplement"] = ( auxiliary_data_supplement.__dict__ ) - response = client.call(messages.CardanoTxHostAck()) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(m.CardanoTxHostAck(), expect=m.CardanoTxItemAck) for tx_item in chain( _get_mint_items(mint), _get_collateral_inputs_items(collateral_inputs), required_signers, ): - response = client.call(tx_item) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(tx_item, expect=m.CardanoTxItemAck) if collateral_return is not None: for tx_item in _get_output_items(collateral_return): - response = client.call(tx_item) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(tx_item, expect=m.CardanoTxItemAck) for reference_input in reference_inputs: - response = client.call(reference_input) - if not isinstance(response, messages.CardanoTxItemAck): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(reference_input, expect=m.CardanoTxItemAck) sign_tx_response["witnesses"] = [] for witness_request in witness_requests: - response = client.call(witness_request) - if not isinstance(response, messages.CardanoTxWitnessResponse): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(witness_request, expect=m.CardanoTxWitnessResponse) sign_tx_response["witnesses"].append( { "type": response.type, @@ -1009,13 +986,9 @@ def sign_tx( } ) - response = client.call(messages.CardanoTxHostAck()) - if not isinstance(response, messages.CardanoTxBodyHash): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(m.CardanoTxHostAck(), expect=m.CardanoTxBodyHash) sign_tx_response["tx_hash"] = response.tx_hash - response = client.call(messages.CardanoTxHostAck()) - if not isinstance(response, messages.CardanoSignTxFinished): - raise UNEXPECTED_RESPONSE_ERROR + response = client.call(m.CardanoTxHostAck(), expect=m.CardanoSignTxFinished) return sign_tx_response diff --git a/python/src/trezorlib/cli/debug.py b/python/src/trezorlib/cli/debug.py index 50613a04eee..d9d936c7ab9 100644 --- a/python/src/trezorlib/cli/debug.py +++ b/python/src/trezorlib/cli/debug.py @@ -107,16 +107,16 @@ def record_screen_from_connection( @cli.command() @with_client -def prodtest_t1(client: "TrezorClient") -> str: +def prodtest_t1(client: "TrezorClient") -> None: """Perform a prodtest on Model One. Only available on PRODTEST firmware and on T1B1. Formerly named self-test. """ - return debuglink_prodtest_t1(client) + debuglink_prodtest_t1(client) @cli.command() @with_client -def optiga_set_sec_max(client: "TrezorClient") -> str: +def optiga_set_sec_max(client: "TrezorClient") -> None: """Set Optiga's security event counter to maximum.""" - return debuglink_optiga_set_sec_max(client) + debuglink_optiga_set_sec_max(client) diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py index 07dfcd75242..0803b85a695 100644 --- a/python/src/trezorlib/cli/device.py +++ b/python/src/trezorlib/cli/device.py @@ -29,7 +29,6 @@ if t.TYPE_CHECKING: from ..client import TrezorClient - from ..protobuf import MessageType from . import TrezorConnection RECOVERY_DEVICE_INPUT_METHOD = { @@ -66,7 +65,7 @@ def cli() -> None: is_flag=True, ) @with_client -def wipe(client: "TrezorClient", bootloader: bool) -> str: +def wipe(client: "TrezorClient", bootloader: bool) -> None: """Reset device to factory defaults and remove all private data.""" if bootloader: if not client.features.bootloader_mode: @@ -87,11 +86,7 @@ def wipe(client: "TrezorClient", bootloader: bool) -> str: else: click.echo("Wiping user data!") - try: - return device.wipe(client) - except exceptions.TrezorFailure as e: - click.echo("Action failed: {} {}".format(*e.args)) - sys.exit(3) + device.wipe(client) @cli.command() @@ -116,7 +111,7 @@ def load( academic: bool, needs_backup: bool, no_backup: bool, -) -> str: +) -> None: """Upload seed and custom configuration to the device. This functionality is only available in debug mode. @@ -136,7 +131,7 @@ def load( label = "ACADEMIC" try: - return debuglink.load_device( + debuglink.load_device( client, mnemonic=list(mnemonic), pin=pin, @@ -184,7 +179,7 @@ def recover( input_method: messages.RecoveryDeviceInputMethod, dry_run: bool, unlock_repeated_backup: bool, -) -> "MessageType": +) -> None: """Start safe recovery workflow.""" if input_method == messages.RecoveryDeviceInputMethod.ScrambledWords: input_callback = ui.mnemonic_words(expand) @@ -201,7 +196,7 @@ def recover( if unlock_repeated_backup: type = messages.RecoveryType.UnlockRepeatedBackup - return device.recover( + device.recover( client, word_count=int(words), passphrase_protection=passphrase_protection, @@ -236,21 +231,13 @@ def setup( no_backup: bool, backup_type: messages.BackupType | None, entropy_check_count: int | None, -) -> str: +) -> None: """Perform device setup and generate new seed.""" if strength: strength = int(strength) BT = messages.BackupType - if backup_type is None: - if client.version >= (2, 7, 1): - # SLIP39 extendable was introduced in 2.7.1 - backup_type = BT.Slip39_Single_Extendable - else: - # this includes both T1 and older trezor-cores - backup_type = BT.Bip39 - if ( backup_type in (BT.Slip39_Single_Extendable, BT.Slip39_Basic, BT.Slip39_Basic_Extendable) @@ -264,7 +251,7 @@ def setup( "backup type. Traditional BIP39 backup may be generated instead." ) - resp, path_xpubs = device.reset_entropy_check( + path_xpubs = device.setup( client, strength=strength, passphrase_protection=passphrase_protection, @@ -277,13 +264,10 @@ def setup( entropy_check_count=entropy_check_count, ) - if isinstance(resp, messages.Success): + if path_xpubs: click.echo("XPUBs for the generated seed") for path, xpub in path_xpubs: click.echo(f"{format_path(path)}: {xpub}") - return resp.message or "" - else: - raise RuntimeError(f"Received {resp.__class__}") @cli.command() @@ -294,10 +278,9 @@ def backup( client: "TrezorClient", group_threshold: int | None = None, groups: t.Sequence[tuple[int, int]] = (), -) -> str: +) -> None: """Perform device seed backup.""" - - return device.backup(client, group_threshold, groups) + device.backup(client, group_threshold, groups) @cli.command() @@ -305,7 +288,7 @@ def backup( @with_client def sd_protect( client: "TrezorClient", operation: messages.SdProtectOperationType -) -> str: +) -> None: """Secure the device with SD card protection. When SD card protection is enabled, a randomly generated secret is stored @@ -321,12 +304,12 @@ def sd_protect( """ if client.features.model == "1": raise click.ClickException("Trezor One does not support SD card protection.") - return device.sd_protect(client, operation) + device.sd_protect(client, operation) @cli.command() @click.pass_obj -def reboot_to_bootloader(obj: "TrezorConnection") -> str: +def reboot_to_bootloader(obj: "TrezorConnection") -> None: """Reboot device into bootloader mode. Currently only supported on Trezor Model One. @@ -334,21 +317,21 @@ def reboot_to_bootloader(obj: "TrezorConnection") -> str: # avoid using @with_client because it closes the session afterwards, # which triggers double prompt on device with obj.client_context() as client: - return device.reboot_to_bootloader(client) + device.reboot_to_bootloader(client) @cli.command() @with_client -def tutorial(client: "TrezorClient") -> str: +def tutorial(client: "TrezorClient") -> None: """Show on-device tutorial.""" - return device.show_device_tutorial(client) + device.show_device_tutorial(client) @cli.command() @with_client -def unlock_bootloader(client: "TrezorClient") -> str: +def unlock_bootloader(client: "TrezorClient") -> None: """Unlocks bootloader. Irreversible.""" - return device.unlock_bootloader(client) + device.unlock_bootloader(client) @cli.command() @@ -360,10 +343,11 @@ def unlock_bootloader(client: "TrezorClient") -> str: help="Dialog expiry in seconds.", ) @with_client -def set_busy(client: "TrezorClient", enable: bool | None, expiry: int | None) -> str: +def set_busy(client: "TrezorClient", enable: bool | None, expiry: int | None) -> None: """Show a "Do not disconnect" dialog.""" if enable is False: - return device.set_busy(client, None) + device.set_busy(client, None) + return if expiry is None: raise click.ClickException("Missing option '-e' / '--expiry'.") @@ -373,7 +357,7 @@ def set_busy(client: "TrezorClient", enable: bool | None, expiry: int | None) -> f"Invalid value for '-e' / '--expiry': '{expiry}' is not a positive integer." ) - return device.set_busy(client, expiry * 1000) + device.set_busy(client, expiry * 1000) PUBKEY_WHITELIST_URL_TEMPLATE = ( diff --git a/python/src/trezorlib/cli/fido.py b/python/src/trezorlib/cli/fido.py index 5983c572493..b51bb74e123 100644 --- a/python/src/trezorlib/cli/fido.py +++ b/python/src/trezorlib/cli/fido.py @@ -80,12 +80,12 @@ def credentials_list(client: "TrezorClient") -> None: @credentials.command(name="add") @click.argument("hex_credential_id") @with_client -def credentials_add(client: "TrezorClient", hex_credential_id: str) -> str: +def credentials_add(client: "TrezorClient", hex_credential_id: str) -> None: """Add the credential with the given ID as a resident credential. HEX_CREDENTIAL_ID is the credential ID as a hexadecimal string. """ - return fido.add_credential(client, bytes.fromhex(hex_credential_id)) + fido.add_credential(client, bytes.fromhex(hex_credential_id)) @credentials.command(name="remove") @@ -93,9 +93,9 @@ def credentials_add(client: "TrezorClient", hex_credential_id: str) -> str: "-i", "--index", required=True, type=click.IntRange(0, 99), help="Credential index." ) @with_client -def credentials_remove(client: "TrezorClient", index: int) -> str: +def credentials_remove(client: "TrezorClient", index: int) -> None: """Remove the resident credential at the given index.""" - return fido.remove_credential(client, index) + fido.remove_credential(client, index) # @@ -111,9 +111,9 @@ def counter() -> None: @counter.command(name="set") @click.argument("counter", type=int) @with_client -def counter_set(client: "TrezorClient", counter: int) -> str: +def counter_set(client: "TrezorClient", counter: int) -> None: """Set FIDO/U2F counter value.""" - return fido.set_counter(client, counter) + fido.set_counter(client, counter) @counter.command(name="get-next") diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py index eac93eb7965..946f3fbffcf 100644 --- a/python/src/trezorlib/cli/settings.py +++ b/python/src/trezorlib/cli/settings.py @@ -181,17 +181,17 @@ def cli() -> None: @click.option("-r", "--remove", is_flag=True, hidden=True) @click.argument("enable", type=ChoiceType({"on": True, "off": False}), required=False) @with_client -def pin(client: "TrezorClient", enable: Optional[bool], remove: bool) -> str: +def pin(client: "TrezorClient", enable: Optional[bool], remove: bool) -> None: """Set, change or remove PIN.""" # Remove argument is there for backwards compatibility - return device.change_pin(client, remove=_should_remove(enable, remove)) + device.change_pin(client, remove=_should_remove(enable, remove)) @cli.command() @click.option("-r", "--remove", is_flag=True, hidden=True) @click.argument("enable", type=ChoiceType({"on": True, "off": False}), required=False) @with_client -def wipe_code(client: "TrezorClient", enable: Optional[bool], remove: bool) -> str: +def wipe_code(client: "TrezorClient", enable: Optional[bool], remove: bool) -> None: """Set or remove the wipe code. The wipe code functions as a "self-destruct PIN". If the wipe code is ever @@ -199,7 +199,7 @@ def wipe_code(client: "TrezorClient", enable: Optional[bool], remove: bool) -> s removed and the device will be reset to factory defaults. """ # Remove argument is there for backwards compatibility - return device.change_wipe_code(client, remove=_should_remove(enable, remove)) + device.change_wipe_code(client, remove=_should_remove(enable, remove)) @cli.command() @@ -207,24 +207,24 @@ def wipe_code(client: "TrezorClient", enable: Optional[bool], remove: bool) -> s @click.option("-l", "--label", "_ignore", is_flag=True, hidden=True, expose_value=False) @click.argument("label") @with_client -def label(client: "TrezorClient", label: str) -> str: +def label(client: "TrezorClient", label: str) -> None: """Set new device label.""" - return device.apply_settings(client, label=label) + device.apply_settings(client, label=label) @cli.command() @with_client -def brightness(client: "TrezorClient") -> str: +def brightness(client: "TrezorClient") -> None: """Set display brightness.""" - return device.set_brightness(client) + device.set_brightness(client) @cli.command() @click.argument("enable", type=ChoiceType({"on": True, "off": False})) @with_client -def haptic_feedback(client: "TrezorClient", enable: bool) -> str: +def haptic_feedback(client: "TrezorClient", enable: bool) -> None: """Enable or disable haptic feedback.""" - return device.apply_settings(client, haptic_feedback=enable) + device.apply_settings(client, haptic_feedback=enable) @cli.command() @@ -236,7 +236,7 @@ def haptic_feedback(client: "TrezorClient", enable: bool) -> str: @with_client def language( client: "TrezorClient", path_or_url: str | None, remove: bool, display: bool | None -) -> str: +) -> None: """Set new language with translations.""" if remove != (path_or_url is None): raise click.ClickException("Either provide a path or URL or use --remove") @@ -259,27 +259,27 @@ def language( raise click.ClickException( f"Failed to load translations from {path_or_url}" ) from None - return device.change_language( - client, language_data=language_data, show_display=display - ) + device.change_language(client, language_data=language_data, show_display=display) @cli.command() @click.argument("rotation", type=ChoiceType(ROTATION)) @with_client -def display_rotation(client: "TrezorClient", rotation: messages.DisplayRotation) -> str: +def display_rotation( + client: "TrezorClient", rotation: messages.DisplayRotation +) -> None: """Set display rotation. Configure display rotation for Trezor Model T. The options are north, east, south or west. """ - return device.apply_settings(client, display_rotation=rotation) + device.apply_settings(client, display_rotation=rotation) @cli.command() @click.argument("delay", type=str) @with_client -def auto_lock_delay(client: "TrezorClient", delay: str) -> str: +def auto_lock_delay(client: "TrezorClient", delay: str) -> None: """Set auto-lock delay (in seconds).""" if not client.features.pin_protection: @@ -291,13 +291,13 @@ def auto_lock_delay(client: "TrezorClient", delay: str) -> str: seconds = float(value) * units[unit] else: seconds = float(delay) # assume seconds if no unit is specified - return device.apply_settings(client, auto_lock_delay_ms=int(seconds * 1000)) + device.apply_settings(client, auto_lock_delay_ms=int(seconds * 1000)) @cli.command() @click.argument("flags") @with_client -def flags(client: "TrezorClient", flags: str) -> str: +def flags(client: "TrezorClient", flags: str) -> None: """Set device flags.""" if flags.lower().startswith("0b"): flags_int = int(flags, 2) @@ -305,7 +305,7 @@ def flags(client: "TrezorClient", flags: str) -> str: flags_int = int(flags, 16) else: flags_int = int(flags) - return device.apply_flags(client, flags=flags_int) + device.apply_flags(client, flags=flags_int) @cli.command() @@ -315,7 +315,7 @@ def flags(client: "TrezorClient", flags: str) -> str: ) @click.option("-q", "--quality", type=int, default=90, help="JPEG quality (0-100)") @with_client -def homescreen(client: "TrezorClient", filename: str, quality: int) -> str: +def homescreen(client: "TrezorClient", filename: str, quality: int) -> None: """Set new homescreen. To revert to default homescreen, use 'trezorctl set homescreen default' @@ -369,7 +369,7 @@ def homescreen(client: "TrezorClient", filename: str, quality: int) -> str: "Unknown image format requested by the device." ) - return device.apply_settings(client, homescreen=img) + device.apply_settings(client, homescreen=img) @cli.command() @@ -380,7 +380,7 @@ def homescreen(client: "TrezorClient", filename: str, quality: int) -> str: @with_client def safety_checks( client: "TrezorClient", always: bool, level: messages.SafetyCheckLevel -) -> str: +) -> None: """Set safety check level. Set to "strict" to get the full Trezor security (default setting). @@ -392,18 +392,18 @@ def safety_checks( """ if always and level == messages.SafetyCheckLevel.PromptTemporarily: level = messages.SafetyCheckLevel.PromptAlways - return device.apply_settings(client, safety_checks=level) + device.apply_settings(client, safety_checks=level) @cli.command() @click.argument("enable", type=ChoiceType({"on": True, "off": False})) @with_client -def experimental_features(client: "TrezorClient", enable: bool) -> str: +def experimental_features(client: "TrezorClient", enable: bool) -> None: """Enable or disable experimental message types. This is a developer feature. Use with caution. """ - return device.apply_settings(client, experimental_features=enable) + device.apply_settings(client, experimental_features=enable) # @@ -427,13 +427,13 @@ def passphrase_main() -> None: @passphrase.command(name="on") @click.option("-f/-F", "--force-on-device/--no-force-on-device", default=None) @with_client -def passphrase_on(client: "TrezorClient", force_on_device: Optional[bool]) -> str: +def passphrase_on(client: "TrezorClient", force_on_device: Optional[bool]) -> None: """Enable passphrase.""" if client.features.passphrase_protection is not True: use_passphrase = True else: use_passphrase = None - return device.apply_settings( + device.apply_settings( client, use_passphrase=use_passphrase, passphrase_always_on_device=force_on_device, @@ -442,9 +442,9 @@ def passphrase_on(client: "TrezorClient", force_on_device: Optional[bool]) -> st @passphrase.command(name="off") @with_client -def passphrase_off(client: "TrezorClient") -> str: +def passphrase_off(client: "TrezorClient") -> None: """Disable passphrase.""" - return device.apply_settings(client, use_passphrase=False) + device.apply_settings(client, use_passphrase=False) # Registering the aliases for backwards compatibility @@ -458,9 +458,9 @@ def passphrase_off(client: "TrezorClient") -> str: @passphrase.command(name="hide") @click.argument("hide", type=ChoiceType({"on": True, "off": False})) @with_client -def hide_passphrase_from_host(client: "TrezorClient", hide: bool) -> str: +def hide_passphrase_from_host(client: "TrezorClient", hide: bool) -> None: """Enable or disable hiding passphrase coming from host. This is a developer feature. Use with caution. """ - return device.apply_settings(client, hide_passphrase_from_host=hide) + device.apply_settings(client, hide_passphrase_from_host=hide) diff --git a/python/src/trezorlib/cli/solana.py b/python/src/trezorlib/cli/solana.py index 3fe80a51646..590b4f79146 100644 --- a/python/src/trezorlib/cli/solana.py +++ b/python/src/trezorlib/cli/solana.py @@ -26,7 +26,7 @@ def get_public_key( client: "TrezorClient", address: str, show_display: bool, -) -> messages.SolanaPublicKey: +) -> bytes: """Get Solana public key.""" address_n = tools.parse_path(address) return solana.get_public_key(client, address_n, show_display) @@ -42,7 +42,7 @@ def get_address( address: str, show_display: bool, chunkify: bool, -) -> messages.SolanaAddress: +) -> str: """Get Solana address.""" address_n = tools.parse_path(address) return solana.get_address(client, address_n, show_display, chunkify) @@ -58,7 +58,7 @@ def sign_tx( address: str, serialized_tx: str, additional_information_file: Optional[TextIO], -) -> messages.SolanaTxSignature: +) -> bytes: """Sign Solana transaction.""" address_n = tools.parse_path(address) diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index fa7992ab0e4..4e432bd0123 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -27,7 +27,7 @@ from .log import DUMP_BYTES from .messages import Capability from .protobuf import MessageType -from .tools import expect, parse_path, session +from .tools import parse_path, session if TYPE_CHECKING: from .transport import Transport @@ -397,12 +397,7 @@ def check_firmware_version(self, warn_only: bool = False) -> None: else: raise exceptions.OutdatedFirmwareError(OUTDATED_FIRMWARE_ERROR) - @expect(messages.Success, field="message", ret_type=str) - def ping( - self, - msg: str, - button_protection: bool = False, - ) -> MessageType: + def ping(self, msg: str, button_protection: bool = False) -> str: # We would like ping to work on any valid TrezorClient instance, but # due to the protection modes, we need to go through self.call, and that will # raise an exception if the firmware is too old. @@ -416,13 +411,18 @@ def ping( # device is PIN-locked. # respond and hope for the best resp = self._callback_button(resp) - return resp + resp = messages.Success.ensure_isinstance(resp) + assert resp.message is not None + return resp.message finally: self.close() - return self.call( - messages.Ping(message=msg, button_protection=button_protection) + resp = self.call( + messages.Ping(message=msg, button_protection=button_protection), + expect=messages.Success, ) + assert resp.message is not None + return resp.message def get_device_id(self) -> Optional[str]: return self.features.device_id diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index d1f89db35fa..56c1dfa344f 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -44,10 +44,9 @@ from . import mapping, messages, models, protobuf from .client import TrezorClient -from .exceptions import TrezorFailure +from .exceptions import TrezorFailure, UnexpectedMessageError from .log import DUMP_BYTES from .messages import DebugWaitType -from .tools import expect if TYPE_CHECKING: from typing_extensions import Protocol @@ -775,9 +774,10 @@ def stop_recording(self) -> None: else: self.t1_take_screenshots = False - @expect(messages.DebugLinkMemory, field="memory", ret_type=bytes) - def memory_read(self, address: int, length: int) -> protobuf.MessageType: - return self._call(messages.DebugLinkMemoryRead(address=address, length=length)) + def memory_read(self, address: int, length: int) -> bytes: + return self._call( + messages.DebugLinkMemoryRead(address=address, length=length) + ).memory def memory_write(self, address: int, memory: bytes, flash: bool = False) -> None: self._write( @@ -787,9 +787,11 @@ def memory_write(self, address: int, memory: bytes, flash: bool = False) -> None def flash_erase(self, sector: int) -> None: self._write(messages.DebugLinkFlashErase(sector=sector)) - @expect(messages.Success) def erase_sd_card(self, format: bool = True) -> messages.Success: - return self._call(messages.DebugLinkEraseSdCard(format=format)) + res = self._call(messages.DebugLinkEraseSdCard(format=format)) + if not isinstance(res, messages.Success): + raise UnexpectedMessageError(messages.Success, res) + return res def snapshot_legacy(self) -> None: """Snapshot the current state of the device.""" @@ -1350,7 +1352,6 @@ def mnemonic_callback(self, _) -> str: raise RuntimeError("Unexpected call") -@expect(messages.Success, field="message", ret_type=str) def load_device( client: "TrezorClient", mnemonic: Union[str, Iterable[str]], @@ -1360,7 +1361,7 @@ def load_device( skip_checksum: bool = False, needs_backup: bool = False, no_backup: bool = False, -) -> protobuf.MessageType: +) -> None: if isinstance(mnemonic, str): mnemonic = [mnemonic] @@ -1371,7 +1372,7 @@ def load_device( "Device is initialized already. Call device.wipe() and try again." ) - resp = client.call( + client.call( messages.LoadDevice( mnemonics=mnemonics, pin=pin, @@ -1380,25 +1381,25 @@ def load_device( skip_checksum=skip_checksum, needs_backup=needs_backup, no_backup=no_backup, - ) + ), + expect=messages.Success, ) client.init_device() - return resp # keep the old name for compatibility load_device_by_mnemonic = load_device -@expect(messages.Success, field="message", ret_type=str) -def prodtest_t1(client: "TrezorClient") -> protobuf.MessageType: +def prodtest_t1(client: "TrezorClient") -> None: if client.features.bootloader_mode is not True: raise RuntimeError("Device must be in bootloader mode") - return client.call( + client.call( messages.ProdTestT1( payload=b"\x00\xFF\x55\xAA\x66\x99\x33\xCCABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\x00\xFF\x55\xAA\x66\x99\x33\xCC" - ) + ), + expect=messages.Success, ) @@ -1450,6 +1451,5 @@ def _is_emulator(debug_client: "TrezorClientDebugLink") -> bool: return debug_client.features.fw_vendor == "EMULATOR" -@expect(messages.Success, field="message", ret_type=str) -def optiga_set_sec_max(client: "TrezorClient") -> protobuf.MessageType: - return client.call(messages.DebugLinkOptigaSetSecMax()) +def optiga_set_sec_max(client: "TrezorClient") -> None: + client.call(messages.DebugLinkOptigaSetSecMax(), expect=messages.Success) diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index fcf948f5233..3d7cb401001 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -18,7 +18,6 @@ import hashlib import hmac -import os import random import secrets import time @@ -29,11 +28,16 @@ from . import messages from .exceptions import Cancelled, TrezorException -from .tools import Address, expect, parse_path, session +from .tools import ( + Address, + _deprecation_retval_helper, + _return_success, + parse_path, + session, +) if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType RECOVERY_BACK = "\x08" # backspace character, sent literally @@ -42,7 +46,6 @@ ENTROPY_CHECK_MIN_VERSION = (2, 8, 7) -@expect(messages.Success, field="message", ret_type=str) @session def apply_settings( client: "TrezorClient", @@ -57,7 +60,7 @@ def apply_settings( experimental_features: Optional[bool] = None, hide_passphrase_from_host: Optional[bool] = None, haptic_feedback: Optional[bool] = None, -) -> "MessageType": +) -> str | None: if language is not None: warnings.warn( "language ignored. Use change_language() to set device language.", @@ -76,87 +79,78 @@ def apply_settings( haptic_feedback=haptic_feedback, ) - out = client.call(settings) + out = client.call(settings, expect=messages.Success) client.refresh_features() - return out + return _return_success(out) def _send_language_data( client: "TrezorClient", request: "messages.TranslationDataRequest", language_data: bytes, -) -> "MessageType": - response: MessageType = request +) -> None: + response = request while not isinstance(response, messages.Success): - assert isinstance(response, messages.TranslationDataRequest) + response = messages.TranslationDataRequest.ensure_isinstance(response) data_length = response.data_length data_offset = response.data_offset chunk = language_data[data_offset : data_offset + data_length] response = client.call(messages.TranslationDataAck(data_chunk=chunk)) - return response - -@expect(messages.Success, field="message", ret_type=str) @session def change_language( client: "TrezorClient", language_data: bytes, show_display: bool | None = None, -) -> "MessageType": +) -> str | None: data_length = len(language_data) msg = messages.ChangeLanguage(data_length=data_length, show_display=show_display) response = client.call(msg) if data_length > 0: - assert isinstance(response, messages.TranslationDataRequest) - response = _send_language_data(client, response, language_data) - assert isinstance(response, messages.Success) + response = messages.TranslationDataRequest.ensure_isinstance(response) + _send_language_data(client, response, language_data) client.refresh_features() # changing the language in features - return response + return _return_success(messages.Success(message="Language changed.")) -@expect(messages.Success, field="message", ret_type=str) @session -def apply_flags(client: "TrezorClient", flags: int) -> "MessageType": - out = client.call(messages.ApplyFlags(flags=flags)) +def apply_flags(client: "TrezorClient", flags: int) -> str | None: + out = client.call(messages.ApplyFlags(flags=flags), expect=messages.Success) client.refresh_features() - return out + return _return_success(out) -@expect(messages.Success, field="message", ret_type=str) @session -def change_pin(client: "TrezorClient", remove: bool = False) -> "MessageType": - ret = client.call(messages.ChangePin(remove=remove)) +def change_pin(client: "TrezorClient", remove: bool = False) -> str | None: + ret = client.call(messages.ChangePin(remove=remove), expect=messages.Success) client.refresh_features() - return ret + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) @session -def change_wipe_code(client: "TrezorClient", remove: bool = False) -> "MessageType": - ret = client.call(messages.ChangeWipeCode(remove=remove)) +def change_wipe_code(client: "TrezorClient", remove: bool = False) -> str | None: + ret = client.call(messages.ChangeWipeCode(remove=remove), expect=messages.Success) client.refresh_features() - return ret + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) @session def sd_protect( client: "TrezorClient", operation: messages.SdProtectOperationType -) -> "MessageType": - ret = client.call(messages.SdProtect(operation=operation)) +) -> str | None: + ret = client.call(messages.SdProtect(operation=operation), expect=messages.Success) client.refresh_features() - return ret + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) @session -def wipe(client: "TrezorClient") -> "MessageType": - ret = client.call(messages.WipeDevice()) +def wipe(client: "TrezorClient") -> str | None: + ret = client.call(messages.WipeDevice(), expect=messages.Success) if not client.features.bootloader_mode: client.init_device() - return ret + return _return_success(ret) @session @@ -173,7 +167,7 @@ def recover( u2f_counter: Optional[int] = None, *, type: Optional[messages.RecoveryType] = None, -) -> "MessageType": +) -> messages.Success | None: if language is not None: warnings.warn( "language ignored. Use change_language() to set device language.", @@ -235,8 +229,12 @@ def recover( except Cancelled: res = client.call(messages.Cancel()) + # check that the result is a Success + res = messages.Success.ensure_isinstance(res) + # reinitialize the device client.init_device() - return res + + return _deprecation_retval_helper(res) def is_slip39_backup_type(backup_type: messages.BackupType): @@ -279,13 +277,7 @@ def _seed_from_entropy( return seed -@expect(messages.Success, field="message", ret_type=str) -def reset(*args: Any, **kwargs: Any) -> "MessageType": - return reset_entropy_check(*args, **kwargs)[0] - - -@session -def reset_entropy_check( +def reset( client: "TrezorClient", display_random: bool = False, strength: Optional[int] = None, @@ -576,13 +568,12 @@ def verify_entropy_commitment( return xpubs -@expect(messages.Success, field="message", ret_type=str) @session def backup( client: "TrezorClient", group_threshold: Optional[int] = None, groups: Iterable[tuple[int, int]] = (), -) -> "MessageType": +) -> str | None: ret = client.call( messages.BackupDevice( group_threshold=group_threshold, @@ -590,38 +581,39 @@ def backup( messages.Slip39Group(member_threshold=t, member_count=c) for t, c in groups ], - ) + ), + expect=messages.Success, ) client.refresh_features() - return ret + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) -def cancel_authorization(client: "TrezorClient") -> "MessageType": - return client.call(messages.CancelAuthorization()) +def cancel_authorization(client: "TrezorClient") -> str | None: + ret = client.call(messages.CancelAuthorization(), expect=messages.Success) + return _return_success(ret) -@expect(messages.UnlockedPathRequest, field="mac", ret_type=bytes) -def unlock_path(client: "TrezorClient", n: "Address") -> "MessageType": - resp = client.call(messages.UnlockPath(address_n=n)) +def unlock_path(client: "TrezorClient", n: "Address") -> bytes: + resp = client.call( + messages.UnlockPath(address_n=n), expect=messages.UnlockedPathRequest + ) # Cancel the UnlockPath workflow now that we have the authentication code. try: client.call(messages.Cancel()) except Cancelled: - return resp + return resp.mac else: raise TrezorException("Unexpected response in UnlockPath flow") @session -@expect(messages.Success, field="message", ret_type=str) def reboot_to_bootloader( client: "TrezorClient", boot_command: messages.BootCommand = messages.BootCommand.STOP_AND_WAIT, firmware_header: Optional[bytes] = None, language_data: bytes = b"", -) -> "MessageType": +) -> str | None: response = client.call( messages.RebootToBootloader( boot_command=boot_command, @@ -631,41 +623,42 @@ def reboot_to_bootloader( ) if isinstance(response, messages.TranslationDataRequest): response = _send_language_data(client, response, language_data) - return response + return _return_success(messages.Success(message="")) @session -@expect(messages.Success, field="message", ret_type=str) -def show_device_tutorial(client: "TrezorClient") -> "MessageType": - return client.call(messages.ShowDeviceTutorial()) +def show_device_tutorial(client: "TrezorClient") -> str | None: + ret = client.call(messages.ShowDeviceTutorial(), expect=messages.Success) + return _return_success(ret) @session -@expect(messages.Success, field="message", ret_type=str) -def unlock_bootloader(client: "TrezorClient") -> "MessageType": - return client.call(messages.UnlockBootloader()) +def unlock_bootloader(client: "TrezorClient") -> str | None: + ret = client.call(messages.UnlockBootloader(), expect=messages.Success) + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) @session -def set_busy(client: "TrezorClient", expiry_ms: Optional[int]) -> "MessageType": +def set_busy(client: "TrezorClient", expiry_ms: Optional[int]) -> str | None: """Sets or clears the busy state of the device. In the busy state the device shows a "Do not disconnect" message instead of the homescreen. Setting `expiry_ms=None` clears the busy state. """ - ret = client.call(messages.SetBusy(expiry_ms=expiry_ms)) + ret = client.call(messages.SetBusy(expiry_ms=expiry_ms), expect=messages.Success) client.refresh_features() - return ret + return _return_success(ret) -@expect(messages.AuthenticityProof) -def authenticate(client: "TrezorClient", challenge: bytes): - return client.call(messages.AuthenticateDevice(challenge=challenge)) +def authenticate( + client: "TrezorClient", challenge: bytes +) -> messages.AuthenticityProof: + return client.call( + messages.AuthenticateDevice(challenge=challenge), + expect=messages.AuthenticityProof, + ) -@expect(messages.Success, field="message", ret_type=str) -def set_brightness( - client: "TrezorClient", value: Optional[int] = None -) -> "MessageType": - return client.call(messages.SetBrightness(value=value)) +def set_brightness(client: "TrezorClient", value: Optional[int] = None) -> str | None: + ret = client.call(messages.SetBrightness(value=value), expect=messages.Success) + return _return_success(ret) diff --git a/python/src/trezorlib/eos.py b/python/src/trezorlib/eos.py index 1ffaafb4ab7..eb491f204c1 100644 --- a/python/src/trezorlib/eos.py +++ b/python/src/trezorlib/eos.py @@ -18,11 +18,10 @@ from typing import TYPE_CHECKING, List, Tuple from . import exceptions, messages -from .tools import b58decode, expect, session +from .tools import b58decode, session if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address @@ -319,14 +318,13 @@ def parse_transaction_json( # ====== Client functions ====== # -@expect(messages.EosPublicKey) def get_public_key( client: "TrezorClient", n: "Address", show_display: bool = False -) -> "MessageType": - response = client.call( - messages.EosGetPublicKey(address_n=n, show_display=show_display) +) -> messages.EosPublicKey: + return client.call( + messages.EosGetPublicKey(address_n=n, show_display=show_display), + expect=messages.EosPublicKey, ) - return response @session diff --git a/python/src/trezorlib/ethereum.py b/python/src/trezorlib/ethereum.py index 1cf2eeeaed1..96ce4d10663 100644 --- a/python/src/trezorlib/ethereum.py +++ b/python/src/trezorlib/ethereum.py @@ -18,11 +18,10 @@ from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Tuple from . import definitions, exceptions, messages -from .tools import expect, prepare_message_bytes, session, unharden +from .tools import prepare_message_bytes, session, unharden if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address @@ -161,30 +160,32 @@ def network_from_address_n( # ====== Client functions ====== # -@expect(messages.EthereumAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", n: "Address", show_display: bool = False, encoded_network: Optional[bytes] = None, chunkify: bool = False, -) -> "MessageType": - return client.call( +) -> str: + resp = client.call( messages.EthereumGetAddress( address_n=n, show_display=show_display, encoded_network=encoded_network, chunkify=chunkify, - ) + ), + expect=messages.EthereumAddress, ) + assert resp.address is not None + return resp.address -@expect(messages.EthereumPublicKey) def get_public_node( client: "TrezorClient", n: "Address", show_display: bool = False -) -> "MessageType": +) -> messages.EthereumPublicKey: return client.call( - messages.EthereumGetPublicKey(address_n=n, show_display=show_display) + messages.EthereumGetPublicKey(address_n=n, show_display=show_display), + expect=messages.EthereumPublicKey, ) @@ -297,25 +298,24 @@ def sign_tx_eip1559( return response.signature_v, response.signature_r, response.signature_s -@expect(messages.EthereumMessageSignature) def sign_message( client: "TrezorClient", n: "Address", message: AnyStr, encoded_network: Optional[bytes] = None, chunkify: bool = False, -) -> "MessageType": +) -> messages.EthereumMessageSignature: return client.call( messages.EthereumSignMessage( address_n=n, message=prepare_message_bytes(message), encoded_network=encoded_network, chunkify=chunkify, - ) + ), + expect=messages.EthereumMessageSignature, ) -@expect(messages.EthereumTypedDataSignature) def sign_typed_data( client: "TrezorClient", n: "Address", @@ -323,7 +323,7 @@ def sign_typed_data( *, metamask_v4_compat: bool = True, definitions: Optional[messages.EthereumDefinitions] = None, -) -> "MessageType": +) -> messages.EthereumTypedDataSignature: data = sanitize_typed_data(data) types = data["types"] @@ -387,7 +387,7 @@ def sign_typed_data( request = messages.EthereumTypedDataValueAck(value=encoded_data) response = client.call(request) - return response + return messages.EthereumTypedDataSignature.ensure_isinstance(response) def verify_message( @@ -398,32 +398,33 @@ def verify_message( chunkify: bool = False, ) -> bool: try: - resp = client.call( + client.call( messages.EthereumVerifyMessage( address=address, signature=signature, message=prepare_message_bytes(message), chunkify=chunkify, - ) + ), + expect=messages.Success, ) + return True except exceptions.TrezorFailure: return False - return isinstance(resp, messages.Success) -@expect(messages.EthereumTypedDataSignature) def sign_typed_data_hash( client: "TrezorClient", n: "Address", domain_hash: bytes, message_hash: Optional[bytes], encoded_network: Optional[bytes] = None, -) -> "MessageType": +) -> messages.EthereumTypedDataSignature: return client.call( messages.EthereumSignTypedHash( address_n=n, domain_separator_hash=domain_hash, message_hash=message_hash, encoded_network=encoded_network, - ) + ), + expect=messages.EthereumTypedDataSignature, ) diff --git a/python/src/trezorlib/fido.py b/python/src/trezorlib/fido.py index 4ed6f22951f..a2618b72dbb 100644 --- a/python/src/trezorlib/fido.py +++ b/python/src/trezorlib/fido.py @@ -14,42 +14,45 @@ # You should have received a copy of the License along with this library. # If not, see . -from typing import TYPE_CHECKING, List +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence from . import messages -from .tools import expect +from .tools import _return_success if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType -@expect( - messages.WebAuthnCredentials, - field="credentials", - ret_type=List[messages.WebAuthnCredential], -) -def list_credentials(client: "TrezorClient") -> "MessageType": - return client.call(messages.WebAuthnListResidentCredentials()) +def list_credentials(client: "TrezorClient") -> Sequence[messages.WebAuthnCredential]: + return client.call( + messages.WebAuthnListResidentCredentials(), expect=messages.WebAuthnCredentials + ).credentials -@expect(messages.Success, field="message", ret_type=str) -def add_credential(client: "TrezorClient", credential_id: bytes) -> "MessageType": - return client.call( - messages.WebAuthnAddResidentCredential(credential_id=credential_id) +def add_credential(client: "TrezorClient", credential_id: bytes) -> str | None: + ret = client.call( + messages.WebAuthnAddResidentCredential(credential_id=credential_id), + expect=messages.Success, ) + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) -def remove_credential(client: "TrezorClient", index: int) -> "MessageType": - return client.call(messages.WebAuthnRemoveResidentCredential(index=index)) +def remove_credential(client: "TrezorClient", index: int) -> str | None: + ret = client.call( + messages.WebAuthnRemoveResidentCredential(index=index), expect=messages.Success + ) + return _return_success(ret) -@expect(messages.Success, field="message", ret_type=str) -def set_counter(client: "TrezorClient", u2f_counter: int) -> "MessageType": - return client.call(messages.SetU2FCounter(u2f_counter=u2f_counter)) +def set_counter(client: "TrezorClient", u2f_counter: int) -> str | None: + ret = client.call( + messages.SetU2FCounter(u2f_counter=u2f_counter), expect=messages.Success + ) + return _return_success(ret) -@expect(messages.NextU2FCounter, field="u2f_counter", ret_type=int) -def get_next_counter(client: "TrezorClient") -> "MessageType": - return client.call(messages.GetNextU2FCounter()) +def get_next_counter(client: "TrezorClient") -> int: + ret = client.call(messages.GetNextU2FCounter(), expect=messages.NextU2FCounter) + return ret.u2f_counter diff --git a/python/src/trezorlib/firmware/__init__.py b/python/src/trezorlib/firmware/__init__.py index 5cc5d8830cb..4cfc11dd40a 100644 --- a/python/src/trezorlib/firmware/__init__.py +++ b/python/src/trezorlib/firmware/__init__.py @@ -20,7 +20,7 @@ from typing_extensions import Protocol, TypeGuard from .. import messages -from ..tools import expect, session +from ..tools import session from .core import VendorFirmware from .legacy import LegacyFirmware, LegacyV2Firmware @@ -106,6 +106,7 @@ def update( raise RuntimeError(f"Unexpected message {resp}") -@expect(messages.FirmwareHash, field="hash", ret_type=bytes) -def get_hash(client: "TrezorClient", challenge: t.Optional[bytes]): - return client.call(messages.GetFirmwareHash(challenge=challenge)) +def get_hash(client: "TrezorClient", challenge: t.Optional[bytes]) -> bytes: + return client.call( + messages.GetFirmwareHash(challenge=challenge), expect=messages.FirmwareHash + ).hash diff --git a/python/src/trezorlib/misc.py b/python/src/trezorlib/misc.py index 4ed6f5aa81c..578c1fa19f1 100644 --- a/python/src/trezorlib/misc.py +++ b/python/src/trezorlib/misc.py @@ -17,54 +17,50 @@ from typing import TYPE_CHECKING, Optional from . import messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address -@expect(messages.Entropy, field="entropy", ret_type=bytes) -def get_entropy(client: "TrezorClient", size: int) -> "MessageType": - return client.call(messages.GetEntropy(size=size)) +def get_entropy(client: "TrezorClient", size: int) -> bytes: + return client.call(messages.GetEntropy(size=size), expect=messages.Entropy).entropy -@expect(messages.SignedIdentity) def sign_identity( client: "TrezorClient", identity: messages.IdentityType, challenge_hidden: bytes, challenge_visual: str, ecdsa_curve_name: Optional[str] = None, -) -> "MessageType": +) -> messages.SignedIdentity: return client.call( messages.SignIdentity( identity=identity, challenge_hidden=challenge_hidden, challenge_visual=challenge_visual, ecdsa_curve_name=ecdsa_curve_name, - ) + ), + expect=messages.SignedIdentity, ) -@expect(messages.ECDHSessionKey) def get_ecdh_session_key( client: "TrezorClient", identity: messages.IdentityType, peer_public_key: bytes, ecdsa_curve_name: Optional[str] = None, -) -> "MessageType": +) -> messages.ECDHSessionKey: return client.call( messages.GetECDHSessionKey( identity=identity, peer_public_key=peer_public_key, ecdsa_curve_name=ecdsa_curve_name, - ) + ), + expect=messages.ECDHSessionKey, ) -@expect(messages.CipheredKeyValue, field="value", ret_type=bytes) def encrypt_keyvalue( client: "TrezorClient", n: "Address", @@ -73,7 +69,7 @@ def encrypt_keyvalue( ask_on_encrypt: bool = True, ask_on_decrypt: bool = True, iv: bytes = b"", -) -> "MessageType": +) -> bytes: return client.call( messages.CipherKeyValue( address_n=n, @@ -83,11 +79,11 @@ def encrypt_keyvalue( ask_on_encrypt=ask_on_encrypt, ask_on_decrypt=ask_on_decrypt, iv=iv, - ) - ) + ), + expect=messages.CipheredKeyValue, + ).value -@expect(messages.CipheredKeyValue, field="value", ret_type=bytes) def decrypt_keyvalue( client: "TrezorClient", n: "Address", @@ -96,7 +92,7 @@ def decrypt_keyvalue( ask_on_encrypt: bool = True, ask_on_decrypt: bool = True, iv: bytes = b"", -) -> "MessageType": +) -> bytes: return client.call( messages.CipherKeyValue( address_n=n, @@ -106,10 +102,10 @@ def decrypt_keyvalue( ask_on_encrypt=ask_on_encrypt, ask_on_decrypt=ask_on_decrypt, iv=iv, - ) - ) + ), + expect=messages.CipheredKeyValue, + ).value -@expect(messages.Nonce, field="nonce", ret_type=bytes) -def get_nonce(client: "TrezorClient"): - return client.call(messages.GetNonce()) +def get_nonce(client: "TrezorClient") -> bytes: + return client.call(messages.GetNonce(), expect=messages.Nonce).nonce diff --git a/python/src/trezorlib/monero.py b/python/src/trezorlib/monero.py index 5bce7574e82..b2e3214fb95 100644 --- a/python/src/trezorlib/monero.py +++ b/python/src/trezorlib/monero.py @@ -17,11 +17,9 @@ from typing import TYPE_CHECKING from . import messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address @@ -31,30 +29,30 @@ # FAKECHAIN = 3 -@expect(messages.MoneroAddress, field="address", ret_type=bytes) def get_address( client: "TrezorClient", n: "Address", show_display: bool = False, network_type: messages.MoneroNetworkType = messages.MoneroNetworkType.MAINNET, chunkify: bool = False, -) -> "MessageType": +) -> bytes: return client.call( messages.MoneroGetAddress( address_n=n, show_display=show_display, network_type=network_type, chunkify=chunkify, - ) - ) + ), + expect=messages.MoneroAddress, + ).address -@expect(messages.MoneroWatchKey) def get_watch_key( client: "TrezorClient", n: "Address", network_type: messages.MoneroNetworkType = messages.MoneroNetworkType.MAINNET, -) -> "MessageType": +) -> messages.MoneroWatchKey: return client.call( - messages.MoneroGetWatchKey(address_n=n, network_type=network_type) + messages.MoneroGetWatchKey(address_n=n, network_type=network_type), + expect=messages.MoneroWatchKey, ) diff --git a/python/src/trezorlib/nem.py b/python/src/trezorlib/nem.py index 3a67aec72c2..744dc3205f3 100644 --- a/python/src/trezorlib/nem.py +++ b/python/src/trezorlib/nem.py @@ -18,11 +18,9 @@ from typing import TYPE_CHECKING from . import exceptions, messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address TYPE_TRANSACTION_TRANSFER = 0x0101 @@ -196,25 +194,24 @@ def create_sign_tx(transaction: dict, chunkify: bool = False) -> messages.NEMSig # ====== Client functions ====== # -@expect(messages.NEMAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", n: "Address", network: int, show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.NEMGetAddress( address_n=n, network=network, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.NEMAddress, + ).address -@expect(messages.NEMSignedTx) def sign_tx( client: "TrezorClient", n: "Address", transaction: dict, chunkify: bool = False -) -> "MessageType": +) -> messages.NEMSignedTx: try: msg = create_sign_tx(transaction, chunkify=chunkify) except ValueError as e: @@ -222,4 +219,4 @@ def sign_tx( assert msg.transaction is not None msg.transaction.address_n = n - return client.call(msg) + return client.call(msg, expect=messages.NEMSignedTx) diff --git a/python/src/trezorlib/protobuf.py b/python/src/trezorlib/protobuf.py index 14f61dff875..5a5315f186c 100644 --- a/python/src/trezorlib/protobuf.py +++ b/python/src/trezorlib/protobuf.py @@ -35,6 +35,8 @@ import typing_extensions as tx +from .exceptions import UnexpectedMessageError + if t.TYPE_CHECKING: from IPython.lib.pretty import RepresentationPrinter # noqa: I900 @@ -312,6 +314,27 @@ def ByteSize(self) -> int: dump_message(data, self) return len(data.getvalue()) + @classmethod + def ensure_isinstance(cls, msg: t.Any) -> tx.Self: + """Ensure that the received `msg` is an instance of this class. + + If `msg` is not an instance of this class, raise an `UnexpectedMessageError`. + otherwise, return it. This is useful for type-checking like so: + + >>> msg = client.call(SomeMessage()) + >>> if isinstance(msg, Foo): + >>> return msg.foo_attr # attribute of Foo, type-checks OK + >>> else: + >>> msg = Bar.ensure_isinstance(msg) # raises if msg is something else + >>> return msg.bar_attr # attribute of Bar, type-checks OK + + If there is just one expected message, you should use the `expect` parameter of + `Client.call` instead. + """ + if not isinstance(msg, cls): + raise UnexpectedMessageError(cls, msg) + return msg + class LimitedReader: def __init__(self, reader: Reader, limit: int) -> None: diff --git a/python/src/trezorlib/ripple.py b/python/src/trezorlib/ripple.py index 7a953b8fac5..00a027c6d97 100644 --- a/python/src/trezorlib/ripple.py +++ b/python/src/trezorlib/ripple.py @@ -18,41 +18,39 @@ from . import messages from .protobuf import dict_to_proto -from .tools import dict_from_camelcase, expect +from .tools import dict_from_camelcase if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address REQUIRED_FIELDS = ("Fee", "Sequence", "TransactionType", "Payment") REQUIRED_PAYMENT_FIELDS = ("Amount", "Destination") -@expect(messages.RippleAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", address_n: "Address", show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.RippleGetAddress( address_n=address_n, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.RippleAddress, + ).address -@expect(messages.RippleSignedTx) def sign_tx( client: "TrezorClient", address_n: "Address", msg: messages.RippleSignTx, chunkify: bool = False, -) -> "MessageType": +) -> messages.RippleSignedTx: msg.address_n = address_n msg.chunkify = chunkify - return client.call(msg) + return client.call(msg, expect=messages.RippleSignedTx) def create_sign_tx_msg(transaction: dict) -> messages.RippleSignTx: diff --git a/python/src/trezorlib/solana.py b/python/src/trezorlib/solana.py index be7f2e5fcb5..0054e0fd924 100644 --- a/python/src/trezorlib/solana.py +++ b/python/src/trezorlib/solana.py @@ -1,51 +1,49 @@ from typing import TYPE_CHECKING, List, Optional from . import messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType -@expect(messages.SolanaPublicKey) def get_public_key( client: "TrezorClient", address_n: List[int], show_display: bool, -) -> "MessageType": +) -> bytes: return client.call( - messages.SolanaGetPublicKey(address_n=address_n, show_display=show_display) - ) + messages.SolanaGetPublicKey(address_n=address_n, show_display=show_display), + expect=messages.SolanaPublicKey, + ).public_key -@expect(messages.SolanaAddress) def get_address( client: "TrezorClient", address_n: List[int], show_display: bool, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.SolanaGetAddress( address_n=address_n, show_display=show_display, chunkify=chunkify, - ) - ) + ), + expect=messages.SolanaAddress, + ).address -@expect(messages.SolanaTxSignature) def sign_tx( client: "TrezorClient", address_n: List[int], serialized_tx: bytes, additional_info: Optional[messages.SolanaTxAdditionalInfo], -) -> "MessageType": +) -> bytes: return client.call( messages.SolanaSignTx( address_n=address_n, serialized_tx=serialized_tx, additional_info=additional_info, - ) - ) + ), + expect=messages.SolanaTxSignature, + ).signature diff --git a/python/src/trezorlib/stellar.py b/python/src/trezorlib/stellar.py index ebf81e4fd04..f863110465f 100644 --- a/python/src/trezorlib/stellar.py +++ b/python/src/trezorlib/stellar.py @@ -18,11 +18,9 @@ from typing import TYPE_CHECKING, List, Tuple, Union from . import exceptions, messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address StellarMessageType = Union[ @@ -323,18 +321,18 @@ def _read_asset(asset: "Asset") -> messages.StellarAsset: # ====== Client functions ====== # -@expect(messages.StellarAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", address_n: "Address", show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.StellarGetAddress( address_n=address_n, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.StellarAddress, + ).address def sign_tx( diff --git a/python/src/trezorlib/tezos.py b/python/src/trezorlib/tezos.py index cff06ed6c83..9319aa1eaa1 100644 --- a/python/src/trezorlib/tezos.py +++ b/python/src/trezorlib/tezos.py @@ -17,49 +17,46 @@ from typing import TYPE_CHECKING from . import messages -from .tools import expect if TYPE_CHECKING: from .client import TrezorClient - from .protobuf import MessageType from .tools import Address -@expect(messages.TezosAddress, field="address", ret_type=str) def get_address( client: "TrezorClient", address_n: "Address", show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.TezosGetAddress( address_n=address_n, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.TezosAddress, + ).address -@expect(messages.TezosPublicKey, field="public_key", ret_type=str) def get_public_key( client: "TrezorClient", address_n: "Address", show_display: bool = False, chunkify: bool = False, -) -> "MessageType": +) -> str: return client.call( messages.TezosGetPublicKey( address_n=address_n, show_display=show_display, chunkify=chunkify - ) - ) + ), + expect=messages.TezosPublicKey, + ).public_key -@expect(messages.TezosSignedTx) def sign_tx( client: "TrezorClient", address_n: "Address", sign_tx_msg: messages.TezosSignTx, chunkify: bool = False, -) -> "MessageType": +) -> messages.TezosSignedTx: sign_tx_msg.address_n = address_n sign_tx_msg.chunkify = chunkify - return client.call(sign_tx_msg) + return client.call(sign_tx_msg, expect=messages.TezosSignedTx) From 463ce35b8354c7b4fd24ddc44718211f358b477c Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 9 Jan 2025 15:11:29 +0100 Subject: [PATCH 07/14] style(python): improve type hints for input flows --- python/src/trezorlib/debuglink.py | 9 ++++++--- tests/input_flows.py | 4 ++-- tests/input_flows_helpers.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 56c1dfa344f..3af5711a7cf 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -67,6 +67,8 @@ def __call__( wait: bool | None = None, ) -> "LayoutContent": ... + InputFlowType = Generator[None, messages.ButtonRequest, None] + EXPECTED_RESPONSES_CONTEXT_LINES = 3 @@ -1108,7 +1110,7 @@ def _filter_message(self, msg: protobuf.MessageType) -> protobuf.MessageType: return msg def set_input_flow( - self, input_flow: Generator[None, messages.ButtonRequest | None, None] + self, input_flow: InputFlowType | Callable[[], InputFlowType] ) -> None: """Configure a sequence of input events for the current with-block. @@ -1142,7 +1144,7 @@ def set_input_flow( if not hasattr(input_flow, "send"): raise RuntimeError("input_flow should be a generator function") self.ui.input_flow = input_flow - input_flow.send(None) # start the generator + next(input_flow) # start the generator def watch_layout(self, watch: bool = True) -> None: """Enable or disable watching layout changes. @@ -1190,7 +1192,8 @@ def __exit__(self, exc_type: Any, value: Any, traceback: Any) -> None: input_flow.throw(exc_type, value, traceback) def set_expected_responses( - self, expected: list[Union["ExpectedMessage", Tuple[bool, "ExpectedMessage"]]] + self, + expected: Sequence[Union["ExpectedMessage", Tuple[bool, "ExpectedMessage"]]], ) -> None: """Set a sequence of expected responses to client calls. diff --git a/tests/input_flows.py b/tests/input_flows.py index 2609cada306..00bd63ea083 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -12,7 +12,7 @@ from __future__ import annotations import time -from typing import Callable, Generator +from typing import Callable, Generator, Sequence from trezorlib import messages from trezorlib.debuglink import DebugLink, LayoutContent, LayoutType @@ -2049,7 +2049,7 @@ def input_flow_common(self) -> BRGeneratorType: class InputFlowSlip39BasicRecovery(InputFlowBase): - def __init__(self, client: Client, shares: list[str], pin: str | None = None): + def __init__(self, client: Client, shares: Sequence[str], pin: str | None = None): super().__init__(client) self.shares = shares self.pin = pin diff --git a/tests/input_flows_helpers.py b/tests/input_flows_helpers.py index 3ad5f580ed3..d51d3a83153 100644 --- a/tests/input_flows_helpers.py +++ b/tests/input_flows_helpers.py @@ -301,7 +301,7 @@ def input_mnemonic(self, mnemonic: list[str]) -> BRGeneratorType: def input_all_slip39_shares( self, - shares: list[str], + shares: t.Sequence[str], has_groups: bool = False, click_info: bool = False, ) -> BRGeneratorType: From a045bcb2b608941e4909f5a53e1022cb2cba155f Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 9 Jan 2025 15:20:47 +0100 Subject: [PATCH 08/14] fix(tests): update tests for newly introduced device.setup() --- tests/click_tests/test_autolock.py | 2 +- .../click_tests/test_backup_slip39_custom.py | 15 ++-- tests/click_tests/test_recovery.py | 3 +- tests/click_tests/test_repeated_backup.py | 18 ++-- tests/click_tests/test_reset_bip39.py | 12 ++- .../click_tests/test_reset_slip39_advanced.py | 11 ++- tests/click_tests/test_reset_slip39_basic.py | 12 ++- tests/common.py | 3 +- tests/device_handler.py | 17 +++- .../bitcoin/test_authorize_coinjoin.py | 4 +- .../test_recovery_slip39_advanced.py | 4 +- .../test_recovery_slip39_advanced_dryrun.py | 7 +- .../test_recovery_slip39_basic.py | 9 +- .../test_recovery_slip39_basic_dryrun.py | 7 +- .../reset_recovery/test_reset_backup.py | 14 +-- .../reset_recovery/test_reset_bip39_t1.py | 2 +- .../reset_recovery/test_reset_bip39_t2.py | 85 ++++++++++++------- .../test_reset_recovery_bip39.py | 12 +-- .../test_reset_recovery_slip39_advanced.py | 12 +-- .../test_reset_recovery_slip39_basic.py | 14 +-- .../test_reset_slip39_advanced.py | 11 ++- .../reset_recovery/test_reset_slip39_basic.py | 15 ++-- tests/device_tests/solana/test_address.py | 2 +- tests/device_tests/solana/test_public_key.py | 2 +- tests/device_tests/solana/test_sign_tx.py | 2 +- tests/device_tests/test_msg_applysettings.py | 9 +- tests/device_tests/test_msg_wipedevice.py | 9 +- tests/device_tests/test_protection_levels.py | 10 ++- tests/device_tests/test_repeated_backup.py | 17 +--- tests/upgrade_tests/test_firmware_upgrades.py | 19 +++-- .../test_passphrase_consistency.py | 4 +- 31 files changed, 205 insertions(+), 158 deletions(-) diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py index 4cc7740895a..599c0df6add 100644 --- a/tests/click_tests/test_autolock.py +++ b/tests/click_tests/test_autolock.py @@ -73,7 +73,7 @@ def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int) if debug.layout_type is LayoutType.Quicksilver: layout = tap_to_confirm(debug) assert layout.main_component() == "Homescreen" - assert device_handler.result() == "Settings applied" + device_handler.result() @pytest.mark.setup_client(pin=PIN4) diff --git a/tests/click_tests/test_backup_slip39_custom.py b/tests/click_tests/test_backup_slip39_custom.py index 8318ab5f4b3..bc05170dc0b 100644 --- a/tests/click_tests/test_backup_slip39_custom.py +++ b/tests/click_tests/test_backup_slip39_custom.py @@ -22,7 +22,7 @@ from trezorlib.debuglink import LayoutType from .. import translations as TR -from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy from . import reset if TYPE_CHECKING: @@ -41,7 +41,6 @@ ], ) @pytest.mark.setup_client(uninitialized=True) -@WITH_MOCK_URANDOM def test_backup_slip39_custom( device_handler: "BackgroundDeviceHandler", group_threshold: int, @@ -54,10 +53,13 @@ def test_backup_slip39_custom( assert features.initialized is False device_handler.run( - device.reset, + device.setup, strength=128, backup_type=messages.BackupType.Slip39_Basic, pin_protection=False, + passphrase_protection=False, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # confirm new wallet @@ -66,7 +68,8 @@ def test_backup_slip39_custom( # cancel back up reset.cancel_backup(debug, confirm=True) - assert device_handler.result() == "Initialized" + # retrieve the result to check that it's not a TrezorFailure exception + device_handler.result() device_handler.run( device.backup, @@ -116,7 +119,9 @@ def test_backup_slip39_custom( # validate that all combinations will result in the correct master secret reset.validate_mnemonics(all_words[:share_threshold], secret) - assert device_handler.result() == "Seed successfully backed up" + # retrieve the result to check that it's not a TrezorFailure exception + device_handler.result() + features = device_handler.features() assert features.initialized is True assert features.backup_availability == messages.BackupAvailability.NotAvailable diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py index 8770649296a..7161e774dda 100644 --- a/tests/click_tests/test_recovery.py +++ b/tests/click_tests/test_recovery.py @@ -44,7 +44,8 @@ def prepare_recovery_and_evaluate( yield debug - assert isinstance(device_handler.result(), messages.Success) + device_handler.result() + features = device_handler.features() assert features.initialized is True assert features.recovery_status == messages.RecoveryStatus.Nothing diff --git a/tests/click_tests/test_repeated_backup.py b/tests/click_tests/test_repeated_backup.py index d61d97962df..e0d62b53d2a 100644 --- a/tests/click_tests/test_repeated_backup.py +++ b/tests/click_tests/test_repeated_backup.py @@ -21,7 +21,7 @@ from trezorlib import device, messages from .. import buttons -from ..common import WITH_MOCK_URANDOM +from ..common import MOCK_GET_ENTROPY from . import recovery, reset from .common import go_next @@ -33,7 +33,6 @@ @pytest.mark.setup_client(uninitialized=True) -@WITH_MOCK_URANDOM def test_repeated_backup( device_handler: "BackgroundDeviceHandler", ): @@ -43,10 +42,13 @@ def test_repeated_backup( assert features.initialized is False device_handler.run( - device.reset, + device.setup, strength=128, backup_type=messages.BackupType.Slip39_Basic, pin_protection=False, + passphrase_protection=False, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # confirm new wallet @@ -84,8 +86,9 @@ def test_repeated_backup( # Your backup is done go_next(debug) + # retrieve the result to check that it does not raise a failure + device_handler.result() # great ... device is initialized, backup done, and we are not in recovery mode! - assert device_handler.result() == "Initialized" features = device_handler.features() assert features.backup_type is messages.BackupType.Slip39_Basic_Extendable assert features.initialized is True @@ -109,8 +112,8 @@ def test_repeated_backup( "recovery__unlock_repeated_backup", ) - # backup is enabled - assert device_handler.result().message == "Backup unlocked" + # check non-exception result + device_handler.result() # we are now in recovery mode features = device_handler.features() @@ -178,7 +181,8 @@ def test_repeated_backup( "recovery__unlock_repeated_backup", ) - assert device_handler.result().message == "Backup unlocked" + # check non-exception result + device_handler.result() # we are now in recovery mode again! features = device_handler.features() diff --git a/tests/click_tests/test_reset_bip39.py b/tests/click_tests/test_reset_bip39.py index 907246fb51d..d405f514413 100644 --- a/tests/click_tests/test_reset_bip39.py +++ b/tests/click_tests/test_reset_bip39.py @@ -21,7 +21,7 @@ from trezorlib import device, messages from .. import translations as TR -from ..common import WITH_MOCK_URANDOM +from ..common import MOCK_GET_ENTROPY from . import reset from .common import go_next @@ -33,7 +33,6 @@ @pytest.mark.setup_client(uninitialized=True) -@WITH_MOCK_URANDOM def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): features = device_handler.features() debug = device_handler.debuglink() @@ -41,10 +40,13 @@ def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): assert features.initialized is False device_handler.run( - device.reset, + device.setup, strength=128, backup_type=messages.BackupType.Bip39, pin_protection=False, + passphrase_protection=False, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # confirm new wallet @@ -82,7 +84,9 @@ def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): # TODO: some validation of the generated secret? - assert device_handler.result() == "Initialized" + # retrieve the result to check that it's not a TrezorFailure exception + device_handler.result() + features = device_handler.features() assert features.initialized is True assert features.backup_availability == messages.BackupAvailability.NotAvailable diff --git a/tests/click_tests/test_reset_slip39_advanced.py b/tests/click_tests/test_reset_slip39_advanced.py index d9752124252..0b43095ad12 100644 --- a/tests/click_tests/test_reset_slip39_advanced.py +++ b/tests/click_tests/test_reset_slip39_advanced.py @@ -21,7 +21,7 @@ from trezorlib import device, messages from .. import buttons -from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy from . import reset if TYPE_CHECKING: @@ -39,7 +39,6 @@ pytest.param(16, 16, 16, 16, id="16of16", marks=pytest.mark.slow), ], ) -@WITH_MOCK_URANDOM def test_reset_slip39_advanced( device_handler: "BackgroundDeviceHandler", group_count: int, @@ -53,9 +52,12 @@ def test_reset_slip39_advanced( assert features.initialized is False device_handler.run( - device.reset, + device.setup, backup_type=messages.BackupType.Slip39_Advanced, pin_protection=False, + passphrase_protection=False, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # confirm new wallet @@ -162,7 +164,8 @@ def test_reset_slip39_advanced( # validate that all combinations will result in the correct master secret reset.validate_mnemonics(all_words, secret) - assert device_handler.result() == "Initialized" + # retrieve the result to check that it's not a TrezorFailure exception + device_handler.result() features = device_handler.features() assert features.initialized is True diff --git a/tests/click_tests/test_reset_slip39_basic.py b/tests/click_tests/test_reset_slip39_basic.py index f8c6592f6d4..d18bf7a4fba 100644 --- a/tests/click_tests/test_reset_slip39_basic.py +++ b/tests/click_tests/test_reset_slip39_basic.py @@ -21,7 +21,7 @@ from trezorlib import device, messages from .. import buttons -from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy from . import reset if TYPE_CHECKING: @@ -39,7 +39,6 @@ ], ) @pytest.mark.setup_client(uninitialized=True) -@WITH_MOCK_URANDOM def test_reset_slip39_basic( device_handler: "BackgroundDeviceHandler", num_of_shares: int, threshold: int ): @@ -49,10 +48,13 @@ def test_reset_slip39_basic( assert features.initialized is False device_handler.run( - device.reset, + device.setup, strength=128, backup_type=messages.BackupType.Slip39_Basic, pin_protection=False, + passphrase_protection=False, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # confirm new wallet @@ -137,7 +139,9 @@ def test_reset_slip39_basic( # validate that all combinations will result in the correct master secret reset.validate_mnemonics(all_words, secret) - assert device_handler.result() == "Initialized" + # retrieve the result to check that it's not a TrezorFailure exception + device_handler.result() + features = device_handler.features() assert features.initialized is True assert features.backup_availability == messages.BackupAvailability.NotAvailable diff --git a/tests/common.py b/tests/common.py index 7d5f6fb069f..51b404cb482 100644 --- a/tests/common.py +++ b/tests/common.py @@ -83,8 +83,7 @@ ) # So that all the random things are consistent -MOCK_OS_URANDOM = mock.Mock(return_value=EXTERNAL_ENTROPY) -WITH_MOCK_URANDOM = mock.patch("os.urandom", MOCK_OS_URANDOM) +MOCK_GET_ENTROPY = mock.Mock(return_value=EXTERNAL_ENTROPY) def parametrize_using_common_fixtures(*paths: str) -> "MarkDecorator": diff --git a/tests/device_handler.py b/tests/device_handler.py index 45ec1df9f78..74eb77a5a52 100644 --- a/tests/device_handler.py +++ b/tests/device_handler.py @@ -1,18 +1,22 @@ from __future__ import annotations +import typing as t from concurrent.futures import ThreadPoolExecutor -from typing import TYPE_CHECKING, Any, Callable + +import typing_extensions as tx from trezorlib.client import PASSPHRASE_ON_DEVICE from trezorlib.messages import DebugWaitType from trezorlib.transport import udp -if TYPE_CHECKING: +if t.TYPE_CHECKING: from trezorlib._internal.emulator import Emulator from trezorlib.debuglink import DebugLink from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.messages import Features + P = tx.ParamSpec("P") + udp.SOCKET_TIMEOUT = 0.1 @@ -48,7 +52,12 @@ def _configure_client(self, client: "Client") -> None: self.client.watch_layout(True) self.client.debug.input_wait_type = DebugWaitType.CURRENT_LAYOUT - def run(self, function: Callable[..., Any], *args: Any, **kwargs: Any) -> None: + def run( + self, + function: t.Callable[tx.Concatenate["Client", P], t.Any], + *args: P.args, + **kwargs: P.kwargs, + ) -> None: """Runs some function that interacts with a device. Makes sure the UI is updated before returning. @@ -79,7 +88,7 @@ def restart(self, emulator: "Emulator") -> None: emulator.restart() self._configure_client(emulator.client) # type: ignore [client cannot be None] - def result(self, timeout: float | None = None) -> Any: + def result(self, timeout: float | None = None) -> t.Any: if self.task is None: raise RuntimeError("No task running") try: diff --git a/tests/device_tests/bitcoin/test_authorize_coinjoin.py b/tests/device_tests/bitcoin/test_authorize_coinjoin.py index b149ff53d16..f1cc7af0bde 100644 --- a/tests/device_tests/bitcoin/test_authorize_coinjoin.py +++ b/tests/device_tests/bitcoin/test_authorize_coinjoin.py @@ -457,7 +457,7 @@ def test_sign_tx_spend(client: Client): client.set_expected_responses( [ messages.ButtonRequest(code=B.Other), - messages.UnlockedPathRequest(), + messages.UnlockedPathRequest, request_input(0), request_output(0), request_output(1), @@ -531,7 +531,7 @@ def test_sign_tx_migration(client: Client): client.set_expected_responses( [ messages.ButtonRequest(code=B.Other), - messages.UnlockedPathRequest(), + messages.UnlockedPathRequest, request_input(0), request_input(1), request_output(0), diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py b/tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py index fa181117357..ad6f51ed43f 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py @@ -51,15 +51,13 @@ def _test_secret( with client: IF = InputFlowSlip39AdvancedRecovery(client, shares, click_info=click_info) client.set_input_flow(IF.get()) - ret = device.recover( + device.recover( client, pin_protection=False, passphrase_protection=False, label="label", ) - # Workflow succesfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.initialized is True assert client.features.pin_protection is False assert client.features.passphrase_protection is False diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py index 73e18a8686c..52309834974 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py @@ -45,7 +45,7 @@ def test_2of3_dryrun(client: Client): client, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20 ) client.set_input_flow(IF.get()) - ret = device.recover( + device.recover( client, passphrase_protection=False, pin_protection=False, @@ -53,11 +53,6 @@ def test_2of3_dryrun(client: Client): type=messages.RecoveryType.DryRun, ) - # Dry run was successful - assert ret == messages.Success( - message="The seed is valid and matches the one in the device" - ) - @pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20) def test_2of3_invalid_seed_dryrun(client: Client): diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py b/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py index 3f7ed75e730..9c84af71184 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py @@ -75,10 +75,9 @@ def test_secret( with client: IF = InputFlowSlip39BasicRecovery(client, shares) client.set_input_flow(IF.get()) - ret = device.recover(client, pin_protection=False, label="label") + device.recover(client, pin_protection=False, label="label") # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.pin_protection is False assert client.features.passphrase_protection is False assert client.features.backup_type is backup_type @@ -94,7 +93,7 @@ def test_recover_with_pin_passphrase(client: Client): client, MNEMONIC_SLIP39_BASIC_20_3of6, pin="654" ) client.set_input_flow(IF.get()) - ret = device.recover( + device.recover( client, pin_protection=True, passphrase_protection=True, @@ -102,7 +101,6 @@ def test_recover_with_pin_passphrase(client: Client): ) # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.pin_protection is True assert client.features.passphrase_protection is True assert client.features.backup_type is messages.BackupType.Slip39_Basic @@ -194,7 +192,7 @@ def test_1of1(client: Client): with client: IF = InputFlowSlip39BasicRecovery(client, MNEMONIC_SLIP39_BASIC_20_1of1) client.set_input_flow(IF.get()) - ret = device.recover( + device.recover( client, pin_protection=False, passphrase_protection=False, @@ -202,7 +200,6 @@ def test_1of1(client: Client): ) # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.initialized is True assert client.features.pin_protection is False assert client.features.passphrase_protection is False diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py index 4c9ddf8036f..8d5d57f9a13 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py @@ -41,7 +41,7 @@ def test_2of3_dryrun(client: Client): with client: IF = InputFlowSlip39BasicRecoveryDryRun(client, SHARES_20_2of3[1:3]) client.set_input_flow(IF.get()) - ret = device.recover( + device.recover( client, passphrase_protection=False, pin_protection=False, @@ -49,11 +49,6 @@ def test_2of3_dryrun(client: Client): type=messages.RecoveryType.DryRun, ) - # Dry run was successful - assert ret == messages.Success( - message="The seed is valid and matches the one in the device" - ) - @pytest.mark.setup_client(mnemonic=SHARES_20_2of3[0:2]) def test_2of3_invalid_seed_dryrun(client: Client): diff --git a/tests/device_tests/reset_recovery/test_reset_backup.py b/tests/device_tests/reset_recovery/test_reset_backup.py index 148087d4f4b..db7e3c88454 100644 --- a/tests/device_tests/reset_recovery/test_reset_backup.py +++ b/tests/device_tests/reset_recovery/test_reset_backup.py @@ -22,7 +22,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.messages import BackupAvailability, BackupType -from ...common import WITH_MOCK_URANDOM +from ...common import MOCK_GET_ENTROPY from ...input_flows import ( InputFlowBip39Backup, InputFlowResetSkipBackup, @@ -75,13 +75,15 @@ def backup_flow_slip39_advanced(client: Client): @pytest.mark.parametrize("backup_type, backup_flow", VECTORS) @pytest.mark.setup_client(uninitialized=True) def test_skip_backup_msg(client: Client, backup_type, backup_flow): - with WITH_MOCK_URANDOM, client: - device.reset( + with client: + device.setup( client, skip_backup=True, passphrase_protection=False, pin_protection=False, backup_type=backup_type, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) assert client.features.initialized is True @@ -108,14 +110,16 @@ def test_skip_backup_msg(client: Client, backup_type, backup_flow): @pytest.mark.parametrize("backup_type, backup_flow", VECTORS) @pytest.mark.setup_client(uninitialized=True) def test_skip_backup_manual(client: Client, backup_type: BackupType, backup_flow): - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowResetSkipBackup(client) client.set_input_flow(IF.get()) - device.reset( + device.setup( client, pin_protection=False, passphrase_protection=False, backup_type=backup_type, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) assert client.features.initialized is True diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py index 689b81b0d61..0c96ee4f5c8 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py @@ -209,7 +209,7 @@ def test_failed_pin(client: Client): def test_already_initialized(client: Client): with pytest.raises(Exception): - device.reset( + device.setup( client, strength=128, passphrase_protection=True, diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py index 56f3f8f12b9..a1eb1856aaa 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py @@ -23,7 +23,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.exceptions import TrezorFailure -from ...common import EXTERNAL_ENTROPY, MNEMONIC12, WITH_MOCK_URANDOM, generate_entropy +from ...common import EXTERNAL_ENTROPY, MNEMONIC12, MOCK_GET_ENTROPY, generate_entropy from ...input_flows import ( InputFlowBip39ResetBackup, InputFlowBip39ResetFailedCheck, @@ -34,21 +34,25 @@ def reset_device(client: Client, strength: int): - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowBip39ResetBackup(client) client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, + _get_entropy=MOCK_GET_ENTROPY, ) # generate mnemonic locally internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) @@ -56,12 +60,13 @@ def reset_device(client: Client, strength: int): assert IF.mnemonic == expected_mnemonic # Check if device is properly initialized - resp = client.call_raw(messages.Initialize()) - assert resp.initialized is True - assert resp.backup_availability == messages.BackupAvailability.NotAvailable - assert resp.pin_protection is False - assert resp.passphrase_protection is False - assert resp.backup_type is messages.BackupType.Bip39 + assert client.features.initialized is True + assert ( + client.features.backup_availability == messages.BackupAvailability.NotAvailable + ) + assert client.features.pin_protection is False + assert client.features.passphrase_protection is False + assert client.features.backup_type is messages.BackupType.Bip39 # backup attempt fails because backup was done in reset with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): @@ -82,21 +87,25 @@ def test_reset_device_192(client: Client): def test_reset_device_pin(client: Client): strength = 256 # 24 words - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowBip39ResetPIN(client) client.set_input_flow(IF.get()) # PIN, passphrase, display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=True, pin_protection=True, label="test", + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, + _get_entropy=MOCK_GET_ENTROPY, ) # generate mnemonic locally internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) @@ -104,33 +113,37 @@ def test_reset_device_pin(client: Client): assert IF.mnemonic == expected_mnemonic # Check if device is properly initialized - resp = client.call_raw(messages.Initialize()) - assert resp.initialized is True - assert resp.backup_availability == messages.BackupAvailability.NotAvailable - assert resp.pin_protection is True - assert resp.passphrase_protection is True + assert client.features.initialized is True + assert ( + client.features.backup_availability == messages.BackupAvailability.NotAvailable + ) + assert client.features.pin_protection is True + assert client.features.passphrase_protection is True @pytest.mark.setup_client(uninitialized=True) def test_reset_entropy_check(client: Client): strength = 128 # 12 words - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowBip39ResetBackup(client) client.set_input_flow(IF.get()) # No PIN, no passphrase - _, path_xpubs = device.reset_entropy_check( + path_xpubs = device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", entropy_check_count=2, + backup_type=messages.BackupType.Bip39, + _get_entropy=MOCK_GET_ENTROPY, ) # Generate the mnemonic locally. internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) @@ -138,12 +151,13 @@ def test_reset_entropy_check(client: Client): assert IF.mnemonic == expected_mnemonic # Check that the device is properly initialized. - resp = client.call_raw(messages.Initialize()) - assert resp.initialized is True - assert resp.backup_availability == messages.BackupAvailability.NotAvailable - assert resp.pin_protection is False - assert resp.passphrase_protection is False - assert resp.backup_type is messages.BackupType.Bip39 + assert client.features.initialized is True + assert ( + client.features.backup_availability == messages.BackupAvailability.NotAvailable + ) + assert client.features.pin_protection is False + assert client.features.passphrase_protection is False + assert client.features.backup_type is messages.BackupType.Bip39 # Check that the XPUBs are the same as those from the entropy check. for path, xpub in path_xpubs: @@ -155,21 +169,25 @@ def test_reset_entropy_check(client: Client): def test_reset_failed_check(client: Client): strength = 256 # 24 words - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowBip39ResetFailedCheck(client) client.set_input_flow(IF.get()) # PIN, passphrase, display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, + _get_entropy=MOCK_GET_ENTROPY, ) # generate mnemonic locally internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) @@ -177,12 +195,13 @@ def test_reset_failed_check(client: Client): assert IF.mnemonic == expected_mnemonic # Check if device is properly initialized - resp = client.call_raw(messages.Initialize()) - assert resp.initialized is True - assert resp.backup_availability == messages.BackupAvailability.NotAvailable - assert resp.pin_protection is False - assert resp.passphrase_protection is False - assert resp.backup_type is messages.BackupType.Bip39 + assert client.features.initialized is True + assert ( + client.features.backup_availability == messages.BackupAvailability.NotAvailable + ) + assert client.features.pin_protection is False + assert client.features.passphrase_protection is False + assert client.features.backup_type is messages.BackupType.Bip39 @pytest.mark.setup_client(uninitialized=True) @@ -223,7 +242,7 @@ def test_failed_pin(client: Client): @pytest.mark.setup_client(mnemonic=MNEMONIC12) def test_already_initialized(client: Client): with pytest.raises(Exception): - device.reset( + device.setup( client, strength=128, passphrase_protection=True, diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py b/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py index 89c327fb8fe..ac24ccbcfa6 100644 --- a/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py +++ b/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py @@ -21,7 +21,7 @@ from trezorlib.messages import BackupType from trezorlib.tools import parse_path -from ...common import WITH_MOCK_URANDOM +from ...common import MOCK_GET_ENTROPY from ...input_flows import InputFlowBip39Recovery, InputFlowBip39ResetBackup from ...translations import set_language @@ -41,18 +41,20 @@ def test_reset_recovery(client: Client): def reset(client: Client, strength: int = 128, skip_backup: bool = False) -> str: - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowBip39ResetBackup(client) client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", backup_type=BackupType.Bip39, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # Check if device is properly initialized @@ -73,9 +75,7 @@ def recover(client: Client, mnemonic: str): IF = InputFlowBip39Recovery(client, words) client.set_input_flow(IF.get()) client.watch_layout() - ret = device.recover(client, pin_protection=False, label="label") + device.recover(client, pin_protection=False, label="label") - # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.pin_protection is False assert client.features.passphrase_protection is False diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py index 8b42940d758..ffa9e73f772 100644 --- a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py +++ b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py @@ -21,7 +21,7 @@ from trezorlib.messages import BackupType from trezorlib.tools import parse_path -from ...common import WITH_MOCK_URANDOM +from ...common import MOCK_GET_ENTROPY from ...input_flows import ( InputFlowSlip39AdvancedRecovery, InputFlowSlip39AdvancedResetRecovery, @@ -62,18 +62,20 @@ def test_reset_recovery(client: Client): def reset(client: Client, strength: int = 128) -> list[str]: - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowSlip39AdvancedResetRecovery(client, False) client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", backup_type=BackupType.Slip39_Advanced, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # Check if device is properly initialized @@ -92,10 +94,8 @@ def recover(client: Client, shares: list[str]): with client: IF = InputFlowSlip39AdvancedRecovery(client, shares, False) client.set_input_flow(IF.get()) - ret = device.recover(client, pin_protection=False, label="label") + device.recover(client, pin_protection=False, label="label") - # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.pin_protection is False assert client.features.passphrase_protection is False assert client.features.backup_type is BackupType.Slip39_Advanced_Extendable diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py index 6b72246a106..44baf4cff36 100644 --- a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py +++ b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py @@ -15,6 +15,7 @@ # If not, see . import itertools +import typing as t import pytest @@ -23,7 +24,7 @@ from trezorlib.messages import BackupType from trezorlib.tools import parse_path -from ...common import WITH_MOCK_URANDOM +from ...common import MOCK_GET_ENTROPY from ...input_flows import ( InputFlowSlip39BasicRecovery, InputFlowSlip39BasicResetRecovery, @@ -33,7 +34,6 @@ @pytest.mark.models("core") @pytest.mark.setup_client(uninitialized=True) -@WITH_MOCK_URANDOM def test_reset_recovery(client: Client): mnemonics = reset(client) address_before = btc.get_address(client, "Bitcoin", parse_path("m/44h/0h/0h/0/0")) @@ -56,13 +56,15 @@ def reset(client: Client, strength: int = 128) -> list[str]: client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", backup_type=BackupType.Slip39_Basic, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # Check if device is properly initialized @@ -77,14 +79,12 @@ def reset(client: Client, strength: int = 128) -> list[str]: return IF.mnemonics -def recover(client: Client, shares: list[str]): +def recover(client: Client, shares: t.Sequence[str]): with client: IF = InputFlowSlip39BasicRecovery(client, shares) client.set_input_flow(IF.get()) - ret = device.recover(client, pin_protection=False, label="label") + device.recover(client, pin_protection=False, label="label") - # Workflow successfully ended - assert ret == messages.Success(message="Device recovered") assert client.features.pin_protection is False assert client.features.passphrase_protection is False assert client.features.backup_type is BackupType.Slip39_Basic_Extendable diff --git a/tests/device_tests/reset_recovery/test_reset_slip39_advanced.py b/tests/device_tests/reset_recovery/test_reset_slip39_advanced.py index 6aa9d2bf3d0..840841d734e 100644 --- a/tests/device_tests/reset_recovery/test_reset_slip39_advanced.py +++ b/tests/device_tests/reset_recovery/test_reset_slip39_advanced.py @@ -22,7 +22,7 @@ from trezorlib.exceptions import TrezorFailure from trezorlib.messages import BackupAvailability, BackupType -from ...common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from ...common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy from ...input_flows import InputFlowSlip39AdvancedResetRecovery pytestmark = pytest.mark.models("core") @@ -34,22 +34,25 @@ def test_reset_device_slip39_advanced(client: Client): strength = 128 member_threshold = 3 - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowSlip39AdvancedResetRecovery(client, False) client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", backup_type=BackupType.Slip39_Advanced, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # generate secret locally internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None secret = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) # validate that all combinations will result in the correct master secret @@ -68,7 +71,7 @@ def test_reset_device_slip39_advanced(client: Client): def validate_mnemonics( - mnemonics: list[list[str]], threshold: int, expected_ems: bytes + mnemonics: list[str], threshold: int, expected_ems: bytes ) -> None: # 3of5 shares 3of5 groups # TODO: test all possible group+share combinations? diff --git a/tests/device_tests/reset_recovery/test_reset_slip39_basic.py b/tests/device_tests/reset_recovery/test_reset_slip39_basic.py index b0d39f9eb1d..b284012cbe6 100644 --- a/tests/device_tests/reset_recovery/test_reset_slip39_basic.py +++ b/tests/device_tests/reset_recovery/test_reset_slip39_basic.py @@ -25,7 +25,7 @@ from trezorlib.exceptions import TrezorFailure from trezorlib.messages import BackupAvailability, BackupType -from ...common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from ...common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy from ...input_flows import InputFlowSlip39BasicResetRecovery pytestmark = pytest.mark.models("core") @@ -34,22 +34,25 @@ def reset_device(client: Client, strength: int): member_threshold = 3 - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowSlip39BasicResetRecovery(client) client.set_input_flow(IF.get()) # No PIN, no passphrase, don't display random - device.reset( + device.setup( client, strength=strength, passphrase_protection=False, pin_protection=False, label="test", backup_type=BackupType.Slip39_Basic, + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) # generate secret locally internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None secret = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) # validate that all combinations will result in the correct master secret @@ -83,12 +86,12 @@ def test_reset_entropy_check(client: Client): strength = 128 # 20 words - with WITH_MOCK_URANDOM, client: + with client: IF = InputFlowSlip39BasicResetRecovery(client) client.set_input_flow(IF.get()) # No PIN, no passphrase. - _, path_xpubs = device.reset_entropy_check( + path_xpubs = device.setup( client, strength=strength, passphrase_protection=False, @@ -96,10 +99,12 @@ def test_reset_entropy_check(client: Client): label="test", backup_type=BackupType.Slip39_Basic, entropy_check_count=3, + _get_entropy=MOCK_GET_ENTROPY, ) # Generate the master secret locally. internal_entropy = client.debug.state().reset_entropy + assert internal_entropy is not None secret = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) # Check that all combinations will result in the correct master secret. diff --git a/tests/device_tests/solana/test_address.py b/tests/device_tests/solana/test_address.py index dca1126c056..b3af4ea8ed0 100644 --- a/tests/device_tests/solana/test_address.py +++ b/tests/device_tests/solana/test_address.py @@ -37,4 +37,4 @@ def test_solana_get_address(client: Client, parameters, result): client, address_n=parse_path(parameters["path"]), show_display=True ) - assert actual_result.address == result["expected_address"] + assert actual_result == result["expected_address"] diff --git a/tests/device_tests/solana/test_public_key.py b/tests/device_tests/solana/test_public_key.py index 864852b116a..e12c345fc3e 100644 --- a/tests/device_tests/solana/test_public_key.py +++ b/tests/device_tests/solana/test_public_key.py @@ -37,4 +37,4 @@ def test_solana_get_public_key(client: Client, parameters, result): client, address_n=parse_path(parameters["path"]), show_display=True ) - assert actual_result.public_key.hex() == result["expected_public_key"] + assert actual_result.hex() == result["expected_public_key"] diff --git a/tests/device_tests/solana/test_sign_tx.py b/tests/device_tests/solana/test_sign_tx.py index 241a3d3b34f..de7ec344625 100644 --- a/tests/device_tests/solana/test_sign_tx.py +++ b/tests/device_tests/solana/test_sign_tx.py @@ -70,7 +70,7 @@ def test_solana_sign_tx(client: Client, parameters, result): ), ) - assert actual_result.signature == bytes.fromhex(result["expected_signature"]) + assert actual_result == bytes.fromhex(result["expected_signature"]) def _serialize_tx(tx_construct): diff --git a/tests/device_tests/test_msg_applysettings.py b/tests/device_tests/test_msg_applysettings.py index 65ea9357481..9e3161bb8be 100644 --- a/tests/device_tests/test_msg_applysettings.py +++ b/tests/device_tests/test_msg_applysettings.py @@ -426,11 +426,4 @@ def test_label_too_long(client: Client): @pytest.mark.models(skip=["legacy", "safe3"]) @pytest.mark.setup_client(pin=None) def test_set_brightness(client: Client): - with client: - assert ( - device.set_brightness( - client, - None, - ) - == "Settings applied" - ) + device.set_brightness(client, None) diff --git a/tests/device_tests/test_msg_wipedevice.py b/tests/device_tests/test_msg_wipedevice.py index d94f392f1b5..6009dd624d3 100644 --- a/tests/device_tests/test_msg_wipedevice.py +++ b/tests/device_tests/test_msg_wipedevice.py @@ -54,7 +54,14 @@ def test_autolock_not_retained(client: Client): with client: client.use_pin_sequence([PIN4, PIN4]) - device.reset(client, skip_backup=True, pin_protection=True) + device.setup( + client, + skip_backup=True, + pin_protection=True, + passphrase_protection=False, + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, + ) time.sleep(10.5) with client: diff --git a/tests/device_tests/test_protection_levels.py b/tests/device_tests/test_protection_levels.py index 9176df30512..b05a5b02c23 100644 --- a/tests/device_tests/test_protection_levels.py +++ b/tests/device_tests/test_protection_levels.py @@ -22,7 +22,7 @@ from trezorlib.exceptions import TrezorFailure from trezorlib.tools import parse_path -from ..common import MNEMONIC12, WITH_MOCK_URANDOM, get_test_address, is_core +from ..common import MNEMONIC12, MOCK_GET_ENTROPY, get_test_address, is_core from ..tx_cache import TxCache from .bitcoin.signtx import ( request_finished, @@ -214,24 +214,26 @@ def test_wipe_device(client: Client): def test_reset_device(client: Client): assert client.features.pin_protection is False assert client.features.passphrase_protection is False - with WITH_MOCK_URANDOM, client: + with client: client.set_expected_responses( [messages.ButtonRequest] + [messages.EntropyRequest] + [messages.ButtonRequest] * 24 + [messages.Success, messages.Features] ) - device.reset( + device.setup( client, strength=128, passphrase_protection=True, pin_protection=False, label="label", + entropy_check_count=0, + _get_entropy=MOCK_GET_ENTROPY, ) with pytest.raises(TrezorFailure): # This must fail, because device is already initialized - # Using direct call because `device.reset` has its own check + # Using direct call because `device.setup` has its own check client.call( messages.ResetDevice( strength=128, diff --git a/tests/device_tests/test_repeated_backup.py b/tests/device_tests/test_repeated_backup.py index 3bf2d42510d..9fc25ad202d 100644 --- a/tests/device_tests/test_repeated_backup.py +++ b/tests/device_tests/test_repeated_backup.py @@ -25,7 +25,6 @@ from ..common import ( MNEMONIC_SLIP39_SINGLE_EXT_20, TEST_ADDRESS_N, - WITH_MOCK_URANDOM, MNEMONIC_SLIP39_BASIC_20_3of6, ) from ..input_flows import InputFlowSlip39BasicBackup, InputFlowSlip39BasicRecoveryDryRun @@ -34,7 +33,6 @@ @pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) -@WITH_MOCK_URANDOM def test_repeated_backup(client: Client): assert client.features.backup_availability == messages.BackupAvailability.Required assert client.features.recovery_status == messages.RecoveryStatus.Nothing @@ -63,8 +61,7 @@ def test_repeated_backup(client: Client): client, mnemonics[:3], unlock_repeated_backup=True ) client.set_input_flow(IF.get()) - ret = device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) - assert ret == messages.Success(message="Backup unlocked") + device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) assert ( client.features.backup_availability == messages.BackupAvailability.Available ) @@ -86,7 +83,6 @@ def test_repeated_backup(client: Client): @pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_SINGLE_EXT_20) -@WITH_MOCK_URANDOM def test_repeated_backup_upgrade_single(client: Client): assert ( client.features.backup_availability == messages.BackupAvailability.NotAvailable @@ -100,8 +96,7 @@ def test_repeated_backup_upgrade_single(client: Client): client, MNEMONIC_SLIP39_SINGLE_EXT_20, unlock_repeated_backup=True ) client.set_input_flow(IF.get()) - ret = device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) - assert ret == messages.Success(message="Backup unlocked") + device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) assert ( client.features.backup_availability == messages.BackupAvailability.Available ) @@ -125,7 +120,6 @@ def test_repeated_backup_upgrade_single(client: Client): @pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) -@WITH_MOCK_URANDOM def test_repeated_backup_cancel(client: Client): assert client.features.backup_availability == messages.BackupAvailability.Required assert client.features.recovery_status == messages.RecoveryStatus.Nothing @@ -154,8 +148,7 @@ def test_repeated_backup_cancel(client: Client): client, mnemonics[:3], unlock_repeated_backup=True ) client.set_input_flow(IF.get()) - ret = device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) - assert ret == messages.Success(message="Backup unlocked") + device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) assert ( client.features.backup_availability == messages.BackupAvailability.Available ) @@ -181,7 +174,6 @@ def test_repeated_backup_cancel(client: Client): @pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) -@WITH_MOCK_URANDOM def test_repeated_backup_send_disallowed_message(client: Client): assert client.features.backup_availability == messages.BackupAvailability.Required assert client.features.recovery_status == messages.RecoveryStatus.Nothing @@ -210,8 +202,7 @@ def test_repeated_backup_send_disallowed_message(client: Client): client, mnemonics[:3], unlock_repeated_backup=True ) client.set_input_flow(IF.get()) - ret = device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) - assert ret == messages.Success(message="Backup unlocked") + device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup) assert ( client.features.backup_availability == messages.BackupAvailability.Available ) diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py index bafe67f511d..5d8c8778650 100644 --- a/tests/upgrade_tests/test_firmware_upgrades.py +++ b/tests/upgrade_tests/test_firmware_upgrades.py @@ -208,12 +208,14 @@ def asserts(client: "Client"): assert not client.features.no_backup with EmulatorWrapper(gen, tag) as emu: - device.reset( + device.setup( emu.client, strength=STRENGTH, passphrase_protection=False, pin_protection=False, label=LABEL, + entropy_check_count=0, + backup_type=BackupType.Bip39, ) device_id = emu.client.features.device_id asserts(emu.client) @@ -238,13 +240,15 @@ def asserts(client: "Client"): assert not client.features.no_backup with EmulatorWrapper(gen, tag) as emu: - device.reset( + device.setup( emu.client, strength=STRENGTH, passphrase_protection=False, pin_protection=False, label=LABEL, skip_backup=True, + entropy_check_count=0, + backup_type=BackupType.Bip39, ) device_id = emu.client.features.device_id asserts(emu.client) @@ -269,14 +273,17 @@ def asserts(client: "Client"): assert client.features.no_backup with EmulatorWrapper(gen, tag) as emu: - device.reset( + device.setup( emu.client, strength=STRENGTH, passphrase_protection=False, pin_protection=False, label=LABEL, no_backup=True, + entropy_check_count=0, + backup_type=BackupType.Bip39, ) + device_id = emu.client.features.device_id asserts(emu.client) address = btc.get_address(emu.client, "Bitcoin", PATH) @@ -344,11 +351,12 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]): def test_upgrade_shamir_backup(gen: str, tag: Optional[str]): with EmulatorWrapper(gen, tag) as emu: # Generate a new encrypted master secret and record it. - device.reset( + device.setup( emu.client, pin_protection=False, skip_backup=True, backup_type=BackupType.Slip39_Basic, + entropy_check_count=0, ) device_id = emu.client.features.device_id backup_type = emu.client.features.backup_type @@ -414,8 +422,7 @@ def test_upgrade_u2f(gen: str, tag: str): label=LABEL, ) - success = fido.set_counter(emu.client, 10) - assert "U2F counter set" in success + fido.set_counter(emu.client, 10) counter = fido.get_next_counter(emu.client) assert counter == 11 diff --git a/tests/upgrade_tests/test_passphrase_consistency.py b/tests/upgrade_tests/test_passphrase_consistency.py index 67a4c406fb9..a368c75bc50 100644 --- a/tests/upgrade_tests/test_passphrase_consistency.py +++ b/tests/upgrade_tests/test_passphrase_consistency.py @@ -46,10 +46,12 @@ class ApplySettingsCompat(protobuf.MessageType): def emulator(gen: str, tag: str) -> Iterator[Emulator]: with EmulatorWrapper(gen, tag) as emu: # set up a passphrase-protected device - device.reset( + device.setup( emu.client, pin_protection=False, skip_backup=True, + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, ) resp = emu.client.call( ApplySettingsCompat(use_passphrase=True, passphrase_source=SOURCE_HOST) From 940f9e61f24a3434c0a2a75f7f54fdc658a76de3 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 10 Jan 2025 13:40:25 +0100 Subject: [PATCH 09/14] fix(tests): implement expected responses for entropy check fixes #4464 --- .../reset_recovery/test_reset_bip39_t2.py | 63 +++++++++++++++++++ tests/ui_tests/fixtures.json | 6 ++ 2 files changed, 69 insertions(+) diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py index a1eb1856aaa..44cabbac935 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py @@ -249,3 +249,66 @@ def test_already_initialized(client: Client): pin_protection=True, label="label", ) + + +@pytest.mark.setup_client(uninitialized=True) +def test_entropy_check(client: Client): + with client: + quicksilver = client.debug.layout_type is LayoutType.Quicksilver + client.set_expected_responses( + [ + messages.ButtonRequest(name="setup_device"), + (quicksilver, messages.ButtonRequest(name="confirm_setup_device")), + messages.EntropyRequest, + messages.EntropyCheckReady, + messages.PublicKey, + messages.PublicKey, + messages.EntropyRequest, + messages.EntropyCheckReady, + messages.PublicKey, + messages.PublicKey, + messages.EntropyRequest, + messages.EntropyCheckReady, + messages.PublicKey, + messages.PublicKey, + (quicksilver, messages.ButtonRequest(name="backup_device")), + messages.Success, + messages.Features, + ] + ) + device.setup( + client, + strength=128, + entropy_check_count=2, + backup_type=messages.BackupType.Bip39, + skip_backup=True, + pin_protection=False, + passphrase_protection=False, + _get_entropy=MOCK_GET_ENTROPY, + ) + + +@pytest.mark.setup_client(uninitialized=True) +def test_no_entropy_check(client: Client): + with client: + quicksilver = client.debug.layout_type is LayoutType.Quicksilver + client.set_expected_responses( + [ + messages.ButtonRequest(name="setup_device"), + (quicksilver, messages.ButtonRequest(name="confirm_setup_device")), + messages.EntropyRequest, + (quicksilver, messages.ButtonRequest(name="backup_device")), + messages.Success, + messages.Features, + ] + ) + device.setup( + client, + strength=128, + entropy_check_count=0, + backup_type=messages.BackupType.Bip39, + skip_backup=True, + pin_protection=False, + passphrase_protection=False, + _get_entropy=MOCK_GET_ENTROPY, + ) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 7fa75fce1b1..30b69f60c15 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -5054,7 +5054,9 @@ "T2T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "c4d39c0bb7d54c71a0a87e3ee912b5bb5ce6919693c210596cc316ab57deb48f", "T2T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "ff2d873c486d3f6f55bf0d6558aa759db2b42a8aa1e697a76eb3abf0b0144844", "T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "8b1ccc0dbd6e6e3d02a896650ab90dd332ba4edbbcc4095e0fbb6a96e5256f75", +"T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "f4d850259404fdef6a340ba2245e2d1cc3e68f06ef5eea819b91b3015bc4b4f4", "T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "2561ba9b866f53847e8b00bf1cf2eb29946fd1df66e96686b327ea63b067aa71", +"T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "f4d850259404fdef6a340ba2245e2d1cc3e68f06ef5eea819b91b3015bc4b4f4", "T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "18d8e092e8d8bc6bca6f9d3d2059a62dae0770b52a7789f10f63bf36d0317370", "T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "db10ae4b9e2db5d77080b8066d7dc6e2b79b900cc7865fde52d21dbb01813a78", "T2T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "07302f85240003a3c88e8f6ba500f0e6c4b179b1592fd04a130b4e41663607af", @@ -13829,7 +13831,9 @@ "T3B1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "2bdf9c4e5985471c7ca846075f0c6401cf7e8e193c104dc20135f129cb82517b", "T3B1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "339fe910eaa0ec50d7856339dcdfff13c7f5bf3cec24b2a3de7c5310cbecc6df", "T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "1477d62e338f4d7c1bfac2fc5d2fc231218da5768666c11482dc1f83229506f3", +"T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "a359a52df521328ed0964dfee3a8197bc0a31518129b24624e26a7bf0ad1754e", "T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "ec341a977b9a38b31fa15741cd0b38956844f4dc25441c6f30fa59576301c62b", +"T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "a359a52df521328ed0964dfee3a8197bc0a31518129b24624e26a7bf0ad1754e", "T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "c759377e9c5181979b3f111de2f4498b002a743fa714f63f1059ff33cf7a94e8", "T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "af5d234a3e234325074e99e2c7d3a36672d7c29af140783dc2329c016f16fe8b", "T3B1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "ebc2178d5cf193cb1c33369224e836e29bb557646f6ce25f4fe902233d0fa5e5", @@ -22443,7 +22447,9 @@ "T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "405244c6f883533179159214fbbe64ad1d3a9c86c82860bedfcebf2a307f205a", "T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "73d98479c5672f58960dc0848b7f41fd5eabb52ab34c552c1882175a7db3a864", "T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "3c5fb7d6110128ed52024a6b92654210b7acad6fe08b568d5238bfceb257a524", +"T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "02072595edb96be5a6a5472ab8abaa6768120aef28342c2dc078d4824b511877", "T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "738b2375bb340b017517800af4db5c4240056ca1965712846da2ef61ca803780", +"T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "02072595edb96be5a6a5472ab8abaa6768120aef28342c2dc078d4824b511877", "T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "5d648df9d2cd53810bdacfb192fa52b728734b02d6908e42e9a724f9db05c459", "T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "61fdfd287602289777af97acf680b15fc0e70cc817ec66cb9a7e9640e321f614", "T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "6295b1fb0f61c8bb16a233feb641d0489e7f7a115243ab9b0c616b68c8d88d1c", From f787013c9548e6b8ca311a412af89cfc95509f89 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 13 Jan 2025 13:49:30 +0100 Subject: [PATCH 10/14] fix(core): improve ButtonRequest.name in backup confirmation [no changelog] --- core/src/trezor/ui/layouts/bolt/reset.py | 2 +- core/src/trezor/ui/layouts/quicksilver/reset.py | 2 +- core/src/trezor/ui/layouts/samson/reset.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/trezor/ui/layouts/bolt/reset.py b/core/src/trezor/ui/layouts/bolt/reset.py index ec4e1675a04..f722409c212 100644 --- a/core/src/trezor/ui/layouts/bolt/reset.py +++ b/core/src/trezor/ui/layouts/bolt/reset.py @@ -372,7 +372,7 @@ async def show_share_confirmation_success( ) text = TR.reset__continue_with_next_share - return await show_success("success_recovery", text, subheader) + return await show_success("success_share_confirm", text, subheader) async def show_share_confirmation_failure() -> None: diff --git a/core/src/trezor/ui/layouts/quicksilver/reset.py b/core/src/trezor/ui/layouts/quicksilver/reset.py index 709f32035f8..766d34d9af1 100644 --- a/core/src/trezor/ui/layouts/quicksilver/reset.py +++ b/core/src/trezor/ui/layouts/quicksilver/reset.py @@ -372,7 +372,7 @@ async def show_share_confirmation_success( ) ) - await show_success("success_recovery", title, subheader=footer_description) + await show_success("success_share_confirm", title, subheader=footer_description) def show_share_confirmation_failure() -> Awaitable[None]: diff --git a/core/src/trezor/ui/layouts/samson/reset.py b/core/src/trezor/ui/layouts/samson/reset.py index fb0218fe06a..ef4a785f14f 100644 --- a/core/src/trezor/ui/layouts/samson/reset.py +++ b/core/src/trezor/ui/layouts/samson/reset.py @@ -339,7 +339,7 @@ async def show_share_confirmation_success( ) text = TR.reset__continue_with_next_share - return await show_success("success_recovery", text, subheader) + return await show_success("success_share_confirm", text, subheader) async def show_share_confirmation_failure() -> None: From 8392b2a6b0a048d71316d04a5aa3db669cca1b73 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 13 Jan 2025 15:45:30 +0100 Subject: [PATCH 11/14] fixup! feat(python): introduce a deprecation helper --- python/src/trezorlib/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py index 5c5262f37dc..33420f08d00 100644 --- a/python/src/trezorlib/tools.py +++ b/python/src/trezorlib/tools.py @@ -16,6 +16,7 @@ from __future__ import annotations +import copy import functools import hashlib import inspect @@ -368,6 +369,7 @@ class Deprecated(value.__class__): else: # - for other types (we assume it's MessageType so a user-defined class) # assign __class__ directly + value = copy.copy(value) value.__class__ = Deprecated ret = value From f99c9f46b044c373299c1873709d210278f16138 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 14 Jan 2025 14:35:27 +0100 Subject: [PATCH 12/14] fixup! refactor(python): replace usages of @expect --- python/src/trezorlib/btc.py | 15 +++++++-------- python/src/trezorlib/device.py | 2 ++ python/src/trezorlib/stellar.py | 5 +---- python/src/trezorlib/tools.py | 16 ++++++++++------ 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/python/src/trezorlib/btc.py b/python/src/trezorlib/btc.py index d273a97ff5b..078f486d9e6 100644 --- a/python/src/trezorlib/btc.py +++ b/python/src/trezorlib/btc.py @@ -116,11 +116,10 @@ def get_public_node( unlock_path_mac: Optional[bytes] = None, ) -> messages.PublicKey: if unlock_path: - res = client.call( - messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac) + client.call( + messages.UnlockPath(address_n=unlock_path, mac=unlock_path_mac), + expect=messages.UnlockedPathRequest, ) - if not isinstance(res, messages.UnlockedPathRequest): - raise exceptions.TrezorException("Unexpected message") return client.call( messages.GetPublicKey( @@ -135,7 +134,7 @@ def get_public_node( ) -def get_address(*args: Any, **kwargs: Any): +def get_address(*args: Any, **kwargs: Any) -> str: return get_authenticated_address(*args, **kwargs).address @@ -322,7 +321,7 @@ def sign_tx( elif preauthorized: client.call(messages.DoPreauthorized(), expect=messages.PreauthorizedRequest) - res = client.call(signtx) + res = client.call(signtx, expect=messages.TxRequest) # Prepare structure for signatures signatures: List[Optional[bytes]] = [None] * len(inputs) @@ -350,7 +349,7 @@ def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: ) R = messages.RequestType - while isinstance(res, messages.TxRequest): + while True: # If there's some part of signed transaction, let's add it if res.serialized: if res.serialized.serialized_tx: @@ -381,7 +380,7 @@ def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: if res.request_type == R.TXPAYMENTREQ: assert res.details.request_index is not None msg = payment_reqs[res.details.request_index] - res = client.call(msg) + res = client.call(msg, expect=messages.TxRequest) else: msg = messages.TransactionType() if res.request_type == R.TXMETA: diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index 3d7cb401001..c08d485ed0b 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -111,6 +111,8 @@ def change_language( if data_length > 0: response = messages.TranslationDataRequest.ensure_isinstance(response) _send_language_data(client, response, language_data) + else: + messages.Success.ensure_isinstance(response) client.refresh_features() # changing the language in features return _return_success(messages.Success(message="Language changed.")) diff --git a/python/src/trezorlib/stellar.py b/python/src/trezorlib/stellar.py index f863110465f..5bd0a749e42 100644 --- a/python/src/trezorlib/stellar.py +++ b/python/src/trezorlib/stellar.py @@ -362,10 +362,7 @@ def sign_tx( "Reached end of operations without a signature." ) from None - if not isinstance(resp, messages.StellarSignedTx): - raise exceptions.TrezorException( - f"Unexpected message: {resp.__class__.__name__}" - ) + resp = messages.StellarSignedTx.ensure_isinstance(resp) if operations: raise exceptions.TrezorException( diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py index 33420f08d00..96592877ac6 100644 --- a/python/src/trezorlib/tools.py +++ b/python/src/trezorlib/tools.py @@ -362,16 +362,20 @@ class Deprecated(value.__class__): # construct an instance: if isinstance(value, str): - # - for str, do it naively: + # for str, invoke the copy constructor ret = Deprecated(value) - # (this branch should cover all builtin types, but we don't use the wrapper - # for anything other than str) - else: - # - for other types (we assume it's MessageType so a user-defined class) - # assign __class__ directly + elif isinstance(value, MessageType): + # MessageTypes don't have a copy constructor, so + # 1. we make an explicit copy value = copy.copy(value) + # 2. we change the class of the copy value.__class__ = Deprecated + # note: we don't need deep copy because all accesses to inner objects already + # trigger the warning via __getattribute__ ret = value + else: + # we don't support other types currently + raise NotImplementedError # enable warnings warning_enabled = True From ff33167d24e984ff958d11251cf6d34288c31948 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 15 Jan 2025 09:51:50 +0100 Subject: [PATCH 13/14] fixup! fixup! refactor(python): replace usages of @expect --- python/src/trezorlib/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py index 96592877ac6..6ba8c64dba3 100644 --- a/python/src/trezorlib/tools.py +++ b/python/src/trezorlib/tools.py @@ -360,6 +360,8 @@ class Deprecated(value.__class__): # replace the method with a wrapper that emits a warning setattr(Deprecated, key, deprecation_warning_wrapper(orig_value)) + from .protobuf import MessageType + # construct an instance: if isinstance(value, str): # for str, invoke the copy constructor From f7015ddebebae55944969ffd39f7487a079e9225 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 15 Jan 2025 13:27:58 +0100 Subject: [PATCH 14/14] fixup! fix(tests): implement expected responses for entropy check --- tests/ui_tests/fixtures.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 30b69f60c15..6c3d13c3057 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -2152,7 +2152,9 @@ "T2T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "23c32a45ad48e5c21f63a1bfdbd552ea37c3ba30a305c3db8d390f3b5f774f0f", "T2T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "ce6d00e05ef089e26e101099aa200b2309bc7d62702ee92be7458b8c30ad9c33", "T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "32d380b2c0942f8a2ab6a32e0e4c8a2ad2ab6750ee39c6fa4d4f0bacf59a4b7c", +"T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "4013ef095959ae008ae0597fd3e305e00885cea1677b76769906d46b8e63480c", "T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "27956239af6e927ad7233a0580167f60be70396b0579b079658273004afe4a14", +"T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "4013ef095959ae008ae0597fd3e305e00885cea1677b76769906d46b8e63480c", "T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "b44158f07bced49a07c7b434fa0b414df910f996b607490f100d2d14bf48829f", "T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "8c0ab2946c5a9a399b454f2113f8bab05858c66168dc45fe8ff95402888645c3", "T2T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "c052c17e3475df47766abf5eabe21d26bae6f10db060ebf653ae65542202cc8d", @@ -3603,7 +3605,9 @@ "T2T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "e60bd107d08a10e27f61194697ffe484bcbd9ac47576366c11666eca4d244b04", "T2T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "a2e778952284665b32fc6f0f91e2b153b1e917ad3d7f836ed1c51cb0d70da3f7", "T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "a3edf3ced8fa1fa6b9f67f869a28bc880ce5e214b0adfaf839cd867875845912", +"T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "cebaea21dda0937859e4265a63ebcc3cd2f2467363e3e97e9ffad0ca78714865", "T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "a6d8c8e07cf632efbf83d6819a09cea3a893a36631f1e67349048c0024f34fb0", +"T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "cebaea21dda0937859e4265a63ebcc3cd2f2467363e3e97e9ffad0ca78714865", "T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "f8caff57807e18731dc278e9292c3411ab602852b12a438213e6a254d3a95ab8", "T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "76d1e94b79ad7b1961b534513759075655e1eeffa9cb2aa62bce3c6f24e0ae51", "T2T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "eeb0a3222dabe32f96e3cce537227fe352fad1bee1efe72f218d7a386b751433", @@ -6525,7 +6529,9 @@ "T2T1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "d85c4a0fd95c7cfccefbbba17fa4ae77d371e0ac3d033d74eefb2d482af1ef85", "T2T1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "a72328d3d647c04af48bf925c37c2c6f56f7906eb2da5f34d27a0f47abaeb84d", "T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "1775658fa541fff7933453c1d346449c492079a39341e07135ac9e7662b3bbd3", +"T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "5c2db31742eca00619c640bed7f93ddc77eec023aca5a321abf18e2589ae5b2c", "T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "61014fc8a1ebf08493d81b02aad81de3f98d1d632a7313d8368d55431b5b035f", +"T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "5c2db31742eca00619c640bed7f93ddc77eec023aca5a321abf18e2589ae5b2c", "T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "58f54e7400e61e705272b0c9c4c42d02a38bd0524fb39339a3c621eaab1bd751", "T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "de5dc8c743daf293131a04557bf553d8e56ccca71a64bfe061a3ea9706cdd13e", "T2T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "5bd7a32f1e18d4800891c915082f3031f33f26e0df332e51250091e8921cc432", @@ -7976,7 +7982,9 @@ "T2T1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "ea249af1d2d16d7ff4bdc6412b50493672968d407932e7fc9051799a5391493e", "T2T1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "49de6516f8a7f4dee0b4fd97cfd4c2275c0c1f0a940ea2f03a89ac65b44fe99f", "T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "94d3ba1cba15b836e6ebc48ab307aeea71275ef2d9849aaabc8e883df555ce3f", +"T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "d2bd1f8640fe69a29eb9063c228ca440119689e5366e882fb2d3ad8bb0a7f231", "T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "3d55750a9941724fc9b9da0286055335fa719d7117c10873b173fbef168620e7", +"T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "d2bd1f8640fe69a29eb9063c228ca440119689e5366e882fb2d3ad8bb0a7f231", "T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "e8c1cc162ce790ca1205328ee86a01e50338b7c09a93dfdb730208083716d915", "T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "b5e26ffb19968a8e221e7efcbc02540788055d05b34683bdca93cb0241e23c3f", "T2T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "875a9bf82cbbc5be429f6eec8985fef87695d386f8ad0315986f62521ce3f736", @@ -9427,7 +9435,9 @@ "T2T1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "ffe23e244045d7d0483657c3c210414770354c87e58d9e4bb60a8b037c642efb", "T2T1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "b3ea81954b35517dd317b18b35552b9820222009e6504dac882b6709dcaa604f", "T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "e9e80e1bfd347b598699a7a84deb8932eff90fc5fb8b56771453e06fe3c4c216", +"T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "92960d3f1c8bcb8713eb15c7011746ca78ab6b63e0431bfd6dc63ac6c1c7e112", "T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "ecae4feb38eccde3b97934048e747d16b0be324a0736dbfc1fbdf02d4a654c73", +"T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "92960d3f1c8bcb8713eb15c7011746ca78ab6b63e0431bfd6dc63ac6c1c7e112", "T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "645418057ee26504fd927a20e5a19cbd19a2221300f47db889668cb720e22dec", "T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "45dd169782c0c01380412741b44a92574ca0b9f9b9afa83a049e73a0d2b27500", "T2T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "6f53ada20c96536abbf60bcd9f2702b31dd734caeb0658acf2fdd04110de040f", @@ -11095,7 +11105,9 @@ "T3B1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "11fe7b5ad08740f65b762b56e6c5a3afa10b60835e22ed1758f10f3708563698", "T3B1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "f67c01f69eb207a1aa5d57b46de610a6477825855f8fb3722d2a45eb51342b7b", "T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "92840d4624bb07440bb1ab5cb4b57251c9950388accbc8334a9a7d609ededb01", +"T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "0022249681ceef76cf016e8397830e060caaee8cf3630632f1f04d3876e560aa", "T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "9b410b862ba8af4cc5cbf1d549aaeb803acaf65b90dbe951e7bd7aefed57b077", +"T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "0022249681ceef76cf016e8397830e060caaee8cf3630632f1f04d3876e560aa", "T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "c0955d31843f143c264bafccb9a45e5e3107de84242b837808d934bc528a42d8", "T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "6bf8318485133d5bee6f06af77dbf6639153cca2bfc9ddf20999db8fcf42af53", "T3B1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "66efb3513862220ebc8ff58b8f87006016aa34289a69fb7604720f7b2d156908", @@ -12463,7 +12475,9 @@ "T3B1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "c8c435069b1bb6763e625d6faf5fd5b6da432e108dd4392f62a2c3940c8eed3c", "T3B1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "6f29a1dfd8e44951b55162f0e2184db0278d10722cc33d324025fd3be2dca25e", "T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "c92ee4e050daadcfc7cc95657b00deed76bb463d486eefcc66455bdbe60faa33", +"T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "0278b6bf0bbff1647d7db174113facfa08cce7c961a03d3d156f3db5d2de58d5", "T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "44f6c83e382149b8d3984f72cbb161ce87624c33f8721f1ab3af2cce9bc8bd5e", +"T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "0278b6bf0bbff1647d7db174113facfa08cce7c961a03d3d156f3db5d2de58d5", "T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "144aa1e815ea7a371eb9e30eeaf9e349bcc3134ca37174adbe6e2a46f147dab8", "T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "5d0a9c23ea00b7f152db0400669ded5c727a19afc6a8b9162c722f51d7877265", "T3B1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "c16fff45ea67f139c00bcea1a837fd4e4a3330e56facc41a74c5f8507262b116", @@ -15201,7 +15215,9 @@ "T3B1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "de8ac61432fdc80b52214b99f8d7773d4654dcaa775088f6c0ef7555b6341c4f", "T3B1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "bd8b35b569869d74fd4a35ffc87e0f14be5d3ca02cada6328851cb20d538e520", "T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "211e7c3419bd48ceefc570971fc522f809b39614563be948fd1e545366506dee", +"T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "d7c96ef17a536c3286daad336b68ddb795bb694f121a1217bb84b2f32dc6775f", "T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "cd56ec2f8f395d5b3c0ac72d1ae29f8fc3a40801b4b710fdbbd166f507a0411d", +"T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "d7c96ef17a536c3286daad336b68ddb795bb694f121a1217bb84b2f32dc6775f", "T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "d63d5752eb92c316c6ac8dc69b20db872adca22338a02e8ae86aca6e0559b33b", "T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "c607e9cb72cb9262945fa9191e26411d612ed2718581c197e1c5ec8206b20aca", "T3B1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "75dbf011cb39842024a3f4bf232eee87f087de29b5d02ddf123f4281da183350", @@ -16569,7 +16585,9 @@ "T3B1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "da0f9332a92fb42bb3754d28b163f9aa4e1d24fdce2ba9ae79a95074b937d0c1", "T3B1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "15faf627093e6bc5d52d6528719a33c3988c40f526342b26715b824bdf6873a7", "T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "35c9c9529c59ca5c386ac05856f7452f26cde732203b5b61ec9321230b52df89", +"T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "99dd9addf07ce9d680e1d07195945e2ae295cc8e669241bf8de10611a5127922", "T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "ca2c955918f9709362f004ab1fb9902462a1aeb9333919f0a2dd0a63d9463249", +"T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "99dd9addf07ce9d680e1d07195945e2ae295cc8e669241bf8de10611a5127922", "T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "1efffd7a1d08afc0a7fba5e62167d083db30a1e450d23a33eb4a924c4f85a1e8", "T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "ec4dab82a1b294135843f9051007a236135cf37899319a4cc18ca4c53af8467b", "T3B1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "f4278b2613658e750efa25393ab287689a61c6bd1b8ecb28c58314211b55b3db", @@ -17937,7 +17955,9 @@ "T3B1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "d9231f568fd73bea129a344a16a71e7e83613e81dfe47f7ff907d96b33d26c18", "T3B1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "0a36a586ebd52439cd59ae98fa6589abae2e5252d08e91a62ccaa40a7caa41a5", "T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "6ba5ca7223cd8ad675e081407f186acdfc8420304eea96de0fde5eda45ef0a57", +"T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "e5efdb5d52fd8420ff6910907c05fc30dcef24b78ea1ee130bbe03e9c3d09721", "T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "fca068e2cbfa53b5da6b11cc38d49dc4acca8c875a78dff9a8fd3826786e843a", +"T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "e5efdb5d52fd8420ff6910907c05fc30dcef24b78ea1ee130bbe03e9c3d09721", "T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "87ea85c22c51be957e5e7cb4e10cf72534364d11ced4d5bf5df06d2e7ad8eeeb", "T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "2ac9968f2625eb9ce1e1424f6d6bf7906f8b866b64a82623916d876f23dc7f4d", "T3B1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "d9f062aecbcb474458d03641ceefe219f10d982a35bf6f943dda4322ba70f5be", @@ -19667,7 +19687,9 @@ "T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "3139fd8d30a58eacddbe4f113e6fc09b16c4f4b6c1e54dfd5d3eb98c6086a2c7", "T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "967796b77397c9dabb8044298fa8b9d5dbd73583e55511139f30da1030452c2f", "T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "991a101e4cda5811a95f6c57fa316b1665f85c6f29cd7863ab672082ba3cddbb", +"T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "fd1b5110232cbbd6b05db397598ece9609f55043752036ec5e0986f2a49940d6", "T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "2d8dd3c3adb6ff63c12b5e644c1a410a962be2ec04a17c21b7ff302e157a5b78", +"T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "fd1b5110232cbbd6b05db397598ece9609f55043752036ec5e0986f2a49940d6", "T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "ebf6ce72825f728636b861d40dde4385e9f95597975c02647a619583586ea1a0", "T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "62f86ce6a5608dab124a85ff925a52de17e255343af1cbf05acd11a2e6d3d888", "T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "035ea3187fdaed9d6810f33423700059adeb395a9e9b1befd1229ab3d8f8b569", @@ -21057,7 +21079,9 @@ "T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "e83e719c0a4960768d1738dc2b18339f9630dc4a7f47de8755ce025ee4765f57", "T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "d471ab4fe6883607cdba04b82a4114cf7d6090a0c06ebe53c495b066ef519032", "T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "656c427f48600d0cfc3e3c739f9959f680a8c45686fe503e81bfbf17e39595eb", +"T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "096c190b22df18dbd6ab97668300439743bafa9af78dfdb8ff760498207e7fdf", "T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "dce8e4eb6c1404555686cf8c0f84fc312867b1bb8d6a4b8d52f4610da5d89bb6", +"T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "096c190b22df18dbd6ab97668300439743bafa9af78dfdb8ff760498207e7fdf", "T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "62a331b327daf9c2ff1b66de6f8945cff6c1e60b392bc2e454916fddc889b02c", "T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "e0d5985ad8c317b683b6dff243bae4a7f170b5d10ae980ef9f6c33d1a8dce1a4", "T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "663aefa17a7fd0bcb126600c47dd0ccc9aec5e514b53e32b40f194eb7a685396", @@ -23839,7 +23863,9 @@ "T3T1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "c9812d3208b45dec6eb87e5c242f2d4b9ed894912358afa059918734c8b47f84", "T3T1_es_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "e03e44f589ef72af631b5def9b41e6df3528d35799ea181f61ae6523f091cf85", "T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "cf6e2f75443a6d0a64227c79d4f7fd3ebe3799697afc90932fbd87108b288f87", +"T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "d4c41a82668924ca2193aaf5887f07d3aa033d535244be9925403df3d08dc956", "T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "ec6a1af90acbf1921a55ae92dc405f46a51277b0ba78d532b7db9e956a27ac40", +"T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "d4c41a82668924ca2193aaf5887f07d3aa033d535244be9925403df3d08dc956", "T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "d41474560fdc449cc7111efbf36b534d6dd9ed4e2318217fcec526c8fb32eba3", "T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "dd13b568d5d7c428e6bc7a91b47263c1322fbcaf06f0d20e8d789697b467680f", "T3T1_es_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "04f00b1b8a77afbde5b006f6709d3717312119675d3f9efb4931d8b21ce7404a", @@ -25229,7 +25255,9 @@ "T3T1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "520f7126ee02f5e9d42c11bc3efdd12ea804789ebf16eadaf8fb1e8d5c5b426a", "T3T1_fr_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "87931f15e70a557c930eb0a597b4218403ef1372afb9dcc05128712aaed82965", "T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "133bae24a65426ab6f29b12ba1ea0998096e511df637194e27eac3fad505c98f", +"T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "cc4da2ccc6e93dead7c8aa12e39e2555571208c9760865e81435072735fcf207", "T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "bfd73bef19d197f5f65d57f4ba78e4592a353e74f3ba3733a7de68be02fdeb97", +"T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "cc4da2ccc6e93dead7c8aa12e39e2555571208c9760865e81435072735fcf207", "T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "ef415598357e5724806bfb4c0f0c0d7cbcb772f90c4c6392a79a32ef9ed849c9", "T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "f106d4e73457c9193b026413495be09cacd5fab8b2fcdca735918845800edc09", "T3T1_fr_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "639aadb97f8160177071f919b5bc5146771f961cd426665250d1d5da1084ca1d", @@ -26619,7 +26647,9 @@ "T3T1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "6f4a5366df5c338c898a445a96bccd6e039139fe3ecb0c44ae6b67880c11b0ac", "T3T1_pt_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "5ebe2b9905f96dda0d9a4b4824158f46d2378189b9364c97053eed85f0c70ff8", "T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "e8eb9b57d62689b40a58c053da11694819eb927d8b82b505c6487505cb60a889", +"T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_entropy_check": "f39ad16318d43f7527688519214a36472f38a26bddd5c8fb600361560c41601a", "T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "8d32192e968b6ff3442de2d1d676199711e6eed859df2d30a2425ad3c8bc41b4", +"T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_no_entropy_check": "f39ad16318d43f7527688519214a36472f38a26bddd5c8fb600361560c41601a", "T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "0e9d96247d51eb85039b0edcfb7e4a445909b090cd458309fe30472d4dd7f64d", "T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "157850441ce34f8270ade338f8c0a757fd97247a79b210423e55197c2a999421", "T3T1_pt_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "02acc7b8cbf59d21944fd9f3bb5773ddeaa1ef825f32626640b5ea4b953bf3e0",