Trezor Firmware documentation

This documentation can also be found at docs.trezor.io where it is available in a HTML-built version compiled using mdBook.

Welcome to the Trezor Firmware repository. This repository is so called monorepo, it contains several different yet very related projects that together form the Trezor Firmware ecosystem.

Repository Structure

  • ci: Gitlab CI configuration files
  • common/defs: JSON coin definitions and support tables
  • common/protob: Common protobuf definitions for the Trezor protocol
  • common/tools: Tools for managing coin definitions and related data
  • core: Trezor Core, firmware implementation for Trezor T
  • crypto: Stand-alone cryptography library used by both Trezor Core and the Trezor One firmware
  • docs: Assorted documentation
  • legacy: Trezor One firmware implementation
  • python: Python client library and the trezorctl command
  • storage: NORCOW storage implementation used by both Trezor Core and the Trezor One firmware
  • tests: Firmware unit test suite
  • tools: Miscellaneous build and helper scripts
  • vendor: Submodules for external dependencies

Contribute

See CONTRIBUTING.md.

Also please have a look at the docs, either in the docs folder or at docs.trezor.io before contributing. The misc chapter should be read in particular because it contains some useful assorted knowledge.

Security vulnerability disclosure

Please report suspected security vulnerabilities in private to [email protected], also see the disclosure section on the Trezor.io website. Please do NOT create publicly viewable issues for suspected security vulnerabilities.

Note on terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Trezor Core

