From b25537f6b060a3c52ec4b32529fff4ac31314ab5 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 26 Sep 2019 15:01:33 +0200 Subject: [PATCH 01/22] core: nicer output for cancellations --- core/src/trezor/wire/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/trezor/wire/__init__.py b/core/src/trezor/wire/__init__.py index c12cc809d..f1a277cff 100644 --- a/core/src/trezor/wire/__init__.py +++ b/core/src/trezor/wire/__init__.py @@ -40,7 +40,7 @@ from trezor import log, loop, messages, ui, utils, workflow from trezor.messages import FailureType from trezor.messages.Failure import Failure from trezor.wire import codec_v1 -from trezor.wire.errors import Error +from trezor.wire.errors import ActionCancelled, Error # Import all errors into namespace, so that `wire.Error` is available from # other packages. @@ -364,7 +364,10 @@ async def handle_session(iface: WireInterface, session_id: int) -> None: # - the first workflow message was not a valid protobuf # - workflow raised some kind of an exception while running if __debug__: - log.exception(__name__, exc) + if isinstance(exc, ActionCancelled): + log.debug(__name__, "cancelled: {}".format(exc.message)) + else: + log.exception(__name__, exc) res_msg = failure(exc) finally: From 06e10f948d8b460768b7a75f3d9b3b85450af708 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 27 Sep 2019 15:34:15 +0200 Subject: [PATCH 02/22] protobuf: send x/y coordinates, allow waiting for layout change --- common/protob/messages-debug.proto | 15 ++++++++++- common/protob/messages.proto | 1 + core/src/trezor/messages/DebugLinkDecision.py | 9 +++++++ core/src/trezor/messages/DebugLinkLayout.py | 26 +++++++++++++++++++ core/src/trezor/messages/DebugLinkState.py | 3 +++ core/src/trezor/messages/MessageType.py | 1 + .../trezorlib/messages/DebugLinkDecision.py | 9 +++++++ .../src/trezorlib/messages/DebugLinkLayout.py | 26 +++++++++++++++++++ .../src/trezorlib/messages/DebugLinkState.py | 3 +++ python/src/trezorlib/messages/MessageType.py | 1 + python/src/trezorlib/messages/__init__.py | 1 + 11 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 core/src/trezor/messages/DebugLinkLayout.py create mode 100644 python/src/trezorlib/messages/DebugLinkLayout.py diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index d660360c2..e3ff1cd9a 100644 --- a/common/protob/messages-debug.proto +++ b/common/protob/messages-debug.proto @@ -10,7 +10,7 @@ import "messages-common.proto"; /** * Request: "Press" the button on the device * @start - * @next Success + * @next DebugLinkLayout */ message DebugLinkDecision { optional bool yes_no = 1; // true for "Confirm", false for "Cancel" @@ -25,6 +25,18 @@ message DebugLinkDecision { LEFT = 2; RIGHT = 3; } + + optional uint32 x = 4; // touch X coordinate + optional uint32 y = 5; // touch Y coordinate + optional bool wait = 6; // wait for layout change +} + +/** + * Response: Device text layout + * @end + */ +message DebugLinkLayout { + repeated string lines = 1; } /** @@ -54,6 +66,7 @@ message DebugLinkState { optional uint32 recovery_word_pos = 10; // index of mnemonic word the device is expecting during RecoveryDevice workflow optional uint32 reset_word_pos = 11; // index of mnemonic word the device is expecting during ResetDevice workflow optional uint32 mnemonic_type = 12; // current mnemonic type (BIP-39/SLIP-39) + repeated string layout_lines = 13; // current layout text } /** diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 4bfa3e941..92d71162c 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -103,6 +103,7 @@ enum MessageType { MessageType_DebugLinkMemory = 111 [(wire_debug_out) = true]; MessageType_DebugLinkMemoryWrite = 112 [(wire_debug_in) = true]; MessageType_DebugLinkFlashErase = 113 [(wire_debug_in) = true]; + MessageType_DebugLinkLayout = 9001 [(wire_debug_out) = true]; // Ethereum MessageType_EthereumGetPublicKey = 450 [(wire_in) = true]; diff --git a/core/src/trezor/messages/DebugLinkDecision.py b/core/src/trezor/messages/DebugLinkDecision.py index 55d2a4092..715269cc3 100644 --- a/core/src/trezor/messages/DebugLinkDecision.py +++ b/core/src/trezor/messages/DebugLinkDecision.py @@ -19,10 +19,16 @@ class DebugLinkDecision(p.MessageType): yes_no: bool = None, swipe: EnumTypeDebugSwipeDirection = None, input: str = None, + x: int = None, + y: int = None, + wait: bool = None, ) -> None: self.yes_no = yes_no self.swipe = swipe self.input = input + self.x = x + self.y = y + self.wait = wait @classmethod def get_fields(cls) -> Dict: @@ -30,4 +36,7 @@ class DebugLinkDecision(p.MessageType): 1: ('yes_no', p.BoolType, 0), 2: ('swipe', p.EnumType("DebugSwipeDirection", (0, 1, 2, 3)), 0), 3: ('input', p.UnicodeType, 0), + 4: ('x', p.UVarintType, 0), + 5: ('y', p.UVarintType, 0), + 6: ('wait', p.BoolType, 0), } diff --git a/core/src/trezor/messages/DebugLinkLayout.py b/core/src/trezor/messages/DebugLinkLayout.py new file mode 100644 index 000000000..8fca66cd2 --- /dev/null +++ b/core/src/trezor/messages/DebugLinkLayout.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkLayout(p.MessageType): + MESSAGE_WIRE_TYPE = 9001 + + def __init__( + self, + lines: List[str] = None, + ) -> None: + self.lines = lines if lines is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('lines', p.UnicodeType, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/DebugLinkState.py b/core/src/trezor/messages/DebugLinkState.py index 14b79c704..2c7a17fcf 100644 --- a/core/src/trezor/messages/DebugLinkState.py +++ b/core/src/trezor/messages/DebugLinkState.py @@ -29,6 +29,7 @@ class DebugLinkState(p.MessageType): recovery_word_pos: int = None, reset_word_pos: int = None, mnemonic_type: int = None, + layout_lines: List[str] = None, ) -> None: self.layout = layout self.pin = pin @@ -42,6 +43,7 @@ class DebugLinkState(p.MessageType): self.recovery_word_pos = recovery_word_pos self.reset_word_pos = reset_word_pos self.mnemonic_type = mnemonic_type + self.layout_lines = layout_lines if layout_lines is not None else [] @classmethod def get_fields(cls) -> Dict: @@ -58,4 +60,5 @@ class DebugLinkState(p.MessageType): 10: ('recovery_word_pos', p.UVarintType, 0), 11: ('reset_word_pos', p.UVarintType, 0), 12: ('mnemonic_type', p.UVarintType, 0), + 13: ('layout_lines', p.UnicodeType, p.FLAG_REPEATED), } diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index 5215fb445..3f58f8a0e 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -70,6 +70,7 @@ DebugLinkMemoryRead = 110 # type: Literal[110] DebugLinkMemory = 111 # type: Literal[111] DebugLinkMemoryWrite = 112 # type: Literal[112] DebugLinkFlashErase = 113 # type: Literal[113] +DebugLinkLayout = 9001 # type: Literal[9001] if not utils.BITCOIN_ONLY: EthereumGetPublicKey = 450 # type: Literal[450] EthereumPublicKey = 451 # type: Literal[451] diff --git a/python/src/trezorlib/messages/DebugLinkDecision.py b/python/src/trezorlib/messages/DebugLinkDecision.py index 177c0dc57..08030c611 100644 --- a/python/src/trezorlib/messages/DebugLinkDecision.py +++ b/python/src/trezorlib/messages/DebugLinkDecision.py @@ -19,10 +19,16 @@ class DebugLinkDecision(p.MessageType): yes_no: bool = None, swipe: EnumTypeDebugSwipeDirection = None, input: str = None, + x: int = None, + y: int = None, + wait: bool = None, ) -> None: self.yes_no = yes_no self.swipe = swipe self.input = input + self.x = x + self.y = y + self.wait = wait @classmethod def get_fields(cls) -> Dict: @@ -30,4 +36,7 @@ class DebugLinkDecision(p.MessageType): 1: ('yes_no', p.BoolType, 0), 2: ('swipe', p.EnumType("DebugSwipeDirection", (0, 1, 2, 3)), 0), 3: ('input', p.UnicodeType, 0), + 4: ('x', p.UVarintType, 0), + 5: ('y', p.UVarintType, 0), + 6: ('wait', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/DebugLinkLayout.py b/python/src/trezorlib/messages/DebugLinkLayout.py new file mode 100644 index 000000000..7b8b41b78 --- /dev/null +++ b/python/src/trezorlib/messages/DebugLinkLayout.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkLayout(p.MessageType): + MESSAGE_WIRE_TYPE = 9001 + + def __init__( + self, + lines: List[str] = None, + ) -> None: + self.lines = lines if lines is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('lines', p.UnicodeType, p.FLAG_REPEATED), + } diff --git a/python/src/trezorlib/messages/DebugLinkState.py b/python/src/trezorlib/messages/DebugLinkState.py index bce8b84b4..a0548c6a7 100644 --- a/python/src/trezorlib/messages/DebugLinkState.py +++ b/python/src/trezorlib/messages/DebugLinkState.py @@ -29,6 +29,7 @@ class DebugLinkState(p.MessageType): recovery_word_pos: int = None, reset_word_pos: int = None, mnemonic_type: int = None, + layout_lines: List[str] = None, ) -> None: self.layout = layout self.pin = pin @@ -42,6 +43,7 @@ class DebugLinkState(p.MessageType): self.recovery_word_pos = recovery_word_pos self.reset_word_pos = reset_word_pos self.mnemonic_type = mnemonic_type + self.layout_lines = layout_lines if layout_lines is not None else [] @classmethod def get_fields(cls) -> Dict: @@ -58,4 +60,5 @@ class DebugLinkState(p.MessageType): 10: ('recovery_word_pos', p.UVarintType, 0), 11: ('reset_word_pos', p.UVarintType, 0), 12: ('mnemonic_type', p.UVarintType, 0), + 13: ('layout_lines', p.UnicodeType, p.FLAG_REPEATED), } diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index 39ca8b2ff..2b216a8de 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -68,6 +68,7 @@ DebugLinkMemoryRead = 110 # type: Literal[110] DebugLinkMemory = 111 # type: Literal[111] DebugLinkMemoryWrite = 112 # type: Literal[112] DebugLinkFlashErase = 113 # type: Literal[113] +DebugLinkLayout = 9001 # type: Literal[9001] EthereumGetPublicKey = 450 # type: Literal[450] EthereumPublicKey = 451 # type: Literal[451] EthereumGetAddress = 56 # type: Literal[56] diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 85fa35eab..8aa242e52 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -41,6 +41,7 @@ from .CosiSignature import CosiSignature from .DebugLinkDecision import DebugLinkDecision from .DebugLinkFlashErase import DebugLinkFlashErase from .DebugLinkGetState import DebugLinkGetState +from .DebugLinkLayout import DebugLinkLayout from .DebugLinkLog import DebugLinkLog from .DebugLinkMemory import DebugLinkMemory from .DebugLinkMemoryRead import DebugLinkMemoryRead From a1a543f781634042e772dc3da30e809e5b25a8bc Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 27 Sep 2019 15:34:31 +0200 Subject: [PATCH 03/22] core: boot debuglink in recovery homescreen --- core/src/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main.py b/core/src/main.py index bc6efc95e..de5f0eaf5 100644 --- a/core/src/main.py +++ b/core/src/main.py @@ -21,6 +21,8 @@ def _boot_recovery() -> None: # boot applications apps.homescreen.boot(features_only=True) + if __debug__: + apps.debug.boot() from apps.management.recovery_device.homescreen import recovery_homescreen From 8c3d93619eef48a896954d0c67d44cb4db68beb5 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 27 Sep 2019 15:34:52 +0200 Subject: [PATCH 04/22] core: allow inserting synthetic touch events into event loop --- core/src/trezor/loop.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/trezor/loop.py b/core/src/trezor/loop.py index 1bf13db50..82aacdf32 100644 --- a/core/src/trezor/loop.py +++ b/core/src/trezor/loop.py @@ -50,6 +50,9 @@ if __debug__: log_delay_rb_len = const(10) log_delay_rb = array.array("i", [0] * log_delay_rb_len) + # synthetic event queue + synthetic_events = [] # type: List[Tuple[int, Any]] + def schedule( task: Task, value: Any = None, deadline: int = None, finalizer: Finalizer = None @@ -125,6 +128,15 @@ def run() -> None: log_delay_rb[log_delay_pos] = delay log_delay_pos = (log_delay_pos + 1) % log_delay_rb_len + # process synthetic events + if synthetic_events: + iface, event = synthetic_events[0] + msg_tasks = _paused.pop(iface, ()) + if msg_tasks: + synthetic_events.pop(0) + for task in msg_tasks: + _step(task, event) + if io.poll(_paused, msg_entry, delay): # message received, run tasks paused on the interface msg_tasks = _paused.pop(msg_entry[0], ()) From 3664a5f06f836991dceaf88763565788c45a85ac Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 27 Sep 2019 15:36:44 +0200 Subject: [PATCH 05/22] core/debug: reading layouts, inserting synthetic events --- core/src/apps/debug/__init__.py | 30 +++++++++++++++---- .../apps/management/recovery_device/layout.py | 5 ++++ .../apps/management/reset_device/layout.py | 5 ++++ core/src/trezor/ui/__init__.py | 10 +++++++ core/src/trezor/ui/button.py | 7 ++++- core/src/trezor/ui/confirm.py | 17 ++++++++++- core/src/trezor/ui/container.py | 8 +++++ core/src/trezor/ui/scroll.py | 22 ++++++++++++-- core/src/trezor/ui/text.py | 11 +++++++ 9 files changed, 106 insertions(+), 9 deletions(-) diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 98636fcd8..e133337df 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -4,12 +4,13 @@ if not __debug__: halt("debug mode inactive") if __debug__: - from trezor import config, log, loop, utils + from trezor import config, io, log, loop, ui, utils from trezor.messages import MessageType, DebugSwipeDirection + from trezor.messages.DebugLinkLayout import DebugLinkLayout from trezor.wire import register if False: - from typing import Optional + from typing import List, Optional from trezor import wire from trezor.messages.DebugLinkDecision import DebugLinkDecision from trezor.messages.DebugLinkGetState import DebugLinkGetState @@ -28,6 +29,15 @@ if __debug__: debuglink_decision_chan = loop.chan() + layout_change_chan = loop.chan() + current_content = [] # type: List[str] + + def notify_layout_change(layout: ui.Layout) -> None: + global current_content + current_content = layout.read_content() + if layout_change_chan.takers: + layout_change_chan.publish(current_content) + async def debuglink_decision_dispatcher() -> None: from trezor.ui import confirm, swipe @@ -53,12 +63,21 @@ if __debug__: async def dispatch_DebugLinkDecision( ctx: wire.Context, msg: DebugLinkDecision - ) -> None: - + ) -> Optional[DebugLinkLayout]: if debuglink_decision_chan.putters: log.warning(__name__, "DebugLinkDecision queue is not empty") - debuglink_decision_chan.publish(msg) + if msg.x is not None: + evt_down = io.TOUCH_START, msg.x, msg.y + evt_up = io.TOUCH_END, msg.x, msg.y + loop.synthetic_events.append((io.TOUCH, evt_down)) + loop.synthetic_events.append((io.TOUCH, evt_up)) + else: + debuglink_decision_chan.publish(msg) + + if msg.wait: + content = await layout_change_chan.take() + return DebugLinkLayout(lines=content) async def dispatch_DebugLinkGetState( ctx: wire.Context, msg: DebugLinkGetState @@ -72,6 +91,7 @@ if __debug__: m.mnemonic_type = mnemonic.get_type() m.passphrase_protection = has_passphrase() m.reset_entropy = reset_internal_entropy + m.layout_lines = current_content if msg.wait_word_pos: m.reset_word_pos = await reset_word_index.take() diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index 905a74adb..fb216dbc9 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -310,6 +310,11 @@ class RecoveryHomescreen(ui.Component): self.repaint = False + if __debug__: + + def read_content(self): + return [self.__class__.__name__, self.text, self.subtext or ""] + async def homescreen_dialog( ctx: wire.GenericContext, diff --git a/core/src/apps/management/reset_device/layout.py b/core/src/apps/management/reset_device/layout.py index 808f72174..257697017 100644 --- a/core/src/apps/management/reset_device/layout.py +++ b/core/src/apps/management/reset_device/layout.py @@ -643,6 +643,11 @@ class MnemonicWordSelect(ui.Layout): return fn + if __debug__: + + def read_content(self): + return self.text.read_content() + [b.text for b in self.buttons] + async def show_reset_device_warning(ctx, backup_type: BackupType = BackupType.Bip39): text = Text("Create new wallet", ui.ICON_RESET, new_lines=False) diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 6ea7cdb4a..68f680f72 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -5,6 +5,9 @@ from trezorui import Display from trezor import io, loop, res, utils +if __debug__: + from apps.debug import notify_layout_change + if False: from typing import Any, Awaitable, Generator, Tuple, TypeVar @@ -226,6 +229,11 @@ class Component: def on_touch_end(self, x: int, y: int) -> None: pass + if __debug__: + + def read_content(self) -> List[str]: + return [self.__class__.__name__] + class Result(Exception): """ @@ -279,6 +287,8 @@ class Layout(Component): # layout channel. This allows other layouts to cancel us, and the # layout tasks to trigger restart by exiting (new tasks are created # and we continue, because we are in a loop). + if __debug__: + notify_layout_change(self) while True: await loop.race(layout_chan.take(), *self.create_tasks()) except Result as result: diff --git a/core/src/trezor/ui/button.py b/core/src/trezor/ui/button.py index 7fed32e1b..014866573 100644 --- a/core/src/trezor/ui/button.py +++ b/core/src/trezor/ui/button.py @@ -4,7 +4,7 @@ from trezor import ui from trezor.ui import display, in_area if False: - from typing import Type, Union, Optional + from typing import List, Type, Union class ButtonDefault: @@ -239,3 +239,8 @@ class Button(ui.Component): def on_click(self) -> None: pass + + if __debug__: + + def read_content(self) -> List[str]: + return ["".format(self.text)] diff --git a/core/src/trezor/ui/confirm.py b/core/src/trezor/ui/confirm.py index 31bc5b34f..70a50a6f4 100644 --- a/core/src/trezor/ui/confirm.py +++ b/core/src/trezor/ui/confirm.py @@ -8,7 +8,7 @@ if __debug__: from apps.debug import swipe_signal if False: - from typing import Any, Optional, Tuple + from typing import Any, Optional, List, Tuple from trezor.ui.button import ButtonContent, ButtonStyleType from trezor.ui.loader import LoaderStyleType @@ -74,6 +74,11 @@ class Confirm(ui.Layout): def on_cancel(self) -> None: raise ui.Result(CANCELLED) + if __debug__: + + def read_content(self) -> List[str]: + return self.content.read_content() + class Pageable: def __init__(self) -> None: @@ -201,6 +206,11 @@ class InfoConfirm(ui.Layout): def on_info(self) -> None: raise ui.Result(INFO) + if __debug__: + + def read_content(self) -> List[str]: + return self.content.read_content() + class HoldToConfirm(ui.Layout): DEFAULT_CONFIRM = "Hold To Confirm" @@ -250,3 +260,8 @@ class HoldToConfirm(ui.Layout): def on_confirm(self) -> None: raise ui.Result(CONFIRMED) + + if __debug__: + + def read_content(self) -> List[str]: + return self.content.read_content() diff --git a/core/src/trezor/ui/container.py b/core/src/trezor/ui/container.py index 2c97bc861..b660b248b 100644 --- a/core/src/trezor/ui/container.py +++ b/core/src/trezor/ui/container.py @@ -1,5 +1,8 @@ from trezor import ui +if False: + from typing import List + class Container(ui.Component): def __init__(self, *children: ui.Component): @@ -8,3 +11,8 @@ class Container(ui.Component): def dispatch(self, event: int, x: int, y: int) -> None: for child in self.children: child.dispatch(event, x, y) + + if __debug__: + + def read_content(self) -> List[str]: + return sum((c.read_content() for c in self.children), []) diff --git a/core/src/trezor/ui/scroll.py b/core/src/trezor/ui/scroll.py index fe6d3f515..3e968f31e 100644 --- a/core/src/trezor/ui/scroll.py +++ b/core/src/trezor/ui/scroll.py @@ -6,10 +6,10 @@ from trezor.ui.confirm import CANCELLED, CONFIRMED from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe if __debug__: - from apps.debug import swipe_signal + from apps.debug import swipe_signal, notify_layout_change if False: - from typing import Tuple, List + from typing import List, Tuple def render_scrollbar(pages: int, page: int) -> None: @@ -89,6 +89,9 @@ class Paginated(ui.Layout): self.pages[self.page].dispatch(ui.REPAINT, 0, 0) self.repaint = True + if __debug__: + notify_layout_change(self) + self.on_change() def create_tasks(self) -> Tuple[loop.Task, ...]: @@ -98,6 +101,11 @@ class Paginated(ui.Layout): if self.one_by_one: raise ui.Result(self.page) + if __debug__: + + def read_content(self) -> List[str]: + return self.pages[self.page].read_content() + class PageWithButtons(ui.Component): def __init__( @@ -154,6 +162,11 @@ class PageWithButtons(ui.Component): else: self.paginated.on_down() + if __debug__: + + def read_content(self) -> List[str]: + return self.content.read_content() + class PaginatedWithButtons(ui.Layout): def __init__( @@ -191,3 +204,8 @@ class PaginatedWithButtons(ui.Layout): def on_change(self) -> None: if self.one_by_one: raise ui.Result(self.page) + + if __debug__: + + def read_content(self) -> List[str]: + return self.pages[self.page].read_content() diff --git a/core/src/trezor/ui/text.py b/core/src/trezor/ui/text.py index 0d84180f1..9349f49b2 100644 --- a/core/src/trezor/ui/text.py +++ b/core/src/trezor/ui/text.py @@ -171,6 +171,12 @@ class Text(ui.Component): render_text(self.content, self.new_lines, self.max_lines) self.repaint = False + if __debug__: + + def read_content(self) -> List[str]: + lines = [w for w in self.content if isinstance(w, str)] + return [self.header_text] + lines[: self.max_lines] + LABEL_LEFT = const(0) LABEL_CENTER = const(1) @@ -209,6 +215,11 @@ class Label(ui.Component): ) self.repaint = False + if __debug__: + + def read_content(self) -> List[str]: + return [self.content] + def text_center_trim_left( x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16 From 370b2c4c49e4b50bf85b784607ac876212120d51 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 27 Sep 2019 15:38:15 +0200 Subject: [PATCH 06/22] tests: add click-based recovery test --- python/src/trezorlib/debuglink.py | 16 +++++-- tests/buttons.py | 44 ++++++++++++++++++++ tests/click_tests/__init__.py | 0 tests/click_tests/test_recovery.py | 67 ++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 tests/buttons.py create mode 100644 tests/click_tests/__init__.py create mode 100644 tests/click_tests/test_recovery.py diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 300256b98..fb2364247 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -83,16 +83,24 @@ class DebugLink: obj = self._call(proto.DebugLinkGetState()) return obj.passphrase_protection - def input(self, word=None, button=None, swipe=None): + def input(self, word=None, button=None, swipe=None, x=None, y=None, wait=False): if not self.allow_interactions: return - args = sum(a is not None for a in (word, button, swipe)) + args = sum(a is not None for a in (word, button, swipe, x)) if args != 1: raise ValueError("Invalid input - must use one of word, button, swipe") - decision = proto.DebugLinkDecision(yes_no=button, swipe=swipe, input=word) - self._call(decision, nowait=True) + decision = proto.DebugLinkDecision( + yes_no=button, swipe=swipe, input=word, x=x, y=y, wait=wait + ) + ret = self._call(decision, nowait=not wait) + if ret is not None: + return ret.lines + + def click(self, click, wait=False): + x, y = click + return self.input(x=x, y=y, wait=wait) def press_yes(self): self.input(button=True) diff --git a/tests/buttons.py b/tests/buttons.py new file mode 100644 index 000000000..6bdbd9f1c --- /dev/null +++ b/tests/buttons.py @@ -0,0 +1,44 @@ +DISPLAY_WIDTH = 240 +DISPLAY_HEIGHT = 240 + + +def grid(dim, grid_cells, cell): + step = dim // grid_cells + ofs = step // 2 + return cell * step + ofs + + +LEFT = grid(DISPLAY_WIDTH, 3, 0) +MID = grid(DISPLAY_WIDTH, 3, 1) +RIGHT = grid(DISPLAY_WIDTH, 3, 2) + +TOP = grid(DISPLAY_HEIGHT, 4, 0) +BOTTOM = grid(DISPLAY_HEIGHT, 4, 3) + +OK = (RIGHT, BOTTOM) +CANCEL = (LEFT, BOTTOM) +INFO = (MID, BOTTOM) + +CONFIRM_WORD = (MID, TOP) + +MINUS = (LEFT, grid(DISPLAY_HEIGHT, 5, 2)) +PLUS = (RIGHT, grid(DISPLAY_HEIGHT, 5, 2)) + + +BUTTON_LETTERS = ("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz") + + +def grid35(x, y): + return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 5, y) + + +def grid34(x, y): + return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 5, y) + + +def type_word(word): + for l in word: + idx = next(i for i, letters in enumerate(BUTTON_LETTERS) if l in letters) + grid_x = idx % 3 + grid_y = idx // 3 + 1 # first line is empty + yield grid34(grid_x, grid_y) diff --git a/tests/click_tests/__init__.py b/tests/click_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py new file mode 100644 index 000000000..4e8b1b909 --- /dev/null +++ b/tests/click_tests/test_recovery.py @@ -0,0 +1,67 @@ +import pytest + +from trezorlib import device, messages + +from .. import buttons +from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 + + +def enter_word(debug, word): + word = word[:4] + for coords in buttons.type_word(word): + debug.click(coords) + return " ".join(debug.click(buttons.CONFIRM_WORD, wait=True)) + + +def click_ok(debug): + return " ".join(debug.click(buttons.OK, wait=True)) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_recovery(client): + with client: + client.set_expected_responses( + [ + messages.ButtonRequest(code=messages.ButtonRequestType.ProtectCall), + messages.Success(), + messages.Features(), + ] + ) + device.recover(client, pin_protection=False) + + assert client.features.initialized is False + assert client.features.recovery_mode is True + + # select number of words + state = client.debug.state() + text = " ".join(state.layout_lines) + assert "Select number of words" in text + text = click_ok(client.debug) + + assert text == "WordSelector" + # click "20" at 2, 2 + coords = buttons.grid34(2, 2) + lines = client.debug.click(coords, wait=True) + text = " ".join(lines) + + expected_text = "Enter any share (20 words)" + remaining = len(MNEMONIC_SLIP39_BASIC_20_3of6) + for share in MNEMONIC_SLIP39_BASIC_20_3of6: + assert expected_text in text + text = click_ok(client.debug) + + assert text == "Slip39Keyboard" + for word in share.split(" "): + text = enter_word(client.debug, word) + + remaining -= 1 + expected_text = "RecoveryHomescreen {} more".format(remaining) + + assert "You have successfully recovered your wallet" in text + text = click_ok(client.debug) + + assert text == "Homescreen" + client.init_device() + assert client.features.initialized is True + assert client.features.recovery_mode is False From 34823b6fcb298fdc629a2a7106573934e47de4b3 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 30 Sep 2019 12:38:26 +0200 Subject: [PATCH 07/22] legacy: add field options to fix build --- legacy/firmware/protob/messages-debug.options | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/legacy/firmware/protob/messages-debug.options b/legacy/firmware/protob/messages-debug.options index 056133622..c4553f569 100644 --- a/legacy/firmware/protob/messages-debug.options +++ b/legacy/firmware/protob/messages-debug.options @@ -13,3 +13,9 @@ DebugLinkLog.text max_size:256 DebugLinkMemory.memory max_size:1024 DebugLinkMemoryWrite.memory max_size:1024 + +# unused fields +DebugLinkState.layout_lines max_count:0 +DebugLinkState.layout_lines max_size:1 +DebugLinkLayout.lines max_size:1 +DebugLinkLayout.lines max_count:0 From 49931007e7a6931023067c2c8a852a05a4d2f427 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 30 Sep 2019 12:59:20 +0200 Subject: [PATCH 08/22] tests: fix grid34 --- tests/buttons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/buttons.py b/tests/buttons.py index 6bdbd9f1c..fe8169644 100644 --- a/tests/buttons.py +++ b/tests/buttons.py @@ -33,7 +33,7 @@ def grid35(x, y): def grid34(x, y): - return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 5, y) + return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 4, y) def type_word(word): From 97525654bb761e90e9b4e10b30e03057f10ab1c2 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 16 Oct 2019 17:38:00 +0200 Subject: [PATCH 09/22] core/debug: avoid running a handler when waiting for layout change otherwise a running handler would prevent the default task from starting, which would deadlock a test waiting for the default task --- core/src/apps/debug/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index e133337df..b4e1ee9b9 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -61,9 +61,13 @@ if __debug__: loop.schedule(debuglink_decision_dispatcher()) + async def return_layout_change(ctx: wire.Context): + content = await layout_change_chan.take() + await ctx.write(DebugLinkLayout(lines=content)) + async def dispatch_DebugLinkDecision( ctx: wire.Context, msg: DebugLinkDecision - ) -> Optional[DebugLinkLayout]: + ) -> None: if debuglink_decision_chan.putters: log.warning(__name__, "DebugLinkDecision queue is not empty") @@ -76,8 +80,7 @@ if __debug__: debuglink_decision_chan.publish(msg) if msg.wait: - content = await layout_change_chan.take() - return DebugLinkLayout(lines=content) + loop.schedule(return_layout_change(ctx)) async def dispatch_DebugLinkGetState( ctx: wire.Context, msg: DebugLinkGetState From a8fc56901640d116436c162af5761d5125f08eca Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 16 Oct 2019 17:38:48 +0200 Subject: [PATCH 10/22] debug: add support for general layout waiting --- common/protob/messages-debug.proto | 1 + core/src/apps/debug/__init__.py | 6 +++++- core/src/trezor/messages/DebugLinkGetState.py | 3 +++ python/src/trezorlib/debuglink.py | 4 ++++ python/src/trezorlib/messages/DebugLinkGetState.py | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index e3ff1cd9a..8465cb22c 100644 --- a/common/protob/messages-debug.proto +++ b/common/protob/messages-debug.proto @@ -47,6 +47,7 @@ message DebugLinkLayout { message DebugLinkGetState { optional bool wait_word_list = 1; // Trezor T only - wait until mnemonic words are shown optional bool wait_word_pos = 2; // Trezor T only - wait until reset word position is requested + optional bool wait_layout = 3; // wait until current layout changes } /** diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index b4e1ee9b9..e74df7c1e 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -94,7 +94,11 @@ if __debug__: m.mnemonic_type = mnemonic.get_type() m.passphrase_protection = has_passphrase() m.reset_entropy = reset_internal_entropy - m.layout_lines = current_content + + if msg.wait_layout: + m.layout_lines = await layout_change_chan.take() + else: + m.layout_lines = current_content if msg.wait_word_pos: m.reset_word_pos = await reset_word_index.take() diff --git a/core/src/trezor/messages/DebugLinkGetState.py b/core/src/trezor/messages/DebugLinkGetState.py index 74403cb42..0eb5d7278 100644 --- a/core/src/trezor/messages/DebugLinkGetState.py +++ b/core/src/trezor/messages/DebugLinkGetState.py @@ -17,13 +17,16 @@ class DebugLinkGetState(p.MessageType): self, wait_word_list: bool = None, wait_word_pos: bool = None, + wait_layout: bool = None, ) -> None: self.wait_word_list = wait_word_list self.wait_word_pos = wait_word_pos + self.wait_layout = wait_layout @classmethod def get_fields(cls) -> Dict: return { 1: ('wait_word_list', p.BoolType, 0), 2: ('wait_word_pos', p.BoolType, 0), + 3: ('wait_layout', p.BoolType, 0), } diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index fb2364247..ca48682fa 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -46,6 +46,10 @@ class DebugLink: def state(self): return self._call(proto.DebugLinkGetState()) + def wait_layout(self): + obj = self._call(proto.DebugLinkGetState(wait_layout=True)) + return obj.layout_lines + def read_pin(self): state = self.state() return state.pin, state.matrix diff --git a/python/src/trezorlib/messages/DebugLinkGetState.py b/python/src/trezorlib/messages/DebugLinkGetState.py index d84d2ddc6..188493d0e 100644 --- a/python/src/trezorlib/messages/DebugLinkGetState.py +++ b/python/src/trezorlib/messages/DebugLinkGetState.py @@ -17,13 +17,16 @@ class DebugLinkGetState(p.MessageType): self, wait_word_list: bool = None, wait_word_pos: bool = None, + wait_layout: bool = None, ) -> None: self.wait_word_list = wait_word_list self.wait_word_pos = wait_word_pos + self.wait_layout = wait_layout @classmethod def get_fields(cls) -> Dict: return { 1: ('wait_word_list', p.BoolType, 0), 2: ('wait_word_pos', p.BoolType, 0), + 3: ('wait_layout', p.BoolType, 0), } From 3f9b0b7f4fb08c13f6a28b93fa7349c4a0d50929 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 16 Oct 2019 17:39:06 +0200 Subject: [PATCH 11/22] tests: improved click tests with BackgroundDeviceHandler --- tests/background.py | 53 ++++++++++++++++++++++++++++++ tests/click_tests/test_recovery.py | 43 +++++++++++------------- tests/conftest.py | 22 +++++++++++++ 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 tests/background.py diff --git a/tests/background.py b/tests/background.py new file mode 100644 index 000000000..a8a5f3cec --- /dev/null +++ b/tests/background.py @@ -0,0 +1,53 @@ +from concurrent.futures import ThreadPoolExecutor + + +class NullUI: + @staticmethod + def button_request(code): + pass + + @staticmethod + def get_pin(code=None): + raise NotImplementedError("Should not be used with T1") + + @staticmethod + def get_passphrase(): + raise NotImplementedError("Should not be used with T1") + + +class BackgroundDeviceHandler: + _pool = ThreadPoolExecutor() + + def __init__(self, client): + self.client = client + self.client.ui = NullUI + self.task = None + + def run(self, function, *args, **kwargs): + if self.task is not None: + raise RuntimeError("Wait for previous task first") + self.task = self._pool.submit(function, self.client, *args, **kwargs) + + def result(self): + if self.task is None: + raise RuntimeError("No task running") + try: + return self.task.result() + finally: + self.task = None + + def features(self): + if self.task is not None: + raise RuntimeError("Cannot query features while task is running") + self.client.init_device() + return self.client.features + + def debuglink(self): + return self.client.debug + + def check_finalize(self): + if self.task is not None: + self.task.cancel() + self.task = None + return False + return True diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py index 4e8b1b909..ec596f020 100644 --- a/tests/click_tests/test_recovery.py +++ b/tests/click_tests/test_recovery.py @@ -19,49 +19,46 @@ def click_ok(debug): @pytest.mark.skip_t1 @pytest.mark.setup_client(uninitialized=True) -def test_recovery(client): - with client: - client.set_expected_responses( - [ - messages.ButtonRequest(code=messages.ButtonRequestType.ProtectCall), - messages.Success(), - messages.Features(), - ] - ) - device.recover(client, pin_protection=False) - - assert client.features.initialized is False - assert client.features.recovery_mode is True +def test_recovery(device_handler): + features = device_handler.features() + debug = device_handler.debuglink() + + assert features.initialized is False + device_handler.run(device.recover, pin_protection=False) # select number of words - state = client.debug.state() - text = " ".join(state.layout_lines) + text = " ".join(debug.wait_layout()) + assert text.startswith("Recovery mode") + text = click_ok(debug) + assert "Select number of words" in text - text = click_ok(client.debug) + text = click_ok(debug) assert text == "WordSelector" # click "20" at 2, 2 coords = buttons.grid34(2, 2) - lines = client.debug.click(coords, wait=True) + lines = debug.click(coords, wait=True) text = " ".join(lines) expected_text = "Enter any share (20 words)" remaining = len(MNEMONIC_SLIP39_BASIC_20_3of6) for share in MNEMONIC_SLIP39_BASIC_20_3of6: assert expected_text in text - text = click_ok(client.debug) + text = click_ok(debug) assert text == "Slip39Keyboard" for word in share.split(" "): - text = enter_word(client.debug, word) + text = enter_word(debug, word) remaining -= 1 expected_text = "RecoveryHomescreen {} more".format(remaining) assert "You have successfully recovered your wallet" in text - text = click_ok(client.debug) + text = click_ok(debug) assert text == "Homescreen" - client.init_device() - assert client.features.initialized is True - assert client.features.recovery_mode is False + + assert isinstance(device_handler.result(), messages.Success) + features = device_handler.features() + assert features.initialized is True + assert features.recovery_mode is False diff --git a/tests/conftest.py b/tests/conftest.py index faa1ad250..464b35722 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,8 @@ from trezorlib.device import apply_settings, wipe as wipe_device from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST from trezorlib.transport import enumerate_devices, get_transport +from .background import BackgroundDeviceHandler + def get_device(): path = os.environ.get("TREZOR_PATH") @@ -156,3 +158,23 @@ def pytest_runtest_setup(item): skip_altcoins = int(os.environ.get("TREZOR_PYTEST_SKIP_ALTCOINS", 0)) if item.get_closest_marker("altcoin") and skip_altcoins: pytest.skip("Skipping altcoin test") + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + # Make test results available in fixtures. + # See https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures + outcome = yield + rep = outcome.get_result() + setattr(item, f"rep_{rep.when}", rep) + + +@pytest.fixture +def device_handler(client, request): + device_handler = BackgroundDeviceHandler(client) + yield device_handler + + # make sure all background tasks are done + finalized_ok = device_handler.check_finalize() + if request.node.rep_call.passed and not finalized_ok: + raise RuntimeError("Test did not check result of background task") From 598e828844c59a77a4ae2faac6ca522764396130 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:31:13 +0200 Subject: [PATCH 12/22] python: ensure client session counter is never negative --- python/src/trezorlib/client.py | 4 ++-- python/src/trezorlib/transport/protocol.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index 49b399d97..fccddaf08 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -132,9 +132,9 @@ class TrezorClient: self.session_counter += 1 def close(self): - if self.session_counter == 1: + self.session_counter = max(self.session_counter - 1, 0) + if self.session_counter == 0: self.transport.end_session() - self.session_counter -= 1 def cancel(self): self._raw_write(messages.Cancel()) diff --git a/python/src/trezorlib/transport/protocol.py b/python/src/trezorlib/transport/protocol.py index 8bee47374..92afcfb3d 100644 --- a/python/src/trezorlib/transport/protocol.py +++ b/python/src/trezorlib/transport/protocol.py @@ -93,9 +93,9 @@ class Protocol: self.session_counter += 1 def end_session(self) -> None: - if self.session_counter == 1: + self.session_counter = max(self.session_counter - 1, 0) + if self.session_counter == 0: self.handle.close() - self.session_counter -= 1 def read(self) -> protobuf.MessageType: raise NotImplementedError From c970ad437d47bb206c81609260b6651313aa4e2a Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:31:57 +0200 Subject: [PATCH 13/22] tests: improve layout_lines API --- python/src/trezorlib/debuglink.py | 15 +++++++-- tests/click_tests/test_recovery.py | 50 ++++++++++++++++++------------ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index ca48682fa..9a1c847af 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +from collections import namedtuple from copy import deepcopy from mnemonic import Mnemonic @@ -25,6 +26,13 @@ from .tools import expect EXPECTED_RESPONSES_CONTEXT_LINES = 3 +LayoutLines = namedtuple("LayoutLines", "lines text") + + +def layout_lines(lines): + return LayoutLines(lines, " ".join(lines)) + + class DebugLink: def __init__(self, transport, auto_interact=True): self.transport = transport @@ -46,9 +54,12 @@ class DebugLink: def state(self): return self._call(proto.DebugLinkGetState()) + def read_layout(self): + return layout_lines(self.state().layout_lines) + def wait_layout(self): obj = self._call(proto.DebugLinkGetState(wait_layout=True)) - return obj.layout_lines + return layout_lines(obj.layout_lines) def read_pin(self): state = self.state() @@ -100,7 +111,7 @@ class DebugLink: ) ret = self._call(decision, nowait=not wait) if ret is not None: - return ret.lines + return layout_lines(ret.lines) def click(self, click, wait=False): x, y = click diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py index ec596f020..1421fb2b0 100644 --- a/tests/click_tests/test_recovery.py +++ b/tests/click_tests/test_recovery.py @@ -1,3 +1,19 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + import pytest from trezorlib import device, messages @@ -10,11 +26,7 @@ def enter_word(debug, word): word = word[:4] for coords in buttons.type_word(word): debug.click(coords) - return " ".join(debug.click(buttons.CONFIRM_WORD, wait=True)) - - -def click_ok(debug): - return " ".join(debug.click(buttons.OK, wait=True)) + return debug.click(buttons.CONFIRM_WORD, wait=True) @pytest.mark.skip_t1 @@ -27,36 +39,36 @@ def test_recovery(device_handler): device_handler.run(device.recover, pin_protection=False) # select number of words - text = " ".join(debug.wait_layout()) - assert text.startswith("Recovery mode") - text = click_ok(debug) + layout = debug.wait_layout() + assert layout.text.startswith("Recovery mode") + layout = debug.click(buttons.OK, wait=True) - assert "Select number of words" in text - text = click_ok(debug) + assert "Select number of words" in layout.text + layout = debug.click(buttons.OK, wait=True) - assert text == "WordSelector" + assert layout.text == "WordSelector" # click "20" at 2, 2 coords = buttons.grid34(2, 2) lines = debug.click(coords, wait=True) - text = " ".join(lines) + layout = " ".join(lines) expected_text = "Enter any share (20 words)" remaining = len(MNEMONIC_SLIP39_BASIC_20_3of6) for share in MNEMONIC_SLIP39_BASIC_20_3of6: - assert expected_text in text - text = click_ok(debug) + assert expected_text in layout.text + layout = debug.click(buttons.OK, wait=True) - assert text == "Slip39Keyboard" + assert layout.text == "Slip39Keyboard" for word in share.split(" "): - text = enter_word(debug, word) + layout = enter_word(debug, word) remaining -= 1 expected_text = "RecoveryHomescreen {} more".format(remaining) - assert "You have successfully recovered your wallet" in text - text = click_ok(debug) + assert "You have successfully recovered your wallet" in layout.text + layout = debug.click(buttons.OK, wait=True) - assert text == "Homescreen" + assert layout.text == "Homescreen" assert isinstance(device_handler.result(), messages.Success) features = device_handler.features() From 32074c7bffe9a5115fce1488121fba06c1f6f7b7 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:32:10 +0200 Subject: [PATCH 14/22] python/udp: make socket timeout configurable --- python/src/trezorlib/transport/udp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/src/trezorlib/transport/udp.py b/python/src/trezorlib/transport/udp.py index 78326be09..6a04c2421 100644 --- a/python/src/trezorlib/transport/udp.py +++ b/python/src/trezorlib/transport/udp.py @@ -20,6 +20,8 @@ from typing import Iterable, Optional, cast from . import TransportException from .protocol import ProtocolBasedTransport, get_protocol +SOCKET_TIMEOUT = 10 + class UdpTransport(ProtocolBasedTransport): @@ -85,7 +87,7 @@ class UdpTransport(ProtocolBasedTransport): def open(self) -> None: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.connect(self.device) - self.socket.settimeout(10) + self.socket.settimeout(SOCKET_TIMEOUT) def close(self) -> None: if self.socket is not None: From c9f4341949c05fa6e466ca8d51f7e98f370d19eb Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:32:43 +0200 Subject: [PATCH 15/22] tests: make background handler killable --- tests/background.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/background.py b/tests/background.py index a8a5f3cec..4080c498a 100644 --- a/tests/background.py +++ b/tests/background.py @@ -1,5 +1,9 @@ from concurrent.futures import ThreadPoolExecutor +from trezorlib.transport import udp + +udp.SOCKET_TIMEOUT = 0.1 + class NullUI: @staticmethod @@ -28,6 +32,19 @@ class BackgroundDeviceHandler: raise RuntimeError("Wait for previous task first") self.task = self._pool.submit(function, self.client, *args, **kwargs) + def kill_task(self): + if self.task is not None: + # Force close the client, which should raise an exception in a client + # waiting on IO. Does not work over Bridge, because bridge doesn't have + # a close() method. + while self.client.session_counter > 0: + self.client.close() + try: + self.task.result() + except Exception: + pass + self.task = None + def result(self): if self.task is None: raise RuntimeError("No task running") @@ -47,7 +64,13 @@ class BackgroundDeviceHandler: def check_finalize(self): if self.task is not None: - self.task.cancel() - self.task = None + self.kill_task() return False return True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if not self.check_finalize(): + raise RuntimeError("Exit while task is unfinished") From 7591e98897585caac1a02c5c236b2d3f2307de49 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:35:55 +0200 Subject: [PATCH 16/22] tests: rename background.py to device_handler.py --- tests/conftest.py | 4 +++- tests/{background.py => device_handler.py} | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename tests/{background.py => device_handler.py} (100%) diff --git a/tests/conftest.py b/tests/conftest.py index 464b35722..1cf7ca0e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ from trezorlib.device import apply_settings, wipe as wipe_device from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST from trezorlib.transport import enumerate_devices, get_transport -from .background import BackgroundDeviceHandler +from .device_handler import BackgroundDeviceHandler def get_device(): @@ -164,6 +164,8 @@ def pytest_runtest_setup(item): def pytest_runtest_makereport(item, call): # Make test results available in fixtures. # See https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures + # The device_handler fixture uses this as 'request.node.rep_call.passed' attribute, + # in order to raise error only if the test passed. outcome = yield rep = outcome.get_result() setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/background.py b/tests/device_handler.py similarity index 100% rename from tests/background.py rename to tests/device_handler.py From bc1696b947af1a75f0f805a9ceaf4a21d4db8baf Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 18 Oct 2019 13:36:08 +0200 Subject: [PATCH 17/22] tests: add scaffold for Shamir persistence testing --- .../upgrade_tests/test_shamir_persistence.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/upgrade_tests/test_shamir_persistence.py diff --git a/tests/upgrade_tests/test_shamir_persistence.py b/tests/upgrade_tests/test_shamir_persistence.py new file mode 100644 index 000000000..923a6c2cf --- /dev/null +++ b/tests/upgrade_tests/test_shamir_persistence.py @@ -0,0 +1,68 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from trezorlib import device + +from .. import buttons +from ..device_handler import BackgroundDeviceHandler +from ..emulators import EmulatorWrapper + + +def enter_word(debug, word): + word = word[:4] + for coords in buttons.type_word(word): + debug.click(coords) + return debug.click(buttons.CONFIRM_WORD, wait=True) + + +def test_persistence(): + with EmulatorWrapper("core") as emu, BackgroundDeviceHandler( + emu.client + ) as device_handler: + debug = device_handler.debuglink() + features = device_handler.features() + + assert features.recovery_mode is False + + device_handler.run(device.recover, pin_protection=False) + layout = debug.wait_layout() + assert layout.text.startswith("Recovery mode") + + layout = debug.click(buttons.OK, wait=True) + assert "Select number of words" in layout.text + + storage = emu.storage() + device_handler.kill_task() + + with EmulatorWrapper("core", storage=storage) as emu, BackgroundDeviceHandler( + emu.client + ) as device_handler: + debug = device_handler.debuglink() + features = device_handler.features() + + assert features.recovery_mode is True + + # no waiting for layout because layout doesn't change + layout = debug.read_layout() + assert "Select number of words" in layout.text + layout = debug.click(buttons.CANCEL, wait=True) + + assert layout.text.startswith("Abort recovery") + layout = debug.click(buttons.OK, wait=True) + + assert layout.text == "Homescreen" + features = device_handler.features() + assert features.recovery_mode is False From f22f4d920e2b2cca557852ccbc06a4da5dc28de4 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 21 Oct 2019 16:49:34 +0200 Subject: [PATCH 18/22] tests: ensure Shamir persistence test runs only for core --- tests/device_handler.py | 3 +- tests/upgrade_tests/__init__.py | 37 +++++++++++++++++++ tests/upgrade_tests/test_firmware_upgrades.py | 10 ++--- .../upgrade_tests/test_shamir_persistence.py | 2 + 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/tests/device_handler.py b/tests/device_handler.py index 4080c498a..11969841d 100644 --- a/tests/device_handler.py +++ b/tests/device_handler.py @@ -72,5 +72,6 @@ class BackgroundDeviceHandler: return self def __exit__(self, exc_type, exc_value, traceback): - if not self.check_finalize(): + finalized_ok = self.check_finalize() + if exc_type is None and not finalized_ok: raise RuntimeError("Exit while task is unfinished") diff --git a/tests/upgrade_tests/__init__.py b/tests/upgrade_tests/__init__.py index e69de29bb..efeb28409 100644 --- a/tests/upgrade_tests/__init__.py +++ b/tests/upgrade_tests/__init__.py @@ -0,0 +1,37 @@ +import os + +import pytest + +from ..emulators import EmulatorWrapper + +SELECTED_GENS = [ + gen.strip() for gen in os.environ.get("TREZOR_UPGRADE_TEST", "").split(",") if gen +] + +if SELECTED_GENS: + # if any gens were selected via the environment variable, force enable all selected + LEGACY_ENABLED = "legacy" in SELECTED_GENS + CORE_ENABLED = "core" in SELECTED_GENS + +else: + # if no selection was provided, select those for which we have emulators + try: + EmulatorWrapper("legacy") + LEGACY_ENABLED = True + except Exception: + LEGACY_ENABLED = False + + try: + EmulatorWrapper("core") + CORE_ENABLED = True + except Exception: + CORE_ENABLED = False + + +legacy_only = pytest.mark.skipif( + not LEGACY_ENABLED, reason="This test requires legacy emulator" +) + +core_only = pytest.mark.skipif( + not CORE_ENABLED, reason="This test requires core emulator" +) diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py index bea99e290..39f3d078b 100644 --- a/tests/upgrade_tests/test_firmware_upgrades.py +++ b/tests/upgrade_tests/test_firmware_upgrades.py @@ -14,14 +14,13 @@ # You should have received a copy of the License along with this library. # If not, see . -import os - import pytest from trezorlib import MINIMUM_FIRMWARE_VERSION, btc, debuglink, device from trezorlib.tools import H_ from ..emulators import ALL_TAGS, EmulatorWrapper +from . import SELECTED_GENS MINIMUM_FIRMWARE_VERSION["1"] = (1, 0, 0) MINIMUM_FIRMWARE_VERSION["T"] = (2, 0, 0) @@ -41,11 +40,8 @@ def for_all(*args, minimum_version=(1, 0, 0)): if not args: args = ("core", "legacy") - specified_gens = os.environ.get("TREZOR_UPGRADE_TEST") - if specified_gens is not None: - enabled_gens = specified_gens.split(",") - else: - enabled_gens = args + # If any gens were selected, use them. If none, select all. + enabled_gens = SELECTED_GENS or args all_params = [] for gen in args: diff --git a/tests/upgrade_tests/test_shamir_persistence.py b/tests/upgrade_tests/test_shamir_persistence.py index 923a6c2cf..8cfea04e9 100644 --- a/tests/upgrade_tests/test_shamir_persistence.py +++ b/tests/upgrade_tests/test_shamir_persistence.py @@ -19,6 +19,7 @@ from trezorlib import device from .. import buttons from ..device_handler import BackgroundDeviceHandler from ..emulators import EmulatorWrapper +from . import core_only def enter_word(debug, word): @@ -28,6 +29,7 @@ def enter_word(debug, word): return debug.click(buttons.CONFIRM_WORD, wait=True) +@core_only def test_persistence(): with EmulatorWrapper("core") as emu, BackgroundDeviceHandler( emu.client From 8d2ae142f34268171e19c9f17a315c7fbfd2c56a Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 22 Oct 2019 13:51:39 +0200 Subject: [PATCH 19/22] core/debug: at start, wait for first layout to show up --- core/src/apps/debug/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index e74df7c1e..42f2ae05b 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -30,7 +30,7 @@ if __debug__: debuglink_decision_chan = loop.chan() layout_change_chan = loop.chan() - current_content = [] # type: List[str] + current_content = None # type: Optional[List[str]] def notify_layout_change(layout: ui.Layout) -> None: global current_content @@ -95,7 +95,7 @@ if __debug__: m.passphrase_protection = has_passphrase() m.reset_entropy = reset_internal_entropy - if msg.wait_layout: + if msg.wait_layout or current_content is None: m.layout_lines = await layout_change_chan.take() else: m.layout_lines = current_content From 5488270bc33fd1efc21a48fdcd920c6a5d841d91 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 22 Oct 2019 13:54:17 +0200 Subject: [PATCH 20/22] core: improve emulator wrapper * supply a gzipped image of an empty SD card so that we don't spend 5s at every run by formatting it * add start, stop, restart methods for direct control * add restart capability to device_handler (a little awkward for now) --- tests/device_handler.py | 7 ++++ tests/emulators.py | 87 +++++++++++++++++++++++++++++----------- tests/trezor.sdcard.gz | Bin 0 -> 65333 bytes 3 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 tests/trezor.sdcard.gz diff --git a/tests/device_handler.py b/tests/device_handler.py index 11969841d..495fbfcdc 100644 --- a/tests/device_handler.py +++ b/tests/device_handler.py @@ -45,6 +45,13 @@ class BackgroundDeviceHandler: pass self.task = None + def restart(self, emulator): + # TODO handle actual restart as well + self.kill_task() + emulator.restart() + self.client = emulator.client + self.client.ui = NullUI + def result(self): if self.task is None: raise RuntimeError("No task running") diff --git a/tests/emulators.py b/tests/emulators.py index af744ae52..71ee6cf73 100644 --- a/tests/emulators.py +++ b/tests/emulators.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +import gzip import os import subprocess import tempfile @@ -21,15 +22,17 @@ import time from collections import defaultdict from trezorlib.debuglink import TrezorClientDebugLink -from trezorlib.transport import TransportException, get_transport +from trezorlib.transport.udp import UdpTransport -BINDIR = os.path.dirname(os.path.abspath(__file__)) + "/emulators" -ROOT = os.path.dirname(os.path.abspath(__file__)) + "/../" +ROOT = os.path.abspath(os.path.dirname(__file__) + "/..") +BINDIR = ROOT + "/tests/emulators" LOCAL_BUILD_PATHS = { - "core": ROOT + "core/build/unix/micropython", - "legacy": ROOT + "legacy/firmware/trezor.elf", + "core": ROOT + "/core/build/unix/micropython", + "legacy": ROOT + "/legacy/firmware/trezor.elf", } +SD_CARD_GZ = ROOT + "/tests/trezor.sdcard.gz" + ENV = {"SDL_VIDEODRIVER": "dummy"} @@ -84,46 +87,68 @@ class EmulatorWrapper: if storage: open(self._storage_file(), "wb").write(storage) + with gzip.open(SD_CARD_GZ, "rb") as gz: + with open(self.workdir.name + "/trezor.sdcard", "wb") as sd: + sd.write(gz.read()) + self.client = None - def __enter__(self): + def _get_params_core(self): + env = ENV.copy() + args = [self.executable, "-m", "main"] + # for firmware 2.1.2 and newer + env["TREZOR_PROFILE_DIR"] = self.workdir.name + # for firmware 2.1.1 and older + env["TREZOR_PROFILE"] = self.workdir.name + + if self.executable == LOCAL_BUILD_PATHS["core"]: + cwd = ROOT + "/core/src" + else: + cwd = self.workdir.name + + return env, args, cwd + + def _get_params_legacy(self): + env = ENV.copy() args = [self.executable] - env = ENV + cwd = self.workdir.name + return env, args, cwd + + def _get_params(self): if self.gen == "core": - args += ["-m", "main"] - # for firmware 2.1.2 and newer - env["TREZOR_PROFILE_DIR"] = self.workdir.name - # for firmware 2.1.1 and older - env["TREZOR_PROFILE"] = self.workdir.name + return self._get_params_core() + elif self.gen == "legacy": + return self._get_params_legacy() + else: + raise ValueError("Unknown gen") + + def start(self): + env, args, cwd = self._get_params() self.process = subprocess.Popen( - args, cwd=self.workdir.name, env=env, stdout=open(os.devnull, "w") + args, cwd=cwd, env=env, stdout=open(os.devnull, "w") ) + # wait until emulator is listening + transport = UdpTransport("127.0.0.1:21324") + transport.open() for _ in range(300): - try: - time.sleep(0.1) - transport = get_transport("udp:127.0.0.1:21324") + if transport._ping(): break - except TransportException: - pass if self.process.poll() is not None: self._cleanup() raise RuntimeError("Emulator proces died") + time.sleep(0.1) else: # could not connect after 300 attempts * 0.1s = 30s of waiting self._cleanup() raise RuntimeError("Can't connect to emulator") + transport.close() self.client = TrezorClientDebugLink(transport) self.client.open() check_version(self.tag, self.client.version) - return self - def __exit__(self, exc_type, exc_value, traceback): - self._cleanup() - return False - - def _cleanup(self): + def stop(self): if self.client: self.client.close() self.process.terminate() @@ -131,6 +156,20 @@ class EmulatorWrapper: self.process.wait(1) except subprocess.TimeoutExpired: self.process.kill() + + def restart(self): + self.stop() + self.start() + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._cleanup() + + def _cleanup(self): + self.stop() self.workdir.cleanup() def _storage_file(self): diff --git a/tests/trezor.sdcard.gz b/tests/trezor.sdcard.gz new file mode 100644 index 0000000000000000000000000000000000000000..e9c6efc52f0949b9d8faefacfd1004ad875d1cbf GIT binary patch literal 65333 zcmeI&OGp$@6b9f?hZGCbN+B{I8zF=uC^SNFRNRD6q69(qP$9CiOh`0QFgLX-gEmFm za8W{8P*6<|N(ovNSw=9?1A0KEp;$Uox^dUKsqZWY&%?&T#wCcx=>4N45lLa$v9koS)KhEuN`?c)H zm3=Rs%#?qx&ia}5t*RoYDtp`gOE*XFbe!*fbExxaUNq}S#1fA1c@I+c^W z@@UokliU9$5INuUzeSdp009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0Rn%aK%}Rn zIDKxfv9A5WT0?*U0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5)eyFlhdq;#$e z@Gv%*Q4{ZJ>`#CI0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5)uKJQ6Nm@Nb3 z#YQSO^B_Qg009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjZ#USQYawBT$PpnH19 zics9A5r6;z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBo5Eh$c)?E(bGy7mWg z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RnL^kU0@4o$CTTj16Yg#C;qA z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D;k-^2FIPKq$6j Date: Tue, 22 Oct 2019 13:55:17 +0200 Subject: [PATCH 21/22] tests: improve Shamir persistence test --- .../upgrade_tests/test_shamir_persistence.py | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/upgrade_tests/test_shamir_persistence.py b/tests/upgrade_tests/test_shamir_persistence.py index 8cfea04e9..c29d3561f 100644 --- a/tests/upgrade_tests/test_shamir_persistence.py +++ b/tests/upgrade_tests/test_shamir_persistence.py @@ -14,6 +14,8 @@ # You should have received a copy of the License along with this library. # If not, see . +import pytest + from trezorlib import device from .. import buttons @@ -29,42 +31,42 @@ def enter_word(debug, word): return debug.click(buttons.CONFIRM_WORD, wait=True) -@core_only -def test_persistence(): - with EmulatorWrapper("core") as emu, BackgroundDeviceHandler( - emu.client - ) as device_handler: - debug = device_handler.debuglink() - features = device_handler.features() +@pytest.fixture +def emulator(): + emu = EmulatorWrapper("core") + with emu: + yield emu - assert features.recovery_mode is False - device_handler.run(device.recover, pin_protection=False) - layout = debug.wait_layout() - assert layout.text.startswith("Recovery mode") +@core_only +def test_persistence(emulator): + device_handler = BackgroundDeviceHandler(emulator.client) + debug = device_handler.debuglink() + features = device_handler.features() + + assert features.recovery_mode is False - layout = debug.click(buttons.OK, wait=True) - assert "Select number of words" in layout.text + device_handler.run(device.recover, pin_protection=False) + layout = debug.wait_layout() + assert layout.text.startswith("Recovery mode") - storage = emu.storage() - device_handler.kill_task() + layout = debug.click(buttons.OK, wait=True) + assert "Select number of words" in layout.text - with EmulatorWrapper("core", storage=storage) as emu, BackgroundDeviceHandler( - emu.client - ) as device_handler: - debug = device_handler.debuglink() - features = device_handler.features() + device_handler.restart(emulator) + debug = device_handler.debuglink() + features = device_handler.features() - assert features.recovery_mode is True + assert features.recovery_mode is True - # no waiting for layout because layout doesn't change - layout = debug.read_layout() - assert "Select number of words" in layout.text - layout = debug.click(buttons.CANCEL, wait=True) + # no waiting for layout because layout doesn't change + layout = debug.read_layout() + assert "Select number of words" in layout.text + layout = debug.click(buttons.CANCEL, wait=True) - assert layout.text.startswith("Abort recovery") - layout = debug.click(buttons.OK, wait=True) + assert layout.text.startswith("Abort recovery") + layout = debug.click(buttons.OK, wait=True) - assert layout.text == "Homescreen" - features = device_handler.features() - assert features.recovery_mode is False + assert layout.text == "Homescreen" + features = device_handler.features() + assert features.recovery_mode is False From a5ccf952608c5a8f7bf2fb2363a8e96fb11e3630 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 22 Oct 2019 17:01:01 +0200 Subject: [PATCH 22/22] core: fix mypy problems --- core/src/apps/debug/__init__.py | 2 +- core/src/apps/management/recovery_device/layout.py | 2 +- core/src/trezor/ui/__init__.py | 2 +- core/src/trezor/ui/button.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 42f2ae05b..8673210fc 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -61,7 +61,7 @@ if __debug__: loop.schedule(debuglink_decision_dispatcher()) - async def return_layout_change(ctx: wire.Context): + async def return_layout_change(ctx: wire.Context) -> None: content = await layout_change_chan.take() await ctx.write(DebugLinkLayout(lines=content)) diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index fb216dbc9..9c9aceddd 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -312,7 +312,7 @@ class RecoveryHomescreen(ui.Component): if __debug__: - def read_content(self): + def read_content(self) -> List[str]: return [self.__class__.__name__, self.text, self.subtext or ""] diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 68f680f72..e6b1ae990 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -9,7 +9,7 @@ if __debug__: from apps.debug import notify_layout_change if False: - from typing import Any, Awaitable, Generator, Tuple, TypeVar + from typing import Any, Awaitable, Generator, List, Tuple, TypeVar Pos = Tuple[int, int] Area = Tuple[int, int, int, int] diff --git a/core/src/trezor/ui/button.py b/core/src/trezor/ui/button.py index 014866573..9195f60a5 100644 --- a/core/src/trezor/ui/button.py +++ b/core/src/trezor/ui/button.py @@ -4,7 +4,7 @@ from trezor import ui from trezor.ui import display, in_area if False: - from typing import List, Type, Union + from typing import List, Optional, Type, Union class ButtonDefault: