From ee68a694c18cf665d9f96a374a7f68ec6c0de06d Mon Sep 17 00:00:00 2001 From: Tomas Krnak Date: Wed, 6 Jul 2022 15:06:34 +0200 Subject: [PATCH] feat(core): add support for Zcash unified addresses --- common/protob/messages-zcash.proto | 17 +++ core/.changelog.d/2371.added | 1 + core/SConscript.firmware | 14 +- core/SConscript.unix | 14 +- .../extmod/modtrezorutils/modtrezorutils.c | 6 + .../extmod/rustmods/modtrezororchardlib.c | 54 ++++++++ core/embed/rust/Cargo.lock | 39 ++++++ core/embed/rust/Cargo.toml | 8 +- core/embed/rust/librust.h | 6 + core/embed/rust/src/lib.rs | 2 + .../rust/src/orchardlib/f4jumble_bridge.rs | 36 +++++ core/embed/rust/src/orchardlib/mod.rs | 1 + core/mocks/generated/trezororchardlib.pyi | 11 ++ core/mocks/generated/trezorutils.pyi | 1 + core/src/all_modules.py | 4 + core/src/apps/bitcoin/sign_tx/__init__.py | 21 +-- core/src/apps/common/coininfo.py | 117 ++++++++-------- core/src/apps/common/coininfo.py.mako | 23 +++- core/src/apps/zcash/addresses.py | 128 ++++++++++++++++++ core/src/apps/zcash/signer.py | 33 ++++- core/src/trezor/crypto/__init__.py | 3 + .../src/trezor/enums/ZcashReceiverTypecode.py | 8 ++ core/src/trezor/enums/__init__.py | 6 + core/src/trezor/messages.py | 1 + core/src/trezor/utils.py | 1 + core/tests/test_apps.zcash.addresses.py | 64 +++++++++ python/src/trezorlib/messages.py | 7 + tests/device_tests/zcash/test_sign_tx.py | 57 ++++++++ tests/ui_tests/fixtures.json | 1 + 29 files changed, 605 insertions(+), 79 deletions(-) create mode 100644 common/protob/messages-zcash.proto create mode 100644 core/.changelog.d/2371.added create mode 100644 core/embed/extmod/rustmods/modtrezororchardlib.c create mode 100644 core/embed/rust/src/orchardlib/f4jumble_bridge.rs create mode 100644 core/embed/rust/src/orchardlib/mod.rs create mode 100644 core/mocks/generated/trezororchardlib.pyi create mode 100644 core/src/apps/zcash/addresses.py create mode 100644 core/src/trezor/enums/ZcashReceiverTypecode.py create mode 100644 core/tests/test_apps.zcash.addresses.py diff --git a/common/protob/messages-zcash.proto b/common/protob/messages-zcash.proto new file mode 100644 index 00000000000..b3075aefecd --- /dev/null +++ b/common/protob/messages-zcash.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; +package hw.trezor.messages.zcash; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageZcash"; + +/** + * Receiver typecodes for unified addresses. + * see: https://zips.z.cash/zip-0316#encoding-of-unified-addresses + */ +enum ZcashReceiverTypecode { + P2PKH = 0; + P2SH = 1; + SAPLING = 2; + ORCHARD = 3; +} diff --git a/core/.changelog.d/2371.added b/core/.changelog.d/2371.added new file mode 100644 index 00000000000..6966402d82c --- /dev/null +++ b/core/.changelog.d/2371.added @@ -0,0 +1 @@ +Add support for Zcash unified addresses diff --git a/core/SConscript.firmware b/core/SConscript.firmware index d685afd26a5..d87b05eb475 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -5,6 +5,7 @@ import os BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' +USE_ZCASH = BITCOIN_ONLY == '1' # Zcash parasitizes on BITCOIN_ONLY free memory TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R') @@ -198,6 +199,10 @@ SOURCE_MOD += [ SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', ] +if USE_ZCASH: + SOURCE_MOD += [ + 'embed/extmod/rustmods/modtrezororchardlib.c', + ] if UI2: SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorui2.c', @@ -417,7 +422,7 @@ if FEATURE_FLAGS["SYSTEM_VIEW"]: SOURCE_QSTR = SOURCE_MOD + SOURCE_MICROPYTHON + SOURCE_MICROPYTHON_SPEED -env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s -DPYOPT=%s -DBITCOIN_ONLY=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0'), PYOPT, BITCOIN_ONLY)) +env = Environment(ENV=os.environ, CFLAGS='%s -DPYOPT=%s -DBITCOIN_ONLY=%s -DUSE_ZCASH=%s' % (ARGUMENTS.get('CFLAGS', ''), PYOPT, BITCOIN_ONLY, '1' if USE_ZCASH else '0')) env.Tool('micropython') @@ -668,13 +673,14 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/tezos/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Tezos*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py')) + + if EVERYTHING or USE_ZCASH: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) source_mpy = env.FrozenModule(source=SOURCE_PY, source_dir=SOURCE_PY_DIR, bitcoin_only=BITCOIN_ONLY) @@ -712,6 +718,8 @@ def cargo_build(): profile = '' features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}'] + if USE_ZCASH: + features.append('use_zcash') if BITCOIN_ONLY == '1': features.append('bitcoin_only') if UI2: diff --git a/core/SConscript.unix b/core/SConscript.unix index c25358d8cf7..e89916792bc 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -5,6 +5,7 @@ import os BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' +USE_ZCASH = True TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R') @@ -198,6 +199,10 @@ SOURCE_MOD += [ SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', ] +if USE_ZCASH: + SOURCE_MOD += [ + 'embed/extmod/rustmods/modtrezororchardlib.c', + ] if UI2: SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorui2.c', @@ -363,7 +368,7 @@ if PYOPT == '0' or not FROZEN: else: STATIC="" -env = Environment(ENV=os.environ, CFLAGS='%s -DPYOPT=%s -DBITCOIN_ONLY=%s %s' % (ARGUMENTS.get('CFLAGS', ''), PYOPT, BITCOIN_ONLY, STATIC)) +env = Environment(ENV=os.environ, CFLAGS='%s -DPYOPT=%s -DBITCOIN_ONLY=%s -DUSE_ZCASH=%s %s' % (ARGUMENTS.get('CFLAGS', ''), PYOPT, BITCOIN_ONLY, '1' if USE_ZCASH else '0', STATIC)) env.Tool('micropython') @@ -624,13 +629,14 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/tezos/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Tezos*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py')) + + if EVERYTHING or USE_ZCASH: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) source_mpy = env.FrozenModule(source=SOURCE_PY, source_dir=SOURCE_PY_DIR, bitcoin_only=BITCOIN_ONLY) @@ -666,6 +672,8 @@ RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a' def cargo_build(): features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}'] + if USE_ZCASH: + features.append('use_zcash') if BITCOIN_ONLY == '1': features.append('bitcoin_only') if UI2: diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils.c b/core/embed/extmod/modtrezorutils/modtrezorutils.c index 0768ccf1145..1f95a9ccc75 100644 --- a/core/embed/extmod/modtrezorutils/modtrezorutils.c +++ b/core/embed/extmod/modtrezorutils/modtrezorutils.c @@ -271,6 +271,7 @@ STATIC mp_obj_str_t mod_trezorutils_revision_obj = { /// MODEL: str /// EMULATOR: bool /// BITCOIN_ONLY: bool +/// USE_ZCASH: bool /// FIRMWARE_SECTORS_COUNT: int STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { @@ -316,6 +317,11 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { #else {MP_ROM_QSTR(MP_QSTR_BITCOIN_ONLY), mp_const_false}, #endif +#if USE_ZCASH + {MP_ROM_QSTR(MP_QSTR_USE_ZCASH), mp_const_true}, +#else + {MP_ROM_QSTR(MP_QSTR_USE_ZCASH), mp_const_false}, +#endif }; STATIC MP_DEFINE_CONST_DICT(mp_module_trezorutils_globals, diff --git a/core/embed/extmod/rustmods/modtrezororchardlib.c b/core/embed/extmod/rustmods/modtrezororchardlib.c new file mode 100644 index 00000000000..559a3c686e1 --- /dev/null +++ b/core/embed/extmod/rustmods/modtrezororchardlib.c @@ -0,0 +1,54 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "py/runtime.h" + +#if USE_ZCASH + +#include "librust.h" + +/// def f4jumble(message: bytearray) -> None: +/// """Mutates a message by F4Jumble permutation.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_orchardlib_f4jumble, orchardlib_f4jumble); + +/// def f4jumble_inv(message: bytearray) -> None: +/// """Mutates a message by F4Jumble inverse permutation.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_orchardlib_f4jumble_inv, + orchardlib_f4jumble_inv); + +STATIC const mp_rom_map_elem_t mp_module_trezororchardlib_globals_table[] = { + {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezororchardlib)}, + {MP_ROM_QSTR(MP_QSTR_f4jumble), MP_ROM_PTR(&mod_orchardlib_f4jumble)}, + {MP_ROM_QSTR(MP_QSTR_f4jumble_inv), + MP_ROM_PTR(&mod_orchardlib_f4jumble_inv)}, + +}; + +STATIC MP_DEFINE_CONST_DICT(mp_module_trezororchardlib_globals, + mp_module_trezororchardlib_globals_table); + +const mp_obj_module_t mp_module_trezororchardlib = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&mp_module_trezororchardlib_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_trezororchardlib, mp_module_trezororchardlib, + USE_ZCASH); + +#endif // USE_ZCASH diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock index abcb1132b21..197e423856a 100644 --- a/core/embed/rust/Cargo.lock +++ b/core/embed/rust/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "bindgen" version = "0.60.1" @@ -27,6 +39,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2b_simd" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -65,6 +88,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cstr_core" version = "0.2.4" @@ -81,6 +110,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "f4jumble" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a83e8d7fd0c526af4aad893b7c9fe41e2699ed8a776a6c74aecdeafe05afc75" +dependencies = [ + "blake2b_simd", +] + [[package]] name = "glob" version = "0.3.0" @@ -246,6 +284,7 @@ dependencies = [ "cc", "cstr_core", "cty", + "f4jumble", "glob", "heapless", ] diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index fe0295ad3fe..4516723cf44 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -13,12 +13,13 @@ model_t1 = ["buttons"] model_tr = ["buttons"] micropython = [] protobuf = ["micropython"] +use_zcash = ["micropython", "f4jumble"] ui = [] ui_debug = [] buttons = [] touch = [] clippy = [] -test = ["cc", "glob", "micropython", "protobuf", "ui", "ui_debug"] +test = ["cc", "glob", "micropython", "protobuf", "use_zcash", "ui", "ui_debug"] [lib] crate-type = ["staticlib"] @@ -49,6 +50,11 @@ default_features = false version = "0.2.4" default_features = false +[dependencies.f4jumble] +version = "0.1.0" +default-features = false +optional = true + # Build dependencies [build-dependencies.bindgen] diff --git a/core/embed/rust/librust.h b/core/embed/rust/librust.h index a3d943e2029..c5b91807cfd 100644 --- a/core/embed/rust/librust.h +++ b/core/embed/rust/librust.h @@ -17,3 +17,9 @@ extern mp_obj_module_t mp_module_trezorui2; #ifdef TREZOR_EMULATOR mp_obj_t ui_debug_layout_type(); #endif + +// Zcash +#if USE_ZCASH +mp_obj_t orchardlib_f4jumble(mp_obj_t message); +mp_obj_t orchardlib_f4jumble_inv(mp_obj_t message); +#endif diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs index a79fdab4200..1e34028dae8 100644 --- a/core/embed/rust/src/lib.rs +++ b/core/embed/rust/src/lib.rs @@ -8,6 +8,8 @@ mod error; #[cfg(feature = "micropython")] #[macro_use] mod micropython; +#[cfg(feature = "use_zcash")] +mod orchardlib; #[cfg(feature = "protobuf")] mod protobuf; mod time; diff --git a/core/embed/rust/src/orchardlib/f4jumble_bridge.rs b/core/embed/rust/src/orchardlib/f4jumble_bridge.rs new file mode 100644 index 00000000000..f5447aa2cd4 --- /dev/null +++ b/core/embed/rust/src/orchardlib/f4jumble_bridge.rs @@ -0,0 +1,36 @@ +//! This module is a python FFI bridge for f4jumble crate. + +use core::ops::DerefMut; +use f4jumble; + +use crate::error::Error; +use crate::micropython::{buffer::BufferMut, obj::Obj, util}; + +// Length of F4jumbled message must be in range 48..=4194368. +impl From for Error { + fn from(_e: f4jumble::Error) -> Error { + Error::OutOfRange + } +} + +/// Apply the F4jumble permutation to a message. +#[no_mangle] +pub extern "C" fn orchardlib_f4jumble(message: Obj) -> Obj { + let block = || { + let mut message: BufferMut = message.try_into()?; + f4jumble::f4jumble_mut(message.deref_mut())?; + Ok(Obj::const_none()) + }; + unsafe { util::try_or_raise(block) } +} + +/// Apply the F4jumble inverse permutation to a message. +#[no_mangle] +pub extern "C" fn orchardlib_f4jumble_inv(message: Obj) -> Obj { + let block = || { + let mut message: BufferMut = message.try_into()?; + f4jumble::f4jumble_inv_mut(message.deref_mut())?; + Ok(Obj::const_none()) + }; + unsafe { util::try_or_raise(block) } +} diff --git a/core/embed/rust/src/orchardlib/mod.rs b/core/embed/rust/src/orchardlib/mod.rs new file mode 100644 index 00000000000..fbfd3651dd9 --- /dev/null +++ b/core/embed/rust/src/orchardlib/mod.rs @@ -0,0 +1 @@ +mod f4jumble_bridge; diff --git a/core/mocks/generated/trezororchardlib.pyi b/core/mocks/generated/trezororchardlib.pyi new file mode 100644 index 00000000000..aebd928f4f2 --- /dev/null +++ b/core/mocks/generated/trezororchardlib.pyi @@ -0,0 +1,11 @@ +from typing import * + + +# extmod/rustmods/modtrezororchardlib.c +def f4jumble(message: bytearray) -> None: + """Mutates a message by F4Jumble permutation.""" + + +# extmod/rustmods/modtrezororchardlib.c +def f4jumble_inv(message: bytearray) -> None: + """Mutates a message by F4Jumble inverse permutation.""" diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi index 50396a0b0ae..8bcd853dd71 100644 --- a/core/mocks/generated/trezorutils.pyi +++ b/core/mocks/generated/trezorutils.pyi @@ -86,4 +86,5 @@ VERSION_PATCH: int MODEL: str EMULATOR: bool BITCOIN_ONLY: bool +USE_ZCASH: bool FIRMWARE_SECTORS_COUNT: int diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 5aeffb3e843..f5ba2b24b90 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -459,6 +459,8 @@ import trezor.enums.TezosBallotType trezor.enums.TezosContractType import trezor.enums.TezosContractType + trezor.enums.ZcashReceiverTypecode + import trezor.enums.ZcashReceiverTypecode trezor.ui.components.common.webauthn import trezor.ui.components.common.webauthn trezor.ui.components.tt.webauthn @@ -775,6 +777,8 @@ import apps.webauthn.resident_credentials apps.zcash import apps.zcash + apps.zcash.addresses + import apps.zcash.addresses apps.zcash.hasher import apps.zcash.hasher apps.zcash.signer diff --git a/core/src/apps/bitcoin/sign_tx/__init__.py b/core/src/apps/bitcoin/sign_tx/__init__.py index 3c94814921d..1241ceb176e 100644 --- a/core/src/apps/bitcoin/sign_tx/__init__.py +++ b/core/src/apps/bitcoin/sign_tx/__init__.py @@ -9,7 +9,10 @@ from . import approvers, bitcoin, helpers, progress if not utils.BITCOIN_ONLY: - from . import bitcoinlike, decred, zcash_v4 + from . import bitcoinlike, decred + +if utils.USE_ZCASH: + from . import zcash_v4 from apps.zcash.signer import Zcash if TYPE_CHECKING: @@ -65,18 +68,20 @@ async def sign_tx( if authorization: approver = approvers.CoinJoinApprover(msg, coin, authorization) - if utils.BITCOIN_ONLY or coin.coin_name in BITCOIN_NAMES: + if coin.coin_name in BITCOIN_NAMES: signer_class: type[SignerClass] = bitcoin.Bitcoin - else: + elif (utils.USE_ZCASH or not utils.BITCOIN_ONLY) and coin.overwintered: + if msg.version == 5: + signer_class = Zcash + else: + signer_class = zcash_v4.ZcashV4 + elif not utils.BITCOIN_ONLY: if coin.decred: signer_class = decred.Decred - elif coin.overwintered: - if msg.version == 5: - signer_class = Zcash - else: - signer_class = zcash_v4.ZcashV4 else: signer_class = bitcoinlike.Bitcoinlike + else: + raise wire.DataError("Unsupported coin type") signer = signer_class(msg, keychain, coin, approver).signer() diff --git a/core/src/apps/common/coininfo.py b/core/src/apps/common/coininfo.py index 9156e823e08..f77ad361831 100644 --- a/core/src/apps/common/coininfo.py +++ b/core/src/apps/common/coininfo.py @@ -178,6 +178,65 @@ def by_name(name: str) -> CoinInfo: overwintered=False, confidential_assets=None, ) + if utils.USE_ZCASH: + if name == "Zcash": + return CoinInfo( + coin_name=name, + coin_shortcut="ZEC", + decimals=8, + address_type=7352, + address_type_p2sh=7357, + maxfee_kb=51000000, + signed_message_header="Zcash Signed Message:\n", + xpub_magic=0x0488b21e, + xpub_magic_segwit_p2sh=None, + xpub_magic_segwit_native=None, + xpub_magic_multisig_segwit_p2sh=None, + xpub_magic_multisig_segwit_native=None, + bech32_prefix=None, + cashaddr_prefix=None, + slip44=133, + segwit=False, + taproot=False, + fork_id=None, + force_bip143=False, + decred=False, + negative_fee=False, + curve_name='secp256k1', + extra_data=True, + timestamp=False, + overwintered=True, + confidential_assets=None, + ) + if name == "Zcash Testnet": + return CoinInfo( + coin_name=name, + coin_shortcut="TAZ", + decimals=8, + address_type=7461, + address_type_p2sh=7354, + maxfee_kb=10000000, + signed_message_header="Zcash Signed Message:\n", + xpub_magic=0x043587cf, + xpub_magic_segwit_p2sh=None, + xpub_magic_segwit_native=None, + xpub_magic_multisig_segwit_p2sh=None, + xpub_magic_multisig_segwit_native=None, + bech32_prefix=None, + cashaddr_prefix=None, + slip44=1, + segwit=False, + taproot=False, + fork_id=None, + force_bip143=False, + decred=False, + negative_fee=False, + curve_name='secp256k1', + extra_data=True, + timestamp=False, + overwintered=True, + confidential_assets=None, + ) if not utils.BITCOIN_ONLY: if name == "Actinium": return CoinInfo( @@ -1600,64 +1659,6 @@ def by_name(name: str) -> CoinInfo: overwintered=False, confidential_assets=None, ) - if name == "Zcash": - return CoinInfo( - coin_name=name, - coin_shortcut="ZEC", - decimals=8, - address_type=7352, - address_type_p2sh=7357, - maxfee_kb=51000000, - signed_message_header="Zcash Signed Message:\n", - xpub_magic=0x0488b21e, - xpub_magic_segwit_p2sh=None, - xpub_magic_segwit_native=None, - xpub_magic_multisig_segwit_p2sh=None, - xpub_magic_multisig_segwit_native=None, - bech32_prefix=None, - cashaddr_prefix=None, - slip44=133, - segwit=False, - taproot=False, - fork_id=None, - force_bip143=False, - decred=False, - negative_fee=False, - curve_name='secp256k1', - extra_data=True, - timestamp=False, - overwintered=True, - confidential_assets=None, - ) - if name == "Zcash Testnet": - return CoinInfo( - coin_name=name, - coin_shortcut="TAZ", - decimals=8, - address_type=7461, - address_type_p2sh=7354, - maxfee_kb=10000000, - signed_message_header="Zcash Signed Message:\n", - xpub_magic=0x043587cf, - xpub_magic_segwit_p2sh=None, - xpub_magic_segwit_native=None, - xpub_magic_multisig_segwit_p2sh=None, - xpub_magic_multisig_segwit_native=None, - bech32_prefix=None, - cashaddr_prefix=None, - slip44=1, - segwit=False, - taproot=False, - fork_id=None, - force_bip143=False, - decred=False, - negative_fee=False, - curve_name='secp256k1', - extra_data=True, - timestamp=False, - overwintered=True, - confidential_assets=None, - ) if name == "Brhodium": return CoinInfo( coin_name=name, diff --git a/core/src/apps/common/coininfo.py.mako b/core/src/apps/common/coininfo.py.mako index f2716a12fde..d7208c90754 100644 --- a/core/src/apps/common/coininfo.py.mako +++ b/core/src/apps/common/coininfo.py.mako @@ -131,10 +131,16 @@ ATTRIBUTES = ( ("confidential_assets", optional_dict), ) -btc_names = ["Bitcoin", "Testnet", "Regtest"] - -coins_btc = [c for c in supported_on("trezor2", bitcoin) if c.name in btc_names] -coins_alt = [c for c in supported_on("trezor2", bitcoin) if c.name not in btc_names] +coins_btc = [] +coins_zec = [] +coins_alt = [] +for c in supported_on("trezor2", bitcoin): + if c.name in ["Bitcoin", "Testnet", "Regtest"]: + coins_btc.append(c) + elif c.name in ["Zcash", "Zcash Testnet"]: + coins_zec.append(c) + else: + coins_alt.append(c) %>\ def by_name(name: str) -> CoinInfo: @@ -145,6 +151,15 @@ def by_name(name: str) -> CoinInfo: ${attr}=${func(coin[attr])}, % endfor ) +% endfor + if utils.USE_ZCASH: +% for coin in coins_zec: + if name == ${black_repr(coin["coin_name"])}: + return CoinInfo( + % for attr, func in ATTRIBUTES: + ${attr}=${func(coin[attr])}, + % endfor + ) % endfor if not utils.BITCOIN_ONLY: % for coin in coins_alt: diff --git a/core/src/apps/zcash/addresses.py b/core/src/apps/zcash/addresses.py new file mode 100644 index 00000000000..3cd07d1dfbe --- /dev/null +++ b/core/src/apps/zcash/addresses.py @@ -0,0 +1,128 @@ +""" +Implementation of encoding and decoding of Zcash +unified addresses according to the ZIP-316. + +see: https://zips.z.cash/zip-0316 +""" + +from typing import TYPE_CHECKING + +from trezor.crypto import orchardlib +from trezor.crypto.bech32 import Encoding, bech32_decode, bech32_encode, convertbits +from trezor.enums import ZcashReceiverTypecode as Typecode +from trezor.utils import BufferReader, empty_bytearray +from trezor.wire import DataError + +from apps.common.coininfo import CoinInfo +from apps.common.readers import read_compact_size +from apps.common.writers import write_bytes_fixed, write_compact_size + +if TYPE_CHECKING: + from typing import Dict + + pass # this `pass` resolves syntax error in addresses.i + + +def receiver_length(typecode: int): + """Byte length of a receiver.""" + if typecode == Typecode.P2PKH: + return 20 + if typecode == Typecode.P2SH: + return 20 + if typecode == Typecode.SAPLING: + return 43 + if typecode == Typecode.ORCHARD: + return 43 + raise ValueError + + +def unified_prefix(coin: CoinInfo): + """Prefix for a unified address.""" + if coin.coin_name == "Zcash": + return "u" + if coin.coin_name == "Zcash Testnet": + return "utest" + raise ValueError + + +def padding(hrp: str) -> bytes: + assert len(hrp) <= 16 + return bytes(hrp, "utf8") + bytes(16 - len(hrp)) + + +def encode_unified(receivers: Dict[Typecode, bytes], coin: CoinInfo) -> str: + # multiple transparent receivers forbidden + assert not (Typecode.P2PKH in receivers and Typecode.P2SH in receivers) + + length = 16 # 16 bytes for padding + for typecode in receivers.keys(): + length += 2 # typecode (1 byte) + length (1 byte) + length += receiver_length(typecode) + + w = empty_bytearray(length) + + # receivers in decreasing order + receivers_list = list(receivers.items()) + receivers_list.sort(reverse=True) + + for (typecode, raw_bytes) in receivers_list: + write_compact_size(w, typecode) + write_compact_size(w, receiver_length(typecode)) + write_bytes_fixed(w, raw_bytes, receiver_length(typecode)) + + hrp = unified_prefix(coin) + write_bytes_fixed(w, padding(hrp), 16) + orchardlib.f4jumble(w) + assert w is not None # to satisfy typecheckers + converted = convertbits(w, 8, 5) + return bech32_encode(hrp, converted, Encoding.BECH32M) + + +def decode_unified(addr_str: str, coin: CoinInfo) -> Dict[int, bytes]: + (hrp, data, encoding) = bech32_decode(addr_str, max_bech_len=1000) + if (hrp, data, encoding) == (None, None, None): + raise DataError("Bech32m decoding failed.") + assert hrp is not None # to satisfy typecheckers + assert data is not None # to satisfy typecheckers + assert encoding is not None # to satisfy typecheckers + if hrp != unified_prefix(coin): + raise DataError("Unexpected address prefix.") + if encoding != Encoding.BECH32M: + raise DataError("Bech32m encoding required.") + + decoded = bytearray(convertbits(data, 5, 8, False)) + orchardlib.f4jumble_inv(decoded) + + # check trailing padding bytes + if decoded[-16:] != padding(hrp): + raise DataError("Invalid padding bytes") + + r = BufferReader(decoded[:-16]) + + last_typecode = None + receivers = {} + while r.remaining_count() > 0: + typecode = read_compact_size(r) + if typecode in receivers: + raise DataError("Duplicated typecode") + if typecode > 0x02000000: + raise DataError("Invalid typecode") + if last_typecode is not None and typecode > last_typecode: + raise DataError("Invalid receivers order") + last_typecode = typecode + + length = read_compact_size(r) + # if the typecode of the receiver is known, then verify receiver length + try: + expected_length = receiver_length(typecode) + if length != expected_length: + raise DataError("Unexpected receiver length") + except ValueError: + pass + + if r.remaining_count() < length: + raise DataError("Invalid receiver length") + + receivers[typecode] = r.read(length) + + return receivers diff --git a/core/src/apps/zcash/signer.py b/core/src/apps/zcash/signer.py index c25660a93d7..17a46b965bb 100644 --- a/core/src/apps/zcash/signer.py +++ b/core/src/apps/zcash/signer.py @@ -1,16 +1,21 @@ from micropython import const from typing import TYPE_CHECKING +from trezor import utils +from trezor.enums import OutputScriptType, ZcashReceiverTypecode as Receiver from trezor.messages import SignTx -from trezor.utils import ensure from trezor.wire import DataError, ProcessError +from apps.bitcoin import scripts from apps.bitcoin.common import ecdsa_sign from apps.bitcoin.sign_tx.bitcoinlike import Bitcoinlike from apps.common.writers import write_compact_size, write_uint32_le from .hasher import ZcashHasher +if utils.USE_ZCASH: + from . import addresses + if TYPE_CHECKING: from typing import Sequence from apps.common.coininfo import CoinInfo @@ -21,6 +26,7 @@ from trezor.messages import ( PrevTx, TxInput, + TxOutput, ) from apps.bitcoin.keychain import Keychain @@ -35,7 +41,7 @@ def __init__( coin: CoinInfo, approver: Approver | None, ) -> None: - ensure(coin.overwintered) + utils.ensure(coin.overwintered) if tx.version != 5: raise DataError("Expected transaction version 5.") @@ -110,3 +116,26 @@ def write_tx_footer(self, w: Writer, tx: SignTx | PrevTx) -> None: write_compact_size(w, 0) # nOutputsSapling # serialize Orchard bundle write_compact_size(w, 0) # nActionsOrchard + + def output_derive_script(self, txo: TxOutput) -> bytes: + # unified addresses + from trezor import log + + log.warning(__name__, "USE_ZCASH:" + str(utils.USE_ZCASH)) + if utils.USE_ZCASH: + if txo.address is not None and txo.address[0] == "u": + assert txo.script_type is OutputScriptType.PAYTOADDRESS + + receivers = addresses.decode_unified(txo.address, self.coin) + if Receiver.P2PKH in receivers: + pubkeyhash = receivers[Receiver.P2PKH] + return scripts.output_script_p2pkh(pubkeyhash) + if Receiver.P2SH in receivers: + scripthash = receivers[Receiver.P2SH] + return scripts.output_script_p2sh(scripthash) + raise DataError( + "Unified address does not include a transparent receiver." + ) + + # transparent addresses + return super().output_derive_script(txo) diff --git a/core/src/trezor/crypto/__init__.py b/core/src/trezor/crypto/__init__.py index ad1d409c7b5..024358792e1 100644 --- a/core/src/trezor/crypto/__init__.py +++ b/core/src/trezor/crypto/__init__.py @@ -13,3 +13,6 @@ if not utils.BITCOIN_ONLY: from trezorcrypto import cardano, monero, nem # noqa: F401 + +if utils.USE_ZCASH: + import trezororchardlib as orchardlib # noqa: F401 diff --git a/core/src/trezor/enums/ZcashReceiverTypecode.py b/core/src/trezor/enums/ZcashReceiverTypecode.py new file mode 100644 index 00000000000..62d176aeb54 --- /dev/null +++ b/core/src/trezor/enums/ZcashReceiverTypecode.py @@ -0,0 +1,8 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +P2PKH = 0 +P2SH = 1 +SAPLING = 2 +ORCHARD = 3 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 9b0045bb0e2..5b05b85e651 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -504,3 +504,9 @@ class TezosBallotType(IntEnum): Yay = 0 Nay = 1 Pass = 2 + + class ZcashReceiverTypecode(IntEnum): + P2PKH = 0 + P2SH = 1 + SAPLING = 2 + ORCHARD = 3 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 295d82e3af5..66a4a65746e 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -57,6 +57,7 @@ def __getattr__(name: str) -> Any: from trezor.enums import TezosBallotType # noqa: F401 from trezor.enums import TezosContractType # noqa: F401 from trezor.enums import WordRequestType # noqa: F401 + from trezor.enums import ZcashReceiverTypecode # noqa: F401 class BinanceGetAddress(protobuf.MessageType): address_n: "list[int]" diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index ecd67208aed..4176cd0fabc 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -6,6 +6,7 @@ FIRMWARE_SECTORS_COUNT, MODEL, SCM_REVISION, + USE_ZCASH, VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, diff --git a/core/tests/test_apps.zcash.addresses.py b/core/tests/test_apps.zcash.addresses.py new file mode 100644 index 00000000000..cd999c5c9a2 --- /dev/null +++ b/core/tests/test_apps.zcash.addresses.py @@ -0,0 +1,64 @@ +from common import * + +from apps.common import coininfo +from apps.zcash import addresses +from trezor.enums.ZcashReceiverTypecode import P2PKH, P2SH, SAPLING, ORCHARD + +# source: https://github.com/jarys/zcash-test-vectors/blob/trezor/unified_address.py +# (receivers: Dict[ZcashReceiverTypecode, bytes], unified_address: str) +TESTVECTORS_UNIFIED = [ + ({ + ORCHARD: unhexlify("8ff3386971cb64b8e7789908dd8ebd7de92a68e586a34db8fea999efd2016fae76750afae7ee941646bcb9"), + },"u1qylzskzykhk5l5vk6zlyqqruvskzv74hk20lmrllzy3vdz6pvny5t9zwlrm86ukw77y5pu8uep2m33s7sc7gn6aq0jm9neg5tsektyn9"), + ({ + ORCHARD: unhexlify("56e84b1adc9423c3676c0463f7125df4836fd2816b024ee70efe09fb9a7b3863c6eacdf95e03894950692c"), + },"u17j4lvw84jd238ev9ukr0lvqhv4z32v98pxcglctaj3aqfqj7rr2wwvh73247ekczw4smyrvm2wf2v5nfxvn3sl0ycc6w4455yg49yf2m"), + ({ + SAPLING: unhexlify("3484fc3fce5048621e1e164acf29528ce2fa39c9ca97a8114fb3deba18068a68d487c5d063a4a3ce614ec2"), + ORCHARD: unhexlify("513d01f7b5d37f5433c336af84a342af9d833bc01635ceb7a2139d23510ae4f26e42fc79f4f6c467154115"), + },"u1xnkv2m5x02kc5h4fqv3yk0s8z6k4ya7n92an98gsk5dc467ny0rkwevvq3nw7sjll4hvhr2u58xw4cryguzeq0h5yzu4xk3esxgl3glqqth5jqse0za8w5y2ym93ht3zv8wvyw7kvs7ejuvk6xkc375j77ukuzwxkznhuy3x6yvxeq5c"), + ({ + P2SH: unhexlify("3dc166d56a1d62f5a8d7551db5fd9313e8c7203d"), + ORCHARD: unhexlify("1038c28960629162fdd46aca1eb067a1de41e4385d19f92b53341de8b3eeecb0afcbc130808bf42e8de128"), + },"u1qld9m8deq56mpvma9gpvm5ths62f9fcqa9udwn7jvjts3msldpet509nnuguvl6zq0atz9s8t53wm207zq2xyhawanjg4xt2yzqvuwt6kzx2lzzmk9w45mlh6vmacmywmeh6c584t8u"), + ({ + P2SH: unhexlify("362697b0e4e4c763ccb8f676495c222f7fba1e31"), + ORCHARD: unhexlify("3a6f91fb15f130f2778292577d9f32e137ab22c51fafc959b16d42578387b297a124a53bd9b32af59d7f1f"), + },"u1rw3fjc02enpe09gx57ada7rdvy2xy8mxs7kalvq2uhyg2qqmt77ycealmp30wujndt02cdeffrrrgg6d858awwxadspg8yqwmf56w04tqagt0chhg9z7s6h7ndwn92yjnrwky7207zz"), + ({ + P2SH: unhexlify("acd20b183e31d49f25c9a138f49b1a537edcf04b"), + SAPLING: unhexlify("24e657b33ec5d1679d072ba830c1c4412bc775c520e2a1ca9617680397c6708effd84b61fb950222e62516"), + ORCHARD: unhexlify("a391f728b786bbd102ddbfd1318ca4e03e6045c23c3c4826686696c7ba949216adf0e06e6b4bd16b28ac39"), + },"u1j7zuzyc5n2mvp3tvgdztsx0gz3er4n5rl7jvre3hh6tgusnft5lpsme3ldndk482v9t3pcl9umkj2eucnua80vqk9jmelg7had8x5ryqlp2et48tmq9qu824xankrktyms8x6x4dnfyan98fgl7rr7wfxsmrsxqs2x7qwd57qltf5rlrdecevm4tjmz8m6kqkvjlqj3s5uywcr6za7f"), + ({ + P2PKH: unhexlify("47f1e191e00c7a1d48af046827591e9733a97fa6"), + ORCHARD: unhexlify("6424f71a3ad197426498f4eccb6a5780204237987232bc098f89acc475c3f74bd69e2f35d44736f48f3c14"), + },"u1kts2hcsm3nnzm290gh29ga32eg2euvkp9l20yhptwtzqlp0c6tskc59yeywq4e6y64uggzc2pzf9x8yuc805xdjfp0waezff8gzmaxc8rdmp9wr82r78ts2nwk24yrakua84sll8z9h"), + ({ + P2PKH: unhexlify("f73476f21a482ec9378365c8f7393c94e2885315"), + ORCHARD: unhexlify("b4eed75afb7f6b02b626ebb20b0b211507fda98d12229395fdf0d67b6b40d8496ee159251933bbbd7b8228"), + },"u1hn3fgwa6m4h3uulnrlz78x74khmfda20jej5495pxhcteadp8xrpl9z644e7pf3pk4a3wuld24fat7r2z33wxqfaeakhgl9nssnh0sg9ggyadr4696n4p6mdrfnzum5luxs42uc7x7m"), + ({ + P2PKH: unhexlify("03880d780cb07fcfaabe3f1a84b27db59a4a153d"), + SAPLING: unhexlify("137419a9a2123e27dce5836ac3ecdcf9d628b25a744248eae8cf0223f5c444c55650df2301d1417ed3ca59"), + ORCHARD: unhexlify("db8c305524bc0deaa85d9704ea8c1320ffbbadfe96f0c6ff16b607111b5583bfb6f1ea45275ef2aa2d879b"), + },"u1wg3qy8cw9zfs6axtg6204ftf2c6gkx2mz8xksz83paxfaqcc52jfedfz0m2sk50a7ujp6gtlvp7572csfvz4t4rtv9pzfxa0dk9d8van09dymf5s2q3a2ccsevrscm2z6hw9fqw3swjjlcqh3muq703mlrmf5rstd476ngzq9dr4y0dwys4hnjz58wx9erhz7ycmuv7ynnjt2sm2epy") +] + +COIN = coininfo.by_name("Zcash") + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestZcashAddress(unittest.TestCase): + def test_encode_unified(self): + for tv in TESTVECTORS_UNIFIED: + ua = addresses.encode_unified(tv[0], COIN) + self.assertEqual(ua, tv[1]) + + def test_decode_unified(self): + for tv in TESTVECTORS_UNIFIED: + receivers = addresses.decode_unified(tv[1], COIN) + self.assertEqual(receivers, tv[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 76a8a4d38a0..66d45f73bd5 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -551,6 +551,13 @@ class TezosBallotType(IntEnum): Pass = 2 +class ZcashReceiverTypecode(IntEnum): + P2PKH = 0 + P2SH = 1 + SAPLING = 2 + ORCHARD = 3 + + class BinanceGetAddress(protobuf.MessageType): MESSAGE_WIRE_TYPE = 700 FIELDS = { diff --git a/tests/device_tests/zcash/test_sign_tx.py b/tests/device_tests/zcash/test_sign_tx.py index c23aa5e6017..3ab04a61b86 100644 --- a/tests/device_tests/zcash/test_sign_tx.py +++ b/tests/device_tests/zcash/test_sign_tx.py @@ -271,6 +271,63 @@ def test_one_two(client: Client): ) +@pytest.mark.skip_t1 +def test_unified_address(client: Client): + # identical to the test_one_two + # but receiver address is unified with a sapling address + inp1 = messages.TxInputType( + # tmQoJ3PTXgQLaRRZZYT6xk8XtjRbr2kCqwu + address_n=parse_path("m/44h/1h/0h/0/0"), + amount=4_134_720, + prev_hash=TXHASH_c5309b, + prev_index=0, + ) + + out1 = messages.TxOutputType( + # m/44h/1h/0h/0/8 + some Sapling address + address="utest1ehsqx89s6rm02dg3yw8yr9203d9gruazyuqkc4f8p78j9twyd8geum5h47j4fq7mjklgt2ynvkmlphg2yjne6quyqysuyy7286neetcsfm5rzvvwnmtc593k8qw6zrtr5ypz6qculv5", + amount=1_000_000, + script_type=messages.OutputScriptType.PAYTOADDRESS, + ) + + out2 = messages.TxOutputType( + address_n=parse_path("m/44h/1h/0h/0/0"), + amount=4_134_720 - 1_000_000 - 2_000, + script_type=messages.OutputScriptType.PAYTOADDRESS, + ) + + with client: + client.set_expected_responses( + [ + request_input(0), + request_output(0), + messages.ButtonRequest(code=B.ConfirmOutput), + request_output(1), + messages.ButtonRequest(code=B.SignTx), + request_input(0), + request_output(0), + request_output(1), + request_finished(), + ] + ) + + _, serialized_tx = btc.sign_tx( + client, + "Zcash Testnet", + [inp1], + [out1, out2], + version=5, + version_group_id=VERSION_GROUP_ID, + branch_id=BRANCH_ID, + ) + + # Accepted by network: txid = d8cfa377012ca0b8d856586693b530835bf2fa14add0380e24ec6755bed5b931 + assert ( + serialized_tx.hex() + == "050000800a27a726b4d0d6c2000000000000000001bf79ce8a0403de4775d3538c670de802af72e8961c8b9174f36b8fa1d69b30c5000000006b483045022100be78eccf801dda4dd33f9d4e04c2aae01022869d1d506d51669204ec269d71a90220394a51838faf40176058cf45fe7032be9c5c942e21aff35d7dbe4b96ab5e0a500121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0ffffffff0240420f00000000001976a9141efeae5c937bfc7f095a06aabdb5476a5d6d19db88ac30cd2f00000000001976a914a579388225827d9f2fe9014add644487808c695d88ac000000" + ) + + @pytest.mark.skip_t1 def test_external_presigned(client: Client): inp1 = messages.TxInputType( diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 4a3fd0f77da..3172327e83d 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1647,6 +1647,7 @@ "TT_zcash-test_sign_tx.py::test_spend_multisig": "1f7cfe70831cffb4c076d0b4778704a982b37be4cc114e2cd9de85ee3173430e", "TT_zcash-test_sign_tx.py::test_spend_v4_input": "235910c3aa36150bb7012d21d52acdca347a2a216bb09a0588910329e464c8f0", "TT_zcash-test_sign_tx.py::test_spend_v5_input": "dd9b4f0050836b7d881d9be798d2f4d972e39f460b8acc3e4e1c038ba754ae7c", +"TT_zcash-test_sign_tx.py::test_unified_address": "90c260abe850e4821f479a489e79efaa75b7cf9af3ac74752a3cf7099fef6b31", "TT_zcash-test_sign_tx.py::test_version_group_id_missing": "c09de07fbbf1e047442180e2facb5482d06a1a428891b875b7dd93c9e4704ae1", "TTui2_binance-test_get_address.py::test_binance_get_address[m-44h-714h-0h-0-0-bnb1hgm0p7khfk85zpz-68e2cb5a": "577553344793d14026b1d3c7f6185a4858378476dafee67a958319bd8d8f58ac", "TTui2_binance-test_get_address.py::test_binance_get_address[m-44h-714h-0h-0-1-bnb1egswqkszzfc2uq7-1adfb691": "9fedc9ad26c5573b264f28f70c83b8baf6503a71f2296de27be6c6c9166d5488",