From d2c16246025c547796181e528ff996880054b767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Kereke=C5=A1?= Date: Thu, 23 Jul 2020 15:54:49 +0200 Subject: [PATCH] Cardano shelley update 2/3 (#1112) --- common/protob/messages-cardano.proto | 56 +- core/src/apps/cardano/__init__.py | 3 - core/src/apps/cardano/address.py | 403 ++++++++++---- core/src/apps/cardano/byron_address.py | 124 +++++ core/src/apps/cardano/get_address.py | 82 ++- core/src/apps/cardano/helpers/__init__.py | 4 + core/src/apps/cardano/helpers/bech32.py | 34 ++ core/src/apps/cardano/helpers/network_ids.py | 12 + .../cardano/{ => helpers}/protocol_magics.py | 4 + core/src/apps/cardano/helpers/purposes.py | 4 + .../apps/cardano/helpers/seed_namespaces.py | 6 + .../apps/cardano/helpers/staking_use_cases.py | 58 ++ core/src/apps/cardano/helpers/utils.py | 20 + core/src/apps/cardano/layout.py | 224 ++++++++ core/src/apps/cardano/layout/__init__.py | 51 -- core/src/apps/cardano/seed.py | 45 +- core/src/apps/cardano/sign_tx.py | 189 +++++-- core/src/trezor/crypto/bech32.py | 6 +- .../messages/CardanoAddressParametersType.py | 40 ++ .../src/trezor/messages/CardanoAddressType.py | 10 + .../messages/CardanoBlockchainPointerType.py | 31 ++ core/src/trezor/messages/CardanoGetAddress.py | 11 +- core/src/trezor/messages/CardanoSignTx.py | 3 + .../trezor/messages/CardanoTxOutputType.py | 8 +- core/tests/test_apps.cardano.address.py | 328 ++++++++++- core/tests/test_apps.cardano.bech32.py | 28 + .../tests/test_apps.cardano.get_public_key.py | 26 +- core/tests/test_apps.cardano.keychain.py | 65 +++ .../test_apps.cardano.staking_use_cases.py | 100 ++++ core/tests/test_apps.cardano.utils.py | 33 ++ python/src/trezorlib/cardano.py | 146 +++-- python/src/trezorlib/cli/cardano.py | 91 ++- .../messages/CardanoAddressParametersType.py | 40 ++ .../trezorlib/messages/CardanoAddressType.py | 10 + .../messages/CardanoBlockchainPointerType.py | 31 ++ .../trezorlib/messages/CardanoGetAddress.py | 11 +- .../src/trezorlib/messages/CardanoSignTx.py | 3 + .../trezorlib/messages/CardanoTxOutputType.py | 8 +- python/src/trezorlib/messages/__init__.py | 3 + .../test_msg_cardano_get_address.py | 240 +++++++- ...st_msg_cardano_get_address_slip39_basic.py | 15 +- .../test_msg_cardano_get_public_key.py | 22 +- .../test_msg_cardano_sign_transaction.py | 517 ++++++++++++++++-- .../test_msg_cardano_sign_tx_slip39_basic.py | 104 ++-- tests/ui_tests/fixtures.json | 67 ++- 45 files changed, 2886 insertions(+), 430 deletions(-) create mode 100644 core/src/apps/cardano/byron_address.py create mode 100644 core/src/apps/cardano/helpers/__init__.py create mode 100644 core/src/apps/cardano/helpers/bech32.py create mode 100644 core/src/apps/cardano/helpers/network_ids.py rename core/src/apps/cardano/{ => helpers}/protocol_magics.py (67%) create mode 100644 core/src/apps/cardano/helpers/purposes.py create mode 100644 core/src/apps/cardano/helpers/seed_namespaces.py create mode 100644 core/src/apps/cardano/helpers/staking_use_cases.py create mode 100644 core/src/apps/cardano/helpers/utils.py create mode 100644 core/src/apps/cardano/layout.py delete mode 100644 core/src/apps/cardano/layout/__init__.py create mode 100644 core/src/trezor/messages/CardanoAddressParametersType.py create mode 100644 core/src/trezor/messages/CardanoAddressType.py create mode 100644 core/src/trezor/messages/CardanoBlockchainPointerType.py create mode 100644 core/tests/test_apps.cardano.bech32.py create mode 100644 core/tests/test_apps.cardano.keychain.py create mode 100644 core/tests/test_apps.cardano.staking_use_cases.py create mode 100644 core/tests/test_apps.cardano.utils.py create mode 100644 python/src/trezorlib/messages/CardanoAddressParametersType.py create mode 100644 python/src/trezorlib/messages/CardanoAddressType.py create mode 100644 python/src/trezorlib/messages/CardanoBlockchainPointerType.py diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index ccca5a20a..bc076c093 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -7,6 +7,46 @@ option java_outer_classname = "TrezorMessageCardano"; import "messages-common.proto"; +/** + * Values correspond to address header values given by the spec. + */ +enum CardanoAddressType { + BASE = 0; + POINTER = 4; + ENTERPRISE = 6; + BYRON = 8; + REWARD = 14; +} + +/** + * Structure representing cardano PointerAddress pointer, + * which points to a staking key registration certificate. + * @embed + */ +message CardanoBlockchainPointerType { + optional uint32 block_index = 1; + optional uint32 tx_index = 2; + optional uint32 certificate_index = 3; +} + +/** + * Structure to represent address parameters so they can be + * reused in CardanoGetAddress and CardanoTxOutputType. + * NetworkId isn't a part of the parameters, because in a transaction + * this will be included separately in the transaction itself, so it + * shouldn't be duplicated here. + * @embed + */ +message CardanoAddressParametersType { + optional CardanoAddressType address_type = 1; // one of the CardanoAddressType-s + repeated uint32 address_n = 2; // BIP-32-style path to derive the spending key from master node + repeated uint32 address_n_staking = 3; // BIP-32-style path to derive staking key from master node + optional bytes staking_key_hash = 4; // staking key can be derived from address_n_staking, or + // can be sent directly e.g. if it doesn't belong to + // the same account as address_n + optional CardanoBlockchainPointerType certificate_pointer = 5; // a pointer to the staking key registration certificate +} + /** * Request: Ask device for Cardano address * @start @@ -14,9 +54,11 @@ import "messages-common.proto"; * @next Failure */ message CardanoGetAddress { - repeated uint32 address_n = 1; // BIP-32-style path to derive the key from master node - optional bool show_display = 2; // optionally prompt for confirmation on trezor display - optional uint32 protocol_magic = 3; // network's protocol magic - needed for Byron addresses on testnets + // repeated uint32 address_n = 1; // moved to address_parameters + optional bool show_display = 2; // optionally prompt for confirmation on trezor display + optional uint32 protocol_magic = 3; // network's protocol magic - needed for Byron addresses on testnets + optional uint32 network_id = 4; // network id - mainnet or testnet + optional CardanoAddressParametersType address_parameters = 5; // parameters used to derive the address } /** @@ -60,6 +102,7 @@ message CardanoSignTx { optional uint32 protocol_magic = 5; // network's protocol magic optional uint64 fee = 6; // transaction fee - added in shelley optional uint64 ttl = 7; // transaction ttl - added in shelley + optional uint32 network_id = 8; // network id - mainnet or testnet /** * Structure representing cardano transaction input */ @@ -74,9 +117,10 @@ message CardanoSignTx { * Structure representing cardano transaction output */ message CardanoTxOutputType { - optional string address = 1; // target coin address in Base58 encoding - repeated uint32 address_n = 2; // BIP-32 path to derive the key from master node; has higher priority than "address" - optional uint64 amount = 3; // amount to spend + optional string address = 1; // target coin address in bech32 or base58 + // repeated uint32 address_n = 2; // moved to address_parameters + optional uint64 amount = 3; // amount to spend + optional CardanoAddressParametersType address_parameters = 4; // parameters used to derive the address } } diff --git a/core/src/apps/cardano/__init__.py b/core/src/apps/cardano/__init__.py index bb7d3e8b0..0ecb24c58 100644 --- a/core/src/apps/cardano/__init__.py +++ b/core/src/apps/cardano/__init__.py @@ -1,10 +1,7 @@ from trezor import wire from trezor.messages import MessageType -from apps.common import HARDENED - CURVE = "ed25519" -SEED_NAMESPACE = [HARDENED | 44, HARDENED | 1815] def boot() -> None: diff --git a/core/src/apps/cardano/address.py b/core/src/apps/cardano/address.py index ac889813a..718320de6 100644 --- a/core/src/apps/cardano/address.py +++ b/core/src/apps/cardano/address.py @@ -1,145 +1,352 @@ -from trezor import log, wire -from trezor.crypto import base58, crc, hashlib +from micropython import const -from apps.common import HARDENED, cbor +from trezor import wire +from trezor.crypto import base58, hashlib +from trezor.messages import CardanoAddressParametersType, CardanoAddressType + +from apps.common import HARDENED from apps.common.seed import remove_ed25519_prefix -from . import protocol_magics +from .byron_address import derive_byron_address, validate_output_byron_address +from .helpers import INVALID_ADDRESS, NETWORK_MISMATCH, bech32, network_ids, purposes +from .helpers.utils import variable_length_encode +from .seed import is_byron_path, is_shelley_path if False: - from typing import Tuple - from trezor.crypto import bip32 + from typing import List + from trezor.messages import CardanoBlockchainPointerType + from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType from . import seed -PROTOCOL_MAGIC_KEY = 2 -INVALID_ADDRESS = wire.ProcessError("Invalid address") -NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!") +ADDRESS_TYPES_SHELLEY = ( + CardanoAddressType.BASE, + CardanoAddressType.POINTER, + CardanoAddressType.ENTERPRISE, + CardanoAddressType.REWARD, +) + +HEADER_LENGTH = 1 +HASH_LENGTH = 28 +MIN_POINTER_SIZE = 0 +MAX_POINTER_SIZE = 12 + +ADDRESS_BYTES_MIN_LENGTHS = { + CardanoAddressType.BASE: HEADER_LENGTH + HASH_LENGTH + HASH_LENGTH, + CardanoAddressType.POINTER: HEADER_LENGTH + HASH_LENGTH + MIN_POINTER_SIZE, + CardanoAddressType.ENTERPRISE: HEADER_LENGTH + HASH_LENGTH, + CardanoAddressType.REWARD: HEADER_LENGTH + HASH_LENGTH, +} + +ADDRESS_BYTES_MAX_LENGTHS = { + CardanoAddressType.BASE: ADDRESS_BYTES_MIN_LENGTHS[CardanoAddressType.BASE], + CardanoAddressType.POINTER: HEADER_LENGTH + HASH_LENGTH + MAX_POINTER_SIZE, + CardanoAddressType.ENTERPRISE: ADDRESS_BYTES_MIN_LENGTHS[ + CardanoAddressType.ENTERPRISE + ], + CardanoAddressType.REWARD: ADDRESS_BYTES_MIN_LENGTHS[CardanoAddressType.REWARD], +} + + +def validate_full_path(path: List[int]) -> bool: + """ + Validates derivation path to fit {44', 1852'}/1815'/a'/{0,1,2}/i, + where `a` is an account number and i an address index. + The max value for `a` is 20, 1 000 000 for `i`. + """ + if len(path) != 5: + return False + if path[0] not in (purposes.BYRON, purposes.SHELLEY): + return False + if path[1] != 1815 | HARDENED: + return False + if path[2] < HARDENED or path[2] > 20 | HARDENED: + return False + if path[3] not in (0, 1, 2): + return False + if path[4] > 1000000: + return False + return True -def _encode_address_raw(address_data_encoded: bytes) -> str: - return base58.encode( - cbor.encode( - [cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)] - ) - ) +def validate_output_address(address: str, protocol_magic: int, network_id: int) -> None: + if address is None or len(address) == 0: + raise INVALID_ADDRESS + address_bytes = get_address_bytes_unsafe(address) + address_type = _get_address_type(address_bytes) -def derive_address_and_node( - keychain: seed.Keychain, path: list, protocol_magic: int -) -> Tuple[str, bip32.HDNode]: - node = keychain.derive(path) + if address_type == CardanoAddressType.BYRON: + validate_output_byron_address(address_bytes, protocol_magic) + elif address_type in ADDRESS_TYPES_SHELLEY: + _validate_output_shelley_address(address, address_bytes, network_id) + else: + raise INVALID_ADDRESS - address_attributes = get_address_attributes(protocol_magic) - address_root = _get_address_root(node, address_attributes) - address_type = 0 - address_data = [address_root, address_attributes, address_type] - address_data_encoded = cbor.encode(address_data) +def get_address_bytes_unsafe(address: str) -> bytes: + try: + address_bytes = bech32.decode_unsafe(address) + except ValueError: + try: + address_bytes = base58.decode(address) + except ValueError: + raise INVALID_ADDRESS - return (_encode_address_raw(address_data_encoded), node) + return address_bytes -def get_address_attributes(protocol_magic: int) -> dict: - # protocol magic is included in Byron addresses only on testnets - if protocol_magic == protocol_magics.MAINNET: - address_attributes = {} - else: - address_attributes = {PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)} +def _get_address_type(address: bytes) -> int: + return address[0] >> 4 - return address_attributes +def _validate_output_shelley_address( + address_str: str, address_bytes: bytes, network_id: int +) -> None: + address_type = _get_address_type(address_bytes) + # reward address cannot be an output address + if address_type == CardanoAddressType.REWARD: + raise INVALID_ADDRESS -def validate_output_address(address: str, protocol_magic: int) -> None: - address_data_encoded = _decode_address_raw(address) - _validate_address_data_protocol_magic(address_data_encoded, protocol_magic) + _validate_address_size(address_bytes, address_type) + _validate_output_address_bech32_hrp(address_str, address_type, network_id) + _validate_address_network_id(address_bytes, network_id) -def _decode_address_raw(address: str) -> bytes: - try: - address_hex = base58.decode(address) - address_unpacked = cbor.decode(address_hex) - except ValueError as e: - if __debug__: - log.exception(__name__, e) +def _validate_address_size( + address_bytes: bytes, address_type: EnumTypeCardanoAddressType +) -> None: + if not ( + ADDRESS_BYTES_MIN_LENGTHS[address_type] + <= len(address_bytes) + <= ADDRESS_BYTES_MAX_LENGTHS[address_type] + ): raise INVALID_ADDRESS - if not isinstance(address_unpacked, list) or len(address_unpacked) != 2: - raise INVALID_ADDRESS - address_data_encoded = address_unpacked[0] - if not isinstance(address_data_encoded, bytes): - raise INVALID_ADDRESS +def _validate_output_address_bech32_hrp( + address_str: str, address_type: EnumTypeCardanoAddressType, network_id: int +) -> None: + valid_hrp = _get_bech32_hrp_for_address(address_type, network_id) + bech32_hrp = bech32.get_hrp(address_str) - address_crc = address_unpacked[1] - if not isinstance(address_crc, int): + if valid_hrp != bech32_hrp: raise INVALID_ADDRESS - if address_crc != crc.crc32(address_data_encoded): - raise INVALID_ADDRESS - return address_data_encoded +def _get_bech32_hrp_for_address( + address_type: EnumTypeCardanoAddressType, network_id: int +) -> str: + if address_type == CardanoAddressType.BYRON: + # Byron address uses base58 encoding + raise ValueError + if address_type == CardanoAddressType.REWARD: + if network_ids.is_mainnet(network_id): + return bech32.HRP_REWARD_ADDRESS + else: + return bech32.HRP_TESTNET_REWARD_ADDRESS + else: + if network_ids.is_mainnet(network_id): + return bech32.HRP_ADDRESS + else: + return bech32.HRP_TESTNET_ADDRESS -def _validate_address_data_protocol_magic( - address_data_encoded: bytes, protocol_magic: int -) -> None: - """ - Determines whether the correct protocol magic (or none) - is included in the address. Addresses on mainnet don't - contain protocol magic, but addresses on the testnet do. - """ - address_data = cbor.decode(address_data_encoded) - if not isinstance(address_data, list) or len(address_data) < 2: - raise INVALID_ADDRESS - attributes = address_data[1] - if protocol_magic == protocol_magics.MAINNET: - if PROTOCOL_MAGIC_KEY in attributes: - raise NETWORK_MISMATCH - else: # testnet - if len(attributes) == 0 or PROTOCOL_MAGIC_KEY not in attributes: - raise NETWORK_MISMATCH +def _validate_address_network_id(address: bytes, network_id: int) -> None: + if _get_address_network_id(address) != network_id: + raise NETWORK_MISMATCH - protocol_magic_cbor = attributes[PROTOCOL_MAGIC_KEY] - address_protocol_magic = cbor.decode(protocol_magic_cbor) - if not isinstance(address_protocol_magic, int): - raise INVALID_ADDRESS +def _get_address_network_id(address: bytes) -> int: + return address[0] & 0x0F + + +def get_public_key_hash(keychain: seed.Keychain, path: List[int]) -> bytes: + node = keychain.derive(path) + public_key = remove_ed25519_prefix(node.public_key()) + return hashlib.blake2b(data=public_key, outlen=28).digest() + + +def derive_human_readable_address( + keychain: seed.Keychain, + parameters: CardanoAddressParametersType, + protocol_magic: int, + network_id: int, +) -> str: + address = derive_address_bytes(keychain, parameters, protocol_magic, network_id) + + address_type = _get_address_type(address) + if address_type == CardanoAddressType.BYRON: + return base58.encode(address) + elif address_type in ADDRESS_TYPES_SHELLEY: + hrp = _get_bech32_hrp_for_address(_get_address_type(address), network_id) + return bech32.encode(hrp, address) + else: + raise ValueError + + +def derive_address_bytes( + keychain: seed.Keychain, + parameters: CardanoAddressParametersType, + protocol_magic: int, + network_id: int, +) -> bytes: + is_byron_address = parameters.address_type == CardanoAddressType.BYRON + + if is_byron_address: + address = _derive_byron_address(keychain, parameters.address_n, protocol_magic) + else: + address = _derive_shelley_address(keychain, parameters, network_id) + + return address + + +def _derive_byron_address( + keychain: seed.Keychain, path: List[int], protocol_magic: int +) -> bytes: + if not is_byron_path(path): + raise wire.DataError("Invalid path for byron address!") + + address = derive_byron_address(keychain, path, protocol_magic) + return address + + +def _derive_shelley_address( + keychain: seed.Keychain, parameters: CardanoAddressParametersType, network_id: int, +) -> bytes: + if not is_shelley_path(parameters.address_n): + raise wire.DataError("Invalid path for shelley address!") + + if parameters.address_type == CardanoAddressType.BASE: + address = _derive_base_address( + keychain, + parameters.address_n, + parameters.address_n_staking, + parameters.staking_key_hash, + network_id, + ) + elif parameters.address_type == CardanoAddressType.ENTERPRISE: + address = _derive_enterprise_address(keychain, parameters.address_n, network_id) + elif parameters.address_type == CardanoAddressType.POINTER: + address = _derive_pointer_address( + keychain, parameters.address_n, parameters.certificate_pointer, network_id, + ) + elif parameters.address_type == CardanoAddressType.REWARD: + address = _derive_reward_address(keychain, parameters.address_n, network_id) + else: + raise ValueError + + return address + + +def _create_address_header( + address_type: EnumTypeCardanoAddressType, network_id: int +) -> bytes: + header = address_type << 4 | network_id + return header.to_bytes(1, "little") + + +def _derive_base_address( + keychain: seed.Keychain, + path: List[int], + staking_path: List[int], + staking_key_hash: bytes, + network_id: int, +) -> bytes: + header = _create_address_header(CardanoAddressType.BASE, network_id) + spending_key_hash = get_public_key_hash(keychain, path) + + _validate_base_address_staking_info(staking_path, staking_key_hash) + + if staking_key_hash is None: + staking_key_hash = get_public_key_hash(keychain, staking_path) + + return header + spending_key_hash + staking_key_hash + + +def _validate_base_address_staking_info( + staking_path: List[int], staking_key_hash: bytes, +) -> None: + if (staking_key_hash is None) == (not staking_path): + raise wire.DataError( + "Base address needs either a staking path or a staking key hash!" + ) - if address_protocol_magic != protocol_magic: - raise NETWORK_MISMATCH + if staking_key_hash is None and not _is_staking_path(staking_path): + raise wire.DataError("Invalid staking path!") -def validate_full_path(path: list) -> bool: +def _is_staking_path(path: List[int]) -> bool: """ - Validates derivation path to fit 44'/1815'/a'/{0,1}/i, - where `a` is an account number and i an address index. - The max value for `a` is 20, 1 000 000 for `i`. - The derivation scheme v1 allowed a'/0/i only, - but in v2 it can be a'/1/i as well. + Validates path to match 1852'/1815'/a'/2/0. Path must + be a valid Cardano path. It must have a Shelley purpose + (Byron paths are not valid staking paths), it must have + 2 as chain type and currently there is only one staking + path for each account so a 0 is required for address index. """ - if len(path) != 5: - return False - if path[0] != 44 | HARDENED: - return False - if path[1] != 1815 | HARDENED: + if not validate_full_path(path): return False - if path[2] < HARDENED or path[2] > 20 | HARDENED: + + if path[0] != purposes.SHELLEY: return False - if path[3] != 0 and path[3] != 1: + if path[3] != 2: return False - if path[4] > 1000000: + if path[4] != 0: return False + return True -def _address_hash(data: list) -> bytes: - cbor_data = cbor.encode(data) - sha_data_hash = hashlib.sha3_256(cbor_data).digest() - res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest() - return res +def _derive_pointer_address( + keychain: seed.Keychain, + path: List[int], + pointer: CardanoBlockchainPointerType, + network_id: int, +) -> bytes: + header = _create_address_header(CardanoAddressType.POINTER, network_id) + spending_key_hash = get_public_key_hash(keychain, path) + encoded_pointer = _encode_certificate_pointer(pointer) + + return header + spending_key_hash + encoded_pointer + + +def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes: + if ( + pointer is None + or pointer.block_index is None + or pointer.tx_index is None + or pointer.certificate_index is None + ): + raise wire.DataError("Invalid pointer!") + + block_index_encoded = variable_length_encode(pointer.block_index) + tx_index_encoded = variable_length_encode(pointer.tx_index) + certificate_index_encoded = variable_length_encode(pointer.certificate_index) + + return bytes(block_index_encoded + tx_index_encoded + certificate_index_encoded) + + +def _derive_enterprise_address( + keychain: seed.Keychain, path: List[int], network_id: int, +) -> bytes: + header = _create_address_header(CardanoAddressType.ENTERPRISE, network_id) + spending_key_hash = get_public_key_hash(keychain, path) + + return header + spending_key_hash + + +def _derive_reward_address( + keychain: seed.Keychain, path: List[int], network_id: int, +) -> bytes: + if not _is_staking_path(path): + raise wire.DataError("Invalid path for reward address!") + + header = _create_address_header(CardanoAddressType.REWARD, network_id) + staking_key_hash = get_public_key_hash(keychain, path) + + return header + staking_key_hash -def _get_address_root(node: bip32.HDNode, address_attributes: dict) -> bytes: - extpubkey = remove_ed25519_prefix(node.public_key()) + node.chain_code() - return _address_hash([0, [0, extpubkey], address_attributes]) +def to_account_path(path: List[int]) -> List[int]: + ACCOUNT_PATH_LENGTH = const(3) + return path[:ACCOUNT_PATH_LENGTH] diff --git a/core/src/apps/cardano/byron_address.py b/core/src/apps/cardano/byron_address.py new file mode 100644 index 000000000..bd44f9ec9 --- /dev/null +++ b/core/src/apps/cardano/byron_address.py @@ -0,0 +1,124 @@ +from trezor import log +from trezor.crypto import crc, hashlib + +from apps.common import cbor +from apps.common.seed import remove_ed25519_prefix + +from .helpers import INVALID_ADDRESS, NETWORK_MISMATCH, protocol_magics + +if False: + from trezor.crypto import bip32 + from . import seed + +PROTOCOL_MAGIC_KEY = 2 + + +""" +This is the legacy implementation of Byron addresses. Byron +addresses should however remain supported in Shelley with +exactly the same implementation, thus it is kept here +with base58 encoding and all the nuances of Byron addresses. +""" + + +def _encode_address_raw(address_data_encoded) -> bytes: + return cbor.encode( + [cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)] + ) + + +def derive_byron_address( + keychain: seed.Keychain, path: list, protocol_magic: int +) -> bytes: + node = keychain.derive(path) + + address_attributes = get_address_attributes(protocol_magic) + + address_root = _get_address_root(node, address_attributes) + address_type = 0 + address_data = [address_root, address_attributes, address_type] + address_data_encoded = cbor.encode(address_data) + + return _encode_address_raw(address_data_encoded) + + +def get_address_attributes(protocol_magic: int) -> dict: + # protocol magic is included in Byron addresses only on testnets + if protocol_magics.is_mainnet(protocol_magic): + address_attributes = {} + else: + address_attributes = {PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)} + + return address_attributes + + +def validate_output_byron_address(address: bytes, protocol_magic: int) -> None: + address_data_encoded = _decode_address_raw(address) + _validate_address_data_protocol_magic(address_data_encoded, protocol_magic) + + +def _decode_address_raw(address: bytes) -> bytes: + try: + address_unpacked = cbor.decode(address) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise INVALID_ADDRESS + + if not isinstance(address_unpacked, list) or len(address_unpacked) != 2: + raise INVALID_ADDRESS + + address_data_encoded = address_unpacked[0] + if not isinstance(address_data_encoded, bytes): + raise INVALID_ADDRESS + + address_crc = address_unpacked[1] + if not isinstance(address_crc, int): + raise INVALID_ADDRESS + + if address_crc != crc.crc32(address_data_encoded): + raise INVALID_ADDRESS + + return address_data_encoded + + +def _validate_address_data_protocol_magic( + address_data_encoded: bytes, protocol_magic: int +) -> None: + """ + Determines whether the correct protocol magic (or none) + is included in the address. Addresses on mainnet don't + contain protocol magic, but addresses on the testnet do. + """ + address_data = cbor.decode(address_data_encoded) + if not isinstance(address_data, list) or len(address_data) < 2: + raise INVALID_ADDRESS + + attributes = address_data[1] + if protocol_magics.is_mainnet(protocol_magic): + if PROTOCOL_MAGIC_KEY in attributes: + raise NETWORK_MISMATCH + else: # testnet + if len(attributes) == 0 or PROTOCOL_MAGIC_KEY not in attributes: + raise NETWORK_MISMATCH + + protocol_magic_cbor = attributes[PROTOCOL_MAGIC_KEY] + address_protocol_magic = cbor.decode(protocol_magic_cbor) + + if not isinstance(address_protocol_magic, int): + raise INVALID_ADDRESS + + if address_protocol_magic != protocol_magic: + raise NETWORK_MISMATCH + + +def _address_hash(data: list) -> bytes: + cbor_data = cbor.encode(data) + sha_data_hash = hashlib.sha3_256(cbor_data).digest() + res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest() + return res + + +def _get_address_root(node: bip32.HDNode, address_attributes: dict) -> bytes: + extpubkey = remove_ed25519_prefix(node.public_key()) + node.chain_code() + return _address_hash([0, [0, extpubkey], address_attributes]) diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index b3ffeef46..b4ded740f 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -2,30 +2,88 @@ from trezor import log, wire from trezor.messages.CardanoAddress import CardanoAddress from apps.common import paths -from apps.common.layout import address_n_to_str, show_address, show_qr +from apps.common.layout import address_n_to_str, show_qr from . import CURVE, seed -from .address import derive_address_and_node, validate_full_path +from .address import derive_human_readable_address, to_account_path, validate_full_path +from .helpers import protocol_magics, staking_use_cases +from .layout import ( + show_address, + show_warning_address_foreign_staking_key, + show_warning_address_pointer, +) + +if False: + from trezor.messages import CardanoAddressParametersType, CardanoGetAddress @seed.with_keychain -async def get_address(ctx, msg, keychain: seed.Keychain): - await paths.validate_path(ctx, validate_full_path, keychain, msg.address_n, CURVE) +async def get_address( + ctx: wire.Context, msg: CardanoGetAddress, keychain: seed.Keychain +) -> CardanoAddress: + address_parameters = msg.address_parameters + + await paths.validate_path( + ctx, validate_full_path, keychain, address_parameters.address_n, CURVE + ) try: - address, _ = derive_address_and_node( - keychain, msg.address_n, msg.protocol_magic + address = derive_human_readable_address( + keychain, address_parameters, msg.protocol_magic, msg.network_id ) except ValueError as e: if __debug__: log.exception(__name__, e) raise wire.ProcessError("Deriving address failed") + if msg.show_display: - desc = address_n_to_str(msg.address_n) - while True: - if await show_address(ctx, address, desc=desc): - break - if await show_qr(ctx, address, desc=desc): - break + await _display_address( + ctx, keychain, address_parameters, address, msg.protocol_magic + ) return CardanoAddress(address=address) + + +async def _display_address( + ctx: wire.Context, + keychain: seed.Keychain, + address_parameters: CardanoAddressParametersType, + address: str, + protocol_magic: int, +) -> None: + await _show_staking_warnings(ctx, keychain, address_parameters) + + network = None + if not protocol_magics.is_mainnet(protocol_magic): + network = protocol_magic + + while True: + if await show_address( + ctx, + address, + address_parameters.address_type, + address_parameters.address_n, + network=network, + ): + break + if await show_qr( + ctx, address, desc=address_n_to_str(address_parameters.address_n) + ): + break + + +async def _show_staking_warnings( + ctx: wire.Context, + keychain: seed.Keychain, + address_parameters: CardanoAddressParametersType, +) -> None: + staking_type = staking_use_cases.get(keychain, address_parameters) + if staking_type == staking_use_cases.MISMATCH: + await show_warning_address_foreign_staking_key( + ctx, + to_account_path(address_parameters.address_n), + to_account_path(address_parameters.address_n_staking), + address_parameters.staking_key_hash, + ) + elif staking_type == staking_use_cases.POINTER_ADDRESS: + await show_warning_address_pointer(ctx, address_parameters.certificate_pointer) diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py new file mode 100644 index 000000000..093e6e58f --- /dev/null +++ b/core/src/apps/cardano/helpers/__init__.py @@ -0,0 +1,4 @@ +from trezor import wire + +INVALID_ADDRESS = wire.ProcessError("Invalid address") +NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!") diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py new file mode 100644 index 000000000..165cb4717 --- /dev/null +++ b/core/src/apps/cardano/helpers/bech32.py @@ -0,0 +1,34 @@ +from trezor.crypto import bech32 + +HRP_SEPARATOR = "1" + +HRP_ADDRESS = "addr" +HRP_TESTNET_ADDRESS = "addr_test" +HRP_REWARD_ADDRESS = "stake" +HRP_TESTNET_REWARD_ADDRESS = "stake_test" + + +def encode(hrp: str, data: bytes) -> str: + converted_bits = bech32.convertbits(data, 8, 5) + return bech32.bech32_encode(hrp, converted_bits) + + +def decode_unsafe(bech: str) -> bytes: + hrp = get_hrp(bech) + return decode(hrp, bech) + + +def get_hrp(bech: str): + return bech.rsplit(HRP_SEPARATOR, 1)[0] + + +def decode(hrp: str, bech: str) -> bytes: + decoded_hrp, data = bech32.bech32_decode(bech, 130) + if decoded_hrp != hrp: + raise ValueError + + decoded = bech32.convertbits(data, 5, 8, False) + if decoded is None: + raise ValueError + + return bytes(decoded) diff --git a/core/src/apps/cardano/helpers/network_ids.py b/core/src/apps/cardano/helpers/network_ids.py new file mode 100644 index 000000000..854d7e7ea --- /dev/null +++ b/core/src/apps/cardano/helpers/network_ids.py @@ -0,0 +1,12 @@ +MAINNET = 1 +TESTNET = 0 + + +def is_mainnet(network_id: int) -> bool: + """ + In the future there might be 15 mainnet IDs and + still only one testnet ID. Therefore it is safer + to check that it is not a testnet id. Also, if + the mainnet id was to change, this would still work. + """ + return network_id != TESTNET diff --git a/core/src/apps/cardano/protocol_magics.py b/core/src/apps/cardano/helpers/protocol_magics.py similarity index 67% rename from core/src/apps/cardano/protocol_magics.py rename to core/src/apps/cardano/helpers/protocol_magics.py index 5d56e6abb..dc0538896 100644 --- a/core/src/apps/cardano/protocol_magics.py +++ b/core/src/apps/cardano/helpers/protocol_magics.py @@ -7,5 +7,9 @@ NAMES = { } +def is_mainnet(protocol_magic: int) -> bool: + return protocol_magic == MAINNET + + def to_ui_string(value: int) -> str: return NAMES.get(value, "Unknown") diff --git a/core/src/apps/cardano/helpers/purposes.py b/core/src/apps/cardano/helpers/purposes.py new file mode 100644 index 000000000..2f0742181 --- /dev/null +++ b/core/src/apps/cardano/helpers/purposes.py @@ -0,0 +1,4 @@ +from apps.common import HARDENED + +BYRON = 44 | HARDENED +SHELLEY = 1852 | HARDENED diff --git a/core/src/apps/cardano/helpers/seed_namespaces.py b/core/src/apps/cardano/helpers/seed_namespaces.py new file mode 100644 index 000000000..35e6ffede --- /dev/null +++ b/core/src/apps/cardano/helpers/seed_namespaces.py @@ -0,0 +1,6 @@ +from apps.common import HARDENED + +from . import purposes + +BYRON = [purposes.BYRON, 1815 | HARDENED] +SHELLEY = [purposes.SHELLEY, 1815 | HARDENED] diff --git a/core/src/apps/cardano/helpers/staking_use_cases.py b/core/src/apps/cardano/helpers/staking_use_cases.py new file mode 100644 index 000000000..10a23d2cc --- /dev/null +++ b/core/src/apps/cardano/helpers/staking_use_cases.py @@ -0,0 +1,58 @@ +from trezor.messages import CardanoAddressType + +from ..address import get_public_key_hash, to_account_path, validate_full_path +from ..seed import is_shelley_path + +if False: + from typing import List + from trezor.messages import CardanoAddressParametersType + from . import seed + + +""" +Used as a helper when deciding what warnings we should +display to the user during get_address and sign_tx depending +on the type of address and its parameters. +""" + + +NO_STAKING = 0 +MATCH = 1 +MISMATCH = 2 +POINTER_ADDRESS = 3 + + +def get( + keychain: seed.Keychain, address_parameters: CardanoAddressParametersType +) -> int: + address_type = address_parameters.address_type + if address_type == CardanoAddressType.BASE: + if not validate_full_path(address_parameters.address_n): + return MISMATCH + if not is_shelley_path(address_parameters.address_n): + return MISMATCH + + spending_account_staking_path = _path_to_staking_path( + address_parameters.address_n + ) + if address_parameters.address_n_staking: + if address_parameters.address_n_staking != spending_account_staking_path: + return MISMATCH + else: + staking_key_hash = get_public_key_hash( + keychain, spending_account_staking_path + ) + if address_parameters.staking_key_hash != staking_key_hash: + return MISMATCH + + return MATCH + elif address_type == CardanoAddressType.POINTER: + return POINTER_ADDRESS + elif address_type == CardanoAddressType.REWARD: + return MATCH + else: + return NO_STAKING + + +def _path_to_staking_path(path: List[int]) -> List[int]: + return to_account_path(path) + [2, 0] diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py new file mode 100644 index 000000000..f0e872765 --- /dev/null +++ b/core/src/apps/cardano/helpers/utils.py @@ -0,0 +1,20 @@ +def variable_length_encode(number: int) -> bytes: + """ + Used for pointer encoding in pointer address. + Encoding description can be found here: + https://en.wikipedia.org/wiki/Variable-length_quantity + """ + if number < 0: + raise ValueError("Negative numbers not supported. Number supplied: %s" % number) + + encoded = [] + + bit_length = len(bin(number)[2:]) + encoded.append(number & 127) + + while bit_length > 7: + number >>= 7 + bit_length -= 7 + encoded.insert(0, (number & 127) + 128) + + return bytes(encoded) diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py new file mode 100644 index 000000000..65a4033b0 --- /dev/null +++ b/core/src/apps/cardano/layout.py @@ -0,0 +1,224 @@ +from ubinascii import hexlify + +from trezor import ui +from trezor.messages import ButtonRequestType, CardanoAddressType +from trezor.strings import format_amount +from trezor.ui.button import ButtonDefault +from trezor.ui.scroll import Paginated +from trezor.ui.text import Text +from trezor.utils import chunks + +from apps.common.confirm import confirm, require_confirm, require_hold_to_confirm +from apps.common.layout import address_n_to_str, show_warning + +from .helpers import protocol_magics + +if False: + from typing import List + from trezor import wire + from trezor.messages import CardanoBlockchainPointerType + from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType + + +ADDRESS_TYPE_NAMES = { + CardanoAddressType.BYRON: "Legacy", + CardanoAddressType.BASE: "Base", + CardanoAddressType.POINTER: "Pointer", + CardanoAddressType.ENTERPRISE: "Enterprise", + CardanoAddressType.REWARD: "Reward", +} + + +def format_coin_amount(amount: int) -> str: + return "%s %s" % (format_amount(amount, 6), "ADA") + + +async def confirm_sending(ctx: wire.Context, amount: int, to: str): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Confirm sending:") + t1.bold(format_coin_amount(amount)) + t1.normal("to") + + to_lines = list(chunks(to, 17)) + t1.bold(to_lines[0]) + + pages = [t1] + _paginate_lines(to_lines, 1, "Confirm transaction", ui.ICON_SEND) + + await require_confirm(ctx, Paginated(pages)) + + +async def show_warning_tx_no_staking_info( + ctx: wire.Context, address_type: EnumTypeCardanoAddressType, amount: int +): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Change " + ADDRESS_TYPE_NAMES[address_type].lower()) + t1.normal("address has no stake") + t1.normal("rights.") + t1.normal("Change amount:") + t1.bold(format_coin_amount(amount)) + + await require_confirm(ctx, t1) + + +async def show_warning_tx_pointer_address( + ctx: wire.Context, pointer: CardanoBlockchainPointerType, amount: int, +): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Change address has a") + t1.normal("pointer with staking") + t1.normal("rights.") + + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t2.normal("Pointer:") + t2.bold( + "%s, %s, %s" + % (pointer.block_index, pointer.tx_index, pointer.certificate_index) + ) + t2.normal("Change amount:") + t2.bold(format_coin_amount(amount)) + + await require_confirm(ctx, Paginated([t1, t2])) + + +async def show_warning_tx_different_staking_account( + ctx: wire.Context, staking_account_path: List[int], amount: int, +): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Change address staking") + t1.normal("rights do not match") + t1.normal("the current account.") + + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t2.normal("Staking account:") + t2.bold(address_n_to_str(staking_account_path)) + t2.normal("Change amount:") + t2.bold(format_coin_amount(amount)) + + await require_confirm(ctx, Paginated([t1, t2])) + + +async def show_warning_tx_staking_key_hash( + ctx: wire.Context, staking_key_hash: bytes, amount: int, +): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Change address staking") + t1.normal("rights do not match") + t1.normal("the current account.") + + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t2.normal("Staking key hash:") + t2.mono(*chunks(hexlify(staking_key_hash), 17)) + + t3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t3.normal("Change amount:") + t3.bold(format_coin_amount(amount)) + + await require_confirm(ctx, Paginated([t1, t2, t3])) + + +async def confirm_transaction(ctx, amount: int, fee: int, protocol_magic: int): + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Transaction amount:") + t1.bold(format_coin_amount(amount)) + t1.normal("Transaction fee:") + t1.bold(format_coin_amount(fee)) + + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t2.normal("Network:") + t2.bold(protocol_magics.to_ui_string(protocol_magic)) + + await require_hold_to_confirm(ctx, Paginated([t1, t2])) + + +async def show_address( + ctx: wire.Context, + address: str, + address_type: EnumTypeCardanoAddressType, + path: List[int], + network: int = None, +) -> bool: + """ + Custom show_address function is needed because cardano addresses don't + fit on a single screen. + """ + path_str = address_n_to_str(path) + t1 = Text(path_str, ui.ICON_RECEIVE, ui.GREEN) + if network is not None: + t1.normal("%s network" % protocol_magics.to_ui_string(network)) + t1.normal("%s address" % ADDRESS_TYPE_NAMES[address_type]) + + address_lines = list(chunks(address, 17)) + t1.bold(address_lines[0]) + t1.bold(address_lines[1]) + t1.bold(address_lines[2]) + + pages = [t1] + _paginate_lines(address_lines, 3, path_str, ui.ICON_RECEIVE) + + return await confirm( + ctx, + Paginated(pages), + code=ButtonRequestType.Address, + cancel="QR", + cancel_style=ButtonDefault, + ) + + +def _paginate_lines( + lines: List[str], offset: int, desc: str, icon: str, per_page: int = 4 +) -> List[ui.Component]: + pages = [] + if len(lines) > offset: + to_pages = list(chunks(lines[offset:], per_page)) + for page in to_pages: + t = Text(desc, icon, ui.GREEN) + for line in page: + t.bold(line) + pages.append(t) + + return pages + + +async def show_warning_address_foreign_staking_key( + ctx: wire.Context, + account_path: List[int], + staking_account_path: List[int], + staking_key_hash: bytes, +) -> None: + await show_warning( + ctx, + ( + "Stake rights associated", + "with this address do", + "not match your", + "account", + address_n_to_str(account_path), + ), + button="Ok", + ) + + if staking_account_path: + staking_key_message = ( + "Stake account path:", + address_n_to_str(staking_account_path), + ) + else: + staking_key_message = ("Staking key:", hexlify(staking_key_hash)) + + await show_warning( + ctx, staking_key_message, button="Ok", + ) + + +async def show_warning_address_pointer( + ctx: wire.Context, pointer: CardanoBlockchainPointerType +) -> None: + await show_warning( + ctx, + ( + "Pointer address:", + "Block: %s" % pointer.block_index, + "Transaction: %s" % pointer.tx_index, + "Certificate: %s" % pointer.certificate_index, + ), + button="Ok", + ) diff --git a/core/src/apps/cardano/layout/__init__.py b/core/src/apps/cardano/layout/__init__.py deleted file mode 100644 index e84c70f63..000000000 --- a/core/src/apps/cardano/layout/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from micropython import const - -from trezor import ui -from trezor.strings import format_amount -from trezor.ui.scroll import Paginated -from trezor.ui.text import Text -from trezor.utils import chunks - -from apps.common.confirm import require_confirm, require_hold_to_confirm - -from .. import protocol_magics - - -def format_coin_amount(amount): - return "%s %s" % (format_amount(amount, 6), "ADA") - - -async def confirm_sending(ctx, amount, to): - to_lines = list(chunks(to, 17)) - - t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - t1.normal("Confirm sending:") - t1.bold(format_coin_amount(amount)) - t1.normal("to:") - t1.bold(to_lines[0]) - - PER_PAGE = const(4) - pages = [t1] - if len(to_lines) > 1: - to_pages = list(chunks(to_lines[1:], PER_PAGE)) - for page in to_pages: - t = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - for line in page: - t.bold(line) - pages.append(t) - - await require_confirm(ctx, Paginated(pages)) - - -async def confirm_transaction(ctx, amount, fee, protocol_magic): - t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - t1.normal("Total amount:") - t1.bold(format_coin_amount(amount)) - t1.normal("including fee:") - t1.bold(format_coin_amount(fee)) - - t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - t2.normal("Network:") - t2.bold(protocol_magics.to_ui_string(protocol_magic)) - - await require_hold_to_confirm(ctx, Paginated([t1, t2])) diff --git a/core/src/apps/cardano/seed.py b/core/src/apps/cardano/seed.py index 912961121..495f385b2 100644 --- a/core/src/apps/cardano/seed.py +++ b/core/src/apps/cardano/seed.py @@ -5,7 +5,7 @@ from trezor.crypto import bip32 from apps.common import mnemonic from apps.common.passphrase import get as get_passphrase -from . import SEED_NAMESPACE +from .helpers import seed_namespaces if False: from apps.common.paths import Bip32Path @@ -13,19 +13,42 @@ if False: class Keychain: - """Cardano keychain hard-coded to SEED_NAMESPACE.""" + """Cardano keychain hard-coded to Byron and Shelley seed namespaces.""" def __init__(self, root: bip32.HDNode) -> None: self.root = root + self.byron_root = self._create_namespace_root(seed_namespaces.BYRON) + self.shelley_root = self._create_namespace_root(seed_namespaces.SHELLEY) def verify_path(self, path: Bip32Path) -> None: - if path[: len(SEED_NAMESPACE)] != SEED_NAMESPACE: + if not is_byron_path(path) and not is_shelley_path(path): + raise wire.DataError("Forbidden key path") + + def _create_namespace_root(self, namespace: list): + new_root = self.root.clone() + for i in namespace: + new_root.derive_cardano(i) + + return new_root + + def _get_path_root(self, path: list): + if is_byron_path(path): + return self.byron_root + elif is_shelley_path(path): + return self.shelley_root + else: raise wire.DataError("Forbidden key path") def derive(self, node_path: Bip32Path) -> bip32.HDNode: + self.verify_path(node_path) + path_root = self._get_path_root(node_path) + + # this is true now, so for simplicity we don't branch on path type + assert len(seed_namespaces.BYRON) == len(seed_namespaces.SHELLEY) + suffix = node_path[len(seed_namespaces.SHELLEY) :] + # derive child node from the root - node = self.root.clone() - suffix = node_path[len(SEED_NAMESPACE) :] + node = path_root.clone() for i in suffix: node.derive_cardano(i) return node @@ -35,6 +58,14 @@ class Keychain: # self.root.__del__() +def is_byron_path(path: Bip32Path): + return path[: len(seed_namespaces.BYRON)] == seed_namespaces.BYRON + + +def is_shelley_path(path: Bip32Path): + return path[: len(seed_namespaces.SHELLEY)] == seed_namespaces.SHELLEY + + @cache.stored_async(cache.APP_CARDANO_ROOT) async def get_keychain(ctx: wire.Context) -> Keychain: if not device.is_initialized(): @@ -49,10 +80,6 @@ async def get_keychain(ctx: wire.Context) -> Keychain: seed = mnemonic.get_seed(passphrase) root = bip32.from_seed(seed, "ed25519 cardano seed") - # derive the namespaced root node - for i in SEED_NAMESPACE: - root.derive_cardano(i) - keychain = Keychain(root) return keychain diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index a17aaaa7d..eae078467 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -1,8 +1,9 @@ from micropython import const from trezor import log, wire -from trezor.crypto import base58, hashlib +from trezor.crypto import hashlib from trezor.crypto.curve import ed25519 +from trezor.messages import CardanoAddressParametersType from trezor.messages.CardanoSignedTx import CardanoSignedTx from apps.common import cbor @@ -11,12 +12,24 @@ from apps.common.seed import remove_ed25519_prefix from . import CURVE, seed from .address import ( - derive_address_and_node, - get_address_attributes, + derive_address_bytes, + derive_human_readable_address, + get_address_bytes_unsafe, + to_account_path, validate_full_path, validate_output_address, ) -from .layout import confirm_sending, confirm_transaction +from .byron_address import get_address_attributes +from .helpers import network_ids, protocol_magics, staking_use_cases +from .layout import ( + confirm_sending, + confirm_transaction, + show_warning_tx_different_staking_account, + show_warning_tx_no_staking_info, + show_warning_tx_pointer_address, + show_warning_tx_staking_key_hash, +) +from .seed import is_byron_path, is_shelley_path if False: from typing import Dict, List, Tuple @@ -41,10 +54,12 @@ async def sign_tx( if msg.fee > LOVELACE_MAX_SUPPLY: raise wire.ProcessError("Fee is out of range!") + _validate_network_info(msg.network_id, msg.protocol_magic) + for i in msg.inputs: await validate_path(ctx, validate_full_path, keychain, i.address_n, CURVE) - _validate_outputs(keychain, msg.outputs, msg.protocol_magic) + _validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id) # display the transaction in UI await _show_tx(ctx, keychain, msg) @@ -61,18 +76,42 @@ async def sign_tx( return tx +def _validate_network_info(network_id: int, protocol_magic: int) -> None: + """ + We are only concerned about checking that both network_id and protocol_magic + belong to the mainnet or that both belong to a testnet. We don't need to check for + consistency between various testnets (at least for now). + """ + is_mainnet_network_id = network_ids.is_mainnet(network_id) + is_mainnet_protocol_magic = protocol_magics.is_mainnet(protocol_magic) + + if is_mainnet_network_id != is_mainnet_protocol_magic: + raise wire.ProcessError("Invalid network id/protocol magic combination!") + + def _validate_outputs( - keychain: seed.Keychain, outputs: List[CardanoTxOutputType], protocol_magic: int + keychain: seed.Keychain, + outputs: List[CardanoTxOutputType], + protocol_magic: int, + network_id: int, ) -> None: + if not outputs: + raise wire.ProcessError("Transaction has no outputs!") + total_amount = 0 for output in outputs: total_amount += output.amount - if output.address_n: - continue + if output.address_parameters: + # try to derive the address to validate it + derive_address_bytes( + keychain, output.address_parameters, protocol_magic, network_id + ) elif output.address is not None: - validate_output_address(output.address, protocol_magic) + validate_output_address(output.address, protocol_magic, network_id) else: - raise wire.ProcessError("Each output must have address or address_n field!") + raise wire.ProcessError( + "Each output must have an address field or address_parameters!" + ) if total_amount > LOVELACE_MAX_SUPPLY: raise wire.ProcessError("Total transaction amount is out of range!") @@ -82,11 +121,7 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b tx_body = _build_tx_body(keychain, msg) tx_hash = _hash_tx_body(tx_body) - witnesses_for_cbor = _build_witnesses( - keychain, msg.inputs, tx_hash, msg.protocol_magic - ) - # byron witnesses have the key 2 in Shelley - witnesses = {2: witnesses_for_cbor} + witnesses = _build_witnesses(keychain, msg.inputs, tx_hash, msg.protocol_magic) serialized_tx = cbor.encode([tx_body, witnesses, None]) @@ -95,7 +130,9 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: inputs_for_cbor = _build_inputs(msg.inputs) - outputs_for_cbor = _build_outputs(keychain, msg.outputs, msg.protocol_magic) + outputs_for_cbor = _build_outputs( + keychain, msg.outputs, msg.protocol_magic, msg.network_id + ) tx_body = { 0: inputs_for_cbor, @@ -112,19 +149,23 @@ def _build_inputs(inputs: List[CardanoTxInputType]) -> List[Tuple[bytes, int]]: def _build_outputs( - keychain: seed.Keychain, outputs: List[CardanoTxOutputType], protocol_magic: int + keychain: seed.Keychain, + outputs: List[CardanoTxOutputType], + protocol_magic: int, + network_id: int, ) -> List[Tuple[bytes, int]]: result = [] for output in outputs: amount = output.amount - if output.address_n: - address, _ = derive_address_and_node( - keychain, output.address_n, protocol_magic + if output.address_parameters: + address = derive_address_bytes( + keychain, output.address_parameters, protocol_magic, network_id ) else: - address = output.address + # output address is validated in _validate_outputs before this happens + address = get_address_bytes_unsafe(output.address) - result.append((base58.decode(address), amount)) + result.append((address, amount)) return result @@ -139,9 +180,53 @@ def _build_witnesses( inputs: List[CardanoTxInputType], tx_body_hash: bytes, protocol_magic: int, +) -> Dict: + shelley_witnesses = _build_shelley_witnesses(keychain, inputs, tx_body_hash) + byron_witnesses = _build_byron_witnesses( + keychain, inputs, tx_body_hash, protocol_magic + ) + + # use key 0 for shelley witnesses and key 2 for byron witnesses + # according to the spec in shelley.cddl in cardano-ledger-specs + witnesses = {} + if len(shelley_witnesses) > 0: + witnesses[0] = shelley_witnesses + if len(byron_witnesses) > 0: + witnesses[2] = byron_witnesses + + return witnesses + + +def _build_shelley_witnesses( + keychain: seed.Keychain, inputs: List[CardanoTxInputType], tx_body_hash: bytes, +) -> List[Tuple[bytes, bytes]]: + shelley_witnesses = [] + for input in inputs: + if not is_shelley_path(input.address_n): + continue + + node = keychain.derive(input.address_n) + + public_key = remove_ed25519_prefix(node.public_key()) + signature = ed25519.sign_ext( + node.private_key(), node.private_key_ext(), tx_body_hash + ) + shelley_witnesses.append((public_key, signature)) + + return shelley_witnesses + + +def _build_byron_witnesses( + keychain: seed.Keychain, + inputs: List[CardanoTxInputType], + tx_body_hash: bytes, + protocol_magic: int, ) -> List[Tuple[bytes, bytes, bytes, bytes]]: - result = [] + byron_witnesses = [] for input in inputs: + if not is_byron_path(input.address_n): + continue + node = keychain.derive(input.address_n) public_key = remove_ed25519_prefix(node.public_key()) @@ -151,31 +236,69 @@ def _build_witnesses( chain_code = node.chain_code() address_attributes = cbor.encode(get_address_attributes(protocol_magic)) - result.append((public_key, signature, chain_code, address_attributes)) + byron_witnesses.append((public_key, signature, chain_code, address_attributes)) - return result + return byron_witnesses async def _show_tx( ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx ) -> None: + total_amount = await _show_outputs(ctx, keychain, msg) + await confirm_transaction(ctx, total_amount, msg.fee, msg.protocol_magic) + + +async def _show_outputs( + ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx +) -> int: total_amount = 0 for output in msg.outputs: - if _should_hide_output(output.address_n, msg.inputs): - continue - - total_amount += output.amount + if output.address_parameters: + address = derive_human_readable_address( + keychain, output.address_parameters, msg.protocol_magic, msg.network_id + ) - if not output.address: - address, _ = derive_address_and_node( - keychain, output.address_n, msg.protocol_magic + await _show_change_output_staking_warnings( + ctx, keychain, output.address_parameters, address, output.amount ) + + if _should_hide_output(output.address_parameters.address_n, msg.inputs): + continue else: address = output.address + total_amount += output.amount + await confirm_sending(ctx, output.amount, address) - await confirm_transaction(ctx, total_amount, msg.fee, msg.protocol_magic) + return total_amount + + +async def _show_change_output_staking_warnings( + ctx: wire.Context, + keychain: seed.Keychain, + address_parameters: CardanoAddressParametersType, + address: str, + amount: int, +): + address_type = address_parameters.address_type + + staking_use_case = staking_use_cases.get(keychain, address_parameters) + if staking_use_case == staking_use_cases.NO_STAKING: + await show_warning_tx_no_staking_info(ctx, address_type, amount) + elif staking_use_case == staking_use_cases.POINTER_ADDRESS: + await show_warning_tx_pointer_address( + ctx, address_parameters.certificate_pointer, amount, + ) + elif staking_use_case == staking_use_cases.MISMATCH: + if address_parameters.address_n_staking: + await show_warning_tx_different_staking_account( + ctx, to_account_path(address_parameters.address_n_staking), amount, + ) + else: + await show_warning_tx_staking_key_hash( + ctx, address_parameters.staking_key_hash, amount, + ) # addresses from the same account as inputs should be hidden diff --git a/core/src/trezor/crypto/bech32.py b/core/src/trezor/crypto/bech32.py index 2cb78141d..826dec678 100644 --- a/core/src/trezor/crypto/bech32.py +++ b/core/src/trezor/crypto/bech32.py @@ -61,7 +61,9 @@ def bech32_encode(hrp: str, data: List[int]) -> str: return hrp + "1" + "".join([CHARSET[d] for d in combined]) -def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]: +def bech32_decode( + bech: str, max_bech_len: int = 90 +) -> Tuple[Optional[str], Optional[List[int]]]: """Validate a Bech32 string, and determine HRP and data.""" if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( bech.lower() != bech and bech.upper() != bech @@ -69,7 +71,7 @@ def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]: return (None, None) bech = bech.lower() pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + if pos < 1 or pos + 7 > len(bech) or len(bech) > max_bech_len: return (None, None) if not all(x in CHARSET for x in bech[pos + 1 :]): return (None, None) diff --git a/core/src/trezor/messages/CardanoAddressParametersType.py b/core/src/trezor/messages/CardanoAddressParametersType.py new file mode 100644 index 000000000..5cae7b764 --- /dev/null +++ b/core/src/trezor/messages/CardanoAddressParametersType.py @@ -0,0 +1,40 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .CardanoBlockchainPointerType import CardanoBlockchainPointerType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeCardanoAddressType = Literal[0, 4, 6, 8, 14] + except ImportError: + pass + + +class CardanoAddressParametersType(p.MessageType): + + def __init__( + self, + address_type: EnumTypeCardanoAddressType = None, + address_n: List[int] = None, + address_n_staking: List[int] = None, + staking_key_hash: bytes = None, + certificate_pointer: CardanoBlockchainPointerType = None, + ) -> None: + self.address_type = address_type + self.address_n = address_n if address_n is not None else [] + self.address_n_staking = address_n_staking if address_n_staking is not None else [] + self.staking_key_hash = staking_key_hash + self.certificate_pointer = certificate_pointer + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('address_type', p.EnumType("CardanoAddressType", (0, 4, 6, 8, 14)), 0), + 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 3: ('address_n_staking', p.UVarintType, p.FLAG_REPEATED), + 4: ('staking_key_hash', p.BytesType, 0), + 5: ('certificate_pointer', CardanoBlockchainPointerType, 0), + } diff --git a/core/src/trezor/messages/CardanoAddressType.py b/core/src/trezor/messages/CardanoAddressType.py new file mode 100644 index 000000000..804184b6f --- /dev/null +++ b/core/src/trezor/messages/CardanoAddressType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +if False: + from typing_extensions import Literal + +BASE = 0 # type: Literal[0] +POINTER = 4 # type: Literal[4] +ENTERPRISE = 6 # type: Literal[6] +BYRON = 8 # type: Literal[8] +REWARD = 14 # type: Literal[14] diff --git a/core/src/trezor/messages/CardanoBlockchainPointerType.py b/core/src/trezor/messages/CardanoBlockchainPointerType.py new file mode 100644 index 000000000..bd55a83f4 --- /dev/null +++ b/core/src/trezor/messages/CardanoBlockchainPointerType.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoBlockchainPointerType(p.MessageType): + + def __init__( + self, + block_index: int = None, + tx_index: int = None, + certificate_index: int = None, + ) -> None: + self.block_index = block_index + self.tx_index = tx_index + self.certificate_index = certificate_index + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('block_index', p.UVarintType, 0), + 2: ('tx_index', p.UVarintType, 0), + 3: ('certificate_index', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/CardanoGetAddress.py b/core/src/trezor/messages/CardanoGetAddress.py index d6c8f35ec..1db3e45e4 100644 --- a/core/src/trezor/messages/CardanoGetAddress.py +++ b/core/src/trezor/messages/CardanoGetAddress.py @@ -2,6 +2,8 @@ # fmt: off import protobuf as p +from .CardanoAddressParametersType import CardanoAddressParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 @@ -15,18 +17,21 @@ class CardanoGetAddress(p.MessageType): def __init__( self, - address_n: List[int] = None, show_display: bool = None, protocol_magic: int = None, + network_id: int = None, + address_parameters: CardanoAddressParametersType = None, ) -> None: - self.address_n = address_n if address_n is not None else [] self.show_display = show_display self.protocol_magic = protocol_magic + self.network_id = network_id + self.address_parameters = address_parameters @classmethod def get_fields(cls) -> Dict: return { - 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), 2: ('show_display', p.BoolType, 0), 3: ('protocol_magic', p.UVarintType, 0), + 4: ('network_id', p.UVarintType, 0), + 5: ('address_parameters', CardanoAddressParametersType, 0), } diff --git a/core/src/trezor/messages/CardanoSignTx.py b/core/src/trezor/messages/CardanoSignTx.py index 74d817674..b434e4823 100644 --- a/core/src/trezor/messages/CardanoSignTx.py +++ b/core/src/trezor/messages/CardanoSignTx.py @@ -23,12 +23,14 @@ class CardanoSignTx(p.MessageType): protocol_magic: int = None, fee: int = None, ttl: int = None, + network_id: int = None, ) -> None: self.inputs = inputs if inputs is not None else [] self.outputs = outputs if outputs is not None else [] self.protocol_magic = protocol_magic self.fee = fee self.ttl = ttl + self.network_id = network_id @classmethod def get_fields(cls) -> Dict: @@ -38,4 +40,5 @@ class CardanoSignTx(p.MessageType): 5: ('protocol_magic', p.UVarintType, 0), 6: ('fee', p.UVarintType, 0), 7: ('ttl', p.UVarintType, 0), + 8: ('network_id', p.UVarintType, 0), } diff --git a/core/src/trezor/messages/CardanoTxOutputType.py b/core/src/trezor/messages/CardanoTxOutputType.py index a6285c08e..2837881c5 100644 --- a/core/src/trezor/messages/CardanoTxOutputType.py +++ b/core/src/trezor/messages/CardanoTxOutputType.py @@ -2,6 +2,8 @@ # fmt: off import protobuf as p +from .CardanoAddressParametersType import CardanoAddressParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 @@ -15,17 +17,17 @@ class CardanoTxOutputType(p.MessageType): def __init__( self, address: str = None, - address_n: List[int] = None, amount: int = None, + address_parameters: CardanoAddressParametersType = None, ) -> None: self.address = address - self.address_n = address_n if address_n is not None else [] self.amount = amount + self.address_parameters = address_parameters @classmethod def get_fields(cls) -> Dict: return { 1: ('address', p.UnicodeType, 0), - 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), 3: ('amount', p.UVarintType, 0), + 4: ('address_parameters', CardanoAddressParametersType, 0), } diff --git a/core/tests/test_apps.cardano.address.py b/core/tests/test_apps.cardano.address.py index 56ce12246..8d50eb1d6 100644 --- a/core/tests/test_apps.cardano.address.py +++ b/core/tests/test_apps.cardano.address.py @@ -1,16 +1,22 @@ from common import * -from apps.common import seed - -from apps.common import HARDENED +from trezor import wire from trezor.crypto import bip32, slip39 +from trezor.messages import CardanoAddressType +from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType +from trezor.messages.CardanoBlockchainPointerType import CardanoBlockchainPointerType + +from apps.common import HARDENED, seed + if not utils.BITCOIN_ONLY: - from apps.cardano import protocol_magics from apps.cardano.address import ( + derive_human_readable_address, + validate_full_path, + ) + from apps.cardano.byron_address import ( _get_address_root, _address_hash, - validate_full_path, - derive_address_and_node ) + from apps.cardano.helpers import network_ids, protocol_magics from apps.cardano.seed import Keychain @@ -20,8 +26,6 @@ class TestCardanoAddress(unittest.TestCase): mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) addresses = [ @@ -32,7 +36,11 @@ class TestCardanoAddress(unittest.TestCase): for i, expected in enumerate(addresses): # 44'/1815'/0'/0/i' - address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i], protocol_magics.MAINNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i], + ) + address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET) self.assertEqual(expected, address) nodes = [ @@ -57,7 +65,7 @@ class TestCardanoAddress(unittest.TestCase): ] for i, (priv, ext, pub, chain) in enumerate(nodes): - _, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i], protocol_magics.MAINNET) + n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i]) self.assertEqual(hexlify(n.private_key()), priv) self.assertEqual(hexlify(n.private_key_ext()), ext) self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) @@ -67,8 +75,6 @@ class TestCardanoAddress(unittest.TestCase): mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) addresses = [ @@ -79,7 +85,11 @@ class TestCardanoAddress(unittest.TestCase): for i, expected in enumerate(addresses): # 44'/1815'/0'/0/i - address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], + ) + address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET) self.assertEqual(address, expected) nodes = [ @@ -104,7 +114,7 @@ class TestCardanoAddress(unittest.TestCase): ] for i, (priv, ext, pub, chain) in enumerate(nodes): - _, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET) + n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i]) self.assertEqual(hexlify(n.private_key()), priv) self.assertEqual(hexlify(n.private_key_ext()), ext) self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) @@ -115,12 +125,14 @@ class TestCardanoAddress(unittest.TestCase): mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) # 44'/1815' - address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815], protocol_magics.MAINNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815], + ) + address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET) self.assertEqual(address, "Ae2tdPwUPEZ2FGHX3yCKPSbSgyuuTYgMxNq652zKopxT4TuWvEd8Utd92w3") priv, ext, pub, chain = ( @@ -130,7 +142,7 @@ class TestCardanoAddress(unittest.TestCase): b"02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a" ) - _, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815], protocol_magics.MAINNET) + n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815]) self.assertEqual(hexlify(n.private_key()), priv) self.assertEqual(hexlify(n.private_key_ext()), ext) self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) @@ -198,8 +210,6 @@ class TestCardanoAddress(unittest.TestCase): self.assertEqual(hexlify(node.chain_code()), root_chain) # Check derived nodes and addresses. - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) nodes = [ @@ -228,7 +238,12 @@ class TestCardanoAddress(unittest.TestCase): for i, (address, priv, ext, pub, chain) in enumerate(nodes): # 44'/1815'/0'/0/i - a, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], + ) + a = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET) + n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i]) self.assertEqual(a, address) self.assertEqual(hexlify(n.private_key()), priv) self.assertEqual(hexlify(n.private_key_ext()), ext) @@ -262,8 +277,6 @@ class TestCardanoAddress(unittest.TestCase): self.assertEqual(hexlify(node.chain_code()), root_chain) # Check derived nodes and addresses. - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) nodes = [ @@ -292,19 +305,274 @@ class TestCardanoAddress(unittest.TestCase): for i, (address, priv, ext, pub, chain) in enumerate(nodes): # 44'/1815'/0'/0/i - a, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], + ) + a = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET) + n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i]) self.assertEqual(a, address) self.assertEqual(hexlify(n.private_key()), priv) self.assertEqual(hexlify(n.private_key_ext()), ext) self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) self.assertEqual(hexlify(n.chain_code()), chain) - def test_testnet_address(self): + def test_base_address(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + test_vectors = [ + # network id, account, expected result + # data generated with code under test + (network_ids.MAINNET, 4, "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"), + (network_ids.TESTNET, 4, "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"), + ] + + for network_id, account, expected_address in test_vectors: + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 2, 0] + ) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + + def test_base_address_with_staking_key_hash(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + test_vectors = [ + # network id, account, staking key hash, expected result + # own staking key hash + # data generated with code under test + (network_ids.MAINNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"), + (network_ids.TESTNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"), + # staking key hash not owned - derived with "all all..." mnenomnic, data generated with code under test + (network_ids.MAINNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsxrrvc2"), + (network_ids.MAINNET, 0, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzersj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms6xjnst"), + (network_ids.TESTNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms947v54"), + ] + + for network_id, account, staking_key_hash, expected_address in test_vectors: + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0], + staking_key_hash=staking_key_hash, + ) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + + def test_base_address_with_invalid_parameters(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + # both address_n_staking and staking_key_hash are None + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + address_n_staking=None, + staking_key_hash=None, + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + # address_n_staking is not a staking path + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=None, + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + def test_enterprise_address(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + test_vectors = [ + # network id, expected result + (network_ids.MAINNET, "addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8"), + (network_ids.TESTNET, "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz") + ] + + for network_id, expected_address in test_vectors: + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + + def test_pointer_address(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + test_vectors = [ + # network id, pointer, expected result + (network_ids.MAINNET, CardanoBlockchainPointerType(1, 2, 3), "addr1gx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpse33frd"), + (network_ids.TESTNET, CardanoBlockchainPointerType(24157, 177, 42), "addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5ph3wczvf2pfz4ly") + ] + + for network_id, pointer, expected_address in test_vectors: + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=pointer, + ) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + + def test_pointer_address_invalid_pointers(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + # pointer is None + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=None, + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + # block index is None + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(None, 2, 3), + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + # tx index is None + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(1, None, 3), + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + # certificate index is None + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(1, 2, None), + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + def test_reward_address(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + test_vectors = [ + # network id, expected result + (network_ids.MAINNET, "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz"), + (network_ids.TESTNET, "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl") + ] + + for network_id, expected_address in test_vectors: + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + + def test_reward_address_with_non_staking_path(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + def test_shelley_address_with_byron_namespace(self): + """ + It shouldn't be possible to derive Shelley addresses + (Base, Pointer, Enterprise, Reward) with a Byron namespace (44') + """ + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(0, 0, 0) + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + def test_byron_address_with_shelley_namespace(self): + """ + It shouldn't be possible to derive Byron addresses + with a Shelley namespace (1852') + """ + mnemonic = "all all all all all all all all all all all all" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + with self.assertRaises(wire.DataError): + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ) + derive_human_readable_address(keychain, address_parameters, 0, 0) + + def test_testnet_byron_address(self): mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) addresses = [ @@ -315,7 +583,11 @@ class TestCardanoAddress(unittest.TestCase): for i, expected in enumerate(addresses): # 44'/1815'/0'/0/i' - address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.TESTNET) + address_parameters = CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], + ) + address = derive_human_readable_address(keychain, address_parameters, protocol_magics.TESTNET, 0) self.assertEqual(expected, address) if __name__ == '__main__': diff --git a/core/tests/test_apps.cardano.bech32.py b/core/tests/test_apps.cardano.bech32.py new file mode 100644 index 000000000..86a7644fb --- /dev/null +++ b/core/tests/test_apps.cardano.bech32.py @@ -0,0 +1,28 @@ +from common import * + +from apps.cardano.helpers import bech32 + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoBech32(unittest.TestCase): + def test_decode_and_encode(self): + expected_bechs = [ + # human readable part, bech32 + ("a", "a12uel5l"), + ("an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs"), + ("abcdef", "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"), + ("1", "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j"), + ("split", "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w"), + ("addr", "addr1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsw8ezsk"), + ("addr_test", "addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5") + ] + + for expected_human_readable_part, expected_bech in expected_bechs: + decoded = bech32.decode(expected_human_readable_part, expected_bech) + actual_bech = bech32.encode(expected_human_readable_part, decoded) + + self.assertEqual(actual_bech, expected_bech) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/tests/test_apps.cardano.get_public_key.py b/core/tests/test_apps.cardano.get_public_key.py index 042175561..d4b4aa9c0 100644 --- a/core/tests/test_apps.cardano.get_public_key.py +++ b/core/tests/test_apps.cardano.get_public_key.py @@ -11,8 +11,6 @@ class TestCardanoGetPublicKey(unittest.TestCase): mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) derivation_paths = [ @@ -20,6 +18,11 @@ class TestCardanoGetPublicKey(unittest.TestCase): [0x80000000 | 44, 0x80000000 | 1815], [0x80000000 | 44, 0x80000000 | 1815, 0, 0, 0], [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0], + + [0x80000000 | 1852, 0x80000000 | 1815, 0x80000000, 0, 0x80000000], + [0x80000000 | 1852, 0x80000000 | 1815], + [0x80000000 | 1852, 0x80000000 | 1815, 0, 0, 0], + [0x80000000 | 1852, 0x80000000 | 1815, 0x80000000, 0, 0], ] public_keys = [ @@ -27,6 +30,11 @@ class TestCardanoGetPublicKey(unittest.TestCase): b'8c47ebce34234d04fd3dfbac33feaba6133e4e3d77c4b5ab18120ec6878ad4ce', b'17cc0bf978756d0d5c76f931629036a810c61801b78beecb44555773d13e3791', b'b90fb812a2268e9569ff1172e8daed1da3dc7e72c7bded7c5bcb7282039f90d5', + + b'f698a764b23aa6667b1157fc4247c6a1b58c21a3865ac6a47a3590167a9e0211', + b'e9c46841be76e3be0289694fd5c7503c04f40e5b036abac200b98a9006cf6647', + b'6d225f078ca611f00d86cbfd8ba6c6ac7826721434eae6525686efb878b72370', + b'5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1', ] chain_codes = [ @@ -34,6 +42,11 @@ class TestCardanoGetPublicKey(unittest.TestCase): b'02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a', b'646ac4a6295326bae6831be05921edfbcb362de48dfd37b12e74c227dfad768d', b'fd8e71c1543de2cdc7f7623130c5f2cceb53549055fa1f5bc88199989e08cce7', + + b'13cfb6de37a568aae56cadac907e6469b121464fe1b70a10c213eaea2cbb6636', + b'58f3f46f4a93e7a4431e75b10af7497b747c3053cb7466ed53f4277e78a63c52', + b'f72b3c361381db2d88289440268c94c5e7467c9414375e6b63d03026750f3c66', + b'f123474e140a2c360b01f0fa66f2f22e2e965a5b07a80358cf75f77abbd66088', ] xpub_keys = [ @@ -41,6 +54,11 @@ class TestCardanoGetPublicKey(unittest.TestCase): '8c47ebce34234d04fd3dfbac33feaba6133e4e3d77c4b5ab18120ec6878ad4ce02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a', '17cc0bf978756d0d5c76f931629036a810c61801b78beecb44555773d13e3791646ac4a6295326bae6831be05921edfbcb362de48dfd37b12e74c227dfad768d', 'b90fb812a2268e9569ff1172e8daed1da3dc7e72c7bded7c5bcb7282039f90d5fd8e71c1543de2cdc7f7623130c5f2cceb53549055fa1f5bc88199989e08cce7', + + 'f698a764b23aa6667b1157fc4247c6a1b58c21a3865ac6a47a3590167a9e021113cfb6de37a568aae56cadac907e6469b121464fe1b70a10c213eaea2cbb6636', + 'e9c46841be76e3be0289694fd5c7503c04f40e5b036abac200b98a9006cf664758f3f46f4a93e7a4431e75b10af7497b747c3053cb7466ed53f4277e78a63c52', + '6d225f078ca611f00d86cbfd8ba6c6ac7826721434eae6525686efb878b72370f72b3c361381db2d88289440268c94c5e7467c9414375e6b63d03026750f3c66', + '5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1f123474e140a2c360b01f0fa66f2f22e2e965a5b07a80358cf75f77abbd66088', ] for index, derivation_path in enumerate(derivation_paths): @@ -65,8 +83,6 @@ class TestCardanoGetPublicKey(unittest.TestCase): node = bip32.from_seed(master_secret, "ed25519 cardano seed") - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) # 44'/1815'/0'/0/i @@ -116,8 +132,6 @@ class TestCardanoGetPublicKey(unittest.TestCase): node = bip32.from_seed(master_secret, "ed25519 cardano seed") - node.derive_cardano(0x80000000 | 44) - node.derive_cardano(0x80000000 | 1815) keychain = Keychain(node) # 44'/1815'/0'/0/i diff --git a/core/tests/test_apps.cardano.keychain.py b/core/tests/test_apps.cardano.keychain.py new file mode 100644 index 000000000..f7d41ee5d --- /dev/null +++ b/core/tests/test_apps.cardano.keychain.py @@ -0,0 +1,65 @@ +from common import * +from trezor import wire +from trezor.crypto import bip32, slip39 + +from apps.common import HARDENED, seed + +if not utils.BITCOIN_ONLY: + from apps.cardano.seed import Keychain + from apps.cardano.get_public_key import _get_public_key + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoKeychain(unittest.TestCase): + def test_various_paths_at_once(self): + mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + derivation_paths = [ + [44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 0], + [44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 1], + [1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 0], + [1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 1], + [44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 2], + [1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 2] + ] + + public_keys = [ + b'badd2852ccda7492364be0f88f2ba0b78c5f2d7179a941f1d19f756112b66afa', + b'34377409140c061d76778626d43456880d5471c1cbade8c372cb6a3be9678072', + b'73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d', + b'f626ab887eb5f40b502463ccf2ec5a7311676ee9e5d55c492059a366c0b4d4a1', + b'408ee7b2d1c84d7899dba07150fae88c5411974f1762cb659dd928db8aac206b', + b'86e8a3880767e1ed521a47de1e031d47f33d5a8095be467bffbbd3295e27258e' + ] + + chain_codes = [ + b"e1c5d15875d3ed68667978af38fe3fe586511d87a784c0962a333c21e63a865d", + b"15c987276326a82defa4cb6762d43442f09e5dcbcc37fa0c58f24ae2dba3d3eb", + b"dd75e154da417becec55cdd249327454138f082110297d5e87ab25e15fad150f", + b"f7ab126f2884db9059fa09ca83be6b8bd0250426aeb62191bdd9861457b8bc91", + b"18d5c9d20c8d23bed068c9ff3a1126b940f0e537f9d94891828a999dda6fafd1", + b"580bba4bb0b9c56974e16a6998322a91e857e2fac28674404da993f6197fd29f" + ] + + xpub_keys = [ + "badd2852ccda7492364be0f88f2ba0b78c5f2d7179a941f1d19f756112b66afae1c5d15875d3ed68667978af38fe3fe586511d87a784c0962a333c21e63a865d", + "34377409140c061d76778626d43456880d5471c1cbade8c372cb6a3be967807215c987276326a82defa4cb6762d43442f09e5dcbcc37fa0c58f24ae2dba3d3eb", + "73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7ddd75e154da417becec55cdd249327454138f082110297d5e87ab25e15fad150f", + "f626ab887eb5f40b502463ccf2ec5a7311676ee9e5d55c492059a366c0b4d4a1f7ab126f2884db9059fa09ca83be6b8bd0250426aeb62191bdd9861457b8bc91", + "408ee7b2d1c84d7899dba07150fae88c5411974f1762cb659dd928db8aac206b18d5c9d20c8d23bed068c9ff3a1126b940f0e537f9d94891828a999dda6fafd1", + "86e8a3880767e1ed521a47de1e031d47f33d5a8095be467bffbbd3295e27258e580bba4bb0b9c56974e16a6998322a91e857e2fac28674404da993f6197fd29f" + ] + + for index, derivation_path in enumerate(derivation_paths): + key = _get_public_key(keychain, derivation_path) + + self.assertEqual(hexlify(key.node.public_key), public_keys[index]) + self.assertEqual(hexlify(key.node.chain_code), chain_codes[index]) + self.assertEqual(key.xpub, xpub_keys[index]) + + +if __name__ == '__main__': + unittest.main() diff --git a/core/tests/test_apps.cardano.staking_use_cases.py b/core/tests/test_apps.cardano.staking_use_cases.py new file mode 100644 index 000000000..a93e0db2a --- /dev/null +++ b/core/tests/test_apps.cardano.staking_use_cases.py @@ -0,0 +1,100 @@ +from ubinascii import unhexlify + +from common import * + +from apps.common import HARDENED +from trezor.crypto import bip32 +from trezor.messages import CardanoAddressType +from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType +from trezor.messages.CardanoBlockchainPointerType import CardanoBlockchainPointerType + + +if not utils.BITCOIN_ONLY: + from apps.cardano.helpers import staking_use_cases + from apps.cardano.seed import Keychain + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoStakingUseCases(unittest.TestCase): + def test_get(self): + mnemonic = ( + "test walk nut penalty hip pave soap entry language right filter choice" + ) + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + expected_staking_use_cases = [ + # address parameters, expected staking use case + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + staking_use_cases.MATCH, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 2 | HARDENED, 2, 0], + ), + staking_use_cases.MISMATCH, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=unhexlify("32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc"), + ), + staking_use_cases.MATCH, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), + ), + staking_use_cases.MISMATCH, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType( + block_index=1, tx_index=2, certificate_index=3 + ), + ), + staking_use_cases.POINTER_ADDRESS, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + staking_use_cases.MATCH, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + staking_use_cases.NO_STAKING, + ), + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + staking_use_cases.NO_STAKING, + ), + ] + + for address_parameters, expected_staking_use_case in expected_staking_use_cases: + actual_staking_use_case = staking_use_cases.get(keychain, address_parameters) + self.assertEqual(actual_staking_use_case, expected_staking_use_case) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/tests/test_apps.cardano.utils.py b/core/tests/test_apps.cardano.utils.py new file mode 100644 index 000000000..79a37539e --- /dev/null +++ b/core/tests/test_apps.cardano.utils.py @@ -0,0 +1,33 @@ +from common import * + +if not utils.BITCOIN_ONLY: + from apps.cardano.helpers.utils import variable_length_encode + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoUtils(unittest.TestCase): + def test_variable_length_encode(self): + test_vectors = [ + (0, bytes([0x00])), + (42, bytes([0x2A])), + (127, bytes([0x7F])), + (128, bytes([0x81, 0x00])), + (129, bytes([0x81, 0x01])), + (255, bytes([0x81, 0x7F])), + (256, bytes([0x82, 0x00])), + (16383, bytes([0xFF, 0x7F])), + (16384, bytes([0x81, 0x80, 0x00])), + ] + + for number, expected in test_vectors: + actual = variable_length_encode(number) + self.assertEqual(actual, expected) + + + def test_variable_length_encode_negative_number(self): + with self.assertRaises(ValueError): + variable_length_encode(-1) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index 9ee483aa9..f67ae42e5 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -20,19 +20,130 @@ from . import messages, tools from .tools import expect PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 42} +NETWORK_IDS = {"mainnet": 1, "testnet": 0} REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs") REQUIRED_FIELDS_INPUT = ("path", "prev_hash", "prev_index") +INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" + +ADDRESS_TYPES = ( + messages.CardanoAddressType.BYRON, + messages.CardanoAddressType.BASE, + messages.CardanoAddressType.POINTER, + messages.CardanoAddressType.ENTERPRISE, + messages.CardanoAddressType.REWARD, +) + + +def create_address_parameters( + address_type: messages.CardanoAddressType, + address_n: List[int], + address_n_staking: List[int] = None, + staking_key_hash: bytes = None, + block_index: int = None, + tx_index: int = None, + certificate_index: int = None, +) -> messages.CardanoAddressParametersType: + certificate_pointer = None + + if address_type not in ADDRESS_TYPES: + raise ValueError("Unknown address type") + + if address_type == messages.CardanoAddressType.POINTER: + certificate_pointer = create_certificate_pointer( + block_index, tx_index, certificate_index + ) + + return messages.CardanoAddressParametersType( + address_type=address_type, + address_n=address_n, + address_n_staking=address_n_staking, + staking_key_hash=staking_key_hash, + certificate_pointer=certificate_pointer, + ) + + +def create_certificate_pointer( + block_index: int, tx_index: int, certificate_index: int +) -> messages.CardanoBlockchainPointerType: + if block_index is None or tx_index is None or certificate_index is None: + raise ValueError("Invalid pointer parameters") + + return messages.CardanoBlockchainPointerType( + block_index=block_index, tx_index=tx_index, certificate_index=certificate_index + ) + + +def create_input(input) -> messages.CardanoTxInputType: + if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT): + raise ValueError("The input is missing some fields") + + path = input["path"] + + return messages.CardanoTxInputType( + address_n=tools.parse_path(path), + prev_hash=bytes.fromhex(input["prev_hash"]), + prev_index=input["prev_index"], + ) + + +def create_output(output) -> messages.CardanoTxOutputType: + contains_address = output.get("address") is not None + contains_address_type = output.get("addressType") is not None + + if output.get("amount") is None: + raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) + if not (contains_address or contains_address_type): + raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) + + if contains_address: + return messages.CardanoTxOutputType( + address=output["address"], amount=int(output["amount"]) + ) + else: + return _create_change_output(output) + + +def _create_change_output(output) -> messages.CardanoTxOutputType: + if output.get("path") is None: + raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) + + staking_key_hash_bytes = None + if output.get("stakingKeyHash"): + staking_key_hash_bytes = bytes.fromhex(output.get("stakingKeyHash")) + + address_parameters = create_address_parameters( + int(output["addressType"]), + tools.parse_path(output["path"]), + tools.parse_path(output.get("stakingPath")), + staking_key_hash_bytes, + output.get("blockIndex"), + output.get("txIndex"), + output.get("certificateIndex"), + ) + + return messages.CardanoTxOutputType( + address_parameters=address_parameters, amount=int(output["amount"]) + ) + + +# ====== Client functions ====== # + @expect(messages.CardanoAddress, field="address") def get_address( - client, address_n: List[int], protocol_magic: int, show_display=False + client, + address_parameters: messages.CardanoAddressParametersType, + protocol_magic: int, + network_id: int, + show_display=False, ) -> messages.CardanoAddress: return client.call( messages.CardanoGetAddress( - address_n=address_n, + address_parameters=address_parameters, protocol_magic=protocol_magic, + network_id=network_id, show_display=show_display, ) ) @@ -51,6 +162,7 @@ def sign_tx( fee: int, ttl: int, protocol_magic: int, + network_id: int, ) -> messages.CardanoSignedTx: response = client.call( messages.CardanoSignTx( @@ -59,36 +171,8 @@ def sign_tx( fee=fee, ttl=ttl, protocol_magic=protocol_magic, + network_id=network_id, ) ) return response - - -def create_input(input) -> messages.CardanoTxInputType: - if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT): - raise ValueError("The input is missing some fields") - - path = input["path"] - - return messages.CardanoTxInputType( - address_n=tools.parse_path(path), - prev_hash=bytes.fromhex(input["prev_hash"]), - prev_index=input["prev_index"], - ) - - -def create_output(output) -> messages.CardanoTxOutputType: - if not output.get("amount") or not (output.get("address") or output.get("path")): - raise ValueError("The output is missing some fields") - - if output.get("path"): - path = output["path"] - - return messages.CardanoTxOutputType( - address_n=tools.parse_path(path), amount=int(output["amount"]) - ) - - return messages.CardanoTxOutputType( - address=output["address"], amount=int(output["amount"]) - ) diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index d0d1400c2..9937bbba0 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -18,11 +18,19 @@ import json import click -from .. import cardano, tools -from . import with_client +from .. import cardano, messages, tools +from . import ChoiceType, with_client PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +ADDRESS_TYPES = { + "byron": messages.CardanoAddressType.BYRON, + "base": messages.CardanoAddressType.BASE, + "pointer": messages.CardanoAddressType.POINTER, + "enterprise": messages.CardanoAddressType.ENTERPRISE, + "reward": messages.CardanoAddressType.REWARD, +} + @click.group(name="cardano") def cli(): @@ -37,19 +45,27 @@ def cli(): required=True, help="Transaction in JSON format", ) -@click.option("-p", "--protocol-magic", type=int, default=1) +@click.option( + "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] +) +@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"]) +@click.option("-t", "--testnet", is_flag=True) @with_client -def sign_tx(client, file, protocol_magic): +def sign_tx(client, file, protocol_magic, network_id, testnet): """Sign Cardano transaction.""" transaction = json.load(file) + if testnet: + protocol_magic = cardano.PROTOCOL_MAGICS["testnet"] + network_id = cardano.NETWORK_IDS["testnet"] + inputs = [cardano.create_input(input) for input in transaction["inputs"]] outputs = [cardano.create_output(output) for output in transaction["outputs"]] fee = transaction["fee"] ttl = transaction["ttl"] signed_transaction = cardano.sign_tx( - client, inputs, outputs, fee, ttl, protocol_magic + client, inputs, outputs, fee, ttl, protocol_magic, network_id ) return { @@ -61,12 +77,67 @@ def sign_tx(client, file, protocol_magic): @cli.command() @click.option("-n", "--address", required=True, help=PATH_HELP) @click.option("-d", "--show-display", is_flag=True) -@click.option("-p", "--protocol-magic", type=int, default=1) +@click.option("-t", "--address-type", type=ChoiceType(ADDRESS_TYPES), default="base") +@click.option("-s", "--staking-address", type=str, default=None) +@click.option("-h", "--staking-key-hash", type=str, default=None) +@click.option("-b", "--block_index", type=int, default=None) +@click.option("-x", "--tx_index", type=int, default=None) +@click.option("-c", "--certificate_index", type=int, default=None) +@click.option( + "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] +) +@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"]) +@click.option("-e", "--testnet", is_flag=True) @with_client -def get_address(client, address, show_display, protocol_magic): - """Get Cardano address.""" - address_n = tools.parse_path(address) - return cardano.get_address(client, address_n, protocol_magic, show_display) +def get_address( + client, + address, + address_type, + staking_address, + staking_key_hash, + block_index, + tx_index, + certificate_index, + protocol_magic, + network_id, + show_display, + testnet, +): + """ + Get Cardano address. + + All address types require the address, address_type, protocol_magic and + network_id parameters. + + When deriving a base address you can choose to include staking info as + staking_address or staking_key_hash - one has to be chosen. + + When deriving a pointer address you need to specify the block_index, + tx_index and certificate_index parameters. + + Byron, enterprise and reward addresses only require the general parameters. + """ + if testnet: + protocol_magic = cardano.PROTOCOL_MAGICS["testnet"] + network_id = cardano.NETWORK_IDS["testnet"] + + staking_key_hash_bytes = None + if staking_key_hash: + staking_key_hash_bytes = bytes.fromhex(staking_key_hash) + + address_parameters = cardano.create_address_parameters( + address_type, + tools.parse_path(address), + tools.parse_path(staking_address), + staking_key_hash_bytes, + block_index, + tx_index, + certificate_index, + ) + + return cardano.get_address( + client, address_parameters, protocol_magic, network_id, show_display + ) @cli.command() diff --git a/python/src/trezorlib/messages/CardanoAddressParametersType.py b/python/src/trezorlib/messages/CardanoAddressParametersType.py new file mode 100644 index 000000000..b3f7ece94 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoAddressParametersType.py @@ -0,0 +1,40 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .CardanoBlockchainPointerType import CardanoBlockchainPointerType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeCardanoAddressType = Literal[0, 4, 6, 8, 14] + except ImportError: + pass + + +class CardanoAddressParametersType(p.MessageType): + + def __init__( + self, + address_type: EnumTypeCardanoAddressType = None, + address_n: List[int] = None, + address_n_staking: List[int] = None, + staking_key_hash: bytes = None, + certificate_pointer: CardanoBlockchainPointerType = None, + ) -> None: + self.address_type = address_type + self.address_n = address_n if address_n is not None else [] + self.address_n_staking = address_n_staking if address_n_staking is not None else [] + self.staking_key_hash = staking_key_hash + self.certificate_pointer = certificate_pointer + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('address_type', p.EnumType("CardanoAddressType", (0, 4, 6, 8, 14)), 0), + 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 3: ('address_n_staking', p.UVarintType, p.FLAG_REPEATED), + 4: ('staking_key_hash', p.BytesType, 0), + 5: ('certificate_pointer', CardanoBlockchainPointerType, 0), + } diff --git a/python/src/trezorlib/messages/CardanoAddressType.py b/python/src/trezorlib/messages/CardanoAddressType.py new file mode 100644 index 000000000..804184b6f --- /dev/null +++ b/python/src/trezorlib/messages/CardanoAddressType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +if False: + from typing_extensions import Literal + +BASE = 0 # type: Literal[0] +POINTER = 4 # type: Literal[4] +ENTERPRISE = 6 # type: Literal[6] +BYRON = 8 # type: Literal[8] +REWARD = 14 # type: Literal[14] diff --git a/python/src/trezorlib/messages/CardanoBlockchainPointerType.py b/python/src/trezorlib/messages/CardanoBlockchainPointerType.py new file mode 100644 index 000000000..bb0f7f472 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoBlockchainPointerType.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoBlockchainPointerType(p.MessageType): + + def __init__( + self, + block_index: int = None, + tx_index: int = None, + certificate_index: int = None, + ) -> None: + self.block_index = block_index + self.tx_index = tx_index + self.certificate_index = certificate_index + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('block_index', p.UVarintType, 0), + 2: ('tx_index', p.UVarintType, 0), + 3: ('certificate_index', p.UVarintType, 0), + } diff --git a/python/src/trezorlib/messages/CardanoGetAddress.py b/python/src/trezorlib/messages/CardanoGetAddress.py index c89bd872a..81932b1d6 100644 --- a/python/src/trezorlib/messages/CardanoGetAddress.py +++ b/python/src/trezorlib/messages/CardanoGetAddress.py @@ -2,6 +2,8 @@ # fmt: off from .. import protobuf as p +from .CardanoAddressParametersType import CardanoAddressParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 @@ -15,18 +17,21 @@ class CardanoGetAddress(p.MessageType): def __init__( self, - address_n: List[int] = None, show_display: bool = None, protocol_magic: int = None, + network_id: int = None, + address_parameters: CardanoAddressParametersType = None, ) -> None: - self.address_n = address_n if address_n is not None else [] self.show_display = show_display self.protocol_magic = protocol_magic + self.network_id = network_id + self.address_parameters = address_parameters @classmethod def get_fields(cls) -> Dict: return { - 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), 2: ('show_display', p.BoolType, 0), 3: ('protocol_magic', p.UVarintType, 0), + 4: ('network_id', p.UVarintType, 0), + 5: ('address_parameters', CardanoAddressParametersType, 0), } diff --git a/python/src/trezorlib/messages/CardanoSignTx.py b/python/src/trezorlib/messages/CardanoSignTx.py index 2b97a4af4..43f5716d0 100644 --- a/python/src/trezorlib/messages/CardanoSignTx.py +++ b/python/src/trezorlib/messages/CardanoSignTx.py @@ -23,12 +23,14 @@ class CardanoSignTx(p.MessageType): protocol_magic: int = None, fee: int = None, ttl: int = None, + network_id: int = None, ) -> None: self.inputs = inputs if inputs is not None else [] self.outputs = outputs if outputs is not None else [] self.protocol_magic = protocol_magic self.fee = fee self.ttl = ttl + self.network_id = network_id @classmethod def get_fields(cls) -> Dict: @@ -38,4 +40,5 @@ class CardanoSignTx(p.MessageType): 5: ('protocol_magic', p.UVarintType, 0), 6: ('fee', p.UVarintType, 0), 7: ('ttl', p.UVarintType, 0), + 8: ('network_id', p.UVarintType, 0), } diff --git a/python/src/trezorlib/messages/CardanoTxOutputType.py b/python/src/trezorlib/messages/CardanoTxOutputType.py index ead6d3cc7..066d7596b 100644 --- a/python/src/trezorlib/messages/CardanoTxOutputType.py +++ b/python/src/trezorlib/messages/CardanoTxOutputType.py @@ -2,6 +2,8 @@ # fmt: off from .. import protobuf as p +from .CardanoAddressParametersType import CardanoAddressParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 @@ -15,17 +17,17 @@ class CardanoTxOutputType(p.MessageType): def __init__( self, address: str = None, - address_n: List[int] = None, amount: int = None, + address_parameters: CardanoAddressParametersType = None, ) -> None: self.address = address - self.address_n = address_n if address_n is not None else [] self.amount = amount + self.address_parameters = address_parameters @classmethod def get_fields(cls) -> Dict: return { 1: ('address', p.UnicodeType, 0), - 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), 3: ('amount', p.UVarintType, 0), + 4: ('address_parameters', CardanoAddressParametersType, 0), } diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 0460baec4..61090d108 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -21,6 +21,8 @@ from .ButtonAck import ButtonAck from .ButtonRequest import ButtonRequest from .Cancel import Cancel from .CardanoAddress import CardanoAddress +from .CardanoAddressParametersType import CardanoAddressParametersType +from .CardanoBlockchainPointerType import CardanoBlockchainPointerType from .CardanoGetAddress import CardanoGetAddress from .CardanoGetPublicKey import CardanoGetPublicKey from .CardanoPublicKey import CardanoPublicKey @@ -277,6 +279,7 @@ from . import BinanceOrderType from . import BinanceTimeInForce from . import ButtonRequestType from . import Capability +from . import CardanoAddressType from . import DebugLinkShowTextStyle from . import DebugSwipeDirection from . import FailureType diff --git a/tests/device_tests/test_msg_cardano_get_address.py b/tests/device_tests/test_msg_cardano_get_address.py index 9fa68b5a5..1b80f6c8a 100644 --- a/tests/device_tests/test_msg_cardano_get_address.py +++ b/tests/device_tests/test_msg_cardano_get_address.py @@ -16,10 +16,14 @@ import pytest -from trezorlib.cardano import PROTOCOL_MAGICS, get_address -from trezorlib.tools import parse_path - -from ..common import MNEMONIC12 +from trezorlib import tools +from trezorlib.cardano import ( + NETWORK_IDS, + PROTOCOL_MAGICS, + create_address_parameters, + get_address, +) +from trezorlib.messages import CardanoAddressType @pytest.mark.altcoin @@ -32,38 +36,250 @@ from ..common import MNEMONIC12 ( "m/44'/1815'/0'/0/0", PROTOCOL_MAGICS["mainnet"], - "Ae2tdPwUPEZLCq3sFv4wVYxwqjMH2nUzBVt1HFr4v87snYrtYq3d3bq2PUQ", + "Ae2tdPwUPEZ5YUb8sM3eS8JqKgrRLzhiu71crfuH2MFtqaYr5ACNRdsswsZ", ), ( "m/44'/1815'/0'/0/1", PROTOCOL_MAGICS["mainnet"], - "Ae2tdPwUPEZEY6pVJoyuNNdLp7VbMB7U7qfebeJ7XGunk5Z2eHarkcN1bHK", + "Ae2tdPwUPEZJb8r1VZxweSwHDTYtqeYqF39rZmVbrNK62JHd4Wd7Ytsc8eG", ), ( "m/44'/1815'/0'/0/2", PROTOCOL_MAGICS["mainnet"], - "Ae2tdPwUPEZ3gZD1QeUHvAqadAV59Zid6NP9VCR9BG5LLAja9YtBUgr6ttK", + "Ae2tdPwUPEZFm6Y7aPZGKMyMAK16yA5pWWKU9g73ncUQNZsAjzjhszenCsq", ), # testnet # data generated by code under test ( "m/44'/1815'/0'/0/0", PROTOCOL_MAGICS["testnet"], - "2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r", + "2657WMsDfac5F3zbgs9BwNWx3dhGAJERkAL93gPa68NJ2i8mbCHm2pLUHWSj8Mfea", ), ( "m/44'/1815'/0'/0/1", PROTOCOL_MAGICS["testnet"], - "2657WMsDfac61ebUDw53WUX49Dcfya8S8G7iYbhN4nP8JSFuh38T1LuFax1bUnhxA", + "2657WMsDfac6ezKWszxLFqJjSUgpg9NgxKc1koqi24sVpRaPhiwMaExk4useKn5HA", ), ( "m/44'/1815'/0'/0/2", PROTOCOL_MAGICS["testnet"], - "2657WMsDfac5PMpEsxc1md3pgZKUZRZ11MUK8tjkDHBQG9b3TMBsTQc4PmmumVrcn", + "2657WMsDfac7hr1ioJGr6g7r6JRx4r1My8Rj91tcPTeVjJDpfBYKURrPG2zVLx2Sq", ), ], ) -@pytest.mark.setup_client(mnemonic=MNEMONIC12) def test_cardano_get_address(client, path, protocol_magic, expected_address): - address = get_address(client, parse_path(path), protocol_magic) + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.BYRON, address_n=tools.parse_path(path), + ), + protocol_magic=protocol_magic, + network_id=NETWORK_IDS["mainnet"], + ) + assert address == expected_address + + +@pytest.mark.altcoin +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.parametrize( + "path, staking_path, network_id, expected_address", + [ + # data generated with code under test + ( + "m/1852'/1815'/4'/0/0", + "m/1852'/1815'/4'/2/0", + NETWORK_IDS["mainnet"], + "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqsx9990", + ), + ( + "m/1852'/1815'/4'/0/0", + "m/1852'/1815'/4'/2/0", + NETWORK_IDS["testnet"], + "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqnsc9fs", + ), + ], +) +def test_cardano_get_base_address( + client, path, staking_path, network_id, expected_address +): + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.BASE, + address_n=tools.parse_path(path), + address_n_staking=tools.parse_path(staking_path), + ), + protocol_magic=PROTOCOL_MAGICS["mainnet"], + network_id=network_id, + ) + assert address == expected_address + + +@pytest.mark.altcoin +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.parametrize( + "path, staking_key_hash, network_id, expected_address", + [ + # data generated with code under test + ( + "m/1852'/1815'/4'/0/0", + "1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + NETWORK_IDS["mainnet"], + "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsydc62k", + ), + ( + "m/1852'/1815'/4'/0/0", + "1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + NETWORK_IDS["testnet"], + "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hls8m96xf", + ), + # staking key hash not owned - derived with "all all..." mnenomnic, data generated with code under test + ( + "m/1852'/1815'/4'/0/0", + "122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277", + NETWORK_IDS["mainnet"], + "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms06skxl", + ), + ( + "m/1852'/1815'/0'/0/0", + "122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277", + NETWORK_IDS["testnet"], + "addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5", + ), + ( + "m/1852'/1815'/4'/0/0", + "122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277", + NETWORK_IDS["testnet"], + "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsvvdk2q", + ), + ], +) +def test_cardano_get_base_address_with_staking_key_hash( + client, path, staking_key_hash, network_id, expected_address +): + # data form shelley test vectors + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.BASE, + address_n=tools.parse_path(path), + staking_key_hash=bytes.fromhex(staking_key_hash), + ), + protocol_magic=PROTOCOL_MAGICS["mainnet"], + network_id=network_id, + ) + assert address == expected_address + + +@pytest.mark.altcoin +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.parametrize( + "path, network_id, expected_address", + [ + # data generated with code under test + ( + "m/1852'/1815'/0'/0/0", + NETWORK_IDS["mainnet"], + "addr1vxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92su77c6m", + ), + ( + "m/1852'/1815'/0'/0/0", + NETWORK_IDS["testnet"], + "addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47", + ), + ], +) +def test_cardano_get_enterprise_address(client, path, network_id, expected_address): + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.ENTERPRISE, + address_n=tools.parse_path(path), + ), + protocol_magic=PROTOCOL_MAGICS["mainnet"], + network_id=network_id, + ) + assert address == expected_address + + +@pytest.mark.altcoin +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.parametrize( + "path, block_index, tx_index, certificate_index, network_id, expected_address", + [ + # data generated with code under test + ( + "m/1852'/1815'/0'/0/0", + 1, + 2, + 3, + NETWORK_IDS["mainnet"], + "addr1gxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92spqgpsl97q83", + ), + ( + "m/1852'/1815'/0'/0/0", + 24157, + 177, + 42, + NETWORK_IDS["testnet"], + "addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t", + ), + ], +) +def test_cardano_get_pointer_address( + client, + path, + block_index, + tx_index, + certificate_index, + network_id, + expected_address, +): + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.POINTER, + address_n=tools.parse_path(path), + block_index=block_index, + tx_index=tx_index, + certificate_index=certificate_index, + ), + protocol_magic=PROTOCOL_MAGICS["mainnet"], + network_id=network_id, + ) + assert address == expected_address + + +@pytest.mark.altcoin +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.parametrize( + "path, network_id, expected_address", + [ + # data generated with code under test + ( + "m/1852'/1815'/0'/2/0", + NETWORK_IDS["mainnet"], + "stake1uyfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yacalmqha", + ), + ( + "m/1852'/1815'/0'/2/0", + NETWORK_IDS["testnet"], + "stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq", + ), + ], +) +def test_cardano_get_reward_address(client, path, network_id, expected_address): + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.REWARD, address_n=tools.parse_path(path), + ), + protocol_magic=PROTOCOL_MAGICS["mainnet"], + network_id=network_id, + ) assert address == expected_address diff --git a/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py b/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py index 9c357ab8e..5aff6384e 100644 --- a/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py @@ -16,8 +16,9 @@ import pytest -from trezorlib.cardano import PROTOCOL_MAGICS, get_address -from trezorlib.tools import parse_path +from trezorlib import tools +from trezorlib.cardano import PROTOCOL_MAGICS, create_address_parameters, get_address +from trezorlib.messages import CardanoAddressType from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 @@ -70,5 +71,13 @@ def test_cardano_get_address(client, path, protocol_magic, expected_address): assert client.features.passphrase_protection is True client.use_passphrase("TREZOR") - address = get_address(client, parse_path(path), protocol_magic) + address = get_address( + client, + address_parameters=create_address_parameters( + address_type=CardanoAddressType.BYRON, address_n=tools.parse_path(path), + ), + protocol_magic=protocol_magic, + network_id=0, + ) + assert address == expected_address assert address == expected_address diff --git a/tests/device_tests/test_msg_cardano_get_public_key.py b/tests/device_tests/test_msg_cardano_get_public_key.py index f09da1235..d9c30f793 100644 --- a/tests/device_tests/test_msg_cardano_get_public_key.py +++ b/tests/device_tests/test_msg_cardano_get_public_key.py @@ -37,14 +37,24 @@ from trezorlib.tools import parse_path "70f131bb799fd659c997221ad8cae7dcce4e8da701f8101cf15307fd3a3712a1", ), ( - "m/44'/1815'/2'", - "076338cee5ab3dae19f06ccaa80e3d4428cf0e1bdc04243e41bba7be63a90da7", - "5dcdf129f6f2d108292e615c4b67a1fc41a64e6a96130f5c981e5e8e046a6cd7", + "m/1852'/1815'/0'", + "d507c8f866691bd96e131334c355188b1a1d0b2fa0ab11545075aab332d77d9e", + "b19657ad13ee581b56b0f8d744d66ca356b93d42fe176b3de007d53e9c4c4e7a", ), ( - "m/44'/1815'/3'", - "5f769380dc6fd17a4e0f2d23aa359442a712e5e96d7838ebb91eb020003cccc3", - "1197ea234f528987cbac9817ebc31344395b837a3bb7c2332f87e095e70550a5", + "m/1852'/1815'/1'", + "140791584001446365f169c82241c7c214475000180dab39fa0588fc9c3d6d80", + "7f9f812d49816844b52e319857aa75961724ad1a146701679d02d7168622233d", + ), + ( + "m/1852'/1815'/2'", + "ff6ccc3097ca79fc29fe92a9639c47644746780c63acae10a9e6f03bf5c919dd", + "27d985feabf40d83a30aa4645ff008c068187559dd224ba59e26d0d2dc3598ce", + ), + ( + "m/1852'/1815'/3'", + "be81ace1f63f4f0cae74dd274a72d7818f238bc764ab3e0dc0beb1945b756dca", + "29034f036a162ac4f9f9f397b2d1f289754bb6633915f26b199e156f81d05c88", ), ], ) diff --git a/tests/device_tests/test_msg_cardano_sign_transaction.py b/tests/device_tests/test_msg_cardano_sign_transaction.py index 116ef3e05..5f1fc376a 100644 --- a/tests/device_tests/test_msg_cardano_sign_transaction.py +++ b/tests/device_tests/test_msg_cardano_sign_transaction.py @@ -17,21 +17,73 @@ import pytest from trezorlib import cardano, messages -from trezorlib.cardano import PROTOCOL_MAGICS +from trezorlib.cardano import NETWORK_IDS, PROTOCOL_MAGICS from trezorlib.exceptions import TrezorFailure -SAMPLE_INPUT = { - "path": "m/44'/1815'/0'/0/1", - "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", - "prev_index": 0, + +class InputAction: + """ + Test cases don't use the same input flows. These constants are used to define + the expected input flows for each test case. Corresponding input actions + are then executed on the device to simulate user inputs. + """ + + SWIPE = 0 + YES = 1 + + +SAMPLE_INPUTS = { + "byron_input": { + "path": "m/44'/1815'/0'/0/1", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0, + }, + "shelley_input": { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0, + }, } SAMPLE_OUTPUTS = { - "simple_output": { + "simple_byron_output": { "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", "amount": "3003112", }, - "change_output": {"path": "m/44'/1815'/0'/0/1", "amount": "1000000"}, + "byron_change_output": { + "addressType": 8, + "path": "m/44'/1815'/0'/0/1", + "amount": "1000000", + }, + "simple_shelley_output": { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1", + }, + "base_address_change_output": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787", + }, + "staking_key_hash_output": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingKeyHash": "32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc", + "amount": "7120787", + }, + "pointer_address_output": { + "addressType": 4, + "path": "m/1852'/1815'/0'/0/0", + "blockIndex": 1, + "txIndex": 2, + "certificateIndex": 3, + "amount": "7120787", + }, + "enterprise_address_output": { + "addressType": 6, + "path": "m/1852'/1815'/0'/0/0", + "amount": "7120787", + }, "invalid_address": { "address": "jsK75PTH2esX8k4Wvxenyz83LJJWToBbVmGrWUer2CHFHanLseh7r3sW5X5q", "amount": "3003112", @@ -44,14 +96,42 @@ SAMPLE_OUTPUTS = { "address": "Ae2tdPwUPEZ5YUb8sM3eS8JqKgrRLzhiu71crfuH2MFtqaYr5ACNRZR3Mbm", "amount": "3003112", }, - "large_simple_output": { + "invalid_base_address_too_short": { + "address": "addr1q89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqj922xhxkn6twlq2wn4q50q352annk3903tj00h45mggqvpjcf", + "amount": "3003112", + }, + "invalid_base_address_too_long": { + "address": "addr1q89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfm5zhnjqfc", + "amount": "3003112", + }, + "invalid_pointer_address_too_short": { + "address": "addr1g89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcgrfjd3l", + "amount": "3003112", + }, + "invalid_pointer_address_too_long": { + "address": "addr1g89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqysszqgqqysszqgqqysszqgqqzpqv0wa7", + "amount": "3003112", + }, + "invalid_enterprise_address_too_short": { + "address": "addr1v89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcg0c7m2w", + "amount": "3003112", + }, + "invalid_enterprise_address_too_long": { + "address": "addr1v89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqzp9v4srv", + "amount": "3003112", + }, + "large_simple_byron_output": { "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", "amount": "449999999199999999", }, "testnet_output": { - "address": "2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r", + "address": "2657WMsDfac7BteXkJq5Jzdog4h47fPbkwUM49isuWbYAr2cFRHa3rURP236h9PBe", "amount": "3003112", }, + "shelley_testnet_output": { + "address": "addr_test1vr9s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqtmut0e", + "amount": "1", + }, } VALID_VECTORS = [ @@ -59,14 +139,18 @@ VALID_VECTORS = [ ( # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"]], + [SAMPLE_OUTPUTS["simple_byron_output"]], # fee 42, # ttl 10, + # input flow + [[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]], # tx hash "73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6", # serialized tx @@ -74,37 +158,166 @@ VALID_VECTORS = [ ), # Mainnet transaction with change ( - # protocol magic (mainnet) + # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"], SAMPLE_OUTPUTS["change_output"]], + [SAMPLE_OUTPUTS["simple_byron_output"], SAMPLE_OUTPUTS["byron_change_output"]], # fee 42, # ttl 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], # tx hash "81b14b7e62972127eb33c0b1198de6430540ad3a98eec621a3194f2baac43a43", # serialized tx "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e882582b82d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840d909b16038c4fd772a177038242e6793be39c735430b03ee924ed18026bd28d06920b5846247945f1204276e4b759aa5ac05a4a73b49ce705ab0e5e54a3a170e582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63541a0f6", ), + # simple transaction with base address change output + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [ + SAMPLE_OUTPUTS["simple_shelley_output"], + SAMPLE_OUTPUTS["base_address_change_output"], + ], + # fee + 42, + # ttl + 10, + # input flow + [[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]], + # tx hash + "16fe72bb198be423677577e6326f1f648ec5fc11263b072006382d8125a6edda", + # tx body + "83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c158406a78f07836dcf4a303448d2b16b217265a9226be3984a69a04dba5d04f4dbb2a47b5e1cbb345f474c0b9634a2f37b921ab26e6a65d5dfd015dacb4455fb8430af6", + ), + # simple transaction with base address change output with staking key hash + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [ + SAMPLE_OUTPUTS["simple_shelley_output"], + SAMPLE_OUTPUTS["staking_key_hash_output"], + ], + # fee + 42, + # ttl + 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], + # tx hash + "d1610bb89bece22ed3158738bc1fbb31c6af0685053e2993361e3380f49afad9", + # tx body + "83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc1a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840622f22d03bc9651ddc5eb2f5dc709ac4240a64d2b78c70355dd62106543c407d56e8134c4df7884ba67c8a1b5c706fc021df5c4d0ff37385c30572e73c727d00f6", + ), + # simple transaction with pointer address change output + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [ + SAMPLE_OUTPUTS["simple_shelley_output"], + SAMPLE_OUTPUTS["pointer_address_output"], + ], + # fee + 42, + # ttl + 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], + # tx hash + "40535fa8f88515f1da008d3cdf544cf9dbf1675c3cb0adb13b74b9293f1b7096", + # tx body + "83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258204180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa0102031a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840dbbf050cc13d0696b1884113613318a275e6f0f8c7cb3e7828c4f2f3c158b2622a5d65ea247f1eed758a0f6242a52060c319d6f37c8460f5d14be24456cd0b08f6", + ), + # simple transaction with enterprise address change output + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [ + SAMPLE_OUTPUTS["simple_shelley_output"], + SAMPLE_OUTPUTS["enterprise_address_output"], + ], + # fee + 42, + # ttl + 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], + # tx hash + "d3570557b197604109481a80aeb66cd2cfabc57f802ad593bacc12eb658e5d72", + # tx body + "83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff0182581d6180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa1a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840c5996650c438c4493b2c8a94229621bb9b151b8d61d75fb868c305e917031e9a1654f35023f7dbf5d1839ab9d57b153c7f79c2666af51ecf363780397956e00af6", + ), # Testnet transaction ( # protocol magic PROTOCOL_MAGICS["testnet"], + # network id + NETWORK_IDS["testnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["change_output"]], + [ + SAMPLE_OUTPUTS["testnet_output"], + SAMPLE_OUTPUTS["shelley_testnet_output"], + SAMPLE_OUTPUTS["byron_change_output"], + ], # fee 42, # ttl 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + [InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], # tx hash - "5dd03fb44cb88061b2a1c246981bb31adfe4f57be69b58badb5ae8f448450932", + "47cf79f20c6c62edb4162b3b232a57afc1bd0b57c7fd8389555276408a004776", # serialized tx - "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581c586b90cf80c021db288ce1c18ecfd3610acf64f8748768b0eb7335b1a10242182a001aae3129311a002dd2e882582f82d818582583581c98c3a558f39d1d993cc8770e8825c70a6d0f5a9eb243501c4526c29da10242182a001aa8566c011a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840fc30afdd0d4a6d8581e0f6abe895994d208fd382f2b23ff1553d711477a4fedbd1f68a76e7465c4816d5477f4287f7360acf71fca3b3d5902e4448e48c447106582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63545a10242182af6", + "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018382582f82d818582583581cc817d85b524e3d073795819a25cdbb84cff6aa2bbb3a081980d248cba10242182a001a0fb6fc611a002dd2e882581d60cb03849e268f989b5a843107bad7fa2908246986a8f3d643f8c184800182582f82d818582583581c98c3a558f39d1d993cc8770e8825c70a6d0f5a9eb243501c4526c29da10242182a001aa8566c011a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840cc11adf81cb3c3b75a438325f8577666f5cbb4d5d6b73fa6dbbcf5ab36897df34eecacdb54c3bc3ce7fc594ebb2c7aa4db4700f4290facad9b611a035af8710a582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63545a10242182af6", ), ] @@ -113,8 +326,10 @@ INVALID_VECTORS = [ ( # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs [SAMPLE_OUTPUTS["invalid_address"]], # fee @@ -126,10 +341,12 @@ INVALID_VECTORS = [ ), # Output address is invalid CBOR ( - # protocol magic (mainnet) + # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs [SAMPLE_OUTPUTS["invalid_cbor"]], # fee @@ -143,8 +360,10 @@ INVALID_VECTORS = [ ( # protocol magic (mainnet) PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs [SAMPLE_OUTPUTS["invalid_crc"]], # fee @@ -154,14 +373,118 @@ INVALID_VECTORS = [ # error message "Invalid address", ), - # Fee is too high + # Output base address is too short + ( + # protocol magic (mainnet) + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["invalid_base_address_too_short"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Output base address is too long + ( + # protocol magic (mainnet) + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["invalid_base_address_too_long"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Output pointer address is too short ( # protocol magic (mainnet) PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["shelley_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"]], + [SAMPLE_OUTPUTS["invalid_pointer_address_too_short"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Output pointer address is too long + ( + # protocol magic (mainnet) + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["invalid_pointer_address_too_long"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Output enterprise address is too short + ( + # protocol magic (mainnet) + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["invalid_enterprise_address_too_short"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Output enterprise address is too long + ( + # protocol magic (mainnet) + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["invalid_enterprise_address_too_long"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Fee is too high + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["byron_input"]], + # outputs + [SAMPLE_OUTPUTS["simple_byron_output"]], # fee 45000000000000001, # ttl @@ -171,12 +494,17 @@ INVALID_VECTORS = [ ), # Output total is too high ( - # protocol magic (mainnet) + # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["large_simple_output"], SAMPLE_OUTPUTS["change_output"]], + [ + SAMPLE_OUTPUTS["large_simple_byron_output"], + SAMPLE_OUTPUTS["byron_change_output"], + ], # fee 42, # ttl @@ -188,8 +516,10 @@ INVALID_VECTORS = [ ( # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs [SAMPLE_OUTPUTS["testnet_output"]], # fee @@ -203,10 +533,12 @@ INVALID_VECTORS = [ ( # protocol magic PROTOCOL_MAGICS["testnet"], + # network id + NETWORK_IDS["testnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"]], + [SAMPLE_OUTPUTS["simple_byron_output"]], # fee 42, # ttl @@ -214,6 +546,74 @@ INVALID_VECTORS = [ # error message "Output address network mismatch!", ), + # Shelley mainnet transaction with testnet output + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["shelley_testnet_output"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Shelley testnet transaction with mainnet output + ( + # protocol magic + PROTOCOL_MAGICS["testnet"], + # network id + NETWORK_IDS["testnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["simple_shelley_output"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid address", + ), + # Testnet protocol magic with mainnet network id + ( + # protocol magic + PROTOCOL_MAGICS["testnet"], + # network id + NETWORK_IDS["mainnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["simple_shelley_output"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid network id/protocol magic combination!", + ), + # Mainnet protocol magic with testnet network id + ( + # protocol magic + PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["testnet"], + # inputs + [SAMPLE_INPUTS["shelley_input"]], + # outputs + [SAMPLE_OUTPUTS["simple_byron_output"]], + # fee + 42, + # ttl + 10, + # error message + "Invalid network id/protocol magic combination!", + ), ] @@ -221,32 +621,47 @@ INVALID_VECTORS = [ @pytest.mark.cardano @pytest.mark.skip_t1 # T1 support is not planned @pytest.mark.parametrize( - "protocol_magic,inputs,outputs,fee,ttl,tx_hash,serialized_tx", VALID_VECTORS + "protocol_magic,network_id,inputs,outputs,fee,ttl,input_flow_sequences,tx_hash,serialized_tx", + VALID_VECTORS, ) def test_cardano_sign_tx( - client, protocol_magic, inputs, outputs, fee, ttl, tx_hash, serialized_tx + client, + protocol_magic, + network_id, + inputs, + outputs, + fee, + ttl, + input_flow_sequences, + tx_hash, + serialized_tx, ): inputs = [cardano.create_input(i) for i in inputs] outputs = [cardano.create_output(o) for o in outputs] expected_responses = [ - messages.ButtonRequest(code=messages.ButtonRequestType.Other), - messages.ButtonRequest(code=messages.ButtonRequestType.Other), - messages.CardanoSignedTx(), + messages.ButtonRequest(code=messages.ButtonRequestType.Other) + for i in range(len(input_flow_sequences)) ] + expected_responses.append(messages.CardanoSignedTx()) def input_flow(): - yield - client.debug.swipe_up() - client.debug.press_yes() - yield - client.debug.swipe_up() - client.debug.press_yes() + for sequence in input_flow_sequences: + yield + for action in sequence: + if action == InputAction.SWIPE: + client.debug.swipe_up() + elif action == InputAction.YES: + client.debug.press_yes() + else: + raise ValueError("Invalid input action") with client: client.set_expected_responses(expected_responses) client.set_input_flow(input_flow) - response = cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic) + response = cardano.sign_tx( + client, inputs, outputs, fee, ttl, protocol_magic, network_id + ) assert response.tx_hash.hex() == tx_hash assert response.serialized_tx.hex() == serialized_tx @@ -255,10 +670,18 @@ def test_cardano_sign_tx( @pytest.mark.cardano @pytest.mark.skip_t1 # T1 support is not planned @pytest.mark.parametrize( - "protocol_magic,inputs,outputs,fee,ttl,expected_error_message", INVALID_VECTORS + "protocol_magic,network_id,inputs,outputs,fee,ttl,expected_error_message", + INVALID_VECTORS, ) def test_cardano_sign_tx_validation( - client, protocol_magic, inputs, outputs, fee, ttl, expected_error_message + client, + protocol_magic, + network_id, + inputs, + outputs, + fee, + ttl, + expected_error_message, ): inputs = [cardano.create_input(i) for i in inputs] outputs = [cardano.create_output(o) for o in outputs] @@ -269,4 +692,6 @@ def test_cardano_sign_tx_validation( client.set_expected_responses(expected_responses) with pytest.raises(TrezorFailure, match=expected_error_message): - cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic) + cardano.sign_tx( + client, inputs, outputs, fee, ttl, protocol_magic, network_id + ) diff --git a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py index 1c1e50be3..a59ff3bbf 100644 --- a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py @@ -17,41 +17,32 @@ import pytest from trezorlib import cardano, messages -from trezorlib.cardano import PROTOCOL_MAGICS +from trezorlib.cardano import NETWORK_IDS, PROTOCOL_MAGICS from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 - -SAMPLE_INPUT = { - "path": "m/44'/1815'/0'/0/1", - "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", - "prev_index": 0, -} - -SAMPLE_OUTPUTS = { - "simple_output": { - "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", - "amount": "3003112", - }, - "change_output": {"path": "m/44'/1815'/0'/0/1", "amount": "1000000"}, - "testnet_output": { - "address": "2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r", - "amount": "3003112", - }, -} +from .test_msg_cardano_sign_transaction import ( + SAMPLE_INPUTS, + SAMPLE_OUTPUTS, + InputAction, +) VALID_VECTORS = [ # Mainnet transaction without change ( # protocol magic PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"]], + [SAMPLE_OUTPUTS["simple_byron_output"]], # fee 42, # ttl 10, + # input flow + [[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]], # tx hash "73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6", # serialized tx @@ -61,14 +52,22 @@ VALID_VECTORS = [ ( # protocol magic (mainnet) PROTOCOL_MAGICS["mainnet"], + # network id + NETWORK_IDS["mainnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["simple_output"], SAMPLE_OUTPUTS["change_output"]], + [SAMPLE_OUTPUTS["simple_byron_output"], SAMPLE_OUTPUTS["byron_change_output"]], # fee 42, # ttl 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], # tx hash "4c43ce4c72f145b145ae7add414722735e250d048f61c4585a5becafcbffa6ae", # serialized tx @@ -78,18 +77,26 @@ VALID_VECTORS = [ ( # protocol magic PROTOCOL_MAGICS["testnet"], + # network id + NETWORK_IDS["testnet"], # inputs - [SAMPLE_INPUT], + [SAMPLE_INPUTS["byron_input"]], # outputs - [SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["change_output"]], + [SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["byron_change_output"]], # fee 42, # ttl 10, + # input flow + [ + [InputAction.SWIPE, InputAction.YES], + [InputAction.YES], + [InputAction.SWIPE, InputAction.YES], + ], # tx hash - "ac7ef9e4f51ed4d6b791cee111b240dae2f00c39c5cc1a150631eba8aa955528", + "93a2c3cfb67ef1e4bae167b0f443c3370664bdb9171bc9cd41bad98e5cc049b2", # serialized tx - "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581c586b90cf80c021db288ce1c18ecfd3610acf64f8748768b0eb7335b1a10242182a001aae3129311a002dd2e882582f82d818582583581c709bfb5d9733cbdd72f520cd2c8b9f8f942da5e6cd0b6994e1803b0aa10242182a001aef14e76d1a000f424002182a030aa1028184582024c4fe188a39103db88818bc191fd8571eae7b284ebcbdf2462bde97b058a95c5840cfd68676454ad8bed8575dcb8ee91824c0f836da4f07a54112088b12c6b89be0c8f729d4e3fb1df0de10f049a66dea372f3e2888cabb6110d538a0e9a06fbb0758206f7a744035f4b3ddb8f861c18446169643cc3ae85e271b4b4f0eda05cf84c65b45a10242182af6", + "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581cc817d85b524e3d073795819a25cdbb84cff6aa2bbb3a081980d248cba10242182a001a0fb6fc611a002dd2e882582f82d818582583581c709bfb5d9733cbdd72f520cd2c8b9f8f942da5e6cd0b6994e1803b0aa10242182a001aef14e76d1a000f424002182a030aa1028184582024c4fe188a39103db88818bc191fd8571eae7b284ebcbdf2462bde97b058a95c5840552d1d66972598532fa539faa98cdc7889c8dce00577626a62fb22d0e244d9f49732b6ab65593352a7486123077b7e36308c5048cc8ee6dc465e576f065cb70558206f7a744035f4b3ddb8f861c18446169643cc3ae85e271b4b4f0eda05cf84c65b45a10242182af6", ), ] @@ -99,33 +106,48 @@ VALID_VECTORS = [ @pytest.mark.skip_t1 # T1 support is not planned @pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True) @pytest.mark.parametrize( - "protocol_magic,inputs,outputs,fee,ttl,tx_hash,serialized_tx", VALID_VECTORS + "protocol_magic,network_id,inputs,outputs,fee,ttl,input_flow_sequences,tx_hash,serialized_tx", + VALID_VECTORS, ) def test_cardano_sign_tx( - client, protocol_magic, inputs, outputs, fee, ttl, tx_hash, serialized_tx + client, + protocol_magic, + network_id, + inputs, + outputs, + fee, + ttl, + input_flow_sequences, + tx_hash, + serialized_tx, ): inputs = [cardano.create_input(i) for i in inputs] outputs = [cardano.create_output(o) for o in outputs] - expected_responses = [ - messages.PassphraseRequest(), - messages.ButtonRequest(code=messages.ButtonRequestType.Other), - messages.ButtonRequest(code=messages.ButtonRequestType.Other), - messages.CardanoSignedTx(), + expected_responses = [messages.PassphraseRequest()] + expected_responses += [ + messages.ButtonRequest(code=messages.ButtonRequestType.Other) + for i in range(len(input_flow_sequences)) ] + expected_responses.append(messages.CardanoSignedTx()) def input_flow(): - yield - client.debug.swipe_up() - client.debug.press_yes() - yield - client.debug.swipe_up() - client.debug.press_yes() + for sequence in input_flow_sequences: + yield + for action in sequence: + if action == InputAction.SWIPE: + client.debug.swipe_up() + elif action == InputAction.YES: + client.debug.press_yes() + else: + raise ValueError("Invalid input action") client.use_passphrase("TREZOR") with client: client.set_expected_responses(expected_responses) client.set_input_flow(input_flow) - response = cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic) + response = cardano.sign_tx( + client, inputs, outputs, fee, ttl, protocol_magic, network_id + ) assert response.tx_hash.hex() == tx_hash assert response.serialized_tx.hex() == serialized_tx diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index aba01a239..80c7218b5 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -27,29 +27,54 @@ "test_msg_binance_sign_tx.py::test_binance_sign_message[message0-expected_response0]": "d41ee5e01a50f0f96fd7881db1750fab31cfe62c25b4eabbc092cc3daa039c7f", "test_msg_binance_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "7b8bbe5ba7d7b07c95065608fb1cf9aeafcb3f9671835a6e5e5a6997ff4ff99b", "test_msg_binance_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "813ad1b802dee1ace4dfa378edd840dbcea57c1a1b8eed67134def024c40a6e9", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-42-2657WMsDfac5vydkak9a": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-764824073-Ae2tdPwUPEZLC": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-42-2657WMsDfac61ebUDw53": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-764824073-Ae2tdPwUPEZEY": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-42-2657WMsDfac5PMpEsxc1": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-764824073-Ae2tdPwUPEZ3g": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-42-2657WMsDfac5F3zbgs9B": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-764824073-Ae2tdPwUPEZ5Y": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-42-2657WMsDfac6ezKWszxL": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-764824073-Ae2tdPwUPEZJb": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-42-2657WMsDfac7hr1ioJGr": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-764824073-Ae2tdPwUPEZFm": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_base_address[m-1852'-1815'-4'-0-0-m-1852'-1815'-4'": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_base_address_with_staking_key_hash[m-1852'-1815'-0": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_base_address_with_staking_key_hash[m-1852'-1815'-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_enterprise_address[m-1852'-1815'-0'-0-0-0-addr_tes": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_enterprise_address[m-1852'-1815'-0'-0-0-1-addr1vxq": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_pointer_address[m-1852'-1815'-0'-0-0-1-2-3-1-addr1": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_pointer_address[m-1852'-1815'-0'-0-0-24157-177-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_reward_address[m-1852'-1815'-0'-2-0-0-stake_test1u": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_address.py::test_cardano_get_reward_address[m-1852'-1815'-0'-2-0-1-stake1uyfz49": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-0'-d507c8f866691bd96e1": "ea2bf594e6a6cabd44dbc8c994dc455ea1f81eae844fa9df8833afdf46cda517", +"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-1'-140791584001446365f": "231bd3a5ccc02a4c27dd36c181c1338f1ef4c0ce19a558528fa53efae453ac25", +"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-2'-ff6ccc3097ca79fc29f": "f26abf1a1a855bda4a05816f12acf485ff1109f5b97d29e505ede199cf6e2e35", +"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-3'-be81ace1f63f4f0cae7": "ec69415277b55f4d53d2fa3c91ece0bef6d766474dd2f13ade3683e6e88f27e5", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-0'-c0fce1839f1a84c4e7702": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-1'-ea5dde31b9f551e08a5b6": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-2'-076338cee5ab3dae19f06": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-3'-5f769380dc6fd17a4e0f2": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[42-inputs2-outputs2-42-10-5dd03fb44cb8806": "418f782a5d37c227f2d82f144bcee661d65187e07a421f64e6a5465daf544e7d", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs0-outputs0-42-10-73e09bde": "63dc0880175285dc1b3dcadb3ae66439f28b1c18fdaf50245cd84b6c8cb3d0e0", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-42-10-81b14b7e": "63dc0880175285dc1b3dcadb3ae66439f28b1c18fdaf50245cd84b6c8cb3d0e0", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-inputs6-outputs6-42-10-Outp": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs0-outputs0-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs1-outputs1-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs2-outputs2-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs3-outputs3-450": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs4-outputs4-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs5-outputs5-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[42-inputs2-outputs2-42-10-ac7ef9e4f51": "ba354313a87bff3e5079da58c74cdc265a9be8e9a80006e76ea68d0cdb869665", -"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs0-outputs0-42-10-73e0": "37b670a523fca62a6d6d997541722ed070e1acf58fe618bc040e907272aa0f16", -"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-42-10-4c43": "37b670a523fca62a6d6d997541722ed070e1acf58fe618bc040e907272aa0f16", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[42-0-inputs6-outputs6-42-10-input_flow_se": "ae26b018ccceacd0c78de3389a673eca60753a6714f3146296ee17072417ec57", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs0-outputs0-42-10-input_": "fe89b315aeb25a4a449f0cadccdafcf58669d247101b038823aad25e4e7ed670", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs1-outputs1-42-10-input_": "900bd30689093a89966b56fa2663f260b0b2d4c0ba74dc4fb06cb0f45f5b3b4d", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs2-outputs2-42-10-input_": "7a35fb89f41b8730c1df37ab1fc63ee8aa4ba3b14282c68b1b86baf81665ff9c", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs3-outputs3-42-10-input_": "09faf9d0ad559154f74ac9cb3962411be07f81c5f310b23249211af1a6d4e1b2", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs4-outputs4-42-10-input_": "3141c25aef3c98eed72a91833b5b55b2a4e3593bb8e79562765eb82321e99685", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs5-outputs5-42-10-input_": "5a2a0a944774c4cbe6721fb06d41117da09394a9198c465db74dd81f037c701d", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-0-inputs12-outputs12-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-0-inputs14-outputs14-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-1-inputs15-outputs15-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-0-inputs16-outputs16": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs0-outputs0-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs1-outputs1-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs10-outputs10": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs11-outputs11": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs13-outputs13": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs2-outputs2-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs3-outputs3-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs4-outputs4-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs5-outputs5-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs6-outputs6-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs7-outputs7-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs8-outputs8-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs9-outputs9-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", +"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[42-0-inputs2-outputs2-42-10-input_flo": "d488a0f2c127a675a1c2e2d410b6c4f402cdb610e19886a8998aa5ad786a779e", +"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-1-inputs0-outputs0-42-10-in": "6aa71de5007b0faf1eea4b1cfda1da6a739f852c0d875a1e59d83c03178c2f98", +"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-1-inputs1-outputs1-42-10-in": "7abf2e87a9b1e50afdf3502ba9480b07a59d59ccccf24915b46fb81285ae3fa8", "test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "9e11b251c03ef09127da79d92f8483c4db438c7303328774790d45e3f6fb8c96", "test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "d280ed129a2ea4781af9e35542aa31ecf63da75fc6812ed3bd05107809f836a4", "test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "32c808f00bada2059f933f3515337e494c837bdf65e4ea918b457d1c9f4cb42a",