Trezor Core is the second-gen firmware running on Trezor devices. It currently runs on Trezor T and Trezor Safe 3, but it might be used on Trezor One in the future as well (see issue #24).

Trezor Core is part of the trezor-firmware monorepo to be found on GitHub, in the core subdirectory.

Trezor Core uses MicroPython, it is a Python implementation for embedded systems, which allows us to have an application layer in Python, which makes the code significantly more readable and sustainable. This is what you find in the src folder.

Not everything is in Python though, we need to use C occasionally, usually for performance reasons. That is what embed/extmod is for. It extends MicroPython's modules with a number of our owns and serves as a bridge between C and Python codebase. Related to that, mocks contain Python mocks of those functions to improve readability and IDE functioning.

Where appropriate, we also use Rust. For example, all UI components and animations are implemented in embed/rust. Similarly to C bindings, you can find Python mocks for the Rust functions in mocks directory. Developing new features in Rust is preferred in the future.

Boot

Module src/main.py is the first one to be invoked in MicroPython. It starts the USB, initializes the wire codec and boots applications (see Apps).

Build

Building for Trezor Model One? See the legacy documentation.

New Project

Run the following to checkout the project:

git clone --recurse-submodules https://github.com/trezor/trezor-firmware.git
cd trezor-firmware
poetry install
cd core

After this you will need to install some software dependencies based on what flavor of Core you want to build. You can either build the Emulator or the actual firmware running on ARM devices. Emulator (also called unix port) is a unix version that can run on your computer. See Emulator for more information.

Existing Project

If you are building from an existing checkout, do not forget to refresh the submodules and the poetry environment:

git submodule update --init --recursive --force
poetry install --sync

Poetry

We use Poetry to install and track Python dependencies. You need to install it, sync the packages and then use poetry run for every command or enter poetry shell before typing any commands. The commands in this section suppose you are in a poetry shell environment!

sudo pip3 install poetry
poetry install
poetry shell

Build instructions for Embedded (ARM port)

First clone, initialize submodules and install Poetry as defined here. Do not forget you need to be in a poetry shell environment!

Requirements

You will need the GCC ARM toolchain for building and OpenOCD for flashing to a device. You will also need Python dependencies for signing.

Debian/Ubuntu

sudo apt-get install scons gcc-arm-none-eabi libnewlib-arm-none-eabi llvm-dev libclang-dev clang

NixOS

There is a shell.nix file in the root of the project. Just run the following before entering the core directory:

nix-shell

OS X

Consider using Nix. With Nix all you need to do is nix-shell.

For other users:

  1. Download gcc-arm-none-eabi
  2. Follow the install instructions
  3. To install OpenOCD, run brew install open-ocd
  4. Run make vendor build_boardloader build_bootloader build_firmware

Protobuf Compiler

The protocol buffer compiler protoc is needed to (unsurprisingly) compile protocol buffer files. Follow the installation instructions for your system.

Rust

Install the appropriate target with rustup:

rustup target add thumbv7em-none-eabihf  # for TT
rustup target add thumbv7m-none-eabi     # for T1

Building

make vendor build_boardloader build_bootloader build_firmware

Uploading

Use make upload to upload the firmware to a production device.

  • For TT: Do not forget to enter bootloader on the device beforehand.
  • For TS3: You will have to unlock bootloader first. Make sure to read the link in completeness for potentially unwanted effects.

Flashing

For flashing firmware to blank device (without bootloader) use make flash. You need to have OpenOCD installed.

Building in debug mode

You can also build firmware in debug mode to see log output or run tests.

PYOPT=0 make build_firmware

To get a full debug build, use:

make build_firmware BITCOIN_ONLY=0 PYOPT=0

Use screen to enter the device's console. Do not forget to add your user to the dialout group or use sudo. Note that both the group and the tty name can differ, use ls -l /dev/tty* or ls /dev/tty* | grep usb to find out proper names on your machine.

screen /dev/ttyACM0

Build instructions for Emulator (Unix port)

:bulb: Hint: Using emulator as described here is useful during firmware development. If you intend to use the emulator without modifying the firmware, you might be looking for Trezor User Env.

First clone, initialize submodules, install Poetry and enter the Poetry shell as defined here. Do not forget you need to be in a poetry shell environment!

Dependencies

Install the required packages, depending on your operating system.

  • Debian/Ubuntu:
sudo apt-get install scons libsdl2-dev libsdl2-image-dev llvm-dev libclang-dev clang
  • Fedora:
sudo yum install scons SDL2-devel SDL2_image-devel clang-devel
  • OpenSUSE:
sudo zypper install scons libSDL2-devel libSDL2_image-devel
  • Arch:
sudo pacman -S scons sdl2 sdl2_image clang-devel
  • NixOS:

There is a shell.nix file in the root of the project. Just run the following before entering the core directory:

nix-shell
  • Mac OS X:

Consider using Nix. With Nix all you need to do is nix-shell.

For other users:

brew install scons sdl2 sdl2_image pkg-config llvm
  • Windows: not supported yet, sorry.

Protobuf Compiler

The protocol buffer compiler protoc is needed to (unsurprisingly) compile protocol buffer files. Follow the installation instructions for your system.

Rust

You will require Rust and Cargo. The currently supported version is 1.64 nightly. The recommended way to install both is with rustup. Make sure you are up to date:

rustup default nightly
rustup update

The bindgen crate requires libclang for generating MicroPython FFI.

Build

Run the build with:

make build_unix                     # default
make build_unix TREZOR_MODEL=R      # different model

Run

Now you can start the emulator:

./emu.py

The emulator has a number of interesting features all documented in the Emulator section.

Building for debugging and hacking in Emulator (Unix port)

Build the debuggable unix binary so you can attach the gdb or lldb. This removes optimizations and reduces address space randomization. Beware that this will significantly bloat the final binary and the firmware runtime memory limit HEAPSIZE may have to be increased.

make build_unix_debug

Emulator

:bulb: Hint: Using emulator as described here is useful during firmware development. If you intend to use the emulator without modifying the firmware, you might be looking for Trezor User Env.

Emulator is a unix version of Core firmware that runs on your computer.

emulator

There is neither boardloader nor bootloader and no firmware uploads. Emulator runs the current code as is it is and if you want to run some specific firmware version you need to use git for that (simply checkout the right branch/tag). Actually, maybe we should call it simulator to be precise, because it does not emulate the device in its completeness, it just runs the firmware on your host.

Emulator significantly speeds up development and has several features to help you along the way.

⚠️ Disclaimer ⚠️

This emulator is for development purposes only. It uses a pseudo random number generator, and thus no guarantee on its entropy is made. No security or hardening efforts are made here. It is, and will continue to be, intended for development purposes only. Security and hardening efforts are only made available on physical Trezor hardware.

Any other usage of the emulator is discouraged. Doing so runs the risk of losing funds.

How to run

  1. build the emulator
  2. run emu.py inside the poetry environment:
    • either enter poetry shell first, and then use ./emu.py
    • or always use poetry run ./emu.py
  3. start the bridge:
    • to initialise the bridge with emulator support, start it with trezord-go -e 21324
    • alternatively, launch the desktop suite from the command line with the argument --bridge-dev

Now you can use the emulator the same way as you use the device, for example you can use Trezor Suite, use our Python CLI tool (trezorctl), etc. Simply click to emulate screen touches.

Features

Run ./emu.py --help to see all supported command line options and shortcuts. The sections below only list long option names and most notable features.

Debug and production mode

By default the emulator runs in debug mode. Debuglink is available (on port 21325 by default), exceptions and log output goes to console. To indicate debug mode, there is a red square in the upper right corner of Trezor screen.

emulator

To enable production mode, run ./emu.py --production, or set environment variable PYOPT=1.

Initialize with mnemonic words

In debug mode, the emulator can be pre-configured with a mnemonic phrase.

To use a specific mnemonic phrase:

./emu.py --mnemonic "such deposit very security much theme..."

When using Shamir shares, repeat the --mnemonic option:

./emu.py --mnemonic "your first share" --mnemonic "your second share" ...

To use the "all all all" seed defined in SLIP-14:

./emu.py --slip0014
./emu.py -s

Storage and Profiles

Internal Trezor's storage is emulated and stored in the /var/tmp/trezor.flash file by default. Deleting this file is similar to calling wipe device. You can also find /var/tmp/trezor.sdcard for SD card. Starting the emulator with -e / --erase will delete the files beforehand.

You can specify a different location for the storage and log files via the -p / --profile option:

./emu.py -p foobar

This will create a profile directory in your home ~/.trezoremu/foobar containing emulator run files. Alternatively you can set a full path like so:

./emu.py -p /var/tmp/foobar

You can also set a full profile path to TREZOR_PROFILE_DIR environment variable.

Specifying -t / --temporary-profile will start the emulator in a clean temporary profile that will be erased when the emulator stops. This is useful, e.g., for tests.

Logging

By default, emulator output goes to stdout. When silenced with --quiet, it is redirected to ${TREZOR_PROFILE_DIR}/trezor.log. You can specify an alternate output file with --output.

Running subcommands with the emulator

In scripts, it is often necessary to start the emulator, run a command while it is available, and then stop it. The following command runs the device test suite using the emulator:

./emu.py --command pytest ../tests/device_tests

Profiling support

Run ./emu.py --profiling, or set environment variable TREZOR_PROFILING=1, to run the emulator with a profiling wrapper that generates statistics of executed lines.

Memory statistics

Run ./emu.py --log-memory, or set environment variable TREZOR_LOG_MEMORY=1, to dump memory usage information after each workflow task is finished.

Run in gdb

Running ./emu.py --debugger runs emulator inside gdb/lldb.

Watch for file changes

Running ./emu.py --watch watches for file changes and reloads the emulator if any occur. Note that this does not do rebuild, i.e. this works for MicroPython code (which is interpreted) but if you make C changes, you need to rebuild yourself.

Press p on your keyboard to capture emulator's screen. You will find a png screenshot in the src directory.

Disable animation

Run ./emu.py --disable-animation, or set environment variable TREZOR_DISABLE_ANIMATION=1 to disable all animations.

Trezor Core event loop

The event loop is implemented in src/trezor/loop.py and forms the core of the processing. At boot time, default tasks are started and inserted into an event queue. Such task will usually run in an endless loop: wait for event, process event, loop back.

Application code is written with async/await constructs. Low level of the event queue processes running coroutines via coroutine.send() and coroutine.throw() calls.

MicroPython details

MicroPython does not distinguish between coroutines, awaitables, and generators. Some low-level constructs are using yield and yield from constructions.

async def definition marks the function as a generator, even if it does not contain await or yield expressions. It is thus possible to see async def __iter__, which indicates that the function is a generator.

For type-checking purposes, objects usually define an __await__ method that delegates to __iter__. The __await__ method is never executed, however.

Low-level API

Function summary

loop.run() starts the event loop. The call only returns when there are no further waiting tasks -- so, in usual conditions, never.

loop.schedule(task, value, deadline, finalizer, reschedule) schedules an awaitable to be run either as soon as possible, or at a specified time (given as a deadline in microseconds since system bootup.)

In addition, when the task finishes processing or is closed externally, the finalizer callback will be executed, with the task and the return value (or the raised exception) as a parameter.

If reschedule is true, the task is first cleared from the scheduled queue -- in effect, it is rescheduled to run at a different time.

loop.close(task) removes a previously scheduled task from the list of waiting tasks and calls its finalizer.

loop.pause(task, interface) sets the task as waiting for a particular interface: either reading from or writing to one of the USB interfaces, or waiting for a touch event.

Implementation details

Trezor Core runs coroutine-based cooperative multitasking, i.e., there is no preemption.

Every task is a coroutine, which means that it runs uninterrupted until it yields a value (or, in async terms, until it awaits something). In every processing step, the currently selected coroutine is resumed by sending a value to it (which is returned as a result of the yield/await, or raised as an exception if it is an instance of BaseException). The tasks then runs uninterrupted again, until it yields or exits.

A loop in loop.run() spins for as long as any tasks are waiting. Two lists of waiting tasks exist:

  • _queue is a priority queue where the ordering is defined by real-time deadlines. In most cases, tasks are scheduled for "now", which makes them run one after another in FIFO order. It is also possible to schedule a task to run in the future.

  • _paused is a collection of tasks grouped by the interface for which they are waiting.

In each run of the loop, io.poll is called to query I/O events. If an event arrives on an interface, all tasks waiting on that interface are resumed one after another. No scheduled tasks in _queue can execute until the waiting tasks yield again.

At most one I/O event is processsed in this phase.

When the I/O phase is done, a task with the highest priority is popped from _queue and resumed.

I/O wait

When no tasks are paused on a given interface, events on that interface remain in queue.

When multiple tasks are paused on the same interface, all of them receive every event. However, a waiting task receives at most one event. To receive more, it must pause itself again. Event processing is usually done in an endless loop with a pause call.

If two tasks are attempting to read from the same interface, and one of them re-pauses itself immediately while the other doesn't (possibly due to use of loop.race, which introduces scheduling gaps), the other task might lose some events.

For this reason, you should avoid waiting on the same interface from multiple tasks.

Syscalls

Syscalls bridge the gap between await-based application code and the coroutine-based low-level implementation.

Every sequence of awaits will at some point boil down to yielding a Syscall instance. (Yielding anything else is an error.) When that happens, control returns to the event loop.

The handle(task) method is called on the result. This way the syscall gets hold of the task object, and can schedule() or pause() it as appropriate.

As an example, consider pausing on an input event. A running task has no way to call pause() on itself. It would need to pass a separate function as a callback.

The wait syscall can be implemented as a simple wrapper around the pause() low-level call:

class wait(Syscall):
    def __init__(self, msg_iface: int) -> None:
        self.msg_iface = msg_iface

    def handle(self, task: Task) -> None:
        pause(task, self.msg_iface)

The __init__() method takes all the arguments of the "call", and handle() pauses the task on the given interface.

Calling code will look like this:

event = await loop.wait(io.TOUCH)

The loop.wait(io.TOUCH) expression instantiates a new Syscall object. The argument is passed to the constructor, and stored on the instance. The rest boils down to

event = await some_syscall_instance

which is equivalent to

event = yield from some_syscall_instance.__iter__()

The Syscall.__iter__() method yields self, returning control to the event loop. The event loop invokes some_syscall_instance.handle(task_object). The task_object is then set to resume when a touch event arrives.

A side-effect of this design is that it is possible to store and reuse syscall instances. That can be advantageous for avoiding unnecessary allocations.

while True:
    # every run of the loop allocates a new object
    event = await loop.wait(io.TOUCH)
    process_event(event)

touch_source = loop.wait(io.TOUCH)
while True:
    # same instance is reused
    event = await touch_source
    process_event(event)

High-level API

Application code should not be using any of the above low-level functions. Awaiting syscalls is the preferred method of writing code.

The following syscalls and constructs are available:

loop.sleep(delay_ms: int): Suspend execution until the given delay (in milliseconds) elapses. Return value is the planned deadline in milliseconds since system start.

Calling await loop.sleep(0) yields execution to other tasks, and schedules the current task for the next tick.

loop.wait(interface): Wait indefinitely for an event on the given interface. Return value is the event.

Upcoming code modification adds a timeout parameter to loop.wait.

loop.race(*children): Schedule each argument to run, and suspend execution until the first of them finishes.

It is possible to specify wait timeout for loop.wait by using loop.race:

result = await loop.race(loop.wait(io.TOUCH), loop.sleep(1000))

This introduces scheduling gaps: every child is treated as a task and scheduled to run. This means that if the child is a syscall, as in the above example, its action is not done immediately. Instead, the wait begins on the next tick (or whenever the newly created coroutine runs) and the sleep in the tick afterwards. When nesting multiple races, the child races also run later.

Also, when a child task is done, another scheduling gap happens, and the parent task is scheduled to run on the next tick.

Upcoming changes may solve this in relevant cases, by inlining syscall operations.

loop.spawn(task): Start the task asynchronously. Return an object that allows the caller to await its result, or shut the task down.

Example usage:

task = loop.spawn(some_background_task())
await do_something_here()
result = await task

Unlike other syscalls, loop.spawn starts the task at instantiation time. awaiting the same loop.spawn instance a second time will immediately return the result of the original run.

If the task is cancelled (usually by calling task.close()), the awaiter receives a loop.TaskClosed exception.

It is also possible to register a synchronous finalizer callback via task.set_finalizer. This is used internally to implement workflow management.

loop.chan() is a unidirectional communication channel that actually implements two syscalls:

  • chan.put() sends a value to the channel, and waits until it is picked up by a taker task.
  • chan.take() waits until a value is sent to the channel and then returns it.

It is possible to put in a value without waiting for a taker, by calling chan.publish(). It is not possible to take a value without waiting.

Apps

The folder src/apps/ is the place where all the user-facing features are implemented.

Each app must be registered by the register function inside the file workflow_handlers.py. This functions assigns what function should be called if some specific message was received. In other words, it is a link between the MicroPython functions and the Protobuf messages.

Example

For a user facing application you would assign the message to the module in _find_message_handler_module. This binds the message GetAddress to function get_address inside the apps.bitcoin.get_address module.

# in core/src/apps/workflow_handlers.py

# ...

def _find_message_handler_module(msg_type: int) -> str:
    from trezor.enums import MessageType

    # ...

    if msg_type == MessageType.GetAddress:
        return "apps.bitcoin.get_address"

    # ...
# in core/src/apps/bitcoin/get_address.py

# ...

async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Address:
    # ...

Testing

We have two types of tests in Core:

  1. Unit tests that are specific to Trezor Core.
  2. Common tests, which are common to both Trezor Core (Model T, Safe 3) and Legacy (Model one). Device tests belong to this category.

Core unit tests

Unit tests are placed in the core/tests/ directory.

To start them, build unix port and run the following command from core/:

make test                                           # run all unit test
make test TESTOPTS=test_apps.bitcoin.address.py     # run a specific test

Common tests

See the tests section.

Embedded debug of firmware (C and Rust)

Notes on how to get both C and Rust debugging working "nicely".

Building properly

The #1 hassle in embedded debug is proper build because it is very easy to run out of flash space. Size optimizations on the other hand go against comfort or usability of debug.

Therefore it's usually hard to make a single profile or setting, but best way is to start with is probably these build options:

make PYOPT=0 BITCOIN_ONLY=1 V=1 VERBOSE=1 OPTIMIZE=-Og build_firmware

Options mean:

  • PYOPT=0 - enable debuglink and test
  • V=1 VERBOSE=1 - just more of a check to see it's building with options you want
  • BITCOIN_ONLY=1 - most of the time for C/Rust parts you don't need other coins and it saves space on flash to be usable for other than -Os optimization
  • OPTIMIZE=-Og - optimization of C better suited for debug, but it will be larger than default -Os

Micropython has its own optimization setting, so if you need to step through its code as well, set it separately in its build.

Another way to save space in case build overflows flash is changing -fstack-protector-all to -fstack-protector-strong or -fstack-protector-explicit temporarily for debugging in SConscript.firmware.

Debug info is enabled for C and Rust in the flags and profiles (stripped when generating the .bin final image).

Putting it into debugger

Once you have built and flashed the FW, configure debugger for remote debug. General background into remote debug and instructions for basic arm-none-eabi-gdb and VSCode are listed here.

Below are instructions for CLion with Rust plugin.

So far CLion seems the most complete implementation for ARM embedded debug, but these evolve quickly now.

Though all debuggers will have some historic limitations (especially some watch expressions and return values).

Depending on your SWD adapter, either (change speed up to 50000 depending on adapter)

JLinkGDBServerCLExe -select USB -device STM32F427VI -endian little -if SWD -speed 4000 -LocalhostOnly

or with openocd (best to use latest from git)

openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

Set up a debug configuration as remote debug

Default port for "target remote" JLink GDB server is :2331, for openocd :3333

Remote debug settings

It should be also possible to use "Remote GDB Server" setting and let CLion execute openocd or JLink GDB server.

Now you can see variables from both Rust and C, set breakpoints

Remote debug settings

For pointers you can use memory view from variable's context menu.

Known limitations

Rust support is still in progress, so expect bugs sometimes.

Only way so far to get return value of function is to switch to GDB console and use finish GDB command - unless you assign it to variable. GDB may not always show it due to optimizations.

Not all trait info is output into debug info, so you will have issue with watching some expressions like this issue or this one.

Try not to put breakpoints on macro calls, since they may internally expand to too many addresses depending on inlining. This manifests when GDB will complain suddenly you have too many HW breakpoints or when JLink starts using flash breakpoints instead of just HW breakpoints.

Other ideas not thoroughly tested

You can define custom optimization level by choosing the -fxx options for C compiler and similar ones for Rust with llvm-args that target LLVM passes. Note that these change with compiler versions, LLVM 13 has new pass manager.

The point would be to make a optimization level producing somewhat slower code, less inlining, but better debug experience.

Rust does not have equivalent of -Og level, this would be only way to make something similar.

The idea is generally to take an existing optimization level and change/remove some options that affect code size or optimize variables away, force them to stay in memory instead of registers. To look at what is used in passes you can print them out with:

llvm-as < /dev/null | opt -Oz -disable-output -debug-pass=Arguments

The -O0 level often generates too big code to fit in flash which is why this experiment in customizing optimization level exists.

Additional notes on making CLion understand and parse code correctly

Note: Creating a project in CLion doesn't seem necessary for running debug like described above.

CLion remote debugger bindings will gather most information from debug info after connecting to external debugger (JLink or openocd GDB server), but it may be handy for general edit/completion/following definitions and so on.

Since we don't keep a CMakeLists.txt for core because everyone is using different editor/IDE, here is a trick for creating it so that CLion will parse code without having to run the debugger with debug info.

First, clone the repo and build both emulator and embedded code:

  make build_unix
  make build_embed

Now rename Makefile under core to something else, like Makefile.orig. Open the core directory as new project in CLion.

Open any .c file, e.g. embed/firmware/main.c. At this point since CLion does not see Makefile or CMakeLists.txt, it will suggest creating CMakeLists for you based on existing files.

Let it autogenerate one, then add following defines that are taken from build (there are more that should be added, but this suffices for most code including micropython stm32lib):

  add_definitions(
          -DFF_FS_READONLY=0
          -DFF_FS_MINIMIZE=0
          -DFF_USE_STRFUNC=0
          -DFF_USE_FIND=0
          -DFF_USE_FASTSEEK=0
          -DFF_USE_EXPAND=0
          -DFF_USE_CHMOD=0
          -DFF_USE_LABEL=0
          -DFF_USE_FORWARD=0
          -DFF_USE_REPAIR=0
          -DFF_CODE_PAGE=437
          -DFF_USE_LFN=1
          -DFF_LFN_UNICODE=2
          -DFF_STRF_ENCODE=3
          -DFF_FS_RPATH=0
          -DFF_VOLUMES=1
          -DFF_STR_VOLUME_ID=0
          -DFF_MULTI_PARTITION=0
          -DFF_USE_TRIM=0
          -DFF_FS_NOFSINFO=0
          -DFF_FS_TINY=0
          -DFF_FS_EXFAT=0
          -DFF_FS_NORTC=1
          -DFF_FS_LOCK=0
          -DFF_FS_REENTRANT=0
          -DFF_USE_MKFS=1

          -DSTM32_HAL_H=<stm32f4xx.h>

          -DTREZOR_MODEL=T
          -DTREZOR_MODEL_T=1
          -DSTM32F427xx
          -DUSE_HAL_DRIVER
          -DSTM32_HAL_H="<stm32f4xx.h>"
          -DAES_128 -DAES_192
          -DRAND_PLATFORM_INDEPENDENT
          -DUSE_KECCAK=1
          -DUSE_ETHEREUM=1
          -DUSE_MONERO=1
          -DUSE_CARDANO=1
          -DUSE_NEM=1
          -DUSE_EOS=1
          -DSECP256K1_BUILD
          -DUSE_ASM_ARM
          -DUSE_NUM_NONE
          -DUSE_FIELD_INV_BUILTIN
          -DUSE_SCALAR_INV_BUILTIN
          -DUSE_EXTERNAL_ASM
          -DUSE_FIELD_10X26
          -DUSE_SCALAR_8X32
          -DUSE_ECMULT_STATIC_PRECOMPUTATION
          -DUSE_EXTERNAL_DEFAULT_CALLBACKS
          -DECMULT_WINDOW_SIZE=8
          -DENABLE_MODULE_GENERATOR
          -DENABLE_MODULE_RANGEPROOF
          -DENABLE_MODULE_RECOVERY
          -DENABLE_MODULE_ECDH
          -DTREZOR_FONT_BOLD_ENABLE
          -DTREZOR_FONT_NORMAL_ENABLE
          -DTREZOR_FONT_MONO_ENABLE
          -DTREZOR_FONT_MONO_BOLD_ENABLE
  )

  include_directories(vendor/micropython)
  include_directories(build/firmware/genhdr/)
  include_directories(vendor/micropython/lib/stm32lib/STM32L4xx_HAL_Driver/Inc)

Rename the Makefile.orig back to Makefile. This is clumsy, but AFAIK there is no explicit option to autogenerate CMakeLists.txt otherwise.

To make Rust code part of the project, right click embed/rust/Cargo.toml and choose "Attach Cargo Project"

Debugging events with SystemView and Real-time Terminal (Trezor T only)

Systemview is an utility to debug interrupts or other events, counters, logs which does not require any extra pin (except the SWD pins attached to CPU).

An example showing two interrupts, and debug log passed via memory instead of UART.

SystemView

Second example is RTT with color logging (kind of subset what SystemView can do, sans the colors).

Realtime Terminal

Compared to UART the speed of counters or messages is enormously faster as SystemView does trick where it stores all in a small location in RAM (cyclic buffer)

This requires JLink/JTrace adapter and SystemView installed. SystemView is available as free/educational or commercial licensed.

Building with SystemView enabled

Clean build in core:

make clean

Enable SYSTEM_VIEW

Change in SConscript.firmware: PYOPT to 0 (not strictly necessary, but you won't see debug messages otherwise, though this enables to use it also on non-debug build).

Then it's suggested to change OPTIMIZE to -Og instead of -Os which will still does optimizations, but only a subset that does hinder debug by reordering instructions.

PYOPT = ARGUMENTS.get('PYOPT', '0')
COPT=os.getenv('OPTIMIZE', '-Og')

Then enable the SYSTEM_VIEW feature in FEATURE_FLAGS:

FEATURE_FLAGS = {
    "RDI": True,
    "SECP256K1_ZKP": False,
    "SYSTEM_VIEW": True,
}

Then in core/src/trezor/log.py change color to False, SystemView does not support colorful messages (lines will be garbled), but if you want colors you can also use Real-time terminal (RTT, see below)

set color = False

Then build with (change PYOPT or BITCOIN_ONLY as needed):

V=1 VERBOSE=1 PYOPT=0 BITCOIN_ONLY=1 SYSTEM_VIEW=1 make build_embed

After flashing with:

make flash_firmware_jlink

You should be able to conect with SystemView or RTT and collect the data and analyze them.

Sending data to RTT instead of SystemView

There are two mutually exclusive macros, first one is turned on by default and sends data to SystemView. Changing data sending to RTT is just undefining first and defining second (in theory it could be able to send to both destinations, but never tried it.)

  • SYSTEMVIEW_DEST_SYSTEMVIEW
  • SYSTEMVIEW_DEST_RTT

Now when you run JLinkRTTViewerExe you should see data in the terminal:

Realtime Terminal

It is possible to extend this mechanism to include multiple streams/terminals. Terminals work like a usual terminal, so you can use it in debugging also for user input.

Combining SystemView/RTT with other debug tools

In general you can use SystemView along with GDB/CLion/Ozone or other debugger at the same time, it's just advised that you keep all connections at the same frequency, otherwise it may lead to unexpected behavior, weird resets, etc.

So e.g. you can profile the interrupts, DMA in SystemView while also profiling it on instruction-level scale in Ozone:

Ozone ETM trace with code profile

Miscellaneous

Topics that do not fit elsewhere:

Trezor Core Boot Stages

Trezor T initialization is split into two stages. See Memory Layout for info about in which sectors each stage is stored.

First stage (boardloader) is stored in write-protected area, which means it is non-upgradable. Only second stage (bootloader) update is allowed.

First Stage - Boardloader

First stage checks the integrity and signatures of the second stage and runs it if everything is OK.

If first stage boardloader finds a valid second stage bootloader image on the SD card (in raw format, no filesystem), it will replace the internal second stage, allowing a second stage update via SD card.

The boardloader is special in that it is the device's write protected embedded code. The primary purpose for write protecting the boardloader is to make it the immutable portion that can defend against code-based attacks (e.g.- BadUSB) and bugs that would reprogram any/all of the embedded code. It assures that only verified signed embedded code is run on the device (and that the intended code is run, and not skipped). The write protection also provides some defense against attacks where the attacker has physical control of the device.

The boardloader must include an update mechanism for later stage code because if it did not, then a corruption/erasure of later stage flash memory would leave the device unusable (only the boardloader could run and it would not pass execution to a later stage that fails signature validation).

Developer note:

A microSD card can be prepared with the following. Note that the bootloader is allocated 128 KiB.

WARNING: Ensure that you want to overwrite and destroy the contents of /dev/mmcblk0 before running these commands. Likewise, /dev/mmcblk0 may be replaced by your own specific destination.

  1. sudo dd if=/dev/zero of=/dev/mmcblk0 bs=512 count=256 conv=fsync

  2. sudo dd if=build/bootloader/bootloader.bin of=/dev/mmcblk0 bs=512 conv=fsync

Second Stage - Bootloader

Second stage checks the integrity and signatures of the firmware and runs it if everything is OK.

If second stage bootloader detects a pressed finger on the display or there is no firmware loaded in the device, it will start in a firmware update mode, allowing a firmware update via USB.

Common notes

  • Hash function used for computing data digest for signatures is BLAKE2s.
  • Signature system is Ed25519 (allows combining signatures by multiple keys into one).
  • All multibyte integer values are little endian.
  • There is a tool called headertool.py which checks validity of the bootloader/firmware images including their headers.

Bootloader Format

Trezor Core (second stage) bootloader consists of 2 parts:

  1. bootloader header
  2. bootloader code

Bootloader Header

Total length of bootloader header is always 1024 bytes.

offsetlengthnamedescription
0x00004magicfirmware magic TRZB
0x00044hdrlenlength of the bootloader header
0x00084expiryvalid until timestamp (0=infinity)
0x000C4codelenlength of the bootloader code (without the header)
0x00101vmajorversion (major)
0x00111vminorversion (minor)
0x00121vpatchversion (patch)
0x00131vbuildversion (build)
0x00141fix_vmajorversion of last critical bugfix (major)
0x00151fix_vminorversion of last critical bugfix (minor)
0x00161fix_vpatchversion of last critical bugfix (patch)
0x00171fix_vbuildversion of last critical bugfix (build)
0x00188reservednot used yet (zeroed)
0x002032hash1hash of the first code chunk (128 - 1 KiB), this excludes the header
0x004032hash2hash of the second code chunk (128 KiB), zeroed if unused
............
0x020032hash16hash of the last possible code chunk (128 KiB), zeroed if unused
0x0220415reservednot used yet (zeroed)
0x03BF1sigmaskSatoshiLabs signature indexes (bitmap)
0x03C064sigSatoshiLabs aggregated signature of the bootloader header

Firmware Format

Trezor Core firmware consists of 3 parts:

  1. vendor header
  2. firmware header
  3. firmware code

Vendor Header

Total length of vendor header is 84 + 32 * (number of pubkeys) + (length of vendor string rounded up to multiple of 4) + (length of vendor image) bytes rounded up to the closest multiple of 512 bytes.

offsetlengthnamedescription
0x00004magicfirmware magic TRZV
0x00044hdrlenlength of the vendor header (multiple of 512)
0x00084expiryvalid until timestamp (0=infinity)
0x000C1vmajorversion (major)
0x000D1vminorversion (minor)
0x000E1vsig_mnumber of signatures needed to run the firmware from this vendor
0x000F1vsig_nnumber of different pubkeys vendor provides for signing
0x00102vtrustlevel of vendor trust (bitmap)
0x001214reservednot used yet (zeroed)
0x002032vpub1vendor pubkey 1
............
?32vpubnvendor pubkey n
?1vstr_lenvendor string length
??vstrvendor string
??vstrpadpadding to a multiple of 4 bytes
??vimgvendor image (120x120 pixels in TOIf format)
??reservedpadding to an address that is -65 modulo 512 (zeroed)
?1sigmaskSatoshiLabs signature indexes (bitmap)
?64sigSatoshiLabs aggregated signature of the vendor header

Vendor Trust

Vendor trust is stored as bitmap where unset bit means the feature is active.

bithexmeaning
00x0001wait 1 second
10x0002wait 2 seconds
20x0004wait 4 seconds
30x0008wait 8 seconds
40x0010use red background instead of black one
50x0020require user click
60x0040show vendor string (not just the logo)

Firmware Header

Total length of firmware header is always 1024 bytes.

offsetlengthnamedescription
0x00004magicfirmware magic TRZF
0x00044hdrlenlength of the firmware header
0x00084expiryvalid until timestamp (0=infinity)
0x000C4codelenlength of the firmware code (without the header)
0x00101vmajorversion (major)
0x00111vminorversion (minor)
0x00121vpatchversion (patch)
0x00131vbuildversion (build)
0x00141fix_vmajorversion of last critical bugfix (major)
0x00151fix_vminorversion of last critical bugfix (minor)
0x00161fix_vpatchversion of last critical bugfix (patch)
0x00171fix_vbuildversion of last critical bugfix (build)
0x00188reservednot used yet (zeroed)
0x002032hash1hash of the first code chunk excluding both the firmware and the vendor header (128 - 1 - [vendor header length] KiB)
0x004032hash2hash of the second code chunk (128 KiB), zeroed if unused
............
0x020032hash16hash of the last possible code chunk (128 KiB), zeroed if unused
0x0220415reservednot used yet (zeroed)
0x03BF1sigmaskvendor signature indexes (bitmap)
0x03C064sigvendor aggregated signature of the firmware header

Trezor Core coding style

Python coding style

Run make pystyle from repository root to perform all style checks and auto-format where possible.

General style notes

See rules for exceptions in the Exceptions documentation.

Type annotations

We prefer Python 3.10 style annotations:

  • instead of List[int], use list[int], dtto for Tuple, Dict and Set
  • instead of Optional[int], use int | None
  • instead of Union[int, str], use int | str

This also applies inside if TYPE_CHECKING branches.

Type-checking imports

At run-time, the typing module is not available. There is compile-time magic that removes all from typing imports and contents of if TYPE_CHECKING branches.

It is important to put typing-only imports into if TYPE_CHECKING, to make sure that these modules are not needlessly pulled in at run-time.

Due to the compile-time magic, it is always possible to put a from typing import at top level. The style for doing that are as follows:

  • If the module needs to import other modules, create type aliases, TypeVars or Protocols, the only top-level import should be TYPE_CHECKING. Everything else (including other items from typing module) should be imported in the TYPE_CHECKING branch:
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        from typing import Any, TypeVar, Union
        from trezor.messages import SomeMessage
    
        TypeAlias = Union[int, str]
        T = TypeVar("T")
    
  • If the module only needs items from typing, you should not create a TYPE_CHECKING branch, and instead import all required items on top level:
    from typing import Any, Iterator, Sequence
    

Tools

Configurations of specific Python style tools (isort, flake8, pylint) can be found in root setup.cfg.

Formatting

We are auto-formatting code with black and use the black code style.

We use isort to organize imports.

Linting

We use flake8 lints, disabling only those that conflict with black code style.

We use a select subset of pylint checks that are hard-enforced.

Type checking

We use pyright for type-checking. The codebase is fully type-checked, except for the Monero app (as of 2022-01).

C coding style

Formatting is done by clang-format. We are using the Google code style.

Run make cstyle from repository root to auto-format.

Rust coding style

Formatting is done by rustfmt. We are using the Rust style.

Run make ruststyle from repository root to auto-format.

Trezor T Memory Layout

Flash

sectorrangesizefunction
Sector 00x08000000 - 0x08003FFF16 KiBboardloader (1st stage) (write-protected)
Sector 10x08004000 - 0x08007FFF16 KiBboardloader (1st stage) (write-protected)
Sector 20x08008000 - 0x0800BFFF16 KiBboardloader (1st stage) (write-protected)
Sector 30x0800C000 - 0x0800FFFF16 KiBunused
Sector 40x08010000 - 0x0801FFFF64 KiBstorage area #1
Sector 50x08020000 - 0x0803FFFF128 KiBbootloader (2nd stage)
Sector 60x08040000 - 0x0805FFFF128 KiBfirmware
Sector 70x08060000 - 0x0807FFFF128 KiBfirmware
Sector 80x08080000 - 0x0809FFFF128 KiBfirmware
Sector 90x080A0000 - 0x080BFFFF128 KiBfirmware
Sector 100x080C0000 - 0x080DFFFF128 KiBfirmware
Sector 110x080E0000 - 0x080FFFFF128 KiBfirmware
Sector 120x08100000 - 0x08103FFF16 KiBunused
Sector 130x08104000 - 0x08107FFF16 KiBunused
Sector 140x08108000 - 0x0810BFFF16 KiBunused
Sector 150x0810C000 - 0x0810FFFF16 KiBunused
Sector 160x08110000 - 0x0811FFFF64 KiBstorage area #2
Sector 170x08120000 - 0x0813FFFF128 KiBfirmware extra
Sector 180x08140000 - 0x0815FFFF128 KiBfirmware extra
Sector 190x08160000 - 0x0817FFFF128 KiBfirmware extra
Sector 200x08180000 - 0x0819FFFF128 KiBfirmware extra
Sector 210x081A0000 - 0x081BFFFF128 KiBfirmware extra
Sector 220x081C0000 - 0x081DFFFF128 KiBfirmware extra
Sector 230x081E0000 - 0x081FFFFF128 KiBfirmware extra

OTP

blockrangesizefunction
block 00x1FFF7800 - 0x1FFF781F32 Bdevice batch: {MODEL_IDENTIFIER}-YYMMDD
block 10x1FFF7820 - 0x1FFF783F32 Bbootloader downgrade protection
block 20x1FFF7840 - 0x1FFF785F32 Bvendor keys lock
block 30x1FFF7860 - 0x1FFF787F32 Bentropy/randomness
block 40x1FFF7880 - 0x1FFF789F32 Bdevice variant information
block 50x1FFF78A0 - 0x1FFF78BF32 Bunused
block 60x1FFF78C0 - 0x1FFF78DF32 Bunused
block 70x1FFF78E0 - 0x1FFF78FF32 Bunused
block 80x1FFF7900 - 0x1FFF791F32 Bunused
block 90x1FFF7920 - 0x1FFF793F32 Bunused
block 100x1FFF7940 - 0x1FFF795F32 Bunused
block 110x1FFF7960 - 0x1FFF797F32 Bunused
block 120x1FFF7980 - 0x1FFF799F32 Bunused
block 130x1FFF79A0 - 0x1FFF79BF32 Bunused
block 140x1FFF79C0 - 0x1FFF79DF32 Bunused
block 150x1FFF79E0 - 0x1FFF79FF32 Bunused

RAM

regionrangesizefunction
CCMRAM0x10000000 - 0x1000FFFF64 KiBCore Coupled Memory
SRAM10x20000000 - 0x2001BFFF112 KiBGeneral Purpose SRAM
SRAM20x2001C000 - 0x2001FFFF16 KiBGeneral Purpose SRAM
SRAM30x20020000 - 0x2002FFFF64 KiBGeneral Purpose SRAM

Use of SLIP-39 in trezor-core

SLIP-39 describes a way to securely back up a secret value using Shamir's Secret Sharing scheme.

The secret value, called a Master Secret (MS) in SLIP-39 terminology, is first encrypted by a passphrase, producing an Encrypted Master Secret (EMS). The EMS is then split into a number of shares, which are encoded as a set of mnemonic words. Afterwards, it is possible to recombine some or all of the shares to obtain back the EMS, and when the correct passphrase is provided, decrypt the original Master Secret.

This does not quite match Trezor's use of the "passphrase protection" feature, namely that any passphrase is valid, and using any passphrase will yield a working wallet.

SLIP-39 enables this usage by specifying that passphrases are not validated in any way. Decrypting an EMS with any passphrase will produce data usable as the Master Secret, regardless of whether it is the original data or not.

Seed handling in Trezor

Trezor stores a mnemonic secret in a storage field _MNEMONIC_SECRET. This is the input for the root node derivation process: mnemonic.get_seed(passphrase) takes the user-provided passphrase as an argument, and derives the appropriate root node from the mnemonic secret.

With BIP-39, the recovery phrase itself is the mnemonic secret. During device initialization, the raw recovery phrase is given to the user, and also directly stored in the _MNEMONIC_SECRET field. Whenever the root node is required, it is derived by applying PBKDF2 to the mnemonic secret plus passphrase.

For SLIP-39 it is not practical to store the raw data of the recovery shares. During device initialization, a random Encrypted Master Secret is generated and stored as _MNEMONIC_SECRET. SLIP-39 encryption parameters (a random identifier and an iteration exponent) are stored alongside the mnemonic secret in their own storage fields. Whenever the root node is required, it is derived by "decrypting" the stored mnemonic secret with the provided passphrase.

SLIP-39 implementation

The reference implementation of SLIP-39 provides the following high-level API:

  • generate_mnemonics(group parameters, master_secret, passphrase): Encrypt Master Secret with the provided passphrase, and split into a number of shares defined via the group parameters. Implemented using the following:
    • encrypt(master_secret, passphrase, iteration_exponent, identifier): Encrypt the Master Secret with the given passphrase and parameters.
    • split_ems(group parameters, identifier, iteration_exponent, encrypted_master_secret): Split the encrypted secret and encode the metadata into a set of shares defined via the group parameters.
  • combine_mnemonics(set of shares, passphrase): Combine the given set of shares to reconstruct the secret, then decrypt it with the provided passphrase. Implemented using the following:
    • recover_ems(set of shares): Combine the given set of shares to obtain the encrypted master secret, identifier and iteration exponent.
    • decrypt(encrypted_master_secret, passphrase, iteration_exponent, identifier): Decrypt the secret with the given passphrase and parameters, to obtain the original Master Secret.

Only the functions denoted in bold are implemented in trezor-core. Recovery shares are generated with split_ems and combined with recover_ems. Passphrase decryption is done with decrypt. There is never an original "master secret" to be encrypted, so the encrypt function is also omitted.

Step-by-step

Device initialization

This process does not use passphrase.

  1. Generate the required number of random bits (128 or 256), and store as _MNEMONIC_SECRET.
  2. Generate a random identifier and store as _SLIP39_IDENTIFIER.
  3. Store the default iteration exponent 1 as _SLIP39_ITERATION_EXPONENT.
  4. The storage now contains all parameters required for seed derivation.

Seed derivation

This is the only process that uses passphrase.

  1. If passphrase is enabled, prompt user for passphrase. Otherwise use empty string.
  2. Use slip39.decrypt(_MNEMONIC_SECRET, passphrase, _SLIP39_ITERATION_EXPONENT, _SLIP39_IDENTIFIER) to "decrypt" the root node that matches the provided passphrase.

Seed backup

This process does not use passphrase.

  1. Prompt user for group parameters (number of groups, number of shares per group, etc.).
  2. Use slip39.split_ems(group parameters, _SLIP39_IDENTIFIER, _SLIP39_ITERATION_EXPONENT, _MNEMONIC_SECRET) to split the secret into the given number of shares.

Seed recovery

This process does not use passphrase.

  1. Prompt the user to enter enough shares.
  2. Use slip39.recover_ems(shares) to combine the shares and get metadata.
  3. Store the Encrypted Master Secret as _MNEMONIC_SECRET.
  4. Store the identifier as _SLIP39_IDENTIFIER.
  5. Store the iteration exponent as _SLIP39_ITERATION_EXPONENT.
  6. The storage now contains all parameters required for seed derivation.

Exceptions in Core

From version 2.3.0 we try to follow few rules about how we use exceptions. All new code MUST follow these.

Usage

You MAY use any exceptions in Core's logic. Exceptions from wire.errors SHOULD be the final exceptions that are thrown and SHOULD NOT be caught. Note that wire.Error is a type of exception that is intended to be sent out over the wire. It should only be used in contexts where that behavior is appropriate.

Custom exception type hierarchies SHOULD always be derived directly from Exception. They SHOULD NOT be derived from other built-in exceptions (such as ValueError, TypeError, etc.)

Deriving a custom exception type signals an intention to catch and handle it somewhere in the code. For this reason, custom exception types SHOULD NOT be derived from wire.Error and subclasses.

Exception strings, including in internal exceptions, SHOULD only be used in cases where the text is intended to be shown on the host. Exception strings MUST NOT contain any sensitive information. An explanation of an internal exception MAY be placed as a comment on the raise statement, to aid debugging. If an exception is thrown with no arguments, the exception class SHOULD be thrown instead of a new object, i.e., raise CustomError instead of raise CustomError().

Tl;dr

  • Do not use wire.errors for try-catch statements, use other exceptions.
  • Use wire.errors solely as a way to communicate errors to the Host, do not include them somewhere deep in the stack.
  • Do not put sensitive information in exception's message. If you are not sure, do not add any message and provide a comment next to the raise statement.
  • Use raise CustomError instead of raise CustomError() if you are omitting the exception message.

Memory fragmentation management

Trezor-core memory is managed by a mark-and-sweep garbage collector. Throughout the run-time of the firmware, the memory space gets increasingly fragmented as the GC sweep is initiated at arbitrary points.

To combat fragmentation, we attempt to thoroughly clear the memory space after finishing every workflow, and keep only a limited set of modules alive at all times. These must take care to not hold external references.

Always active modules

The following modules are kept loaded at all times:

  • trezor
  • trezor.utils
  • storage
  • storage.common
  • storage.cache
  • storage.device
  • storage.fido2
  • trezor.pin - held alive because the function show_pin_timeout is registered as a callback for trezorconfig and storage unlock operations
  • usb

The above modules are only allowed to import C modules (trezorconfig, trezorutils, trezorcrypto, etc.) or each other. We currently do not have any automation to enforce this, so please be careful when editing them.

Presizing

To save storage, Micropython only preallocates 1 slot in a module dict. Most of our modules use more slots than that. This means that the dict is reallocated, possibly several times. This is inconvenient at most times, but especially undesirable when it would happen to an always-active module at some point at run-time. The allocator would put the newly reallocated dict somewhere in the middle of the GC arena, and it would stay there.

This does happen in practice: e.g., when you import trezor.strings, a new reference strings is inserted into the trezor module.

For this reason, we call utils.presize_module on trezor and storage at first import time. The sizes are determined empirically and it might be necessary to raise them in the future.

The backing storage for sys.modules can also be reallocated at run-time. We configure Micropython to preallocate 160 slots in mpconfigport.h variable MICROPY_LOADED_MODULES_DICT_SIZE. This is asserted at the end of unimport in trezor.utils, so if we ever need more modules than that, the test suite should catch it.

Top-level and function-local imports

In order to keep the imported image size in check, in certain places we avoid importing something at top-level, and instead import it in a function which actually needs the functionality. That way the module can be imported without immediately pulling in all of its possible dependencies.

The following imports trezor.ui at import time - when importing module, trezor.ui is always imported, regardless of whether anyone calls the function draw_foo:

# module.py
import trezor.ui

def draw_foo():
    trezor.ui.display.draw_text("Foo")

The following defers the import until the function is called:

# module.py

def draw_foo():
    import trezor.ui

    trezor.ui.display.draw_text("Foo")

The general rules of thumb are as follows:

C modules can always be imported.

These do not take any space in RAM.

Always-active modules can always be imported.

They are always active, so we do not need to worry about allocating.

In apps.*, we prefer clarity over optimization.

It might still be useful to, e.g., avoid importing trezor.ui.layouts for operations that are sometimes silent, but it is not too important. All of the application code is scrubbed from memory when the workflow exits.

In system modules, we are extra careful.

This means apps.base, apps.common, and everything outside the apps namespace.

A module should only import on top-level if the import is either:

  • C module or an always active module,
  • a module that is expected to already be imported when this module is loaded (this is often the case in apps.common -- e.g., trezor.workflow is not always active, but is presumed active as soon as session is up),
  • small module without further dependencies,
  • something without which the whole module doesn't make sense (this is usually the case with layout code: apps.common.confirm doesn't make sense without importing trezor.ui)

Avoid importing trezor.ui.

The trezor.ui namespace is one of the largest in the codebase, not counting application code. Importing the trezor.ui module alone is not a big problem, but pulling in anything from trezor.ui.layouts or trezor.ui.components usually means loading the full UI machinery. We only want to do that if we are sure that whoever is importing us is going to be drawing things.

Translations

Overview

Trezor stores translated strings in .json files in core/translations directory - e.g. de.json.

When no foreign-language is present, the English version is used - en.json.

Translations files contain the translated strings and also all the special font characters as a link to .json files in fonts directory. Font files are not needed for english, which uses just default/built-in ASCII characters.

Generating blobs

To generate up-to-date blobs, use python core/translations/cli.py gen - they will appear in core/translations as translations-*.bin files. The files contain information about the specific hardware model, language and device version.

Uploading blobs

To upload blobs with foreign-language translations, use trezorctl set language <blob_location> command.

To switch the language back into english, use trezorctl set language -r.

Translations blob format

offsetlengthnamedescriptionhash
0x00006magicblob magic TRTR00
0x00062container_lentotal length (up to padding)
0x00082header_lenheader length
0x000A2header_magicheader magic TR
0x000C8language_tagBCP 47 language tag (e.g. cs-CZ, en-US, ...)header
0x00144version4 bytes of version (major, minor, patch, build)header
0x00182data_lenlength of the raw data, i.e. translations + fontsheader
0x001A32data_hashSHA-256 hash of the dataheader
0x003Aheader_len - 48ignoredreserved for forward compatibilityheader
?2proof_lenlength of merkle proof and signature in bytes
?1proof_countnumber of merkle proof items following
?proof_count * 20proofarray of SHA-256 hashes
?1sig_maskCoSi signature mask
?64siged25519 CoSi signature of merkle root
?2translations_lenlength of the translated stringsdata
?translations_lentranslationstranslated string datadata
?2fonts_lenlength of the font datadata
?fonts_lenfontsfont datadata
??padding0xff bytes padding to flash sector boundary

Translation data

Offsets refer to the strings field, up to the following offset. First offset is always 0, following offset must always be equal or greater (equal denotes empty string).

offsetlengthnamedescription
0x00002countnumber of offsets, excluding the sentinel
0x00022offset[0]offset of string id 0 in the strings field
...2...
?2offset[count - 1]offset of string id count - 1 in the strings field
?2offset[count]offset past the last element
?translations_len - 2 * (count + 2)stringsconcatenation of UTF-8 strings

Fonts

Ids must be in increasing order, offsets must be in non-decreasing order. First offset must be 0.

offsetlengthnamedescription
0x00002countnumber of items in the offset table, excluding the sentinel
0x00022id[0]numeric id of the first font
0x00042offset[0]offset of the first font in the fonts field
.........
??id[count - 1]numeric id of the last font
??offset[count - 1]offset of the last font in the fonts field
??sentinel_idsentinel 0xffff
??sentinel_offsetoffset past the end of last element
?fontsconcatenation of fonts, format defined in the next section
?0-3paddingpadding (any value) for alignment purposes

Font data

The format is exactly the same as the previous table, the only difference is the interpretation of the payload.

offsetlengthnamedescription
0x00002countnumber of items in the offset table, excluding the sentinel
0x00022id[0]id (Unicode code point) of the first glyph
0x00042offset[0]offset of the first glyph in the glyphs field
.........
??id[count - 1]id (Unicode code point) of the last glyph
??offset[count - 1]offset of the last glyph in the glyphs field
??sentinel_idsentinel 0xffff
??sentinel_offsetoffset past the end of last element
?glyphsconcatenation of glyph bitmaps
?0-3paddingpadding (any value) for alignment purposes

Trezor firmware on STM32F429I-DISC1

The STM32F429I-DISC1 evaluation board has similar MCU to Trezor Model T as well as compatible touchscreen.

On the board, mini-USB is used to flash the firmware using the integrated ST-Link, as well as power the board. The micro-USB connector is used by the firmware to communicate with the trezor client. I.e. normally you need both cables connected.

Make sure JP1, JP2, JP3, and CN4 are fitted with jumpers (board is in this state by default).

Building and flashing

Follow the normal build instructions however pass TREZOR_MODEL=DISC1 to make:

# build firmware images
make build_boardloader build_bootloader build_firmware TREZOR_MODEL=DISC1
# use openocd to flash everything through st-link
make flash

Reset board after command finishes.

Trezor One Bootloader and Firmware

Building for Trezor Model T? See the core documentation.

Building with Docker

Ensure that you have Docker installed. You can follow Docker's installation instructions.

Clone this repository, then use build-docker.sh to build all images:

git clone --recurse-submodules https://github.com/trezor/trezor-firmware.git
cd trezor-firmware
./build-docker.sh

When the build is done, you will find the current firmware in build/legacy/firmware/firmware.bin.

Running with sudo

It is possible to run build-docker.sh if either your Docker is configured in rootless mode, or if your user is a member of the docker group; see Docker documentation for details.

If you don't satisfy the above conditions, and run sudo ./build-docker.sh, you might receive a Permission denied error. To work around it, make sure that the directory hierarchy in build/ directory is world-writable - e.g., by running chmod -R a+w build/.

Building older versions

For firmware versions 1.8.1 and newer, you can checkout the respective tag locally. To build firmware 1.8.2, for example, run git checkout legacy/v1.8.2 and then use the instructions below.

Note that the unified Docker build was added after version 1.8.3, so it is not available for older versions.

For firmwares older than 1.8.1, please clone the archived trezor-mcu repository and follow the instructions in its README.

Local development build

Make sure you have Python 3.6 or later and Poetry installed.

If you want to build device firmware, also make sure that you have the GNU ARM Embedded toolchain installed. See Dockerfile for up-to-date version of the toolchain.

The build process is configured via environment variables:

  • EMULATOR=1 specifies that an emulator should be built, instead of the device firmware.
  • DEBUG_LINK=1 specifies that DebugLink should be available in the built image.
  • PRODUCTION=0 disables memory protection. This is necessary for installing unofficial firmware.
  • DEBUG_LOG=1 enables debug messages to be printed on device screen.
  • BITCOIN_ONLY=1 specifies Bitcoin-only version of the firmware.

To run the build process, execute the following commands:

# enter the legacy subdirectory
cd legacy
# set up poetry
poetry install
# set up environment variables. For example, to build emulator with debuglink:
export EMULATOR=1 DEBUG_LINK=1
# clear build artifacts
poetry run ./script/setup
# run build process
poetry run ./script/cibuild

A built device firmware will be located in legacy/firmware/trezor.bin. A built emulator will be located in legacy/firmware/trezor.elf.

Common errors

  • "Exception: bootloader has to be smaller than 32736 bytes": if you didn't modify the bootloader source code, simply make sure you always run ./script/setup before runnning ./script/cibuild

  • "error adding symbols: File in wrong format": This happens when building emulator after building the firmware, or vice-versa. Execute the following command to fix the problem:

    find -L vendor -name "*.o" -delete
    

You can launch the emulator using ./firmware/trezor.elf. To use trezorctl with the emulator, use trezorctl -p udp (for example, trezorctl -p udp get_features).

You can use TREZOR_OLED_SCALE environment variable to make emulator screen bigger.

How to get fingerprint of firmware signed and distributed by SatoshiLabs?

  1. Pick version of firmware binary listed on https://data.trezor.io/firmware/1/releases.json
  2. Download it: wget -O trezor.signed.bin https://data.trezor.io/firmware/1/trezor-1.9.4.bin
  3. Use trezorctl dry-run mode to get the firmware fingerprint:
    trezorctl firmware-update -n -f trezor.signed.bin
    

Step 3 should produce the same fingerprint like your local build (for the same version tag).

How to install custom built firmware?

WARNING: This will erase the recovery seed stored on the device! You should never do this on Trezor that contains coins!

Build with PRODUCTION=0 or you will get a hard fault on your device.

Switch your device to bootloader mode, then execute:

trezorctl firmware-update -f build/legacy/firmware/firmware.bin

Combining bootloader and firmware with various PRODUCTION settings, signed/unsigned

This is an issue before firmware 1.11.2, historical versions need to be built according to this table.

Not all combinations of bootloader and firmware will work. This depends on 3 variables: PRODUCTION of bootloader, PRODUCTION of firmware, whether firmware is signed

This table shows the result for bootloader 1.8.0+ and 1.9.1+:

Bootloader PRODUCTIONFirmware PRODUCTIONIs firmware officially signed?Result
11yesworks, official configuration
11nohardfault in startup.S when setting VTOR and stack
01noworks, but don't forget to comment out check_and_replace_bootloader, otherwise it'll get overwritten
00nohard fault because startup.S doesn't set VTOR and stack right
10noworks

The other three possibilities with signed firmware and PRODUCTION!=0 for bootloader/firmware don't exist.

Parsing existing T1 binary images with ImHex

There is multi-platform hex editor ImHex (another link) that allows for parsing firmware images we distribute such as those from https://github.com/trezor/data/tree/master/firmware

See legacy/imhex/ directory in repo and README.md there how to use it to parse headers from existing images.

Trezor One firmware format

Historically Trezor One has been using 256-byte header (w/ TRZR magic string) followed by the actual firmware. Since version 1.8.0, different 1024-byte header (w/ TRZF magic string) is in use, and building firmware from this repository produces firmware image containing such header followed by firmware code.

Official release firmware contains both these headers for compatibility with old bootloaders. That means there is a 256-byte TRZR header followed by 1024-byte TRZF header followed by code.

  • Hash function used for computing data digest for signatures is SHA256.
  • Signature system is ECDSA over SECP256k1.
  • All multibyte integer values are little endian.

Legacy Header

Total length of legacy header is always 256 bytes.

offsetlengthnamedescription
0x00004magicfirmware magic TRZR
0x00044codelenlength of V2 header + code (length of code before 1.8.0)
0x00081sigindex1index of key for sig1
0x00091sigindex2index of key for sig2
0x000A1sigindex3index of key for sig3
0x000B1flagsunused since 1.8.0 (zeroed)
0x000C52reservednot used yet (zeroed)
0x004064sig1signature #1
0x008064sig2signature #2
0x00C064sig3signature #3

Signature verification:

  • Calculate SHA256 digest of firmware without this header.
  • Verify signature sig1 of the digest against public key with index sigindex1 in V1_BOOTLOADER_KEYS.
  • Repeat for sig2 and sig3. Indexes must be distinct.

V2 Header

This header has the same format as Model T Firmware Header, however due to different signature scheme the sigmask and sig fields are zeroed and part of the reserved space is used for T1-specific fields sig1-sig3, sigindex1-sigindex3. Total length of v2 header is always 1024 bytes.

offsetlengthnamedescription
0x00004magicfirmware magic TRZF
0x00044hdrlenlength of the firmware header
0x00084expiryvalid until timestamp (0=infinity)
0x000C4codelenlength of the firmware code (without the header)
0x00101vmajorversion (major)
0x00111vminorversion (minor)
0x00121vpatchversion (patch)
0x00131vbuildversion (build)
0x00141fix_vmajorversion of last critical bugfix (major)
0x00151fix_vminorversion of last critical bugfix (minor)
0x00161fix_vpatchversion of last critical bugfix (patch)
0x00171fix_vbuildversion of last critical bugfix (build)
0x00188reservednot used yet (zeroed)
0x002032hash1hash of the first code chunk excluding both the legacy and the v2 header (129792 B)
0x004032hash2hash of the second code chunk (128 KiB), zeroed if unused
............
0x020032hash16hash of the last possible code chunk (128 KiB), zeroed if unused
0x022064sig1signature #1
0x026064sig2signature #2
0x02A064sig3signature #3
0x02E01sigindex1index of key for sig1
0x02E11sigindex2index of key for sig2
0x02E21sigindex3index of key for sig3
0x02E3220reservednot used yet (zeroed)
0x03BF1reserved_sigmaskunused in T1 (zeroed)
0x03C064reserved_sigunused in T1 (zeroed)

Signature verification:

  • Calculate SHA256 digest of the entire header with sig1-sig3 and sigindex1-sigindex3 zeroed out.
  • Verify signature sig1 of the digest against public key with index sigindex1 in V1_BOOTLOADER_KEYS.
  • Repeat for sig2 and sig3. Indexes must be distinct.

Python

To be documented by @matejcik, see #229.

In the meantime see python/docs and README for the PyPI package.

trezorlib

repology image

Python library and command-line client for communicating with Trezor Hardware Wallet.

See https://trezor.io for more information.

Install

Python Trezor tools require Python 3.6 or higher, and libusb 1.0. The easiest way to install it is with pip. The rest of this guide assumes you have a working pip; if not, you can refer to this guide.

On a typical system, you already have all you need. Install trezor with:

pip3 install trezor

On Windows, you also need to either install Trezor Bridge, or libusb and the appropriate drivers.

Firmware version requirements

Current trezorlib version supports Trezor One version 1.8.0 and up, and Trezor T version 2.1.0 and up.

For firmware versions below 1.8.0 and 2.1.0 respectively, the only supported operation is "upgrade firmware".

Trezor One with firmware older than 1.7.0 and bootloader older than 1.6.0 (including pre-2021 fresh-out-of-the-box units) will not be recognized, unless you install HIDAPI support (see below).

Installation options

  • Ethereum: To support Ethereum signing from command line, additional packages are needed. Install with:

    pip3 install trezor[ethereum]
    
  • Stellar: To support Stellar signing from command line, additional packages are needed. Install with:

    pip3 install trezor[stellar]
    
  • Firmware-less Trezor One: If you are setting up a brand new Trezor One manufactured before 2021 (with pre-installed bootloader older than 1.6.0), you will need HIDAPI support. On Linux, you will need the following packages (or their equivalents) as prerequisites: python3-dev, cython3, libusb-1.0-0-dev, libudev-dev.

    Install with:

    pip3 install trezor[hidapi]
    

To install all three, use pip3 install trezor[hidapi,ethereum,stellar].

Distro packages

Check out Repology to see if your operating system has an up-to-date python-trezor package.

Installing latest version from GitHub

pip3 install "git+https://github.com/trezor/trezor-firmware#egg=trezor&subdirectory=python"

Running from source

Install the Poetry tool, checkout trezor-firmware from git, and enter the poetry shell:

pip3 install poetry
git clone https://github.com/trezor/trezor-firmware
cd trezor-firmware
poetry install
poetry shell

In this environment, trezorlib and the trezorctl tool is running from the live sources, so your changes are immediately effective.

Command line client (trezorctl)

The included trezorctl python script can perform various tasks such as changing setting in the Trezor, signing transactions, retrieving account info and addresses. See the python/docs/ sub folder for detailed examples and options.

NOTE: An older version of the trezorctl command is available for Debian Stretch (and comes pre-installed on Tails OS).

Python Library

You can use this python library to interact with a Trezor and use its capabilities in your application. See examples here in the tools/ sub folder.

PIN Entering

When you are asked for PIN, you have to enter scrambled PIN. Follow the numbers shown on Trezor display and enter the their positions using the numeric keyboard mapping:

789
456
123

Example: your PIN is 1234 and Trezor is displaying the following:

283
546
791

You have to enter: 3795

Contributing

If you want to change protobuf or coin definitions, you will need to regenerate definitions in the python/ subdirectory.

First, make sure your submodules are up-to-date with:

git submodule update --init --recursive

Then, rebuild the protobuf messages by running, from the trezor-firmware top-level directory:

make gen

To get support for BTC-like coins, these steps are enough and no further changes to the library are necessary.

Trezor Common

Common contains files shared among Trezor projects.

Coin Definitions

JSON coin definitions and support tables.

Ethereum definitions

Description of Ethereum definitions and the process of their generation. See Ethereum definitions.

Protobuf Definitions

Common Protobuf definitions for the Trezor protocol. Also see Communication.

Tools

Tools for managing coin definitions and related data.

Message Workflows

Communication

Note: In this section we describe the internal functioning of the communication protocol. If you wish to implement Trezor support you should use Connect or python-trezor, which will do all this hard work for you.

We use Protobuf v2 for host-device communication. The communication cycle is very simple, Trezor receives a message (request), acts on it and responds with another one (response). Trezor on its own is incapable of initiating the communication.

Definitions

Protobuf messages are defined in the Common project, which is part of this monorepo. This repository is also exported to trezor/trezor-common to be used by third parties, which prefer not to include the whole monorepo. That copy is read-only mirror and all changes are happening in this monorepo.

Notable topics

Sessions

Trezor has limited support for logical sessions. The main purpose is to enable seamless operation with multiple passphrases.

Warning: Session isolation does not exist. Host software is responsible for maintaining isolation. Running multiple host-side applications at the same time is not recommended.

See "Caveats" section below for details.

Support for isolated sessions is in the works, see #79.

Session lifecycle

After Trezor starts up, no session exists. Any attempt to use session data (i.e., the seed) will be rejected with InvalidSession error code.

New session is started by calling Initialize with no arguments. The response is a Features message, which contains a 32-byte session_id. All subsequent commands happen within the given session.

To resume a previous session (after creating a new one), call Initialize with a stored session_id as an argument.

Attempt to resume an unknown session ID will transparently allocate a new session ID.

Since firmwares 1.9.4 / 2.3.4, it is possible to destroy the current session via the EndSession call. The session and all its associated data is wiped from Trezor memory, and it is impossible to resume the session. Trezor returns to the initial state and all requests will return InvalidSession.

There is no explicit way to leave a session and keep its data for later resumption. Instead, you can switch to a new session via Initialize with no arguments.

At the moment, both T1 and TT allow for 10 sessions to exist at the same time. When a new session needs to be allocated and there is no space in the cache, the least recently used session is evicted.

Sessions only exist in RAM and are lost when Trezor is disconnected.

All commands are performed in the context of the current session, until one of the following happens:

  • Host calls EndSession. The current session is destroyed and Trezor returns to the initial state.
  • Host calls Initialize with no arguments, or with an unknown session_id. A new session is allocated and its id returned in the Features message.
  • Host calls Initialize with a known session_id. The specified session is resumed and its session_id is returned in the Features message.
  • Trezor is disconnected.

Caveats

  • Sessions only exist on the protobuf message level. There is no proper isolation. Multiple host applications can insert commands into each other's sessions.

    It is recommended to send Initialize to resume a session immediately before each flow. However, even this does not guarantee that another application doesn't insert its own Initialize in the time it takes you to send the next command.

    The reverse is also true: session management does not prevent other applications from inserting commands under the currently active session (and therefore passphrase), without knowledge of the session ID or the passphrase.

  • It is impossible to run complex flows concurrently. If an application is in the middle of Bitcoin signing, sending Initialize will cancel the signing flow. Resuming the appropriate session later will not continue where it left off.

Examples

Allocate a new session, perform a command, and end the session:

Initialize()
--------->          Features(..., session_id=AAAA)
                       <---------
    ---<now in session AAAA>---
Request
--------->          Response
                       <---------
EndSession()
--------->          Success()
                       <---------
    ---<now in no session>---

Allocate two new sessions, resume the first one later:

Initialize()
--------->          Features(..., session_id=AAAA)
                       <---------
    ---<now in session AAAA>---
Request
--------->          Response
                       <---------

Initialize()
--------->          Features(..., session_id=BBBB)
                       <---------
    ---<now in session BBBB>---
Request
--------->          Response
                       <---------

Initialize(session_id=AAAA)
--------->          Features(..., session_id=AAAA)
                       <---------
    ---<now in session AAAA>---
Request
--------->          Response
                       <---------

Attempt to resume session that is not in the cache:

Initialize()
--------->          Features(..., session_id=AAAA)
                       <---------
    ---<now in session AAAA>---
EndSession()
--------->          Success()
                       <---------
    ---<now in no session>---
Initialize(session_id=AAAA)
--------->          Features(..., session_id=BBBB)
                       <---------
    ---<now in session BBBB>---

Passphrase

As of 1.9.0 / 2.3.0 we have changed how passphrase is communicated between the host and the device. For migration information for existing Hosts communicating with Trezor please see this document.

Passphrase is very tightly coupled with sessions. The reader is encouraged to read on that topic first in the sessions.md section.

Scheme

As soon as Trezor needs the passphrase to do BIP-39/SLIP-39 derivations it prompts the user for passphrase.

GetAddress(...)
--------->          PassphraseRequest()
                       <---------
PassphraseAck
(str passphrase, bool on_device)
--------->          Address(...)
                       <---------

In the default Trezor setting, the passphrase is obtained from the Host. Trezor sends a PassphraseRequest message and awaits PassphraseAck as a response. This message contains field passphrase to transmit it or it has on_device boolean flag to indicate that the user wishes to enter the passphrase on Trezor instead. Setting both passphrase and on_device to true is forbidden.

Note that this has changed as of 2.3.0. In previous firmware versions the on_device flag was in the PassphraseRequest message, since this decision has been made on Trezor. We also had two additional messages PassphraseStateRequest and PassphraseStateAck which were removed.

Example

On an initialized device with passphrase enabled a common communication starts like this:

Initialize()
--------->          Features(..., session_id)
                       <---------
GetAddress(...)
--------->          PassphraseRequest()
                       <---------
PassphraseAck(...)
--------->          Address(...)
                       <---------

The device requested the passphrase since the BIP-39/SLIP-39 seed is not yet cached. After this workflow the seed is cached and the passphrase will therefore never be requested again unless the session is cleared*.

Since we do not have sessions, the Host can not be sure that someone else has not used the device and applied another session id (e.g. changed the Passphrase). To work around this we send the session id again on every subsequent message. See more on that in session.md.

Initialize(session_id)
--------->          Features(..., session_id)
                       <---------
GetPublicKey(...)
--------->          PublicKey(...)
                       <---------

As long as the session_id in Initialize is the same as the one Trezor stores internally, Trezor guarantees the same passphrase is being used.


* There is one exception and that is Cardano. Because Cardano has a different BIP-39/SLIP-39 derivation scheme for passphrase we can not use the cached seed. As a workaround we prompt for the passphrase again in such case and cache the cardano seed in the cardano app directly.

Passphrase always on device

User might want to enforce the passphrase entry on the device every time without the hassle of instructing the Host to do so.

For such cases the user may apply the Passphrase always on device setting. As the name suggests, with this setting the passphrase is prompted on the device right away and no PassphraseRequest/PassphraseAck messages are exchanged. Note that the passphrase is prompted only once for given session id. If the user wishes to enter another passphrase they need to either send Initialize(session_id=None) or replug the device.

Passphrase Redesign In 1.9.0 / 2.3.0

On the T1, passphrase must be entered on the host PC and sent to Trezor. On the TT, the user can choose whether to enter the passphrase on host or on Trezor's touch screen.

In versions 1.9.0 and 2.3.0 we have redesigned how the passphrase is communicated between the Host and the Device. The new design is documented thoroughly in passphrase.md and this document should help with the transition from the old design.

New features

  • Passphrase flow is now identical for T1 and TT.
  • By keeping track of sessions, it is possible to avoid having to send passphrase repeatedly.
  • The choice between entering on Host or Device for TT has been moved from Device to Host.
  • Multiple passphrases are cached simultaneously.

Backwards compatibility

T1 1.9.0 is fully backwards-compatible and works with existing Host code.

TT 2.3.0 communicating with old Host software degrades to T1-level features: entering passphrase on device will not be available, and on-device passphrase caching via the state field will not be available.

As a workaround, it is possible to use the "passphrase always on device" feature on the new TT firmware. When enabled, the passphrase flow is completely hidden from the Host software, and the Device itself prompts the user to enter the passphrase.

Implementation guide

Protobuf changes

Protobuf has built-in backwards compatibility mechanisms, so a conforming implementation should continue to work with old protobuf definitions.

To restore support for TT on-device passphrase entry, and to make use of the new features, you will need to update to newer protobuf definitions from trezor-common (TODO: link to commit in trezor-common).

The gist of the changes is:

  • PassphraseRequest.on_device was deprecated, and renamed to _on_device. New Devices will never send this field.
  • Corresponding field PassphraseAck.on_device was added.
  • PassphraseAck.state was deprecated, and renamed to _state. It is retained for code compatibility, but the field should never be set.
  • PassphraseStateRequest/PassphraseStateAck messages were deprecated, and renamed with a Deprecated_ prefix. New Devices will not send or accept these messages.
  • Initialize.state was renamed to Initialize.session_id.
  • Corresponding field Features.session_id was added. New Devices will always send this field in response to Initialize call.
  • A new value Capability_PassphraseEntry was added to the Features.Capability enum. This capability will be sent from a Device that supports on-device passphrase entry (currently only TT).

Restoring on-device entry for TT

The Host software reacts to a PassphraseRequest message by prompting the user for a passphrase and sending it in the PassphraseAck.passphrase field.

A new UI element should be added: when the passphrase prompt is displayed on Host, there should be an option to "enter passphrase on device". When the user selects this option, the Host must send a PassphraseAck(passphrase=null, on_device=true).

The "enter passphrase on device" option should be displayed when Features.capabilities contain the Capability_PassphraseEntry value, regardless of reported Trezor version or model. Firmwares older than 2.3.0 or 1.9.0 never set this value, so this ensures forwards and backwards compatibility.

Cross-version compatibility for TT

TT version < 2.3.0 will send PassphraseRequest(_on_device=true) if the user selected on-device entry. Neither T1 nor TT >= 2.3.0 will ever set this field to true.

If the Host receives PassphraseRequest(_on_device=true), it should immediately respond with PassphraseAck() with no fields set.

TT version < 2.3.0 will send Deprecated_PassphraseStateRequest(state=[bytes]) after receiving PassphraseAck. The Host should immediately respond with Deprecated_PassphraseStateAck() with no fields set. If the Host does session management, it should store the value of state as the session ID.

Triggering passphrase prompt

Use GetAddress(coin_name="Testnet", address_n=[44'/1'/0'/0/0]) (the first address of the first account of Testnet) to ensure that the Device asks for a passphrase if needed, and caches it for future use.

Validating passphrases

You can store the result of the above call, and in the future, compare it to a newly received address. This is a good way to check if the user is using the same passphrase as last time.

Do not store user-entered passphrases for the purpose of validation, even in hashed, encrypted, or otherwise obfuscated format.

Session support

A call to Initialize can include a session_id field. When starting a new user session, this field should be left empty.

The response Features message will always include a session_id field. The value of this field should be stored. When calling Initialize again, the stored value should be sent as session_id. If the received Features.session_id is the same, it means that session was resumed successfully and the user will not be prompted for passphrase.

--> Initialize()
<-- Features(session_id=0xABCDEF, ...)

--> Initialize(session_id=0xABCDEF)
<-- Features(session_id=0xABCDEF)
# (session resumed successfully)

--> Initialize(session_id=0xABCDEF)
<-- Features(session_id=0x123456)
# (session was not resumed, user will be prompted for passphrase again)

Session support is identical on T1 and TT, and both models support multiple sessions, i.e., it is possible to seamlessly switch between using multiple passphrases.

Cross-version compatible algorithm summary

The following algorithm will ensure that your Host application works properly with both T1 and TT with both older and newer firmwares.

  1. If you have a session ID stored, call Initialize(session_id=stored_session_id)
  2. Check the value of Features.session_id. If it is identical to stored_session_id, the session was resumed and user will not need to be prompted for a passphrase.
    1. If Features.session_id is not set, you are communicating with an older Device. Do not store the null value as session ID.
    2. Otherwise store the value as stored_session_id.
  3. When you receive a PassphraseRequest(_on_device=true), respond with PassphraseAck() with no fields set.
  4. When you receive a PassphraseRequest, prompt the user for passphrase.
    1. If Features.capabilities contains value Capability_PassphraseEntry, display a UI element that allows the user to enter passphrase on-device.
    2. If the user chooses this option, send PassphraseAck(passphrase=null, on_device=true)
    3. If the user enters the passphrase in your application, send PassphraseAck(passphrase="user entered passphrase", on_device=false)
  5. When you receive a Deprecated_PassphraseStateRequest(state=...), store the value of state as stored_session_id, and respond with Deprecated_PassphraseStateAck with no fields set.

Note: up to 64 bytes may be required to store the session ID. Firmwares < 2.3.0 use a 64-byte value, newer firmwares use a 32-byte value.

Bitcoin signing flow

The Bitcoin signing process is one of the most complicated workflows in the Trezor codebase. This is because Trezor cannot store arbitrarily large transactions in memory, so both the input data and the results must be sent in chunks. The Protobuf messages cannot fully encode the data pertaining to a single transaction; instead, the data is spread out over multiple messages.

Overview

The signing flow is initiated by a SignTx command. The message contains the name of the coin, number of inputs and outputs, and transaction metadata: version, lock time, and others that are required for some coins.

In response, Trezor will send a number of TxRequest messages, asking for additional data from the host. The host is supposed to respond with a TxAck providing the requested data.

Trezor can request the following kinds of items:

  • input of the transaction being signed
  • output of the transaction being signed
  • metadata of a previous transaction, i.e., the transaction whose UTXO is being spent
  • metadata of an original transaction, i.e., a transaction that is being replaced by the current transaction
  • input of a previous or original transaction
  • output of a previous or original transaction
  • additional trailing data of a previous transaction

As part of each TxRequest message, Trezor can also send a chunk of the resulting serialized transaction, and/or a signature of one of the inputs.

The flow ends when Trezor sends a TxRequest with request_type of TXFINISHED.

Signing phases

The following content is for reference only, and details might change in the future. Host code should make no assumptions about the order of the phases.

The list of phases here also does not necessarily correspond to internal phase numbering.

Gathering info about current transaction

Trezor will request all inputs and outputs of the transaction to be signed, and set up data structures that allow it to verify that the same data is sent in the following phases.

In this phase, Trezor will also ask the user to confirm destination addresses, the transaction fee, metadata, and the total being sent. If the user confirms, we continue to the next phase.

Validation of input data

Trezor must verify that the host does not lie about input amounts, i.e., that the transaction total in the first phase was calculated correctly.

For this reason, Trezor will ask the host to send each input again. It will then request data about the referenced previous transaction: metadata, all inputs, all outputs, and possible trailing data. This allows Trezor to reconstruct the previous transaction and calculate its hash. If this hash matches the provided one, and the amount on selected UTXO matches the input amount, the given input is considered valid.

If all internal inputs are taproot, then the verification of the previous transactions is skipped. This is possible because if the host provides invalid information about the UTXOs being spent, then the resulting taproot signatures will also be invalid.

Trezor T also supports pre-signed inputs for multi-party signing. If an input has script type EXTERNAL and provides a signature, Trezor will validate the signature against the previous transaction in this step.

Serialization and signing

Trezor will ask once again about every input and begin outputting a serialization of the transaction. For every legacy (non-segwit) input, it is necessary to stream the full set of inputs and outputs again, so that Trezor can compute the transaction digest which it then signs. For segwit inputs this is not necessary.

When all inputs are serialized, Trezor will ask for every output, so that it can serialize it, fill out change addresses, and return the full transaction.

Finally Trezor asks again about segwit inputs to sign them and to serialize the witnesses.

Old versus new data structures

Originally, the TxAck message contained one field of type TransactionType. This, in turn, contained all the possible fields that Trezor could request:

  • TransactionType itself contains fields for all necessary metadata, plus a field extra_data for trailing data.
  • TransactionType.inputs is an array of TxInputType objects, each of which can describe either the current input, or an input of a previous transaction.
  • TransactionType.outputs is an array of TxOutputType objects, each of which can describe an output of the current transaction.
  • TransactionType.bin_outputs is an array of TxOutputBinType objects, each of which can describe an output of a previous transaction.

This organization makes it practical to use the TransactionType for host-side storage: a transaction can be fully stored in one object, and in order to send a TxAck response, you only need to extract the appropriate data.

The cost of this is that this organization makes it extremely unclear which data should be extracted at which points.

To make the constraints more visible, a new set of data types was designed. There is a TxAck<Kind> message for every Kind of data. These only define the fields that are appropriate for that kind of request.

It is possible to use both representations, as they are wire-compatible. However, we recommend using the new definitions for new applications.

Request types

The TxRequest message always contains a request_type field, indicating which kind of data it wants. In addition, request_details specify the particular piece of data requested.

If request_details.tx_hash is set, Trezor is requesting data about a specified previous transaction. If it is unset, Trezor wants data about current transaction.

Transaction input

Trezor sets request_type to TXINPUT, and request_details.tx_hash is unset.

request_details.request_index is the index of the input in the transaction: 0 is the first input, 1 is second, etc.

Old style: Host must respond with a TxAck message. The field tx.inputs must be set to an array of one element, which describes the requested input. All other fields should be left unset.

New style: Host must respond with a TxAckInput message. All relevant data must be set on tx.input.

Normal (internal) inputs

Usually, the user owns, and wants to sign, all inputs of the transaction. For that, the host must specify a derivation path for the key, and script type SPENDADDRESS (legacy), SPENDP2SHWITNESS (P2SH segwit), SPENDWITNESS (native segwit) or SPENDTAPROOT.

Multisig inputs

For multisig inputs, the XPUBs of all signers (including the current user) must be provided in the multisig structure. Legacy multisig uses type SPENDMULTISIG, P2SH segwit and native segwit multisig use the same type as non-multisig inputs, i.e. SPENDP2SHWITNESS or SPENDWITNESS.

Full documentation for multisig is TBD.

External inputs

Trezor can include inputs that it will not sign, typically because they are owned by another party. Such inputs are of type EXTERNAL and the host does not specify a derivation path for the key, but sets the script_pubkey field to the scriptPubKey of the previous output that is being spent by the external input. An external input can either be verified or unverified. The amounts supplied to the transaction by verified external inputs are subtracted from the transaction total that the user is asked to confirm, whereas unverified external inputs are not subtracted.

Verified external inputs are only supported on the Trezor T. They must either already have a valid signature or they must come with an ownership proof. If the input already has a valid signature, then the host provides the script_sig and/or witness fields. If the other signing party hasn't signed their input yet (i.e., with two Trezors, one must sign first so that the other can include a pre-signed input), they can instead provide a SLIP-19 ownership proof in the ownership_proof field, with optional commitment data in commitment_data.

Unverified external inputs are accepted by Trezor only if safety checks are disabled on the device.

Transaction output

Trezor sets request_type to OUTPUT, and request_details.tx_hash is unset.

request_details.request_index is the index of the output in the transaction: 0 is the first input, 1 is second, etc.

Old style: Host must respond with a TxAck message. The field tx.outputs must be set to an array of one element, which describes the requested output. All other fields should be left unset.

New style: Host must respond with a TxAckOutput message. All relevant data must be set on tx.output.

External outputs

Outputs that send coins to a particular address are always of type PAYTOADDRESS. The address is sent as a string in the field address.

Change outputs

Outputs that send coins back to the same owner must specify a derivation path and the appropriate script type. If the derivation path has the same prefix as all inputs, and a matching script type (legacy, p2sh segwit, native segwit), it is considered to be a change output, and its amount is subtracted from the total.

address must not be specified in this case. It is instead derived internally from the provided derivation path.

OP_RETURN outputs

Outputs of type PAYTOOPRETURN must not specify address nor address_n, and the amount must be zero. The OP_RETURN data is sent as op_return_data field.

Previous transaction metadata

Trezor sets request_type to TXMETA. request_details.tx_hash is a transaction hash, matching one of the current transaction inputs.

Old style: Host must respond with a TxAck message. The structure tx must be filled out with relevant data, in particular, inputs_cnt and outputs_cnt must be set to the number of transaction inputs and outputs. Arrays inputs, outputs, bin_outputs and extra_data should be empty.

New style: Host must respond with a TxAckPrevMeta message. All relevant data must be set on tx.

Extra data

Some coins (e.g., Zcash) contain data at the end of transaction serialization that Trezor does not understand. The host must indicate the length of this extra data in the field extra_data_len.

To figure out which is the extra data, the host must parse the serialized previous transaction up to the last field understood by Trezor. In case of Zcash, that is:

  • version + version group ID
  • number of inputs, and every input
  • number of outputs, and every output
  • lock time
  • expiry

All data after the expiry field is considered "extra data".

Previous transaction input

Trezor sets request_type to TXINPUT. request_details.tx_hash is a transaction hash, matching one of the current transaction inputs.

Old style: Host must respond with a TxAck message. The field tx.inputs must be set to an array of one element, which describes the requested input of the specified previous transaction. All other fields should be left unset.

New style: Host must respond with a TxAckPrevInput message. All relevant data must be set on tx.input.

Previous transaction output

Trezor sets request_type to TXOUTPUT. request_details.tx_hash is a transaction hash, matching one of the current transaction inputs.

Old style: Host must respond with a TxAck message. The field tx.bin_outputs must be set to an array of one element, which describes the requested output of the specified previous transaction. All other fields should be left unset.

New style: Host must respond with a TxAckPrevOutput message. All relevant data must be set on tx.output.

Previous transaction trailing data

On some coins, such as Zcash, the transaction serialization can contain data not understood by Trezor. This data is not relevant for validation, but it must be included so that Trezor can correctly compute the previous transaction hash.

Trezor sets request_type to TXEXTRADATA. request_details.tx_hash is a transaction hash, matching one of the current transaction inputs.

request_details.extra_data_offset specifies the offset of the requested data from the start of the extra data. request_details.extra_data_length specifies the length of the requested chunk.

Old style: Host must respond with a TxAck message. The field tx.extra_data must contain the specified chunk, starting at the given offset and of exactly the given length. All other fields should be unset.

New style: Host must respond with a TxAckPrevExtraData message. The chunk must be set to tx.extra_data_chunk.

Original transaction input

Trezor sets request_type to TXORIGINPUT. request_details.tx_hash is the transaction hash of the original transaction.

The host must respond with a TxAckInput message. All relevant data must be set in tx.input. The derivation path and script_type are mandatory for all original internal inputs. All original internal inputs must also be accompanied with full transaction signature data in the script_sig and/or witness fields.

Original transaction output

Trezor sets request_type to TXORIGOUTPUT. request_details.tx_hash is the transaction hash of the original transaction.

Host must respond with a TxAckOutput message. All relevant data must be set in tx.output. The derivation path and script type are mandatory for all original change-outputs.

Payment request

Trezor sets request_type to TXPAYMENTREQ, and request_details.tx_hash is unset. request_details.request_index is the index of the payment request in the transaction: 0 is the first payment request, 1 is second, etc.

The host must respond with a TxAckPaymentRequest message.

Replacement transactions

A replacement transaction is a transaction that uses the same inputs as one or more transactions which have already been signed (the original transactions). Replacement transactions can be used to securely bump the fee of an already signed transaction (BIP-125) or to participate as a sender in PayJoin (BIP-78). Trezor only supports signing of replacement transaction which do not increase the amount that the user is spending on external outputs. Thus when signing a replacement transaction the user only needs to confirm the fee modification and the original TXIDs without being shown any outputs, since the original external outputs must have already been confirmed by the user and any new external outputs can only be paid for by new external inputs.

The host signals that a transaction is a replacement transaction by setting the orig_hash and orig_index fields for at least one TxInput. Trezor will then automatically request metadata about the original transaction and verify the original signatures.

A replacement transaction in Trezor must satisfy the following requirements:

  • All inputs of the original transactions must be inputs of the replacement transaction.
  • All external outputs of the original transactions must be outputs of the replacement transaction.
  • The value of an external output may be decreased only if there are no new external inputs. This should only be used to bump the fee if the original transaction transfers the entire account balance and there is no other source available to bump the fee.
  • The replacement transaction must not increase the amount that the user is spending on external outputs.
  • Original transactions must have the same effective nLockTime as the replacement transaction.
  • The inputs and outputs of the original transactions must not be permuted in the replacement transaction, but they can be interleaved with new inputs or with inputs from another original transaction.
  • New OP_RETURN outputs cannot be added in the replacement transaction.

So the replacement transaction is, for example, allowed to:

  • Increase the user's contribution to the mining fee by adding new inputs or decreasing or removing change outputs.
  • Decrease the user's contribution to the mining fee by increasing or adding change-outputs.
  • Add external inputs (PayJoin) and use them to introduce new outputs, increase the original external outputs or even to increase the user's change outputs so as to decrease the amount that the user is spending.

Payment requests

In Trezor T a set of transaction outputs can be accompanied by a payment request. Multiple payment requests per transaction are also possible. A payment request is a message signed by a trusted party requesting payment of certain amounts to a set of outputs as specified in SLIP-24. The user then does not have to verify the output addresses, but only confirms the payment of the requested amount to the recipient.

The host signals that an output belongs to a payment request by setting the payment_req_index field in the TxOutput message. When Trezor encounters the first output that has this field set to a particular index, it will ask for the payment request that has the given index.

All outputs belonging to one payment request must be consecutive in the transaction.

Implementation notes

Pseudo-code

The following is a rough outline of host-side implementation. See above for detailed info.

transaction_bytes = ""
signatures = [""] * len(INPUTS)

def sign_tx():
    # send initial message
    send_message(
        SignTx(
            coin_name,
            inputs_count=len(INPUTS),
            outputs_count=len(OUTPUTS),
            # ...fill individual metadata fields
        )
    )

    # wait for TxAck forever, until Trezor indicates we are finished
    while True:
        msg = receive_message()

        # extract data first
        extract_streamed_data(msg.serialized)

        if msg.request_type == TXFINISHED:
            # we are done
            break

        if msg.details.tx_hash is not None:
            # Trezor requires data about some previous transaction
            send_response_prev(msg.request_type, msg.details)
        else:
            # Trezor requires data about this transaction
            send_response_current(msg.request_type, msg.details)

def extract_streamed_data(ser: TxRequestSerializedType):
    global transaction_bytes, signatures
    # append serialized data to what we got so far
    transaction_bytes += ser.serialized_tx
    if ser.signature_index is not None:
        # read the signature
        signatures[ser.signature_index] = ser.signature

def send_response_prev(request_type: RequestType, details: TxRequestDetailsType):
    prev_tx = get_prev_tx(details.tx_hash)
    if request_type == TXINPUT:
        send_prev_input(prev_tx.inputs[details.request_index])
    elif request_type == TXOUTPUT:
        send_prev_output(prev_tx.outputs[details.request_index])
    elif request_type == TXMETA:
        send_prev_metadata(prev_tx)
    elif request_type == TXEXTRADATA:
        offset = details.extra_data_offset
        length = details.extra_data_length
        extra_data_chunk = prev_tx.extra_data[offset : offset + length]
        send_prev_extra_data(extra_data_chunk)

def send_response_current(request_type: RequestType, details: TxRequestDetailsType):
    if request_type == TXINPUT:
        send_input(INPUTS[details.request_index])
    elif request_type == TXOUTPUT:
        send_output(OUTPUTS[details.request_index])

Wire compatibility

The new definitions are structured so that the Protobuf binary encoded form can be decoded into both representations. This means that the host can encode data in the old representation, and Trezor will successfully and correctly decode it into the new one.

This is done by reusing field IDs as appropriate, and taking advantage of the fact that Protobuf encodes arrays as a sequence of the same field repeated a number of times.

For example, here is a part of the TxAck definition:

message TxAck {
    optional TransactionType tx = 1;

    message TransactionType {
        // ... some fields omitted ...
        repeated TxInputType inputs = 2;
        // ... some fields omitted ...

        message TxInputType {
            repeated uint32 address_n = 1;
            // ... some fields omitted ...
            optional uint64 amount = 8;
            // ... some fields omitted ...
        }
    }
}

A message carrying these fields would look like this:

FIELD 1 (type NESTED):
    FIELD 2 (type NESTED):
        FIELD 1 (type int): 0x8000002c
        FIELD 1 (type int): 0x80000000
        FIELD 1 (type int): 0x80000000
        FIELD 1 (type int): 0
        FIELD 1 (type int): 0
        FIELD 8 (type int): 1234567

We can see that this is identical as if the type definition looked as follows; indeed, we only renamed the types, removed some fields, and set some optional or repeated to required instead.

message TxAckInput {
    required TxAckInputWrapper tx = 1;

    message TxAckInputWrapper {
        required TxInput input = 2; // the field is now required instead of repeated

        message TxInput {
            repeated uint32 address_n = 1;
            required uint64 amount = 8;
        }
    }
}

A caveat of this approach is that this introduces invisible dependencies: TxInput and PrevInput fold into the same old-style TxInputType, so adding new fields must be done carefully.

We expect to gradually deprecate the TransactionType. At that point, the new-style types will be fully independent.

Reproducible build

We want to invite the wider community to participate in the verification of the firmware built by SatoshiLabs. With reasonable effort you should be able to build the firmware and verify that it's identical to the official firmware.

Trezor Firmware uses Nix, Poetry and Cargo to make the build environment deterministic. We also provide a Docker-based script so that the build can be performed with a single command on usual x86 Linux system.

Building

First you need to determine which version tag you want to build:

  • for Trezor One it is legacy/vX.Y.Z, e.g. legacy/v1.10.3,
  • for Trezor Model T it is core/vX.Y.Z, e.g. core/v2.4.2.

Assuming you want to build core/v2.4.2:

  1. install Docker
  2. clone the firmware repository: git clone https://github.com/trezor/trezor-firmware.git
  3. go into the firmware directory: cd trezor-firmware
  4. checkout the version tag: git checkout core/v2.4.2
  5. run: bash build-docker.sh core/v2.4.2

After the build finishes the firmware images are located in:

  • build/legacy/firmware/firmware.bin and build/legacy-bitcoinonly/firmware/firmware.bin for Trezor One,
  • build/core/firmware/firmware.bin and build/core-bitcoinonly/firmware/firmware.bin for Trezor Model T.

Verifying

The result won't be bit-by-bit identical with the official images because the official images are signed while local builds aren't. Official release of Trezor One firmware also has additional 256-byte legacy header that needs to be removed first.

Trezor T

The firmware header contains 65 bytes of signature data at offset 0x15bf. After overwriting it by zeros in official release the binaries should become identical.

wget https://data.trezor.io/firmware/2/trezor-2.4.2.bin

# the following line removes 65 bytes of signature data from the official firmware
dd if=/dev/zero of=trezor-2.4.2.bin bs=1 seek=5567 count=65 conv=notrunc

# the following two lines print out the hashes of the firmwares
sha256sum trezor-2.4.2.bin
sha256sum build/core/firmware/firmware.bin

Trezor One

Official T1 firmware starts with 256-byte legacy header used for compatibility with old bootloaders. Locally built firmware doesn't have this header.

wget https://data.trezor.io/firmware/1/trezor-1.10.3.bin

# strip legacy header
tail -c +257 trezor-1.10.3.bin > trezor-1.10.3-nolegacyhdr.bin

The v2 header has 3x65 bytes of signature data at offset 0x220. Overwrite by zeros to obtain image identical to the one built locally.

dd if=/dev/zero of=trezor-1.10.3-nolegacyhdr.bin bs=1 seek=544 count=195 conv=notrunc

sha256sum trezor-1.10.3-nolegacyhdr.bin
sha256sum build/legacy/firmware/firmware.bin

Note: Fingerprints displayed for T1 at the end of build-docker.sh do not match fingerprints of official firmware due to the legacy header.

Note: T1 firmware built this way won't boot because unsigned firmware needs to be built with PRODUCTION=0.

Message Workflows

This page was migrated from the Trezor Wiki. The content here may contain outdated information.

In general, the API implements a simple request-response protocol. The computer sends a request to the device and the device sends back a response. The response can be a simple Success message, a Failure message, or an answer to the request giving the requested data. Moreover, the response can be a request from the device to the computer, e.g., for entering the PIN, the passphrase or giving some other information. In that case the computer should send the corresponding Ack packet to answer the request and wait for another response.

Initialize/Features

As first message, the computer should send an empty Initialize packet and expect a Features packet as response. The Initialize packet will cause the device to stop what it is currently doing and should work at any time. Thus, it can also be used to recover from previous errors.

Button meta-workflow

If the device requires the user to press a button, it will reply with a ButtonRequest to the computer. The computer should immediately send a ButtonAck acknowledging the request. But it should also display an indication to the user that it should follow the instruction on the device. The field code in the ButtonRequest message explains what type of request the user should acknowledge with a button.

If the user never presses the button, there will never be a reply to the ButtonAck message. The computer can use Cancel to abort the current operation. This should result in a Failure response.

PinMatrix meta-workflow

If the device requires the user to unlock the device with a PIN, it will reply with a PinMatrixRequest. The field type gives some explanation what PIN is required (current pin, new pin, or confirmation of new pin). The computer should display an empty pin matrix and let the user enter the pin. The computer should encode the PIN as if the numbers are ordered like they are on the numeric keypad. The encoded PIN should then be send with a PinMatrixAck message.

Passphrase meta-workflow

If the device requires the user to enter the passphrase, it will reply with a PassphraseRequest. The computer should ask the user for the passphrase and send it in clear text with a PassphraseAck message.

GetAddress

The message GetAddress (send from the computer to the device) serves two purposes. It can be used to get a valid address or to display the address on the device. The field address_n gives the bip-32 path to the address. The field coin_name should be set to some supported coin (see the Feature message for a list of supported coins). For multisig addresses multisig must be filled out with all participating master public keys and their bip-32 path. The script_type field has the same meaning as for transaction inputs when signing:

  • SPENDADDRESS (standard P2PKH address)
  • SPENDMULTISIG (P2SH multisig address)
  • SPENDWITNESS (native segwit P2WPKH or multisig p2wsh address)
  • SPENDP2SHWITNESS (segwit encapsulated in a p2sh address)

If show_display is set the address is displayed to the user. In any case, it is also sent to the computer with an Address response.

GetPublicKey

The message GetPublicKey can be used to get a bip-32 master public key from the trezor or to display it to the user. The field address_n gives the bip-32 path to the master key. The field ecdsa_curve_name can be used to get Ed25519 or NIST256P1 public keys.

SignTx

The following may contain imprecise/obsolete information. Refer to Bitcoin signing flow for more relevant information.

Signing a transaction is a little bit complicated. The reason is that transactions can be several hundred kilobytes in size, but Trezor has only 64 kilobytes memory. So it is the task of the computer to split the transactions in small pieces and send only those pieces that Trezor requested. The general workflow is given below

The computer starts the transaction signing process by sending a SignTx message. From then on, the device drives all further communications by sending requests to the computer until it finally sends a TxRequest with request set to TXFINISHED. This final message should not be acknowledged by the computer.

The SignTx message contains only the meta data of the transaction that should be signed, i.e., the number of inputs and outputs, the coin name, the version number, and lock_time (only for pre-signed time locked transactions). If the device was not unlocked before, it will respond with the usual PinMatrixRequest and PasswordRequest messages to authenticate the user. See the corresponding sections above. It may also send a ButtonRequest at any time to indicate that the user should confirm a transaction output or the total fee.

Then the main process begins and Trezor will respond with TxRequest messages, which should be answered by TxAck message. A TxRequest message has up to three parts.

  1. Parts of the signed transactions.
  2. A signature for one of the inputs.
  3. A request for one piece of the new transaction or a previous transaction.

If the field serialized.serialized_tx is set, it contains a chunk of the signed transaction in serialized format. The chunks are returned in the right order and just concatenating all returned chunks will result in the signed transaction.

If the field serialized.signature is set, it contains a signature for one of the inputs. The signatures are returned in the same order as they appear in the serialized transactions. I.e., the non-segwit signatures come before the segwit signatures, since the latter are part of the witness, which is serialized at the end. Apart from that, the signatures are returned in the order the inputs appear in the transaction. The signatures are not really needed, as they are already in the serialized transaction. They can be useful for combining multisig signatures without having to parse the transactions again.

If the field request equals TXFINISHED, this message contained the last chunk of the transaction. The signing is finished and the computer must not reply to this packet. In any other case, the device requested some piece of some transaction, which is specified by request and details. This request must be answered by a TxAck package containing the requested piece of data.

If the field details.tx_hash is not set, some piece of the transaction that should be signed is requested. Otherwise, this field contains the hash of some input transaction and some piece of that transaction is requested.

For request = TXMETA, the fields tx.version, tx.lock_time, tx.inputs_cnt (number of inputs), and tx.outputs_cnt must be filled. For ZCash transactions also tx.extra_data_len must be given. This will only be requested for input transactions (for the signed transaction it was given in the SignTx call).

For request = TXINPUT, the field details.request_index contains the number of the input requested (starting with zero). The reply must fill the structure tx.inputs[0] (there must be exactly one input in the reply). Which fields must be set depends on whether details.tx_hash is set (an input of some previous transaction is requested, that is spend in the new transaction), or whether an input of the new transaction is requested. In both cases prev_hash, prev_index and sequence must be set. For a previous transaction, the script_sig must be set to the raw signature data.

But if details.tx_hash is unset, the data must instead describe the private key that should be used to sign the input. This is specified by address_n (the bip-32 path to the private key), script_type and multisig. The field multisig is only given for multisig transactions and contains the master public keys and the derivation paths for all signers. The field script_type can be

  • SPENDADDRESS (standard p2pkh address)
  • SPENDMULTISIG (p2sh multisig address)
  • SPENDWITNESS (native segwit p2wpkh or multisig p2wsh address)
  • SPENDP2SHWITNESS (segwit encapsulated in a p2sh address)

Note, that for segwit script_type does not distinguish between multisig or p2wpkh addresses. Instead the presence of the multisig decides this. For segwit inputs also the amount field must be set to the amount of satoshis in the input transaction.

For request = TXOUTPUT, the field details.request_index contains the number of the output requested (starting with zero). If details.tx_hash is set, this is an output of a previous transaction and the tx.bin_outputs[0] field must be filled in the TxAck reply. Otherwise, the tx.outputs[0] field must be filled. For change outputs, the field address_n must be filled and address must be omitted. If the change is multisig, the multisig must be filled and it must use the same extended public keys as all inputs. For a change address, the script_type should be PAYTOADDRESS, PAYTOMULTISIG, PAYTOWITNESS or PAYTOP2SHWITNESS matching the corresponding cases SPEND... for inputs. For OP_RETURN outputs, set script_type = PAYTOOPRETURN and set the op_return_data field. Otherwise address should be set to a base58 encoded address and script_type to PAYTOADDRESS. Older firmware required script_type = PAYTOSCRIPTHASH for p2sh addresses, though (and newer firmware still support this).

SignMessage/VerifyMessage

Sign message

Signing messages can be used to prove ownership of a specific address. To sign message with Trezor device, it is needed to send the message which the user wants to sign and also specify BIP-32 path which to use for message signing. There are also two optional arguments: to specify coin (Bitcoin is default, for more information about available coins check this GitHub page) and specify script type (0 = SPENDADDRESS/standard P2PKH address, 1 = SPENDMULTISIG/P2SH multisig address, 2 = EXTERNAL/reserved for external inputs (coinjoin), 3 = SPENDWITNESS/native SegWit, 4 = SPENDP2SHWITNESS/SegWit over P2SH (backward compatible))

Verify message

Verify message asks device to verify if the signature is a signed message with the given address. The arguments of the message are signature, message being verified, address and coin which should be used for verifying.

CipherKeyValue

Cipher key value provides symmetric encryption in the Trezor device, where the key doesn't exit the device, and where the user might be forced to confirm the encryption/decryption on the display. The data sent to the device are The following data are BIP-32 derivation path, key (that is being shown on the device), value, encrypt/decrypt direction, should user confirm on encrypt?, should user confirm on decrypt? and optional IV. Value is what is actually being encrypted. The key for the encryption is constructed from the private key on the BIP address, the key displayed on the device, and the two informations about whether to ask for confirmation. It is constructed in such a way, that different path, key or the confirm information will get a different encryption key and IV. So, you cannot "skip" the confirmation by using different input. IV can be either manually set, or it is computed together with the key. The value must be divisible into 16-byte blocks. The application has to pad the blocks itself and ensure safety; for example, by using PKCS7. See https://github.com/satoshilabs/slips/blob/master/slip-0011.md.

ResetDevice

Reset device message performs Trezor device setup and generates new wallet with new recovery seed. The device must be in unitialized state, the firmware is already installed but it has not been initialized yet. If it is initialized and the user wants to perform a reset device, the device must be wiped first. If the Trezor is prepared for its initialization the screen is showing "Go to trezor.io". The reset device can be done in Trezor Wallet interface (https://trezor.io/start) and also with Python trezorctl command. After sending message to the device, device warn us to never make a digital copy of your recovery seed and never upload it online, this message has to be confirmed by pressing on "I understand" on the device. After confirmed, the device produces internal entropy which is random of 32 bytes, requests external entropy which is produced in computer and computes mnemonic (recovery seed) using internal, external entropy and given strength (12, 18 or 24 words). Trezor Wallet interface doesn't provide option to choose how many words there should be in the generated mnemonic (recovery seed). It is hardcoded to 12 words for Trezor Model T but if done with python's trezorctl command it can be chosen (for initialization with python's trezorctl command, 24 words mnemonic is default). After showing mnemonic on the Trezor device, Trezor Model T requires 2 random words to be entered to the device to confirm the user has written down the mnemonic properly. If there are errors in entered words, the device shows the recovery seed again. If the backup check is successful, the setup is finished. If the Trezor Wallet interface is used, user is asked to set the label and pin (setting up the pin can be skipped) for the wallet, this is optional when using python trezorctl command.

RecoveryDevice

Recovery device lets user to recover BIP39 seed into empty Trezor device. First the device asks user for the number of words in recovered seed, the words are typed in one by one - on the device screen when using Trezor model T, with Trezor One the user can decide to do the advanced recovery (with entering seed using matrix similarly to entering PIN) or standard recovery (with entering the seed to the host computer one by one in random order). The process continues with optional check of the seed validity and optional setting up the PIN, which has to be confirmed. Finally the recovered wallet is saved into device storage. The same process is used with the dry run recovery, the differences are that this process can be done only with already initialized deviice and that the mnemonic is not saved into the device but it is only compared to the mnemonic already loaded into the device with the successful result (The seed is valid and matches the one in the device) or unsuccessful result(The seed is valid but does not match the one in the device).

LoadDevice

Load device lets user to load the device with the specific recovery seed. This command is the subset of the recovery device and it can not be done with Trezor Wallet interface, only with python command trezorctl. This message can be used only if the device is not initialized.

WipeDevice

Wipe device lets user wipe the device. It is possible to wipe only user's wallet or erase all the data from the Trezor device including installed firmware. Wiping device in Trezor Wallet interface wipes only user's wallet. It is also possible to wipe the firmware with python trezorctl command, Trezor device must be in bootloader mode.

ApplySettings

Apply settings lets user change settings on the Trezor device, mainly its homescreen, label and passphrase settings. Passphrase can be set to enabled or disabled. Furthermore it can be set that passphrase is entered solely on device or solely on host, by default the device always ask where the user wants to enter the passphrase. All these settings have to be confirmed by user on the device.

ChangePin

This message lets user change, remove or set new PIN. First the user is asked to enter old PIN if it was set before. The user is then asked to enter new PIN and re-enter it to confirm match. It is also possible to not enter new PIN, so the Change PIN message will just remove the old one. The action has to be confirmed by user.

Ethereum definitions

For support of the huge number of EVM chains (networks) and ERC-20 tokens, Trezor needs to know parameters of those networks and tokens, namely:

  • currency symbol and number of decimal places, to correctly display amounts,
  • SLIP44 identifier to unlock the appropriate BIP-32 subtrees.

A subset of Ethereum definitions is built into the firmware image. The rest is generated externally and must be sent to Trezor as a signed blob.

Built-in definitions

The set of built-in definitions is declared in the following files:

These definitions need to be modified manually.

External definitions

A full list of Ethereum definitions is compiled from multiple sources and is available in a separate repository.

From this list, a collection of binary blobs is generated, signed, and made available online.

A given Trezor firmware will only accept signed definitions newer than a certain date, typically one month before firmware release. This means that a client application should either always fetch fresh definitions from the official URLs, or refresh its local copy frequently.

Retrieving the definitions

The base URL for the definitions is https://data.trezor.io/firmware/eth-definitions/.

Known chain ID

To look up a network definition by its chain ID, use the following URL:

https://data.trezor.io/firmware/eth-definitions/chain-id/<CHAIN_ID>/network.dat

<CHAIN_ID> is a decimal number, e.g., 1 for Ethereum mainnet.

To look up a token definition for a given chain ID and token address, use the following URL:

https://data.trezor.io/firmware/eth-definitions/chain-id/<CHAIN_ID>/token-<TOKEN_ADDRESS>.dat

<CHAIN_ID> is again a decimal number.
<TOKEN_ADDRESS> is all lowercase (no checksum) token address hex without the 0x prefix.

E.g., this is the URL for Görli TST token: [https://data.trezor.io/firmware/eth-definitions/chain-id/5/token-7af963cf6d228e564e2a0aa0ddbf06210b38615d.dat]

Unknown chain ID

Certain Ethereum calls, such as EthereumGetAddress and EthereumSignMessage, do not require the caller to know the chain ID, because their results do not depend on it.

For this situation, it is possible to look up a network definition by a SLIP-44 identifier on the following URL:

https://data.trezor.io/firmware/eth-definitions/slip44/<SLIP44_ID>/network.dat

<SLIP44_ID> is a decimal number, e.g., 60 for Ethereum mainnet.

In some cases, multiple network definitions can be registered for the same SLIP-44 number. The retrieved definition is valid for an unspecified one of those colliding networks. This does not matter for purposes of EthereumGetAddress and the like, because the information in the network definition is only used to prove validity of the derivation path.

When using Ethereum's SLIP-44 number 60 in the derivation path, the caller does not need to provide the network definition, because Ethereum network is always built-in.

Full set of definitions

It is possible to download the full set of signed definitions in a single tar archive from the following URL:

https://data.trezor.io/firmware/eth-definitions/definitions.tar.xz.

Definition format

Each definition is encoded as a protobuf message EthereumNetworkInfo or EthereumTokenInfo and packaged in the following binary format.

All numbers are unsigned little endian.

  1. magic string trzd1 (5 bytes)
  2. definition type according to EthereumDefinitionType enum (1 byte)
  3. data version of the definition (4 bytes)
  4. protobuf payload length (2 bytes)
  5. protobuf payload (N bytes)

A Merkle tree is constructed from all binary definitions (see below) and its root is signed by the CoSi algorithm.

The full format of the definition is as follows:

  1. Data payload (see above)
  2. Number of Merkle proof entries (1 byte)
  3. Sequence of 32-byte proof entries (N * 32 bytes)
  4. CoSi sigmask (1 byte)
  5. CoSi signature (64 bytes)

Merkle tree algorithm

The input for the Merkle tree calculation is a collection of binary values.

  1. For each entry, calculate a leaf hash: SHA256(0x00 || entry), with || denoting string concatenation.
  2. Sort the leaf hashes lexicographically in ascending order. This is the base level of a binary tree.
  3. For each level of the tree, build the next level by taking a pair of entries from the left and calculating an internal hash: a. Set min to the smaller of the two entries, and max to the larger one. b. The internal hash is SHA256(0x01 || min || max).
  4. If there is a left-over odd entry, append it to the end of the next level.
  5. Continue until there is only one entry left. This is the root hash.

For each leaf, its proof is a sequence of neighbor hashes going up the tree. One way to keep track of the proof is, whenever constructing an internal node, add the right hash to the left child's proof list and vice versa.

A reference implementation is provided.

Data sources

External Ethereum definitions are generated based on data from external APIs and repositories:

Trezor Storage

The storage folder contains the implementation of Trezor's internal storage, which is common for both Legacy (Trezor One) and Core (Trezor T and later models). This README also contains a detailed description of the cryptographic design.

Due to differences of the underlying hardware, the storage uses two different format of stored data, referenced further as Bitwise and Blockwise

All tests are located in the tests subdirectory, which also includes a Python implementation to run tests against this C production version and the Python one.

Norcow data format

Bitwise flash

All items are stored with single format:

DataKEYAPPLENDATA
Length (bytes)112LEN

If any item is overwritten or deleted, the old entry is erased, i.e., DATA, APP, KEY are set to 0. LEN is kept for purposes of finding next item.

Blockwise flash (16 byte blocks)

For optimization, we use two different formats for items, small items with up to 12 bytes of data, and larger items.

Small items have the following format:

DataKEYAPPLENDATAPADDING
Length (bytes)112LEN12 - LEN

If any item is overwritten or deleted, the old entry is erased, i.e., DATA, APP, KEY and LEN are set to 0. When finding next item, we assume that zero LEN items means small item with 12 bytes of data.

Larger items have the following format:

DataKEYAPPLENPADDINGDATAVALID FLAGPADDING
Length (bytes)11212LEN1to 16 bytes

If any item is overwritten or deleted, the old entry is erased, i.e., DATA, are set to 0. LEN is kept for purposes of finding next item, and with it KEY and APP has to be kept too. To recognize data as deleted, the VALID FLAG is set to 0.

Storage format

Entries fall into three categories:

CategoryConditionReadWrite
PrivateAPP = 0NeverNever
Protected1 ≤ APP ≤ 127Only when unlockedOnly when unlocked
Public128 ≤ APP ≤ 191AlwaysOnly when unlocked
Writable192 ≤ APP ≤ 255AlwaysAlways

The format of public and writable entries is trivial:

DataDATA
Length (bytes)LEN

Private values are used to store storage-specific information and cannot be directly accessed through the storage interface. Protected entries have the following format:

DataIVENCRDATATAG
Length (bytes)12LEN - 2816

The random salt (32 bits), EDEK (256 bits), ESAK (128 bits) and PVC (64 bits) is stored in a single entry under APP=0, KEY=2:

DataSALTEDEKESAKPVC
Length (bytes)432168

The storage authentication tag (128 bits) is stored in a single entry under APP=0, KEY=5:

DataTAG
Length (bytes)16

Storage entries are stored as items in norcow flash. The format of the items is described in the section above.

PIN

The PIN is not stored in the flash storage. An entry is added to the flash storage consisting of a 256-bit encrypted data encryption key (EDEK) followed by a 128-bit encrypted storage authentication key (ESAK) and a 64-bit PIN verification code (PVC). The PIN is used to decrypt the EDEK and ESAK and the PVC is used to verify that the correct PIN was used. The resulting data encryption key (DEK) is then used to encrypt/decrypt protected entries in the flash storage. We use Chacha20Poly1305 as defined in RFC 7539 to encrypt the EDEK and the protected entries. The storage authentication key (SAK) is used to authenticate the list of (APP, KEY) values for all protected entries that have been set in the storage. This prevents an attacker from erasing or adding entries to the storage.

PIN verification and decryption of protected entries in flash storage

  1. From the flash storage read the entry containing the random salt, EDEK and PVC.

  2. Gather constant data from various system resources such as the ProcessorID (aka Unique device ID) and any hardware serial numbers that are available. The concatenation of this data with the random salt will be referred to as salt.

  3. Prompt the user to enter the PIN and compute:

    PBKDF2(PRF = HMAC-SHA256, Password = pin, Salt = salt, iterations = 10000, dkLen = 352 bits)

    The first 256 bits of the output will be used as the key encryption key (KEK) and the remaining 96 bits will be used as the key encryption initialization vector (KEIV).

    Note: Since two blocks of output need to be produced in PBKDF2 the total number of iterations is 20000.

  4. Compute:

    (dek, tag) = ChaCha20Poly1305Decrypt(kek, keiv, edek)

  5. Compare the PVC read from the flash storage with the first 64 bits of the computed tag value. If there is a mismatch, then fail. Otherwise store the DEK in a global variable.

  6. When a protected entry needs to be decrypted, load the IV, ENCRDATA and TAG of the entry and compute:

    (data, tag) = ChaCha20Poly1305Decrypt(dek, iv, (key || app), encrdata)

    where the APP and KEY of the entry is used as two bytes of associated data. Compare the TAG read from the flash storage with the computed tag value. If there is a mismatch, then fail.

summary

Initializing the EDEK

  1. When the storage is initialized, generate the 32 bit random salt and 256 bit DEK using a cryptographically secure random number generator.

  2. Set a boolean value in the storage denoting that the PIN has not been set. Use an empty PIN to derive the KEK and KEIV as described above.

  3. Encrypt the DEK using the derived KEK and KEIV:

    (edek, tag) = ChaCha20Poly1305Encrypt(kek, keiv, dek)

  4. Store the random salt, EDEK value and the first 64 bits of the tag as the PVC.

Setting a new PIN

  1. If the PIN has already been set, then prompt the user to enter the old PIN value, check the PVC and compute the DEK as described above in steps 1-4.

  2. Generate a new 32 bit random salt and prompt the user to enter the new PIN value. Use these values to derive the new KEK and KEIV as described above.

  3. Encrypt the DEK using the new KEK and KEIV:

    (edek, tag) = ChaCha20Poly1305Encrypt(kek, keiv, dek)

  4. Store the new EDEK value and the first 64 bits of the tag as the new PVC. This operation should be atomic, i.e. either both values should be stored or neither. Overwrite the old values of the EDEK and PVC with zeros.

Encryption of protected entries in flash storage

Whenever the value of an entry needs to be updated, a fresh IV is generated using a cryptographically secure random number generator and the data is encrypted as (encrdata, tag) = ChaCha20Poly1305Encrypt(dek, iv, (key || app), data).

Storage authentication

The storage authentication key (SAK) will be used to generate a storage authentication tag (SAT) for the list of all (APP, KEY) values of protected entries (1 ≤ APP ≤ 127) that have been set in the storage. The SAT will be checked during every get operation. When a new protected entry is added to the storage or when a protected entry is deleted from the storage, the value of the SAT will be updated. The value of the SAT is defined as the first 16 bytes of

HMAC-SHA-256(SAK, ⨁_i HMAC-SHA-256(SAK, KEY_i || APP_i))

where denotes the n-ary bitwise XOR operation and KEYi || APPi is a two-byte encoding of the value of the i-th (APP, KEY) such that 1 ≤ APP ≤ 127.

Design rationale

  • The purpose of the PBKDF2 function is to thwart brute-force attacks in case the attacker is able to circumvent the PIN entry counter mechanism but does not have full access to the contents of the flash storage of the device, e.g. fault injection attacks. For an attacker that would be able to read the flash storage and obtain the salt, the PBKDF2 with 20000 iterations and a 4- to 9-digit PIN would not pose an obstacle.

  • The reason why we use a separate data encryption key rather than using the output of PBKDF2 directly to encrypt the sensitive entries is so that when the user decides to change their PIN, only the EDEK needs to be reencrypted, but the remaining entries do not need to be updated.

  • We use ChaCha20 for encryption, because as a stream cipher it has no padding overhead and its implementation is readily available in trezor-crypto. A possible alternative to using ChaCha20Poly1305 for DEK encryption is to use AES-CTR with HMAC in an encrypt-then-MAC scheme. A possible alternative to using ChaCha20 for encryption of other data entries is to use AES-XTS (XEX-based tweaked-codebook mode with ciphertext stealing), which was designed specifically for disk-encryption. The APP || KEY value would be used as the tweak.

    • Advantages of AES-XTS:
      • Does not require an initialization vector.
      • Ensures better diffusion than a stream cipher, which eliminates the above concerns about malleability and fault injection attacks.
    • Disadvantages of AES-XTS:
      • Not implemented in trezor-crypto.
      • Requires two keys of length at least 128 bits.
  • The 64 bit PVC means that there is less than a 1 in 1019 chance that a wrong PIN will happen to have the same PVC as the correct PIN. The existence of false PINs does not pose a security weakness since a false PIN cannot be used to decrypt the protected entries.

  • Instead of using separate IVs for each entry we considered using a single IV for the entire sector. Upon sector compaction a new IV would have to be generated and the encrypted data would have to be reencrypted under the new IV. A possible issue with this approach is that compaction cannot happen without the DEK, i.e. generally data could not be written to the flash storage without knowing the PIN. This property might not always be desirable.

PIN entry counter protection

Bitwise flash

The former implementation of the PIN entry counter was vulnerable to fault injection attacks.

Under the former implementation the PIN counter storage entry consisted of 32 words initialized to 0xFFFFFFFF. The first non-zero word in this area was the current PIN failure counter. Before verifying the PIN the lowest bit with value 1 was set to 0, i.e. a value of FFFFFFFC indicated two PIN entries. Upon successful PIN entry, the word was set to 0x00000000, indicating that the next word was the PIN failure counter. Allegedly, by manipulating the voltage on the USB input an attacker could convince the device to read the PIN entry counter as 0xFFFFFFFF even if some of the bits had been set to 0.

Design goals

  • Make it easy to decrement the counter by changing a 1 bit to 0.
  • Make it hard to reset the counter by a fault injection, i.e. counter values should not have an overly simple binary representation like 0xFFFFFFFF.
  • If possible, use two or more different methods of checking the counter value so that an attacker has to mount different fault injection attacks to succeed.
  • Optimize the format for successful PIN entry.
  • Minimize the number of branching operations. Avoid loops, instead utilize bitwise and arithmetic operations when processing the PIN counter data.

Proposal summary

Under the former implementation, for every unsuccessful PIN entry we discarded one bit from the counter, while for every successful PIN entry we discard an entire word. In the new implementation we optimize the counter operations for successful PIN entry.

The basic idea is that there are two binary logs stored in the flash storage, e.g.:

...0001111111111111... pin_success_log
...0000001111111111... pin_entry_log

Before every PIN verification the highest 1-bit in the pin_entry_log is set to 0. If the verification succeeds, then the corresponding bit in the pin_success_log is also set to 0. The example above shows the status of the logs when the last three PIN entries were not successful.

In actual fact the logs are not written to the flash storage exactly as shown above, but they are stored in a form that should protect them against fault injection attacks. Only half of the stored bits carry information, the other half acts as "guard bits". So a stored value ...001110... could look like ...0g0gg1g11g0g..., where g denotes a guard bit. The positions and the values of the guard bits are determined by a guard key. The guard_key is a randomly generated uint32 value stored as an entry in the flash memory in cleartext. The assumption behind this is that an attacker attempting to reset or decrement the PIN counter by a fault injection is not able to read the flash storage. However, the value of guard_key also needs to be protected against fault injection, so the set of valid guard_key values should be limited by some condition which is easy to verify, such as guard_key mod M == C, where M and C a suitably chosen constants. The constants should be chosen so that the binary representation of any valid guard_key value has Hamming weight between 8 and 24. These conditions are discussed below.

Storage format

The PIN log has APP = 0 and KEY = 1. The DATA part of the entry consists of 33 words (132 bytes, assuming 32-bit words):

  • guard_key (1 word)
  • pin_success_log (16 words)
  • pin_entry_log (16 words)

Each log is stored in big-endian word order. The byte order of each word is platform dependent.

Guard key validation

The guard_key is said to be valid if the following three conditions hold true:

  1. Each byte of the binary representation of the guard_key has a balanced number of zeros and ones at the positions corresponding to the guard values (that is those bits in the mask 0xAAAAAAAA).
  2. The guard_key binary representation does not contain a run of 5 (or more) zeros or ones.
  3. The guard_key integer representation is congruent to 15 modulo 6311.

Key validity can be checked with this function:

int key_validity(uint32_t guard_key)
{
  uint32_t count = (guard_key & 0x22222222) + ((guard_key >> 2) & 0x22222222);
  count = count + (count >> 4);

  uint32_t zero_runs = ~guard_key;
  zero_runs = zero_runs & (zero_runs >> 2);
  zero_runs = zero_runs & (zero_runs >> 1);
  zero_runs = zero_runs & (zero_runs >> 1);
  uint32_t one_runs = guard_key;
  one_runs = one_runs & (one_runs >> 2);
  one_runs = one_runs & (one_runs >> 1);
  one_runs = one_runs & (one_runs >> 1);

  return ((count & 0x0e0e0e0e) == 0x04040404) & (one_runs == 0) & (zero_runs == 0) & (guard_key % 6311 == 15);
}

Key generation

The guard_key may be generated in the following way:

  1. Generate a random integer r in such that 0 ≤ r ≤ 680552 with uniform probability.
  2. Set r = r * 6311 + 15.
  3. If key_validity(r) is not true go back to the step 1.

Note that on average steps 1 to 3 are repeated about one hundred times.

Key expansion

The guard_key is read from storage, its value is checked for validity and used to compute the guard_mask (indicating the positions of the guard bits) and guard value (indicating the values of the guard bits on their actual positions):

LOW_MASK = 0x55555555
guard_mask = ((guard_key & LOW_MASK) << 1) |
             ((~guard_key) & LOW_MASK)
guard = (((guard_key & LOW_MASK) << 1) & guard_key) |
        (((~guard_key) & LOW_MASK) & (guard_key >> 1))

Explanation:

The guard_key contains two pieces of information. The position of the guard bits but also their corresponding values. The bitwise format of the guard_key is vpvpvp...vp. The bits labelled p indicate the position of each guard bit and the bits labelled v indicate its value.

The guard_mask is derived from the guard_key and has the form xyxyxy...xy where x+y = 1 (in other words, there is exactly one 1 bit in each pair xy). First, we set the x bits:

(guard_key & LOW_MASK) << 1

and the y bits to its corresponding complement:

(~guard_key) & LOW_MASK

That ensures that only one 1 bit is present in each pair xy. The guard value is equal to the bits labelled v in the guard_key but only at the positions indicated by the guard_mask. The guard value is therefore equal to:

        -------- x bits mask --------- & -- guard_key --
guard = (((guard_key & LOW_MASK) << 1) & guard_key) |
        ----- y bits mask ---- & - guard_key shifted to v bits
        (((~guard_key) & LOW_MASK) & (guard_key >> 1))

Log initialization

Each log is stored as 16 consecutive words each initialized to:

guard | ~guard_mask

Removing and adding guard bits

After reading a word from the flash storage we verify the format by checking the condition:

(word & guard_mask) == guard

and then remove the guard bits as follows:

word = word & ~guard_mask
word = ((word  >> 1) | word ) & LOW_MASK
word = word | (word << 1)

This operation replaces each guard bit with the value of its neighbouring bit, e.g. …0g0gg1g11g0g… is converted to …000011111100… Thus each non-guard bit is duplicated.

The guard bits can be added back as follows:

word = (word & ~guard_mask) | guard

Determining the number of PIN failures

Remove the guard bits from the words of the pin_entry_log using the operations described above and verify that the result has form 0*1* by checking the condition:

word & (word + 1) == 0

Then verify that the pin_entry_log and pin_success_log are in sync by checking the condition:

pin_entry_log & pin_success_log == pin_entry_log

Finally, determine the current number of PIN failures by counting the number of set bits in the evaluation of the following expression:

pin_success_log xor pin_entry_log

Note that the number of set bits in a word can be counted using bitwise and arithmetic operations. For a 32-bit word the following can be used:

count = word - ((word >> 1) & 0x55555555)
count = (count & 0x33333333) + ((count >> 2) & 0x33333333)
count = (count + (count >> 4)) & 0x0F0F0F0F
count = count + (count >> 8)
count = (count + (count >> 16)) & 0x3F

Blockwise flash

For blockwise flash, the PIN counter protection was significantly simplified. This is fine because models with this type of flash use secure element for additional protection. The flash is also ECC protected, so fault injection attacks are harder.

Design goals

  • Fit one block to not waste too much space when counting PIN failures.
  • Provide a simple way to check the PIN counter value.
  • Make it reasonably hard to reset the counter by a fault injection.
  • Significantly reduce the code complexity.

Format

The counter is 8 bit value, stored in such a way that every 1 bit is expanded to 01 and every 0 bit is expanded to 10. The resulting 16 bit value is written into the flash block as many times as needed to fill the block.

Expanding the counter

The counter is expanded by the following procedure:

c = ((c << 4) | c) & 0x0f0f;
c = ((c << 2) | c) & 0x3333;
c = ((c << 1) | c) & 0x5555;
c = ((c << 1) | c) ^ 0xaaaa;

Compressing the counter

The counter is compressed by the following procedure:

c = c & 0x5555;
c = ((c >> 1) | c) & 0x3333;
c = ((c >> 2) | c) & 0x0f0f;
c = ((c >> 4) | c) & 0x00ff;

Checking the counter

The counter format is checked by the following operation:

((c ^ (c << 1)) & 0xAAAA != 0xAAAA)

Tests

Burn tests

These tests are doing a simple read/write operations on the device to see if the hardware can endure high number of flash writes. Meant to be run on the device directly for a long period of time.

Device tests

Device tests are integration tests that can be run against either emulator or on an actual device. You are responsible to provide either an emulator or a device with Debug mode present.

Device tests

The original version of device tests. These tests can be run against both Model One and Model T.

See device-tests.md for instructions how to run it.

UI tests

UI tests use device tests and take screenshots of every screen change and compare them against fixtures. Currently for model T only.

See ui-tests.md for more info.

Click tests

Click tests are a next-generation of the Device tests. The tests are quite similar, but they are capable of imitating user's interaction with the screen.

Fido tests

Implement U2F/FIDO2 tests.

Upgrade tests

These tests test upgrade from one firmware version to another. They initialize an emulator on some specific version and then pass its storage to another version to see if the firmware operates as expected. They use fixtures from https://data.trezor.io/dev/firmware/releases/emulators/ which can be downloaded using the download_emulators.sh script.

See the upgrade-tests.md for instructions how to run it.

Persistence tests

These tests test the Persistence mode, which is currently used in the device recovery. These tests launch the emulator themselves and they are capable of restarting or stopping it simulating user's plugging in or plugging out the device.

Running device tests

1. Running the full test suite

Note: You need Poetry, as mentioned in the core's documentation section.

In the trezor-firmware checkout, in the root of the monorepo, install the environment:

poetry install

And run the automated tests:

poetry run make -C core test_emu

2. Running tests manually

Install the poetry environment as outlined above. Then switch to a shell inside the environment:

poetry shell

If you want to test against the emulator, run it in a separate terminal:

./core/emu.py

Now you can run the test suite with pytest from the root directory:

pytest tests/device_tests

Useful Tips

The tests are randomized using the pytest-random-order plugin. The random seed is printed in the header of the tests output, in case you need to run the tests in the same order.

If you only want to run a particular test, pick it with -k <keyword> or -m <marker>:

pytest -k nem      # only runs tests that have "nem" in the name
pytest -k "nem or stellar"  # only runs tests that have "nem" or "stellar" in the name
pytest -m stellar  # only runs tests marked with @pytest.mark.stellar

If you want to see debugging information and protocol dumps, run with -v.

Print statements from testing files are not shown by default. To enable them, use -s flag.

If you would like to interact with the device (i.e. press the buttons yourself), just prefix pytest with INTERACT=1:

INTERACT=1 pytest tests/device_tests

When testing transaction signing, there is an option to check transaction hashes on-chain using Blockbook. It is chosen by setting CHECK_ON_CHAIN=1 environment variable before running the tests.

CHECK_ON_CHAIN=1 pytest tests/device_tests

To run the tests quicker, spawn the emulator with disabled animations using -a flag.

./core/emu.py -a

To run the tests even quicker, the emulator should come from a frozen build. (However, then changes to python code files are not reflected in emulator, one needs to build it again each time.)

PYOPT=0 make build_unix_frozen

It is possible to specify the timeout for each test in seconds, using PYTEST_TIMEOUT env variable.

PYTEST_TIMEOUT=15 pytest tests/device_tests

When running tests from Makefile target, it is possible to specify TESTOPTS env variable with testing options, as if pytest would be called normally.

TESTOPTS="-x -v -k test_msg_backup_device.py" make test_emu

When troubleshooting an unstable test that is failing occasionally, following runs it until it fails (so failure is visible on screen):

export TESTOPTS="-x -v -k test_msg_backup_device.py"
while make test_emu; do sleep 1; done

3. Using markers

When you're developing a new currency, you should mark all tests that belong to that currency. For example, if your currency is called NewCoin, your device tests should have the following marker:

@pytest.mark.newcoin

This marker must be registered in REGISTERED_MARKERS file in tests folder.

Tests can be run only for specific models - it is done by disallowing the tests for the other models. @pytest.mark.skip_t1b1 @pytest.mark.skip_t2t1 @pytest.mark.skip_t2b1 @pytest.mark.skip_t3t1 are valid markers to skip current test for Model 1, Model T, Safe 3, and T3T1 respectively.

Extended testing and debugging

Building for debugging (Emulator only)

Build the debuggable unix binary so you can attach the gdb or lldb. This removes optimizations and reduces address space randomizaiton.

make build_unix_debug

The final executable is significantly slower due to ASAN(Address Sanitizer) integration. If you want to catch some memory errors use this.

time ASAN_OPTIONS=verbosity=1:detect_invalid_pointer_pairs=1:strict_init_order=true:strict_string_checks=true TREZOR_PROFILE="" poetry run make test_emu

Coverage (Emulator only)

Get the Python code coverage report.

If you want to get HTML/console summary output you need to install the coverage.py tool.

pip3 install coverage

Run the tests with coverage output.

make build_unix && make coverage

Running Upgrade Tests

  1. As always, use poetry environment:
poetry shell
  1. Download the emulators, if you have not already:
tests/download_emulators.sh
  1. And run the tests using pytest:
pytest tests/upgrade_tests

You can use TREZOR_UPGRADE_TEST environment variable if you would like to run core or legacy upgrade tests exclusively. This will run core only:

TREZOR_UPGRADE_TEST="core" pytest tests/upgrade_tests

Running UI tests

1. Running the full test suite

Note: You need Poetry, as mentioned in the core's documentation section.

In the trezor-firmware checkout, in the root of the monorepo, install the environment:

poetry install

And run the tests:

poetry run make -C core test_emu_ui

2. Running tests manually

Install the poetry environment as outlined above. Then switch to a shell inside the environment:

poetry shell

If you want to test against the emulator, run it with disabled animation in a separate terminal:

./core/emu.py -a

Now you can run the test suite with pytest from the root directory:

pytest tests/device_tests --ui=test

If you wish to check that all test cases in fixtures.json were used set the --ui-check-missing flag. Of course this is meaningful only if you run the tests on the whole device_tests folder.

pytest tests/device_tests --ui=test --ui-check-missing

Updating Fixtures ("Recording")

Short version:

poetry run make -C core test_emu_ui_record

Long version:

The --ui pytest argument has two options:

  • record: Create screenshots and calculate theirs hash for each test. The screenshots are gitignored, but the hash is included in git.
  • test: Create screenshots, calculate theirs hash and test the hash against the one stored in git.

If you want to make a change in the UI you simply run --ui=record. An easy way to proceed is to run --ui=test at first, see what tests fail (see the Reports section below), decide if those changes are the ones you expected and then finally run the --ui=record and commit the new hashes.

Also here we provide an option to check the fixtures.json file. Use --ui-check-missing flag again to make sure there are no extra fixtures in the file:

pytest tests/device_tests --ui=record --ui-check-missing

Reports

Tests

Each --ui=test creates a clear report which tests passed and which failed. The index file is stored in tests/ui_tests/reports/test/index.html. The script tests/show_results.py starts a local HTTP server that serves this page -- this is necessary for access to browser local storage, which enables a simple reviewer UI.

On CI this report is published as an artifact. You can see the latest main branch report here. The reviewer features work directly here.

If needed, you can use python3 -m tests.ui_tests to regenerate the report from local recorded screens.

Master diff

In the ui tests folder you will also find a Python script report_master_diff.py, which creates a report where you find which tests were altered, added, or removed relative to master. This useful for Pull Requests.

This report is available as an artifact on CI as well. You can find it by visiting the "unix ui changes" job in your pipeline - browse the artifacts and open master_diff/index.html.

Click Tests

This set of tests is intended for cases where USB communication must be decoupled from the input stream. They are mainly based on sending simulated clicks and reading screen contents. Unlike device tests that use the client fixture, click tests generally use the device_handler fixture. TODO fixture documentation, the important point is that device_handler runs trezorlib calls in the background and leaves the main thread free to interact with the device from the user's perspective.

Running the full test suite

Note: You need Poetry, as mentioned in the core's documentation section.

In the trezor-firmware checkout, in the root of the monorepo, install the environment:

poetry install

Switch to a shell inside the environment:

poetry shell

If you want to test against the emulator, run it in a separate terminal:

./core/emu.py

Now you can run the test suite with pytest from the root directory:

pytest tests/click_tests

Click test recorder

The repository now includes a tool for automatically generating testcases from user interaction. The resulting test cases must still be tweaked manually, but they can provide a solid starting point for a complex interaction pattern.

Caveat: The testcase recorder is in alpha-stage, both in terms of functionality and code quality. Your mileage may vary.

Run the tool with:

python tests/click_tests/record_layout.py

The tool accepts the same arguments as trezorctl. For example, to record yourself getting an address, use:

python tests/click_tests/record_layout.py btc get-address -n m/44h/0h/0h/0/0 -d

Instead of clicking buttons on the emulator, type commands in the terminal that ran the tool. A list of possible button clicks will be shown in your terminal. These will be sent to the emulator over debuglink.

(Note that if a particular click does not react through the tool, there is a good chance that it won't work in the testcase either. Please file an issue.)

After the session is over (when you type stop), the tool will collect all layout changes and output a testcase in pytest format. Copy-paste that into your test file and tweak as appropriate.

CI

The complete test suite is running on a public GitLab CI. We currently do not have a CI for external contributors. If needed we will repush your PR to enable the CI.

See this list of CI jobs descriptions for more info.

The CI folder contains all the .yml GitLab files that are included in the main .gitlab.yml to provide some basic structure. All GitLab CI Jobs run inside a docker image, which is built using the present Dockerfile. This image is stored in the GitLab registry.

CI pipeline

(Generated automatically by tools/generate_ci_docs.py. Do not edit by hand.)

It consists of multiple stages below, each having one or more jobs Latest CI pipeline of master branch can be seen at https://gitlab.com/satoshilabs/trezor/trezor-firmware/-/pipelines/master/latest

ENVIRONMENT stage - environment.yml

Connected with creating the testing image for CI.

Consists of 3 jobs below:

.environment

Environment job builds the ci/Dockerfile and pushes the built docker image into our GitLab registry. Since modifications of this Dockerfile are very rare this is a manual job which needs to be triggered on GitLab. Almost all CI jobs run inside this docker image.

environment manual

environment scheduled


PREBUILD stage - prebuild.yml

Static checks on the code.

Consists of 6 jobs below:

style prebuild

Check the code for style correctness and perform some static code analysis. Biggest part is the python one - using flake8, isort, black, pylint and pyright, also checking Rust files by rustfmt and C files by clang-format. Changelogs formats are checked.

common prebuild

Check validity of coin definitions and protobuf files.

gen prebuild

Check validity of auto-generated files.

editor prebuild

Checking format of .editorconfig files.

release commit messages prebuild

Checking the format of release commit messages.

changelog prebuild

Verifying that all commits changing some functionality have a changelog entry or contain [no changelog] in the commit message.


BUILD stage - build.yml

All builds are published as artifacts so they can be downloaded and used.

Consists of 33 jobs below:

core fw regular build

Build of Core into firmware. Regular version. Are you looking for Trezor T firmware build? This is most likely it.

core fw regular debug build

Build of Core into firmware with enabled debug mode. In debug mode you can upload mnemonic seed, use debug link etc. which enables device tests. Storage on the device gets wiped on every start in this firmware.

core fw regular production build

core fw btconly build

Build of Core into firmware. Bitcoin-only version.

core fw btconly debug build

core fw btconly production build

core fw DISC1 build

core fw R debug build

core fw R build

core unix regular build

Non-frozen emulator build. This means you still need Python files present which get interpreted.

core unix regular R build

Non-frozen emulator build for model R.

core unix regular asan build

core unix frozen regular build

Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly.

core unix frozen btconly debug build

Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. See Emulator for more info. Debug mode enabled, Bitcoin-only version.

core unix frozen btconly debug asan build

core unix frozen debug build

Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. Are you looking for a Trezor T emulator? This is most likely it.

core unix frozen R debug build

core unix frozen T3T1 debug build

core unix frozen R debug build arm

core unix frozen T3T1 debug build arm

core unix frozen debug asan build

core unix frozen debug build arm

core macos frozen regular build

crypto build

Build of our cryptographic library, which is then incorporated into the other builds.

legacy fw regular build

legacy fw regular debug build

legacy fw btconly build

legacy fw btconly debug build

legacy emu regular debug build

Regular version (not only Bitcoin) of above. Are you looking for a Trezor One emulator? This is most likely it.

legacy emu regular debug asan build

legacy emu regular debug build arm

legacy emu btconly debug build

Build of Legacy into UNIX emulator. Use keyboard arrows to emulate button presses. Bitcoin-only version.

legacy emu btconly debug asan build


TEST stage - test.yml

All the tests run test cases on the freshly built emulators from the previous BUILD stage.

Consists of 39 jobs below:

core unit python test

Python unit tests, checking core functionality.

core unit python R test

Python unit tests, checking core functionality. For model R.

core unit rust test

Rust unit tests.

core unit asan test

core device test

Device tests for Core. Running device tests and also comparing screens with the expected UI result. See artifacts for a comprehensive report of UI. See docs/tests/ui-tests for more info.

core device R test

core device asan test

core btconly device test

Device tests excluding altcoins, only for BTC.

core btconly device asan test

core monero test

Monero tests.

core monero asan test

core u2f test

Tests for U2F and HID.

core u2f asan test

core fido2 test

FIDO2 device tests.

core fido2 asan test

core click test

Click tests - UI. See docs/tests/click-tests for more info.

core click R test

Click tests. See docs/tests/click-tests for more info.

core click asan test

core upgrade test

Upgrade tests. See docs/tests/upgrade-tests for more info.

core upgrade asan test

core persistence test

Persistence tests - UI.

core persistence asan test

core hwi test

crypto test

legacy device test

Legacy device test - UI.

legacy asan test

legacy btconly test

legacy btconly asan test

legacy upgrade test

legacy upgrade asan test

legacy hwi test

python test

python support test

rust test

storage test

core unix memory profiler

core firmware flash size checker

Finds out how much flash space we have left in the firmware build Fails if the free space is less than certain threshold

core firmware flash size compare master

Compares the current flash space with the situation in the current master Fails if the new binary is significantly larger than the master one (the threshold is defined in the script, currently 5kb). Allowing fir failure, not to prevent the merge. Also generates a report with the current situation

connect test core


TEST-NONENGLISH stage - test-nonenglish.yml

Tests for non-english languages, that run only nightly

  • apart from that, they run also for every branch containing "translations" in its name

Consists of 16 jobs below:

core device test czech

START_DEVICE_TESTS

core device test french

core device test german

core device test spanish

core device R test czech

core device R test french

core device R test german

core device R test spanish

core click test czech

START_CLICK_TESTS

core click test french

core click test german

core click test spanish

core click R test czech

core click R test french

core click R test german

core click R test spanish


POSTTEST stage - posttest.yml

Consists of 2 jobs below:

core unix coverage posttest

unix ui changes


DEPLOY stage - deploy.yml

Consists of 14 jobs below:

release core fw regular deploy

release core fw btconly deploy

release core fw regular debug deploy

release core fw btconly debug deploy

release legacy fw regular deploy

release legacy fw btconly deploy

release legacy fw regular debug deploy

release legacy fw btconly debug deploy

release core unix debug deploy

release legacy unix debug deploy

ui tests fixtures deploy

ui tests fixtures deploy nonenglish

sync emulators to aws

common sync


Assorted knowledge

This file serves as a dumping ground for important knowledge tidbits that do not clearly fit in any particular location. Please add any information that you think should be written down.

At any time, information stored here might be restructured or moved to a different location, so as to ensure that the documentation is well structured overall.

List of third parties

That need to be notified when a protocol breaking change occurs.

Using trezorlib:

This usually requires some code changes in the affected software.

  • Electrum https://github.com/spesmilo/electrum
  • HWI https://github.com/bitcoin-core/HWI
  • Trezor Agent https://github.com/romanz/trezor-agent
  • Shadowlands https://github.com/kayagoban/shadowlands

Using HWI

Updating HWI to the latest version should be enough.

  • BTCPay https://github.com/btcpayserver/btcpayserver
  • Wasabi https://github.com/zkSNACKs/WalletWasabi

Using no Trezor libraries

  • Monero https://github.com/monero-project/monero
  • Mycelium Android https://github.com/mycelium-com/wallet-android
  • Mycelium iOS https://github.com/mycelium-com/wallet-ios
  • Blockstream Green Android https://github.com/Blockstream/green_android
  • Blockstream Green iOS https://github.com/Blockstream/green_ios

Using Connect:

See https://github.com/trezor/connect/network/dependents for a full list of projects depending on Connect.

Connect dependencies introduction

Javascript projects that have Connect as a dependency are using the Connect NPM package on version specified in their yarn.lock (or similar). This NPM package is not a complete Connect library, it is a simple layer that deals with opening an iframe and loading the newest Connect from connect.trezor.io.

Such project must have the newest MAJOR version of this NPM package (v8 at the moment). But then the main logic library (dealing with devices etc.) is fetched from connect.trezor.io and is therefore under our control and can be updated easily.

So in a nutshell:

  • If there is a new MAJOR version of Connect we indeed want to notify these parties below.
  • In other cases we do not, we just need to deploy updated Connect before releasing firmwares.

Notable third-parties

  • Trezor Password Manager https://github.com/trezor/trezor-password-manager
  • Exodus (closed source)
  • MagnumWallet (closed source)
  • CoinMate (closed source)
  • MyEtherWallet https://github.com/MyEtherWallet/MyEtherWallet
  • MyCrypto https://github.com/MyCryptoHQ/MyCrypto
  • MetaMask https://github.com/MetaMask/metamask-extension
  • SimpleStaking https://github.com/simplestaking/wallet
  • AdaLite https://github.com/vacuumlabs/adalite
  • Stellarterm https://github.com/stellarterm/stellarterm
  • frame https://github.com/floating/frame
  • web3-react https://github.com/NoahZinsmeister/web3-react
  • KyberSwap https://github.com/KyberNetwork/KyberSwap
  • Balance Manager https://github.com/balance-io/balance-manager
  • www.coinpayments.net
  • www.bancor.network
  • dubiex.com
  • www.coinmap.org
  • mydashwallet.org
  • app.totle.com
  • manager.balance.io
  • faa.st
  • beta.shapeshift.com

BIP-44 derivation paths

Each coin uses BIP-44 derivation path scheme. If the coin is UTXO-based the path should have all five parts, precisely as defined in BIP-32. If it is account-based we follow Stellar's SEP-0005 - paths have only three parts 44'/c'/a'. Unfortunately, lot of exceptions occur due to compatibility reasons.

Keys are derived according to SLIP-10, which is a superset of the BIP-32 derivation algorithm, extended to work on other curves.

List of used derivation paths

coincurvepathpublic nodenote
Bitcoinsecp256k144'/c'/a'/y/iyes1
Ethereumsecp256k144'/c'/0'/0/ayes2
Ethereumsecp256k144'/c'/0'/ayes2
Ripplesecp256k144'/144'/a'/0/03
EOSsecp256k144'/194'/a'/0/03
Binancesecp256k144'/714'/a'/0/03
Tronsecp256k1TODOTODO
Ontologynist256p1TODOTODO
Cardanoed2551944'/1815'/a'/y/iyes4
Stellared2551944'/148'/a'
NEMed2551944'/43'/a'5
Moneroed2551944'/128'/a'
Tezosed2551944'/1729'/a'6

c stands for the SLIP-44 id of the currency, when multiple currencies are handled by the same code. a is an account number, y is change address indicator (must be 0 or 1), and i is address index.

Paths that do not conform to this table are allowed, but user needs to confirm a warning on Trezor.

Public nodes

Some currencies allow exporting a public node, which lets the client derive all non-hardened paths below it. In that case, the conforming path is equal to the hardened prefix.

I.e., for Bitcoin's path 44'/c'/a'/y/i, the allowed public node path is 44'/c'/a'.

Trezor does not check if the path is followed by other non-hardened items (anyone can derive those anyway). This is beneficial for Ethereum and its MEW compatibility, which sends 44'/60'/0'/0 for getPublicKey.

Notes

  1. For Bitcoin and its derivatives it is a little bit more complicated. p is decided based on the following table:

    ptypeinput script type
    44legacySPENDADDRESS
    48legacy multisigSPENDMULTISIG
    49p2sh segwitSPENDP2SHWITNESS
    84native segwitSPENDWITNESS

    Other p are disallowed.

  2. We believe this should be 44'/c'/a', because Ethereum is account-based, rather than UTXO-based. Unfortunately, lot of Ethereum tools (MEW, Metamask) do not use such scheme and set a = 0 and then iterate the address index i. Therefore for compatibility reasons we use the same scheme. Also to support "Ledger Live" legacy paths we allow 44'/60'/0'/a paths.

  3. Similar to Ethereum this should be 44'/c'/a'. But for compatibility with other HW vendors we use 44'/c'/a'/0/0.

  4. Cardano has CIP-0003 algorithm that allows non-hardened derivation on ed25519.

  1. NEM's path should be 44'/43'/a' as per SEP-0005, but we allow 44'/43'/a'/0'/0' as well for compatibility reasons with NanoWallet.

  2. Tezos supports multiple curves, but Trezor currently supports ed25519 only.

Sign message paths are validated in the same way as the sign tx paths are.

Allowed values

For UTXO-based currencies, account number a needs to be in the interval [0, 20] and address index i in the interval [0, 1 000 000].

For account-based currencies (i.e., those that do not use address indexes), account number a needs to be in the interval [0, 1 000 000]

Contribute to Trezor Firmware

Please read the general instructions you can find on our wiki.

Your Pull Request should follow these criteria:

  • The code is properly tested.
  • Tests must pass on CI.
  • The code is properly formatted. Use make style_check to check the format and make style to do the required changes.
  • The generated files are up-to-date. Use make gen in repository root to make it happen.
  • Commits must have concise commit messages, we endorse Conventional Commits.
  • A changelog entry must be part of the pull request.

Please read and follow our review procedure.

Adding a new coin

Forks and derivatives

If the coin you are adding is a fork of Bitcoin or other cryptocurrency we already support (in other words, new app is not needed) you can modify the definitions in the trezor-firmware repository and file a PR. In such case the coin does not have to be in TOP30 (see below), but we still reserve the right not to include the coin. The location depends on the type of the asset to be added:

Other

At the moment, we do not have the capacity to add new coins that do not fit the aforementioned category. Our current product goal is to unite what we support in firmware and in Trezor Suite and since firmware is way ahead of Suite we want to pause for a bit, implement the remaining coins into Suite and then consider adding new coins. This effectively means that our team will not be accepting any requests to add new cryptocurrencies.

Firmware update and device wipe

This document describes under which circumstances the device gets wiped during a firmware update.

Trezor 1

The device gets wiped:

  • If the firmware to be installed is unsigned.
  • If the present firmware is unsigned.
  • If the firmware to be installed has lower version than the current firmware's fix_version [1].

The device gets wiped on every reboot:

  • If the firmware's debug mode is turned on.

Trezor T

In Trezor T this works a bit differently, we have introduced so-called vendors headers. Each firmware has its vendor header and this vendor header is signed by SatoshiLabs. The actual firmware is signed by the vendor header's key. That means that all firmwares are signed by someone to be able to run on Trezor T.

We currently have two vendors:

  1. SatoshiLabs
  2. UNSAFE DO NOT USE

As the names suggest, the first one is the official SatoshiLabs vendor header and all public firmwares are signed with that. The second one is meant for generic audience; if you build firmware this vendor header is automatically applied and the firmware is signed with it (see core/tools/headertool.py).

The device gets wiped:

  • If the firmware to be installed is from different vendor than the present firmware [2].
  • If the firmware to be installed has lower version than the current firmware's fix_version [1].

The device gets wiped on every reboot:

  • If the firmware's debug mode is turned on.

[1] Firmware contains a fix_version, which is the lowest version to which that particular firmware can be downgraded without wiping storage. This is typically used in case the internal storage format is changed. For example, in version 2.2.0, we have introduced Wipe Code, which introduced some changes to storage that the older firmwares (e.g. 2.1.8) would not understand. It can also be used to enforce security fixes.

[2] The most common example is if you have a device with the official firmware (SatoshiLabs) and you install the unofficial (UNSIGNED) firmware -> the device gets wiped. Same thing vice versa.

Generated files

Certain files in the repository are auto-generated from other sources, but the generated content is stored in Git. The command make gen_check, run from CI, ensures that the generated content matches its sources. The command make gen regenerates all relevant files.

In general, generated files are not compatible between branches. After rebasing or merging a different branch, you should immediately run make gen and make sure the result is committed.

Do not fix merge conflicts in generated files. Instead, run make gen and commit the result.

The following is a (possibly incomplete) list of files regenerated by make gen:

  • core/mocks/generated: mock Python stubs for C modules (modtrezor*). Generated from special comments in embed/extmod/modtrezor*.
  • networks.py, tokens.py, coininfo.py and nem_mosaics.py in their respective subdirectories of core/src/apps. In general, any file matching *.py.mako has a corresponding *.py file generated from the Mako template. These files are based on coin data from common/defs.
  • Protobuf class definitions in core/src/trezor/messages and python/src/trezorlib/messages. Generated from common/protob/*.proto.

Git Hooks

Install

Copy docs/git/hooks/ files to .git/hooks/ to activate them. Run in root:

cp docs/git/hooks/* .git/hooks/

Monorepo notes

Generating

Use the create_monorepo script to regenerate from current master(s).

Structure

This is a result of Git merge of several unrelated histories, each of which is moved to its own subdirectory during the merge.

That means that this is actually all the original repos at the same time. You can check out any historical commit hash, or any historical tag.

All tags from the previous history still exist, and in addition, each has a version named by its directory. I.e., for trezor-mcu tag v1.6.3, you can also check out legacy/v1.6.3.

Merging pre-existing branches

Because the repository shares all the histories, merging a branch or PR can be done with a simple git merge. It's often necessary to add hints to git by specifying a merge strategy - especially when some commits add new files.

Use the following options: -s subtree -X subtree=<destdir>.

Example for your local checkout:

$ git remote add core-local ~/git/trezor-core
$ git fetch core-local
$ git merge core-local/wip -s subtree -X subtree=core

Same options should be used for git rebase of a pre-existing branch.

Sub-repositories

The monorepo has two subdirectories that can be exported to separate repos:

  • common exports to https://github.com/trezor/trezor-common
  • crypto exports to https://github.com/trezor/trezor-crypto

These exports are managed with git-subrepo tool. To export all commits that touch one of these directories, run the following command:

$ git subrepo push <dirname>

You will need commit access to the respective GitHub repository.

For installation instructions and detailed usage info, refer to the git-subrepo README.


Sketch of further details:

What git-subrepo does under the hood is create and fetch a remote for the export, check out parent revision and replay all commits since commit using something along the lines of git filter-branch --subdirectory-filter.

So basically a nicely tuned git-subtree.

This can all be done manually if need be (or if you need more advanced usecases like importing changes from the repo commit-by-commit, because git-subrepo will squash on import). See this nice article for hints.

Review Process

  • File a Pull Request (PR) with a number of well-defined clearly described commits. Multiple commits per PR are allowed, but please do not include revert commits, etc. Use rebase.
  • Do not use merge (e.g. merge trezor/master into ...). Again, use rebase.
  • The general review workflow goes as follows:
    1. The author creates a PR. They should make sure it passes lints and anything that can be run quickly on their computer. When creating the PR, the author should also add links to any resources which might be helpful to the reviewer, namely the issue that the PR resolves or a design document, if there is one.
    2. The author assigns a reviewer. In some cases two or more reviewers can be assigned. One reason to do this is if the code under review is security-critical and deserves two pairs of eyes. Another reason is if the PR touches two very distinct areas of the codebase and the author wants a different specialist to review each one. In the latter case the author should clearly state who should review which part.
    3. The reviewer reviews the PR. In case they find something, they create a comment using the Github review system.
    4. The author implements the required changes and pushes the new commits. The author should never force-push during code review. If an earlier commit needs to be fixed, then instead of fixing it and force-pushing, it should be fixed by adding a so called fixup commit. This can be done by git commit --fixup [commithash] which creates a new commit with the message "fixup! [orig_message]", where orig_message is the commit message of the commithash it "fixes". If the fixes are across multiple earlier commits, then they need to be split into multiple fixup commits.
    5. The author informs the reviewer with a simple comment "done" or similar to tell the reviewer their comment was implemented. Bonus points for including a revision of the fixup commit.
    6. The reviewer reviews the modifications and when they are finally satisfied they resolve the Github comment.
    7. The reviewer finally approves the PR with the green tick.
    8. The author runs git rebase -i [main branch] --autosquash which squashes the fixup commits into their respective parents and then force-pushes the rebase branch.
    9. The author makes a final check and merges the PR. If the rebase involved resolving some complicated merge conflicts, then the author may ask the reviewer for a final check. GitHub's Rebase and merge should be used, not Squash and merge.
    10. The author deletes their branch.

Example

If you find the description too difficult, then here is an example to make it more clear.

Andrew tries to add a number of commits very well structured and with nice and consistent commit messages. These will not be squashed together.

Matějčík starts to review and finds something he would like to improve:

Andrew responds with a commit hash 55d883b informing that he has accepted and implemented the comment.

This commit is a fixup commit. Since it is a new commit he does not have to force-push. In the following image he is fixing the "test: Add device tests..." commit.

This way we can end up with number of fixup commits at the end of the review. Note that there is one commit in the following image that is not a fixup commit. That's totally okay in case it makes sense and the author indeed wants it as a separate commit.

Matějčík is happy and approves the PR. After that Andrew squashes his commits via git rebase -i [main branch] --autosquash. This command will squash the fixup commits into their respective places modifying the original commits. After this he force-pushes. As you can see the history we end up with is very nice.

We merge the PR and that's it!

Notes & Rationale

  • If you want to fixup the latest commit, just use git commit --fixup HEAD as written above. If you want to fixup some older commit, use git commit --fixup [commithash]. Or any other reference for that matter.
  • More good git rebase tips can be found at this Atlassian website.
  • Some rationale why we avoid force pushing during code review, i.e. during the period starting with the creation of the PR until the last approval is given:
    1. Force pushing often makes it impossible to see the changes made by the author, so the reviewer has to go through the entire PR again. If it's just an amendment, then GitHub can show the differences, but in more complicated situations it's unable to untangle what happened. Especially if you rebase over master, which adds lots of new changes.
    2. A fixup commit can be easily referenced in the response to the reviewer's comment.
    3. Force pushing often breaks hyperlinks in GitHub comments, which is a real nuisance when somebody is referencing some code in their comment and you have no clue what they are talking about.
    4. It has led to code review comments being lost on multiple occasions. This seems to happen especially if you comment on a particular commit.
  • What to do if you really need to rebase over master during an ongoing code review? This happens rarely, but if it's really necessary in order to implement the requested revisions, then:
    1. Try to resolve as many reviewer comments as possible before rebasing.
    2. Ask the reviewers for approval to go ahead with the rebase, i.e. give them time to confirm that the comments have been well resolved, avoiding as many of the problems mentioned above as possible.
    3. Rebase, do the stuff you need, force-push for a second round of review.

Changelog

Our releases are accompanied by changelogs based on the Keep a Changelog format. We are using the towncrier utility to generate them at the time a new version is released. There are currently 8 such changelogs for different components of the repository:

Adding changelog entry

towncrier aims to create changelogs that are convenient to read, at the expense of being somewhat inconvenient to create.

Furthermore every changelog entry should be linked to a GitHub issue or pull request number. If you don't want to create an issue just to satisfy this rule you can use self-reference to your change's pull request number by first creating the PR and then adding the entry. If this is not suitable, the word noissue can be used in place of the issue number.

There are a few types of changelog entries, as described by the Keep a Changelog format:

  • added
  • changed
  • deprecated
  • removed
  • fixed
  • security
  • incompatible (for backwards incompatible changes)

Entries are added by creating files in the .changelog.d directory where the file name is <number>.<type> and contains single line describing the change. As an example, an entry describing bug fix for issue 1234 in Trezor T firmware is added by creating file core/.changelog.d/1234.fixed. The file can be formatted with markdown. If more entries are desired for single issue number and type you can add numeral suffix, e.g. 1234.fixed.1, 1234.fixed.2, etc.

You can also add this entry using your $VISUAL editor by running towncrier create --edit 1234.fixed in the core directory.

Changes that only affect a subset of hardware models

If an entry is only relevant for certain model, put the internal name in square brackets at the beginning of the entry. If there are multiple relevant models, separate them by comas. Examples: [T2T1] Fix some bug., [T2T1,T2W1] Fix all bugs..

Not adding changelog entry

If you don't add an entry for changes in your branch, the changelog prebuild CI job will remind you by failing. Sometimes adding an entry does not really make sense, in that case you can include [no changelog] anywhere in the commit message to exclude that commit from the check.

Generating changelog at the time of release

When it's time to release new version of a repository component the formatted changelog needs to be generated using the tools/generate-changelog.py script. It accepts repo subdirectory and the version number as arguments and you can specify the release date if it's different from today's date:

tools/generate-changelog.py --date "20th April 2021" legacy/firmware 1.10.0

Cherry-picking changes to release branch

Branches named release/YY.MM already have their corresponding CHANGELOG.md section generated. When cherry-picking bug fix to such branch you need to bypass towncrier and edit CHANGELOG.md directly.

Trezor Optimized Image Format (TOIF)

All multibyte integer values are little endian!

offsetlengthnamedescription
0x00003magicTOI
0x00031fmtdata format: f or g (see below)
0x00042widthwidth of the image
0x00062heightheight of the image
0x00084datasizelength of the compressed data
0x000A?datacompressed data (see below)

Format

TOI currently supports 4 variants:

  • f: full-color big endian
  • F: full-color little endian
  • g: gray-scale odd high
  • G: gray-scale even high

Full-color

For each pixel a 16-bit value is used. First 5 bits are used for red component, next 6 bits are green, final 5 bits are blue:

1514131211109876543210
RRRRRGGGGGGBBBBB

The data is stored according to endianness.

Gray-scale

Each pixel is encoded using a 4-bit value. Each byte contains color of two pixels:

Odd high:

76543210
PoPoPoPoPePePePe

Even high:

76543210
PePePePePoPoPoPo

Where Po is odd pixel and Pe is even pixel.

Compression

Pixel data is compressed using DEFLATE algorithm with 10-bit sliding window and no header. This can be achieved with ZLIB library by using the following:

import zlib
z = zlib.compressobj(level=9, wbits=-10)
zdata = z.compress(pixeldata) + z.flush()

TOIF tool

Tool for converting PNGs into TOI format and back, see the following links for more:

Developers guide

Libraries

This page was migrated from the old wiki, the content might be outdated.

There are several libraries available which can be used to implement Trezor device functionalities.

SatoshiLabs libraries

Trezor Connect (Web)

https://github.com/trezor/trezor-suite/tree/develop/packages/connect

Python-trezor

https://github.com/trezor/trezor-firmware/tree/master/python

https://pypi.org/project/trezor/

Android

https://github.com/trezor/trezor-android

Go

https://github.com/trezor/trezord-go

Third-party libraries

Check these third-party libraries for Trezor device.

Java

For Trezor-java library by Gary Rowe, see this GitHub page.

Trezor.Net

Trezor.Net supports Android, UWP, .NET Core and .NET Framework with Hid.Net. Support for other platforms can be added with the Hid.Net dependency injection. See this page or this GitHub page.

Hello world feature in TT

(How to develop on Trezor)

Overview

This document shows the creation of a custom functionality (feature, application) on TT. It explains how to build both the Trezor (device, core) logic, as well as the client (computer, host, trezorlib) logic needed to speak with Trezor. For most new features, also the communication layer between Trezor and computer (protobuf) needs to be modified, to set up the messages they will exchange.

Intermediate knowledge of python and linux environment is assumed here to easily follow along. For steps how to set up the Trezor dev environment, refer to other docs - build or emulator. The most important part is being in the poetry shell of this project, so all dependencies are installed.

Feature description

We will implement a simple hello-world feature where Trezor gets some information from the host, will do something with it (optionally shows something on the screen), and returns some information back to the host, where we want to display them. (Note that there are no cryptographic operations involved in this example, it focuses only on basic communication between Trezor and host.)

Implementation

As already mentioned, to get something useful from Trezor, writing device logic is not enough. We need to have a specific communication channel between the computer and Trezor, and also the computer needs to know how to speak to the device to trigger wanted action.

TLDR: implementation in a single commit

1. Communication part (protobuf)

Communication between Trezor and the computer is handled by a protocol called protobuf. It allows for the creation of specific messages (containing clearly defined data) that will be exchanged. More details about this can be seen in docs.

Trezor on its own cannot send data to the computer, it can only react to a "request" message it recognizes and send a "response" message. Both of these messages will need to be specified, and both parts of communication will need to understand them.

Protobuf messages are defined in common/protob directory in .proto files. When we are creating a brand-new feature (application), it is worth creating a new .proto file dedicated only for this feature. Let's call it messages-hello.proto and fill it with the content below.

common/protob/messages-helloworld.proto

syntax = "proto2";
package hw.trezor.messages.helloworld;

// Sugar for easier handling in Java
option java_package = "com.satoshilabs.trezor.lib.protobuf";
option java_outer_classname = "TrezorMessageHelloWorld";

import "messages.proto";

/**
 * Request: Hello world request for text
 * @next HelloWorldResponse
 * @next Failure
 */
message HelloWorldRequest {
    required string name = 1;
    optional uint32 amount = 2 [default=1];
    optional bool show_display = 3;
}

/**
 * Response: Hello world text
 * @end
 */
 message HelloWorldResponse {
    required string text = 1;
}

There are some officialities at the top, the most important things are the message declarations. We are defining a HelloWorldRequest, that will be sent from the computer to Trezor, and HelloWorldResponse, that will be sent back from Trezor. There are many features and data-types protobuf supports - see Google docs or other common/protob/messages-*.proto files.

After defining the details of communication messages, we will also need to give these messages their unique IDs and specify the direction in which they are sent (into Trezor or from Trezor). That is done in common/protob/messages.proto file. We will append a new block at the end of the file:

common/protob/messages.proto

// Hello world
MessageType_HelloWorldRequest = 900 [(wire_in) = true];
MessageType_HelloWorldResponse = 901 [(wire_out) = true];

After this, we are almost done with protobuf! The only thing left is to run make gen in the root directory to create all the auto-generated files. By running this, the protobuf definitions will be translated into python classes in both core and python sub-repositories, so that they can understand these messages. Files under core/src/trezor and python/src/trezorlib should be modified by this.

Optional step

This feature will be implemented only on TT and not the older T1 model. If we want to be compatible with CI, we need to define these messages as unused for T1. That is done in legacy/firmware/protob/Makefile, where we will extend the SKIPPED_MESSAGES variable:

legacy/firmware/protob/Makefile

SKIPPED_MESSAGES := ... \
	HelloWorldRequest HelloWorldResponse

2. Trezor part (core)

The second part deals with creating the "application code" on Trezor. Surprisingly, this part is probably the easiest one from all three parts here (as this is just hello-world example).

All the applications running on Trezor are situated under core/src/apps directory. We could create a new application, or reuse the existing one if the feature logically corresponds to it. We will choose to implement this feature under misc application, as it is really a miscellaneous one.

We can therefore create a file core/src/apps/misc/hello_world.py and fill it with the content below:

core/src/apps/misc/hello_world.py

from typing import TYPE_CHECKING

from trezor.messages import HelloWorldResponse
from trezor.ui.layouts import confirm_text

if TYPE_CHECKING:
    from trezor.messages import HelloWorldRequest


async def hello_world(msg: HelloWorldRequest) -> HelloWorldResponse:
    text = _get_text_from_msg(msg)
    if msg.show_display:
        await confirm_text(
            "confirm_hello_world",
            "Hello world",
            text,
            description="Hello world example",
        )
    return HelloWorldResponse(text=text)


def _get_text_from_msg(msg: HelloWorldRequest) -> str:
    return msg.amount * f"Hello {msg.name}!\n"

Note that we need to import the newly created protobuf messages (HelloWorldRequest and HelloWorldResponse) to provide type hints and to be able to return the response. We are also importing a UI layout so that we can show a confirmation dialog.

All the protobuf fields are accessible on the msg object and are accessed via dot notation like class attributes (msg.show_display). When instantiating the response object, keyword-arguments need to be used - HelloWorldResponse(text=text).

Even though the code in core is run by a micropython interpreter, almost all basic features from "classic" python are supported - like f-strings here.

As we want to also write unittests for this module, we define a helper function _get_text_from_msg, even though it could easily be inlined in this case.

To see the details about code style and conventions, refer to codestyle.md.

We have defined all the logic, but it is not being called anywhere. We need to register the function to be called as a response to the appropriate message - in our case HelloWorldRequest. Registration is done in core/src/apps/workflow_handlers.py and the following two lines need to be added there (ideally under the misc section):

core/src/apps/workflow_handlers.py

if msg_type == MessageType.HelloWorldRequest:
    return "apps.misc.hello_world"

The above will make sure that the msg (of type HelloWorldRequest) will be supplied into the hello_world function we created.

Lastly, running make gen in the root directory makes sure the new misc/hello_world.py module will be discovered. core/src/all_modules.py should be modified as a result.

These are all the necessary code changes in core. For this code to work, we will still need to build it, but that will be done in Part 4. Next, we will focus on the client implementation.

3. Host part (trezorlib)

So far we have defined the messages going to the Trezor and back and the Trezor logic itself. What remains is the code sitting on the computer and sending these messages into Trezor and receiving them.

There are more ways how to achieve this, for example Connect is a way of communicating with Trezor from a web browser. However, we will decide to implement this connection via trezorlib, our own python library, which lives under python/src/trezorlib and acts as a CLI (Command-line interface) to communicate with Trezor (via trezorctl command).

This implementation will be split into two parts, as we will create the Trezor-communication logic in one file and the CLI logic taking arguments and calling this code in the second file. (It would be possible to define everything at once in the CLI file, but we want the possibility to call the Trezor-speaking function separately, for example when testing.)

We will create the python/src/trezorlib/hello_world.py file and fill it with code to speak with Trezor:

python/src/trezorlib/hello_world.py

from typing import TYPE_CHECKING, Optional

from . import messages
from .tools import expect

if TYPE_CHECKING:
    from .client import TrezorClient
    from .protobuf import MessageType


@expect(messages.HelloWorldResponse, field="text", ret_type=str)
def say_hello(
    client: "TrezorClient",
    name: str,
    amount: Optional[int],
    show_display: bool,
) -> "MessageType":
    return client.call(
        messages.HelloWorldRequest(
            name=name,
            amount=amount,
            show_display=show_display,
        )
    )

Code above is sending HelloWorldRequest into Trezor and is expecting to get HelloWorldResponse back (from which it extracts the text string as a response).

This function is then called from the CLI function, which we will define in python/src/trezorlib/cli/hello_world.py.

python/src/trezorlib/cli/hello_world.py

from typing import TYPE_CHECKING, Optional

import click

from .. import hello_world
from . import with_client

if TYPE_CHECKING:
    from ..client import TrezorClient


@click.group(name="helloworld")
def cli() -> None:
    """Hello world commands."""


@cli.command()
@click.argument("name")
@click.option("-a", "--amount", type=int, help="How many times to greet.")
@click.option(
    "-d", "--show-display", is_flag=True, help="Whether to show confirmation screen."
)
@with_client
def say_hello(
    client: "TrezorClient", name: str, amount: Optional[int], show_display: bool
) -> str:
    """Simply say hello to the supplied name."""
    return hello_world.say_hello(client, name, amount, show_display=show_display)

Code above is importing the hello_world module defined before and is calling its say_hello() function with arguments received from the user. We are using click library to create the CLI - first the helloworld group and then the say_hello command (which is invoked by say-hello).

Example of calling the say_hello function via command line is trezorctl helloworld say-hello George -a 3 -d, which utilizes all the defined arguments and options (only the name argument is required here).

However, the command above will not work yet, as the helloworld group is not registered in the main CLI file - python/src/trezorlib/cli/trezorctl.py. It will therefore need some small modifications - importing the new module and registering it:

python/src/trezorlib/cli/trezorctl.py

from . import helloworld
...
cli.add_command(hello_world.cli)

If we are currently in poetry shell, the trezorctl command is being evaluated directly from the source code in python/src/trezorlib. That means it should be able to understand our example command trezorctl helloworld say-hello George -a 3 -d.

The example command on its own will however not work without listening Trezor which understands the new messages. In the next and final part, we will build and spawn a Trezor on our computer with all the changes made in Part 1 and 2.

4. Putting it together

Looks like all the code changes have been done, the final part is to build a Trezor image - emulator - so that we can actually run and test all the logic we created.

Detailed information about the emulator can be found in its docs, but we only need two most important commands, that will build and spawn the emulator:

cd core
make build_unix
./emu.py

After this, the emulator screen should be visible. Trying our example command should give a nice confirmation screen, and when confirming it with the green button, we should see the output in our terminal.

$ trezorctl helloworld say-hello George -a 3 -d
Please confirm action on your Trezor device.
Hello George!
Hello George!
Hello George!

For building the new feature into a physical Trezor, refer to embedded.

Testing

It is always good to include some tests exercising the created functionality, so when we break it later, it will be noticed. Trezor model T supports both unit tests and integration tests (which are called device tests).

Unit tests

docs

Unit tests can verify individual (mostly helper) functions that have clearly defined inputs and outputs.

They are stored under core/tests and can be run by make test in the core directory.

To call a specific test (the one we are about to create), run make test TESTOPTS=test_apps.misc.hello_world.py

core/tests/test_apps.misc.hello_world.py

from common import *

from trezor.messages import HelloWorldRequest
from apps.misc.hello_world import _get_text_from_msg


class TestHelloWorld(unittest.TestCase):
    def test_get_text_from_msg(self):
        msg = HelloWorldRequest(name="Satoshi", amount=2, show_display=False)
        self.assertEqual(_get_text_from_msg(msg), "Hello Satoshi!\nHello Satoshi!\n")


if __name__ == "__main__":
    unittest.main()

Code above is using the unittest testing framework, however not directly from python's standard library. As these unit tests are run by micropython, which does not have unittest library, we had to create the functionality ourselves in core/tests/unittest.py - see from common import *.

Current code checks one usage of _get_text_from_msg, the only deterministic helper function we use in our feature. One could create many test vectors trying different inputs and expecting different outputs.

Device tests

docs

Device tests (our name for integration tests) should test the whole workflow from sending the first request into Trezor to Trezor sending the final response.

trezorlib is used extensively in these tests as a way to request something from Trezor and then assert the expected response (it actually uses the code we created in Part 3).

They are closely connected with ui tests, which assert Trezor's screens have a known and expected content during the device tests.

Device tests are stored in tests/device_tests and they can be run by make test_emu in core. Running the specific file we will create can be done by make test_emu TESTOPTS="-k test_hello_world.py".

tests/device_tests/misc/test_hello_world.py

from typing import Optional

import pytest

from trezorlib import hello_world
from trezorlib.debuglink import TrezorClientDebugLink as Client

VECTORS = (  # name, amount, show_display
    ("George", 2, True),
    ("John", 3, False),
    ("Hannah", None, False),
)


@pytest.mark.skip_t1b1
@pytest.mark.parametrize("name, amount, show_display", VECTORS)
def test_hello_world(
    client: Client, name: str, amount: Optional[int], show_display: bool
):
    with client:
        greeting_text = hello_world.say_hello(
            client, name=name, amount=amount, show_display=show_display
        )
        greeting_lines = greeting_text.strip().splitlines()

        assert len(greeting_lines) == amount or 1
        assert all(name in line for line in greeting_lines)

Unlike in unit tests, pytest is used as the test framework, which is more suitable for bigger and more complex test suites.

As the functionality is developed only for TT, to not break the CI, we want to skip this test's execution for model T1, by adding @pytest.mark.skip_t1b1 decorator.

We are also using the @pytest.mark.parametrize decorator, which is an efficient way of testing multiple inputs into the same test case.

We are not asserting the exact result of the greeting (that is done by unit tests), we just check it has the expected structure - but we can check really anything here.

Note the usage of trezorlib.hello_world.say_hello, which we defined earlier, so we see how it can be useful for testing purposes.

Optional step

If we want to be fully compatible with CI, we need to create expected UI-test results. The most straightforward way to do it is to run make test_emu_ui_record in core directory.

Conclusion

All changes in one commit can be seen here.

Ideas for potentially useful Trezor features are welcome. Feel free to submit issues and open PRs, even if incomplete.