From 21281d7cf4134caeb7bd2c6fe98ce04bb3a3fde4 Mon Sep 17 00:00:00 2001 From: gabrielkerekes Date: Fri, 23 Jul 2021 12:14:29 +0200 Subject: [PATCH] feat(cardano): add support for script addresses derivation --- common/protob/messages-cardano.proto | 2 + .../get_base_address_with_script_hashes.json | 80 +++++ .../cardano/get_enterprise_address.json | 22 ++ .../fixtures/cardano/get_pointer_address.json | 28 ++ .../fixtures/cardano/get_reward_address.json | 26 +- core/src/all_modules.py | 4 +- core/src/apps/cardano/address.py | 275 +++++++++------- core/src/apps/cardano/get_address.py | 65 +--- core/src/apps/cardano/helpers/credential.py | 220 +++++++++++++ core/src/apps/cardano/helpers/paths.py | 3 +- .../apps/cardano/helpers/staking_use_cases.py | 54 ---- core/src/apps/cardano/layout.py | 249 ++++++++------- core/src/apps/cardano/sign_tx.py | 185 +++++------ core/src/trezor/messages.py | 4 + core/tests/test_apps.cardano.address.py | 302 +++++++++++------- core/tests/test_apps.cardano.credential.py | 267 ++++++++++++++++ core/tests/test_apps.cardano.sign_tx.py | 42 --- .../test_apps.cardano.staking_use_cases.py | 100 ------ python/src/trezorlib/cardano.py | 44 +-- python/src/trezorlib/cli/cardano.py | 29 +- python/src/trezorlib/messages.py | 6 + .../cardano/test_address_public_key.py | 22 +- 22 files changed, 1284 insertions(+), 745 deletions(-) create mode 100644 common/tests/fixtures/cardano/get_base_address_with_script_hashes.json create mode 100644 core/src/apps/cardano/helpers/credential.py delete mode 100644 core/src/apps/cardano/helpers/staking_use_cases.py create mode 100644 core/tests/test_apps.cardano.credential.py diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index b31b00ab8..654fc8a55 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -128,6 +128,8 @@ message CardanoAddressParametersType { // 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 + optional bytes script_payment_hash = 6; + optional bytes script_staking_hash = 7; } /** diff --git a/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json b/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json new file mode 100644 index 000000000..42699345b --- /dev/null +++ b/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json @@ -0,0 +1,80 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "parameters": { + "address_type": "base_script_key", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "staking_path": "m/1852'/1815'/0'/2/0", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1zyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsf42dkl" + } + }, + { + "parameters": { + "address_type": "base_script_key", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "staking_path": "m/1852'/1815'/0'/2/0", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1zqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms2rhd6q" + } + }, + { + "parameters": { + "address_type": "base_key_script", + "path": "m/1852'/1815'/0'/0/0", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1yxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s8vnrtt" + } + }, + { + "parameters": { + "address_type": "base_key_script", + "path": "m/1852'/1815'/0'/0/0", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1yzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sy6wr85" + } + }, + { + "parameters": { + "address_type": "base_script_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1xyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s3gftll" + } + }, + { + "parameters": { + "address_type": "base_script_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1xqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sj75tnq" + } + } + ] +} diff --git a/common/tests/fixtures/cardano/get_enterprise_address.json b/common/tests/fixtures/cardano/get_enterprise_address.json index 3b9a8c6de..91da4711b 100644 --- a/common/tests/fixtures/cardano/get_enterprise_address.json +++ b/common/tests/fixtures/cardano/get_enterprise_address.json @@ -25,6 +25,28 @@ "result": { "expected_address": "addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47" } + }, + { + "parameters": { + "address_type": "enterprise_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1wyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsqee7sp" + } + }, + { + "parameters": { + "address_type": "enterprise_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1wqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsm3dzly" + } } ] } diff --git a/common/tests/fixtures/cardano/get_pointer_address.json b/common/tests/fixtures/cardano/get_pointer_address.json index 18ac59bcc..6da57286c 100644 --- a/common/tests/fixtures/cardano/get_pointer_address.json +++ b/common/tests/fixtures/cardano/get_pointer_address.json @@ -31,6 +31,34 @@ "result": { "expected_address": "addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t" } + }, + { + "parameters": { + "address_type": "pointer_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "block_index": 24157, + "tx_index": 177, + "certificate_index": 42, + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr12yx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2zmd4yp" + } + }, + { + "parameters": { + "address_type": "pointer_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "block_index": 24157, + "tx_index": 177, + "certificate_index": 42, + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test12qx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2d4sugn" + } } ] } diff --git a/common/tests/fixtures/cardano/get_reward_address.json b/common/tests/fixtures/cardano/get_reward_address.json index 69054dddc..5ea9454a9 100644 --- a/common/tests/fixtures/cardano/get_reward_address.json +++ b/common/tests/fixtures/cardano/get_reward_address.json @@ -6,8 +6,8 @@ "tests": [ { "parameters": { - "path": "m/1852'/1815'/0'/2/0", "address_type": "reward", + "staking_path": "m/1852'/1815'/0'/2/0", "network_id": 1, "protocol_magic": 764824073 }, @@ -17,14 +17,36 @@ }, { "parameters": { - "path": "m/1852'/1815'/0'/2/0", "address_type": "reward", + "staking_path": "m/1852'/1815'/0'/2/0", "network_id": 0, "protocol_magic": 42 }, "result": { "expected_address": "stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq" } + }, + { + "parameters": { + "address_type": "reward_script", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "stake17xxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gt5mad4" + } + }, + { + "parameters": { + "address_type": "reward_script", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "stake_test17zxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gv73lfg" + } } ] } diff --git a/core/src/all_modules.py b/core/src/all_modules.py index b25dae6b0..a8b619c02 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -462,6 +462,8 @@ if not utils.BITCOIN_ONLY: import apps.cardano.helpers.account_path_check apps.cardano.helpers.bech32 import apps.cardano.helpers.bech32 + apps.cardano.helpers.credential + import apps.cardano.helpers.credential apps.cardano.helpers.hash_builder_collection import apps.cardano.helpers.hash_builder_collection apps.cardano.helpers.network_ids @@ -470,8 +472,6 @@ if not utils.BITCOIN_ONLY: import apps.cardano.helpers.paths apps.cardano.helpers.protocol_magics import apps.cardano.helpers.protocol_magics - apps.cardano.helpers.staking_use_cases - import apps.cardano.helpers.staking_use_cases apps.cardano.helpers.utils import apps.cardano.helpers.utils apps.cardano.layout diff --git a/core/src/apps/cardano/address.py b/core/src/apps/cardano/address.py index 2445c1d97..a4e2d8bcc 100644 --- a/core/src/apps/cardano/address.py +++ b/core/src/apps/cardano/address.py @@ -1,4 +1,4 @@ -from trezor.crypto import base58, hashlib +from trezor.crypto import base58 from trezor.enums import CardanoAddressType from .byron_address import derive_byron_address, validate_byron_address @@ -7,17 +7,20 @@ from .helpers import ( INVALID_ADDRESS, INVALID_ADDRESS_PARAMETERS, NETWORK_MISMATCH, + SCRIPT_HASH_SIZE, bech32, network_ids, ) from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT -from .helpers.utils import derive_public_key, variable_length_encode +from .helpers.utils import get_public_key_hash, variable_length_encode from .seed import is_byron_path, is_shelley_path if False: + from typing import Any + from trezor.messages import ( - CardanoBlockchainPointerType, CardanoAddressParametersType, + CardanoBlockchainPointerType, ) from . import seed @@ -33,6 +36,7 @@ ADDRESS_TYPES_SHELLEY = ( CardanoAddressType.REWARD, CardanoAddressType.REWARD_SCRIPT, ) + MIN_ADDRESS_BYTES_LENGTH = 29 MAX_ADDRESS_BYTES_LENGTH = 65 @@ -43,20 +47,56 @@ def validate_address_parameters(parameters: CardanoAddressParametersType) -> Non if parameters.address_type == CardanoAddressType.BYRON: if not is_byron_path(parameters.address_n): raise INVALID_ADDRESS_PARAMETERS - elif parameters.address_type in ADDRESS_TYPES_SHELLEY: + + elif parameters.address_type == CardanoAddressType.BASE: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + _validate_base_address_staking_info( + parameters.address_n_staking, parameters.staking_key_hash + ) + + elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_KEY: + _validate_script_hash(parameters.script_payment_hash) + _validate_base_address_staking_info( + parameters.address_n_staking, parameters.staking_key_hash + ) + + elif parameters.address_type == CardanoAddressType.BASE_KEY_SCRIPT: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + _validate_script_hash(parameters.script_staking_hash) + + elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + _validate_script_hash(parameters.script_staking_hash) + + elif parameters.address_type == CardanoAddressType.POINTER: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + if parameters.certificate_pointer is None: + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.POINTER_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + if parameters.certificate_pointer is None: + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.ENTERPRISE: if not is_shelley_path(parameters.address_n): raise INVALID_ADDRESS_PARAMETERS - if parameters.address_type == CardanoAddressType.BASE: - _validate_base_address_staking_info( - parameters.address_n_staking, parameters.staking_key_hash - ) - elif parameters.address_type == CardanoAddressType.POINTER: - if parameters.certificate_pointer is None: - raise INVALID_ADDRESS_PARAMETERS - elif parameters.address_type == CardanoAddressType.REWARD: - if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.address_n): - raise INVALID_ADDRESS_PARAMETERS + elif parameters.address_type == CardanoAddressType.ENTERPRISE_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + + elif parameters.address_type == CardanoAddressType.REWARD: + if not is_shelley_path(parameters.address_n_staking): + raise INVALID_ADDRESS_PARAMETERS + if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.address_n_staking): + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.REWARD_SCRIPT: + _validate_script_hash(parameters.script_staking_hash) + else: raise INVALID_ADDRESS_PARAMETERS @@ -64,23 +104,86 @@ def validate_address_parameters(parameters: CardanoAddressParametersType) -> Non def _validate_address_parameters_structure( parameters: CardanoAddressParametersType, ) -> None: + address_n = parameters.address_n address_n_staking = parameters.address_n_staking staking_key_hash = parameters.staking_key_hash certificate_pointer = parameters.certificate_pointer + script_payment_hash = parameters.script_payment_hash + script_staking_hash = parameters.script_staking_hash - fields_to_be_empty: tuple = () - if parameters.address_type in ( - CardanoAddressType.BYRON, - CardanoAddressType.REWARD, - CardanoAddressType.ENTERPRISE, + fields_to_be_empty: dict[CardanoAddressType, tuple[Any, ...]] = { + CardanoAddressType.BASE: ( + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.BASE_KEY_SCRIPT: ( + address_n_staking, + certificate_pointer, + script_payment_hash, + ), + CardanoAddressType.BASE_SCRIPT_KEY: ( + address_n, + certificate_pointer, + script_staking_hash, + ), + CardanoAddressType.BASE_SCRIPT_SCRIPT: ( + address_n, + address_n_staking, + certificate_pointer, + ), + CardanoAddressType.POINTER: ( + address_n_staking, + staking_key_hash, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.POINTER_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + script_staking_hash, + ), + CardanoAddressType.ENTERPRISE: ( + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.ENTERPRISE_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + certificate_pointer, + script_staking_hash, + ), + CardanoAddressType.BYRON: ( + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.REWARD: ( + address_n, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.REWARD_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + ), + } + + if parameters.address_type not in fields_to_be_empty or any( + fields_to_be_empty[parameters.address_type] ): - fields_to_be_empty = (address_n_staking, staking_key_hash, certificate_pointer) - elif parameters.address_type == CardanoAddressType.BASE: - fields_to_be_empty = (certificate_pointer,) - elif parameters.address_type == CardanoAddressType.POINTER: - fields_to_be_empty = (address_n_staking, staking_key_hash) - - if any(fields_to_be_empty): raise INVALID_ADDRESS_PARAMETERS @@ -101,6 +204,11 @@ def _validate_base_address_staking_info( raise INVALID_ADDRESS_PARAMETERS +def _validate_script_hash(script_hash: bytes | None) -> None: + if not script_hash or len(script_hash) != SCRIPT_HASH_SIZE: + raise INVALID_ADDRESS_PARAMETERS + + def _validate_address_and_get_type( address: str, protocol_magic: int, network_id: int ) -> int: @@ -189,7 +297,7 @@ def _get_bech32_hrp_for_address( # Byron address uses base58 encoding raise ValueError - if address_type == CardanoAddressType.REWARD: + if address_type in (CardanoAddressType.REWARD, CardanoAddressType.REWARD_SCRIPT): if network_ids.is_mainnet(network_id): return bech32.HRP_REWARD_ADDRESS else: @@ -210,11 +318,6 @@ def _get_address_network_id(address: bytes) -> int: return address[0] & 0x0F -def get_public_key_hash(keychain: seed.Keychain, path: list[int]) -> bytes: - public_key = derive_public_key(keychain, path) - return hashlib.blake2b(data=public_key, outlen=ADDRESS_KEY_HASH_SIZE).digest() - - def derive_human_readable_address( keychain: seed.Keychain, parameters: CardanoAddressParametersType, @@ -258,36 +361,14 @@ def derive_address_bytes( def _derive_shelley_address( - keychain: seed.Keychain, - parameters: CardanoAddressParametersType, - network_id: int, + keychain: seed.Keychain, parameters: CardanoAddressParametersType, network_id: int ) -> bytes: - 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.POINTER: - # ensured by validate_address_parameters - assert parameters.certificate_pointer is not None + header = _create_address_header(parameters.address_type, network_id) - address = _derive_pointer_address( - keychain, - parameters.address_n, - parameters.certificate_pointer, - network_id, - ) - elif parameters.address_type == CardanoAddressType.ENTERPRISE: - address = _derive_enterprise_address(keychain, parameters.address_n, network_id) - elif parameters.address_type == CardanoAddressType.REWARD: - address = _derive_reward_address(keychain, parameters.address_n, network_id) - else: - raise INVALID_ADDRESS_PARAMETERS + payment_part = _get_address_payment_part(keychain, parameters) + staking_part = _get_address_staking_part(keychain, parameters) - return address + return header + payment_part + staking_part def _create_address_header(address_type: CardanoAddressType, network_id: int) -> bytes: @@ -295,33 +376,30 @@ def _create_address_header(address_type: CardanoAddressType, network_id: int) -> return header.to_bytes(1, "little") -def _derive_base_address( - keychain: seed.Keychain, - path: list[int], - staking_path: list[int], - staking_key_hash: bytes | None, - network_id: int, +def _get_address_payment_part( + keychain: seed.Keychain, parameters: CardanoAddressParametersType ) -> bytes: - header = _create_address_header(CardanoAddressType.BASE, network_id) - spending_key_hash = get_public_key_hash(keychain, path) - - if staking_key_hash is None: - staking_key_hash = get_public_key_hash(keychain, staking_path) - - return header + spending_key_hash + staking_key_hash + if parameters.address_n: + return get_public_key_hash(keychain, parameters.address_n) + elif parameters.script_payment_hash: + return parameters.script_payment_hash + else: + return bytes() -def _derive_pointer_address( - keychain: seed.Keychain, - path: list[int], - pointer: CardanoBlockchainPointerType, - network_id: int, +def _get_address_staking_part( + keychain: seed.Keychain, parameters: CardanoAddressParametersType ) -> 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 + if parameters.staking_key_hash: + return parameters.staking_key_hash + elif parameters.address_n_staking: + return get_public_key_hash(keychain, parameters.address_n_staking) + elif parameters.script_staking_hash: + return parameters.script_staking_hash + elif parameters.certificate_pointer: + return _encode_certificate_pointer(parameters.certificate_pointer) + else: + return bytes() def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes: @@ -330,36 +408,3 @@ def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes: 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: - staking_key_hash = get_public_key_hash(keychain, path) - - return pack_reward_address_bytes(staking_key_hash, network_id) - - -def pack_reward_address_bytes( - staking_key_hash: bytes, - network_id: int, -) -> bytes: - """ - Helper function to transform raw staking key hash into reward address - """ - header = _create_address_header(CardanoAddressType.REWARD, network_id) - - return header + staking_key_hash diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index 8f29e0093..ed66f8fa6 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -1,19 +1,10 @@ from trezor import log, wire from trezor.messages import CardanoAddress -from trezor.ui.layouts import show_address - -from apps.common import paths from . import seed from .address import derive_human_readable_address, validate_address_parameters -from .helpers import protocol_magics, staking_use_cases -from .helpers.paths import SCHEMA_PAYMENT, SCHEMA_STAKING -from .helpers.utils import to_account_path -from .layout import ( - ADDRESS_TYPE_NAMES, - show_warning_address_foreign_staking_key, - show_warning_address_pointer, -) +from .helpers.credential import Credential, should_show_address_credentials +from .layout import show_cardano_address, show_credentials from .sign_tx import validate_network_info if False: @@ -29,15 +20,6 @@ async def get_address( ) -> CardanoAddress: address_parameters = msg.address_parameters - await paths.validate_path( - ctx, - keychain, - address_parameters.address_n, - # path must match the PAYMENT or STAKING schema - SCHEMA_PAYMENT.match(address_parameters.address_n) - or SCHEMA_STAKING.match(address_parameters.address_n), - ) - validate_network_info(msg.network_id, msg.protocol_magic) validate_address_parameters(address_parameters) @@ -51,51 +33,22 @@ async def get_address( raise wire.ProcessError("Deriving address failed") if msg.show_display: - await _display_address( - ctx, keychain, address_parameters, address, msg.protocol_magic - ) + await _display_address(ctx, 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_name = None - if not protocol_magics.is_mainnet(protocol_magic): - network_name = protocol_magics.to_ui_string(protocol_magic) - - address_n = paths.address_n_to_str(address_parameters.address_n) - await show_address( - ctx, - address=address, - title="%s address" % ADDRESS_TYPE_NAMES[address_parameters.address_type], - network=network_name, - address_extra=address_n, - title_qr=address_n, - ) - - -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( + if should_show_address_credentials(address_parameters): + await show_credentials( ctx, - to_account_path(address_parameters.address_n), - to_account_path(address_parameters.address_n_staking), - address_parameters.staking_key_hash, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), ) - elif staking_type == staking_use_cases.POINTER_ADDRESS: - # ensured in _derive_shelley_address: - assert address_parameters.certificate_pointer is not None - await show_warning_address_pointer(ctx, address_parameters.certificate_pointer) + + await show_cardano_address(ctx, address_parameters, address, protocol_magic) diff --git a/core/src/apps/cardano/helpers/credential.py b/core/src/apps/cardano/helpers/credential.py new file mode 100644 index 000000000..6b6cd70fd --- /dev/null +++ b/core/src/apps/cardano/helpers/credential.py @@ -0,0 +1,220 @@ +from trezor.enums import CardanoAddressType + +from ...common.paths import address_n_to_str +from .paths import CHAIN_STAKING_KEY, SCHEMA_PAYMENT, SCHEMA_STAKING +from .utils import format_key_hash, format_script_hash, to_account_path + +if False: + from trezor.messages import ( + CardanoBlockchainPointerType, + CardanoAddressParametersType, + ) + from trezor.ui.layouts import PropertyType + + +class Credential: + """ + Serves mainly as a wrapper object for credentials (so that they don't have to be + passed into functions separately) which also determines all properties that should be shown + as warnings. + Also contains functions which simplify displaying the credential. + """ + + type_name: str + address_type: CardanoAddressType + path: list[int] + key_hash: bytes | None + script_hash: bytes | None + pointer: CardanoBlockchainPointerType | None + + is_reward: bool = False + is_no_staking: bool = False + is_mismatch: bool = False + is_unusual_path: bool = False + is_other_warning: bool = False + + def __init__( + self, + type_name: str, + address_type: CardanoAddressType, + path: list[int], + key_hash: bytes | None, + script_hash: bytes | None, + pointer: CardanoBlockchainPointerType | None, + ): + self.type_name = type_name + self.address_type = address_type + self.path = path + self.key_hash = key_hash + self.script_hash = script_hash + self.pointer = pointer + + @classmethod + def payment_credential( + cls, address_params: CardanoAddressParametersType + ) -> "Credential": + address_type = address_params.address_type + credential = cls( + "payment", + address_type, + address_params.address_n, + None, + address_params.script_payment_hash, + None, + ) + + if address_type in ( + CardanoAddressType.BASE, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.POINTER, + CardanoAddressType.ENTERPRISE, + CardanoAddressType.BYRON, + ): + if not SCHEMA_PAYMENT.match(address_params.address_n): + credential.is_unusual_path = True + + elif address_type in ( + CardanoAddressType.BASE_SCRIPT_KEY, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.POINTER_SCRIPT, + CardanoAddressType.ENTERPRISE_SCRIPT, + ): + credential.is_other_warning = True + + elif address_type in ( + CardanoAddressType.REWARD, + CardanoAddressType.REWARD_SCRIPT, + ): + credential.is_reward = True + + else: + raise RuntimeError # we didn't cover all address types + + return credential + + @classmethod + def stake_credential( + cls, address_params: CardanoAddressParametersType + ) -> "Credential": + address_type = address_params.address_type + credential = cls( + "stake", + address_type, + address_params.address_n_staking, + address_params.staking_key_hash, + address_params.script_staking_hash, + address_params.certificate_pointer, + ) + + if address_type == CardanoAddressType.BASE: + if address_params.staking_key_hash: + credential.is_other_warning = True + else: + if not SCHEMA_STAKING.match(address_params.address_n_staking): + credential.is_unusual_path = True + if not _do_base_address_credentials_match( + address_params.address_n, + address_params.address_n_staking, + ): + credential.is_mismatch = True + + elif address_type == CardanoAddressType.BASE_SCRIPT_KEY: + if address_params.address_n_staking and not SCHEMA_STAKING.match( + address_params.address_n_staking + ): + credential.is_unusual_path = True + + elif address_type in ( + CardanoAddressType.POINTER, + CardanoAddressType.POINTER_SCRIPT, + ): + credential.is_other_warning = True + + elif address_type == CardanoAddressType.REWARD: + if not SCHEMA_STAKING.match(address_params.address_n_staking): + credential.is_unusual_path = True + + elif address_type in ( + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.REWARD_SCRIPT, + ): + credential.is_other_warning = True + + elif address_type in ( + CardanoAddressType.ENTERPRISE, + CardanoAddressType.ENTERPRISE_SCRIPT, + CardanoAddressType.BYRON, + ): + credential.is_no_staking = True + + else: + raise RuntimeError # we didn't cover all address types + + return credential + + def should_warn(self) -> bool: + return any( + ( + self.is_reward, + self.is_no_staking, + self.is_mismatch, + self.is_unusual_path, + self.is_other_warning, + ) + ) + + def is_set(self) -> bool: + return any((self.path, self.key_hash, self.script_hash, self.pointer)) + + def get_title(self) -> str: + if self.path: + return "path" + elif self.key_hash: + return "key hash" + elif self.script_hash: + return "script" + elif self.pointer: + return "pointer" + else: + return "" + + def format(self) -> list[PropertyType]: + if self.path: + return [(None, address_n_to_str(self.path))] + elif self.key_hash: + return [(None, format_key_hash(self.key_hash, False))] + elif self.script_hash: + return [(None, format_script_hash(self.script_hash))] + elif self.pointer: + return [ + ("Block: %s" % self.pointer.block_index, None), + ("Transaction: %s" % self.pointer.tx_index, None), + ("Certificate: %s" % self.pointer.certificate_index, None), + ] + else: + return [] + + +def should_show_address_credentials( + address_parameters: CardanoAddressParametersType, +) -> bool: + return not ( + address_parameters.address_type == CardanoAddressType.BASE + and SCHEMA_PAYMENT.match(address_parameters.address_n) + and _do_base_address_credentials_match( + address_parameters.address_n, + address_parameters.address_n_staking, + ) + ) + + +def _do_base_address_credentials_match( + address_n: list[int], + address_n_staking: list[int], +) -> bool: + return address_n_staking == _path_to_staking_path(address_n) + + +def _path_to_staking_path(path: list[int]) -> list[int]: + return to_account_path(path) + [CHAIN_STAKING_KEY, 0] diff --git a/core/src/apps/cardano/helpers/paths.py b/core/src/apps/cardano/helpers/paths.py index 184f90fbb..443886a6f 100644 --- a/core/src/apps/cardano/helpers/paths.py +++ b/core/src/apps/cardano/helpers/paths.py @@ -19,8 +19,9 @@ SCHEMA_STAKING = PathSchema.parse("m/[1852]'/coin_type'/account'/2/0", SLIP44_ID SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse("m/[1852]'/coin_type'/[0-%s]'/2/0" % (HARDENED - 1), SLIP44_ID) # fmt: on -ACCOUNT_PATH_LENGTH = const(3) ACCOUNT_PATH_INDEX = const(2) +ACCOUNT_PATH_LENGTH = const(3) +CHAIN_STAKING_KEY = const(2) CHANGE_OUTPUT_PATH_NAME = "Change output path" CHANGE_OUTPUT_STAKING_PATH_NAME = "Change output staking path" diff --git a/core/src/apps/cardano/helpers/staking_use_cases.py b/core/src/apps/cardano/helpers/staking_use_cases.py deleted file mode 100644 index cf413af6f..000000000 --- a/core/src/apps/cardano/helpers/staking_use_cases.py +++ /dev/null @@ -1,54 +0,0 @@ -from trezor.enums import CardanoAddressType - -from ..address import get_public_key_hash -from ..seed import is_shelley_path -from .utils import to_account_path - -if False: - from trezor.messages import CardanoAddressParametersType - from ..seed import Keychain - - -""" -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: Keychain, address_parameters: CardanoAddressParametersType) -> int: - address_type = address_parameters.address_type - if address_type == CardanoAddressType.BASE: - 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/layout.py b/core/src/apps/cardano/layout.py index cbdfeab83..f18465b97 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -6,6 +6,7 @@ from trezor.enums import ( CardanoNativeScriptHashDisplayFormat, CardanoNativeScriptType, ) +from trezor.messages import CardanoAddressParametersType from trezor.strings import format_amount from trezor.ui.layouts import ( confirm_blob, @@ -13,16 +14,13 @@ from trezor.ui.layouts import ( confirm_output, confirm_path_warning, confirm_properties, + show_address, ) from apps.common.paths import address_n_to_str from . import seed -from .address import ( - encode_human_readable_address, - get_public_key_hash, - pack_reward_address_bytes, -) +from .address import derive_human_readable_address from .helpers import protocol_magics from .helpers.utils import ( format_account_number, @@ -37,7 +35,6 @@ from .helpers.utils import ( if False: from trezor import wire from trezor.messages import ( - CardanoBlockchainPointerType, CardanoNativeScript, CardanoTxCertificate, CardanoTxWithdrawal, @@ -48,14 +45,21 @@ if False: ) from trezor.ui.layouts import PropertyType + from .helpers.credential import Credential ADDRESS_TYPE_NAMES = { CardanoAddressType.BYRON: "Legacy", CardanoAddressType.BASE: "Base", + CardanoAddressType.BASE_SCRIPT_KEY: "Base", + CardanoAddressType.BASE_KEY_SCRIPT: "Base", + CardanoAddressType.BASE_SCRIPT_SCRIPT: "Base", CardanoAddressType.POINTER: "Pointer", + CardanoAddressType.POINTER_SCRIPT: "Pointer", CardanoAddressType.ENTERPRISE: "Enterprise", + CardanoAddressType.ENTERPRISE_SCRIPT: "Enterprise", CardanoAddressType.REWARD: "Reward", + CardanoAddressType.REWARD_SCRIPT: "Reward", } SCRIPT_TYPE_NAMES = { @@ -184,13 +188,15 @@ async def confirm_sending( ctx: wire.Context, ada_amount: int, to: str, + is_change_output: bool, ) -> None: + subtitle = "Change amount:" if is_change_output else "Confirm sending:" await confirm_output( ctx, to, format_coin_amount(ada_amount), title="Confirm transaction", - subtitle="Confirm sending:", + subtitle=subtitle, font_amount=ui.BOLD, width_paginated=17, to_str="\nto\n", @@ -220,13 +226,76 @@ async def confirm_sending_token( ) -async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: - await confirm_metadata( +async def show_credentials( + ctx: wire.Context, + payment_credential: Credential, + stake_credential: Credential, + is_change_output: bool = False, +) -> None: + await _show_credential(ctx, payment_credential, is_change_output) + await _show_credential(ctx, stake_credential, is_change_output) + + +async def _show_credential( + ctx: wire.Context, + credential: Credential, + is_change_output: bool = False, +) -> None: + if is_change_output: + title = "Confirm transaction" + else: + title = "%s address" % ADDRESS_TYPE_NAMES[credential.address_type] + + props: list[PropertyType] = [] + + # Credential can be empty in case of enterprise address stake credential + # and reward address payment credential. In that case we don't want to + # show some of the "props". + if credential.is_set(): + if is_change_output: + address_usage = "Change address" + else: + address_usage = "Address" + + credential_title = credential.get_title() + props.append( + ( + "%s %s credential is a %s:" + % (address_usage, credential.type_name, credential_title), + None, + ) + ) + props.extend(credential.format()) + + if credential.is_unusual_path: + props.append((None, "Path is unusual.")) + if credential.is_mismatch: + props.append((None, "Credential doesn't match payment credential.")) + if credential.is_reward: + props.append(("Address is a reward address.", None)) + if credential.is_no_staking: + props.append( + ( + "%s address - no staking rewards." + % ADDRESS_TYPE_NAMES[credential.address_type], + None, + ) + ) + + if credential.should_warn(): + icon = ui.ICON_WRONG + icon_color = ui.RED + else: + icon = ui.ICON_SEND + icon_color = ui.GREEN + + await confirm_properties( ctx, - "confirm_tokens", - title="Confirm transaction", - content="The following\ntransaction output\ncontains tokens.", - larger_vspace=True, + "confirm_credential", + title=title, + props=props, + icon=icon, + icon_color=icon_color, br_code=ButtonRequestType.Other, ) @@ -235,88 +304,13 @@ async def show_warning_path(ctx: wire.Context, path: list[int], title: str) -> N await confirm_path_warning(ctx, address_n_to_str(path), path_type=title) -async def show_warning_tx_no_staking_info( - ctx: wire.Context, address_type: CardanoAddressType, amount: int -) -> None: - atype = ADDRESS_TYPE_NAMES[address_type].lower() - content = "Change %s address has no stake rights.\nChange amount:\n{}" % atype +async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: await confirm_metadata( ctx, - "warning_staking", + "confirm_tokens", title="Confirm transaction", - content=content, - param=format_coin_amount(amount), - hide_continue=True, - br_code=ButtonRequestType.Other, - ) - - -async def show_warning_tx_pointer_address( - ctx: wire.Context, - pointer: CardanoBlockchainPointerType, - amount: int, -) -> None: - await confirm_properties( - ctx, - "warning_pointer", - title="Confirm transaction", - props=[ - ("Change address has a\npointer with staking\nrights.\n\n\n", None), - ( - "Pointer:", - "%s, %s, %s" - % ( - pointer.block_index, - pointer.tx_index, - pointer.certificate_index, - ), - ), - ("Change amount:", format_coin_amount(amount)), - ], - br_code=ButtonRequestType.Other, - ) - - -async def show_warning_tx_different_staking_account( - ctx: wire.Context, - staking_account_path: list[int], - amount: int, -) -> None: - await confirm_properties( - ctx, - "warning_differentstaking", - title="Confirm transaction", - props=[ - ( - "Change address staking rights do not match the current account.\n\n", - None, - ), - ( - "Staking account %s:" % format_account_number(staking_account_path), - address_n_to_str(staking_account_path), - ), - ("Change amount:", format_coin_amount(amount)), - ], - br_code=ButtonRequestType.Other, - ) - - -async def show_warning_tx_staking_key_hash( - ctx: wire.Context, - staking_key_hash: bytes, - amount: int, -) -> None: - props = [ - ("Change address staking rights do not match the current account.\n\n", None), - ("Staking key hash:", staking_key_hash), - ("Change amount:", format_coin_amount(amount)), - ] - - await confirm_properties( - ctx, - "confirm_different_stakingrights", - title="Confirm transaction", - props=props, + content="The following\ntransaction output\ncontains tokens.", + larger_vspace=True, br_code=ButtonRequestType.Other, ) @@ -414,6 +408,7 @@ async def confirm_stake_pool_owner( ctx: wire.Context, keychain: seed.Keychain, owner: CardanoPoolOwner, + protocol_magic: int, network_id: int, ) -> None: props: list[tuple[str, str | None]] = [] @@ -421,11 +416,14 @@ async def confirm_stake_pool_owner( props.append(("Pool owner:", address_n_to_str(owner.staking_key_path))) props.append( ( - encode_human_readable_address( - pack_reward_address_bytes( - get_public_key_hash(keychain, owner.staking_key_path), - network_id, - ) + derive_human_readable_address( + keychain, + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=owner.staking_key_path, + ), + protocol_magic, + network_id, ), None, ) @@ -435,8 +433,14 @@ async def confirm_stake_pool_owner( props.append( ( "Pool owner:", - encode_human_readable_address( - pack_reward_address_bytes(owner.staking_key_hash, network_id) + derive_human_readable_address( + keychain, + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + staking_key_hash=owner.staking_key_hash, + ), + protocol_magic, + network_id, ), ) ) @@ -605,20 +609,39 @@ async def show_warning_tx_network_unverifiable(ctx: wire.Context) -> None: ) -async def show_warning_address_pointer( - ctx: wire.Context, pointer: CardanoBlockchainPointerType +async def show_cardano_address( + ctx: wire.Context, + address_parameters: CardanoAddressParametersType, + address: str, + protocol_magic: int, ) -> None: - content = "Pointer address:\nBlock: %s\nTransaction: %s\nCertificate: %s" % ( - pointer.block_index, - pointer.tx_index, - pointer.certificate_index, - ) - await confirm_metadata( + network_name = None + if not protocol_magics.is_mainnet(protocol_magic): + network_name = protocol_magics.to_ui_string(protocol_magic) + + title = "%s address" % ADDRESS_TYPE_NAMES[address_parameters.address_type] + address_extra = None + title_qr = title + if address_parameters.address_type in ( + CardanoAddressType.BYRON, + CardanoAddressType.BASE, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.POINTER, + CardanoAddressType.ENTERPRISE, + CardanoAddressType.REWARD, + ): + if address_parameters.address_n: + address_extra = address_n_to_str(address_parameters.address_n) + title_qr = address_n_to_str(address_parameters.address_n) + elif address_parameters.address_n_staking: + address_extra = address_n_to_str(address_parameters.address_n_staking) + title_qr = address_n_to_str(address_parameters.address_n_staking) + + await show_address( ctx, - "warning_pointer", - title="Warning", - icon=ui.ICON_WRONG, - icon_color=ui.RED, - content=content, - br_code=ButtonRequestType.Other, + address=address, + title=title, + network=network_name, + address_extra=address_extra, + title_qr=title_qr, ) diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 40fa16a6b..3178b153c 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -64,9 +64,9 @@ from .helpers import ( LOVELACE_MAX_SUPPLY, network_ids, protocol_magics, - staking_use_cases, ) from .helpers.account_path_check import AccountPathChecker +from .helpers.credential import Credential, should_show_address_credentials from .helpers.hash_builder_collection import HashBuilderDict, HashBuilderList from .helpers.paths import ( CERTIFICATE_PATH_NAME, @@ -89,13 +89,10 @@ from .layout import ( confirm_stake_pool_registration_final, confirm_transaction, confirm_withdrawal, + show_credentials, show_warning_path, - show_warning_tx_different_staking_account, show_warning_tx_network_unverifiable, - show_warning_tx_no_staking_info, show_warning_tx_output_contains_tokens, - show_warning_tx_pointer_address, - show_warning_tx_staking_key_hash, ) from .seed import is_byron_path @@ -302,8 +299,16 @@ async def _process_outputs( total_amount = 0 for _ in range(outputs_count): output: CardanoTxOutput = await ctx.call(CardanoTxItemAck(), CardanoTxOutput) - _validate_output(output, protocol_magic, network_id, account_path_checker) - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + _validate_output( + output, + signing_mode, + protocol_magic, + network_id, + account_path_checker, + ) + + should_show_output = _should_show_output(output, signing_mode) + if should_show_output: await _show_output( ctx, keychain, @@ -335,7 +340,7 @@ async def _process_outputs( ctx, asset_groups_dict, output.asset_groups_count, - _should_show_tokens(output, signing_mode), + should_show_output, ) total_amount += output.amount @@ -634,6 +639,7 @@ def _validate_stake_pool_registration_tx_structure(msg: CardanoSignTxInit) -> No def _validate_output( output: CardanoTxOutput, + signing_mode: CardanoTxSigningMode, protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, @@ -641,8 +647,9 @@ def _validate_output( if output.address_parameters and output.address is not None: raise INVALID_OUTPUT - if output.address_parameters: - validate_address_parameters(output.address_parameters) + if address_parameters := output.address_parameters: + validate_address_parameters(address_parameters) + _fail_if_strict_and_unusual(address_parameters) elif output.address is not None: validate_output_address(output.address, protocol_magic, network_id) else: @@ -651,6 +658,23 @@ def _validate_output( account_path_checker.add_output(output) +def _should_show_output( + output: CardanoTxOutput, + signing_mode: CardanoTxSigningMode, +) -> bool: + if signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + # In a pool registration transaction, there are no inputs belonging to the user + # and no spending witnesses. It is thus safe to not show the outputs. + return False + + if output.address_parameters: # is change output + if not should_show_address_credentials(output.address_parameters): + # we don't need to display simple address outputs + return False + + return True + + async def _show_output( ctx: wire.Context, keychain: seed.Keychain, @@ -659,33 +683,30 @@ async def _show_output( protocol_magic: int, network_id: int, ) -> None: - if output.address_parameters: - await _fail_or_warn_if_invalid_path( - ctx, - SCHEMA_PAYMENT, - output.address_parameters.address_n, - CHANGE_OUTPUT_PATH_NAME, - ) - - await _show_change_output_staking_warnings( - ctx, keychain, output.address_parameters, output.amount - ) - - if _should_hide_output(output.address_parameters.address_n): - return - - address = derive_human_readable_address( - keychain, output.address_parameters, protocol_magic, network_id - ) - else: - assert output.address is not None # _validate_output - address = output.address - if output.asset_groups_count > 0: await show_warning_tx_output_contains_tokens(ctx) - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: - await confirm_sending(ctx, output.amount, address) + is_change_output: bool + if address_parameters := output.address_parameters: + is_change_output = True + + await show_credentials( + ctx, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), + is_change_output=True, + ) + + address = derive_human_readable_address( + keychain, address_parameters, protocol_magic, network_id + ) + else: + is_change_output = False + + assert output.address is not None # _validate_output + address = output.address + + await confirm_sending(ctx, output.amount, address, is_change_output) def _validate_asset_group( @@ -777,7 +798,11 @@ async def _show_stake_pool_registration_certificate( async def _show_pool_owner( - ctx: wire.Context, keychain: seed.Keychain, owner: CardanoPoolOwner, network_id: int + ctx: wire.Context, + keychain: seed.Keychain, + owner: CardanoPoolOwner, + protocol_magic: int, + network_id: int, ) -> None: if owner.staking_key_path: await _fail_or_warn_if_invalid_path( @@ -787,7 +812,7 @@ async def _show_pool_owner( POOL_OWNER_STAKING_PATH_NAME, ) - await confirm_stake_pool_owner(ctx, keychain, owner, network_id) + await confirm_stake_pool_owner(ctx, keychain, owner, protocol_magic, network_id) def _validate_witness_request( @@ -824,78 +849,11 @@ async def _show_witness( ctx: wire.Context, witness_path: list[int], ) -> None: - if not SCHEMA_PAYMENT.match(witness_path) and not SCHEMA_STAKING.match( - witness_path - ): - await _fail_or_warn_path( - ctx, - witness_path, - WITNESS_PATH_NAME, - ) + is_payment = SCHEMA_PAYMENT.match(witness_path) + is_staking = SCHEMA_STAKING.match(witness_path) - -async def _show_change_output_staking_warnings( - ctx: wire.Context, - keychain: seed.Keychain, - address_parameters: CardanoAddressParametersType, - amount: int, -) -> None: - address_type = address_parameters.address_type - - if ( - address_type == CardanoAddressType.BASE - and not address_parameters.staking_key_hash - ): - await _fail_or_warn_if_invalid_path( - ctx, - SCHEMA_STAKING, - address_parameters.address_n_staking, - CHANGE_OUTPUT_STAKING_PATH_NAME, - ) - - 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: - # ensured in _derive_shelley_address: - assert address_parameters.certificate_pointer is not None - 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: - # ensured in _validate_base_address_staking_info: - assert address_parameters.staking_key_hash - await show_warning_tx_staking_key_hash( - ctx, - address_parameters.staking_key_hash, - amount, - ) - - -def _should_hide_output(path: list[int]) -> bool: - """Return whether the output address is from a safe path, so it could be hidden.""" - return SCHEMA_PAYMENT.match(path) - - -def _should_show_tokens( - output: CardanoTxOutput, signing_mode: CardanoTxSigningMode -) -> bool: - if signing_mode != CardanoTxSigningMode.ORDINARY_TRANSACTION: - return False - - if output.address_parameters: - return not _should_hide_output(output.address_parameters.address_n) - - return True + if not is_payment and not is_staking: + await _fail_or_warn_path(ctx, witness_path, WITNESS_PATH_NAME) def _is_network_id_verifiable(msg: CardanoSignTxInit) -> bool: @@ -925,3 +883,16 @@ async def _fail_or_warn_path( raise wire.DataError("Invalid %s" % path_name.lower()) else: await show_warning_path(ctx, path, path_name) + + +def _fail_if_strict_and_unusual( + address_parameters: CardanoAddressParametersType, +) -> None: + if not safety_checks.is_strict(): + return + + if Credential.payment_credential(address_parameters).is_unusual_path: + raise wire.DataError("Invalid %s" % CHANGE_OUTPUT_PATH_NAME.lower()) + + if Credential.stake_credential(address_parameters).is_unusual_path: + raise wire.DataError("Invalid %s" % CHANGE_OUTPUT_STAKING_PATH_NAME.lower()) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index c8ce73a64..86017c6c1 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -1123,6 +1123,8 @@ if TYPE_CHECKING: address_n_staking: "list[int]" staking_key_hash: "bytes | None" certificate_pointer: "CardanoBlockchainPointerType | None" + script_payment_hash: "bytes | None" + script_staking_hash: "bytes | None" def __init__( self, @@ -1132,6 +1134,8 @@ if TYPE_CHECKING: address_n_staking: "list[int] | None" = None, staking_key_hash: "bytes | None" = None, certificate_pointer: "CardanoBlockchainPointerType | None" = None, + script_payment_hash: "bytes | None" = None, + script_staking_hash: "bytes | None" = None, ) -> None: pass diff --git a/core/tests/test_apps.cardano.address.py b/core/tests/test_apps.cardano.address.py index 30410df43..b68d28c6c 100644 --- a/core/tests/test_apps.cardano.address.py +++ b/core/tests/test_apps.cardano.address.py @@ -285,121 +285,6 @@ class TestCardanoAddress(unittest.TestCase): self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) self.assertEqual(hexlify(n.chain_code()), chain) - 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_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(block_index=1, tx_index=2, certificate_index=3), "addr1gx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpse33frd"), - (network_ids.TESTNET, CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=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_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_testnet_byron_address(self): mnemonic = "all all all all all all all all all all all all" passphrase = "" @@ -421,6 +306,133 @@ class TestCardanoAddress(unittest.TestCase): address = derive_human_readable_address(keychain, address_parameters, protocol_magics.TESTNET, 0) self.assertEqual(expected, address) + def test_derive_address(self): + mnemonic = "all all all all all all all all all all all all" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + address_parameters = { + "BASE": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 2, 0] + ), + "BASE_OWN_STAKING_KEY_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff") + ), + "BASE_OWN_STAKING_KEY_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff") + ), + # staking key hash not owned - derived with "all all..." mnenomnic + "BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277") + ), + # staking key hash not owned - derived with "all all..." mnenomnic + "BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_0": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277") + ), + "BASE_SCRIPT_KEY_SCRIPT_HASH":CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "BASE_KEY_SCRIPT_HASH":CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + "BASE_SCRIPT_SCRIPT_HASHES": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + "POINTER1": 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), + ), + "POINTER2": CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + ), + "POINTER_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "ENTERPRISE": CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + "ENTERPRISE_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "REWARD": CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + "REWARD_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD_SCRIPT, + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + } + test_vectors = [ + # base address + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqsx9990"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqnsc9fs"), + # base address with staking key hash + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_OWN_STAKING_KEY_HASH"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsydc62k"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE_OWN_STAKING_KEY_HASH"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hls8m96xf"), + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms06skxl"), + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_0"], "addr1qxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsl3s9zt"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsvvdk2q"), + # base_script_key address + (network_ids.MAINNET, CardanoAddressType.BASE_SCRIPT_KEY, address_parameters["BASE_SCRIPT_KEY_SCRIPT_HASH"], "addr1zyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsf42dkl"), + (network_ids.TESTNET, CardanoAddressType.BASE_SCRIPT_KEY, address_parameters["BASE_SCRIPT_KEY_SCRIPT_HASH"], "addr_test1zqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms2rhd6q"), + # base_key_script address + (network_ids.MAINNET, CardanoAddressType.BASE_KEY_SCRIPT, address_parameters["BASE_KEY_SCRIPT_HASH"], "addr1yxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s8vnrtt"), + (network_ids.TESTNET, CardanoAddressType.BASE_KEY_SCRIPT, address_parameters["BASE_KEY_SCRIPT_HASH"], "addr_test1yzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sy6wr85"), + # base_script_script address + (network_ids.MAINNET, CardanoAddressType.BASE_SCRIPT_SCRIPT, address_parameters["BASE_SCRIPT_SCRIPT_HASHES"], "addr1xyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s3gftll"), + (network_ids.TESTNET, CardanoAddressType.BASE_SCRIPT_SCRIPT, address_parameters["BASE_SCRIPT_SCRIPT_HASHES"], "addr_test1xqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sj75tnq"), + # pointer address + (network_ids.MAINNET, CardanoAddressType.POINTER, address_parameters["POINTER1"], "addr1gxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92spqgpsl97q83"), + (network_ids.TESTNET, CardanoAddressType.POINTER, address_parameters["POINTER2"], "addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t"), + # pointer_script address + (network_ids.MAINNET, CardanoAddressType.POINTER_SCRIPT, address_parameters["POINTER_SCRIPT_HASH"], "addr12yx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2zmd4yp"), + (network_ids.TESTNET, CardanoAddressType.POINTER_SCRIPT, address_parameters["POINTER_SCRIPT_HASH"], "addr_test12qx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2d4sugn"), + # enterprise address + (network_ids.MAINNET, CardanoAddressType.ENTERPRISE, address_parameters["ENTERPRISE"], "addr1vxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92su77c6m"), + (network_ids.TESTNET, CardanoAddressType.ENTERPRISE, address_parameters["ENTERPRISE"], "addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47"), + # enterprise_script address + (network_ids.MAINNET, CardanoAddressType.ENTERPRISE_SCRIPT, address_parameters["ENTERPRISE_SCRIPT_HASH"], "addr1wyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsqee7sp"), + (network_ids.TESTNET, CardanoAddressType.ENTERPRISE_SCRIPT, address_parameters["ENTERPRISE_SCRIPT_HASH"], "addr_test1wqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsm3dzly"), + # reward address + (network_ids.MAINNET, CardanoAddressType.REWARD, address_parameters["REWARD"], "stake1uyfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yacalmqha"), + (network_ids.TESTNET, CardanoAddressType.REWARD, address_parameters["REWARD"], "stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq"), + # reward_script address + (network_ids.MAINNET, CardanoAddressType.REWARD_SCRIPT, address_parameters["REWARD_SCRIPT_HASH"], "stake17xxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gt5mad4"), + (network_ids.TESTNET, CardanoAddressType.REWARD_SCRIPT, address_parameters["REWARD_SCRIPT_HASH"], "stake_test17zxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gv73lfg"), + ] + + + for network_id, address_type, address_parameters, expected_address in test_vectors: + validate_address_parameters(address_parameters) + actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) + + self.assertEqual(actual_address, expected_address) + def test_validate_address_parameters(self): test_vectors = [ # base address - both address_n_staking and staking_key_hash are None @@ -451,22 +463,80 @@ class TestCardanoAddress(unittest.TestCase): address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], staking_key_hash=None, ), + # base_script_key address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + script_payment_hash=None, + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8d"), + ), + # base_script_key address - address_n_staking is not a staking path + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + # base_key_script address - script_staking_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=None, + ), + # base_script_script address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=None, + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + # base_script_script address - script_staking and script_staking_hash are None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + script_staking_hash=None, + ), # pointer address - pointer is None CardanoAddressParametersType( address_type=CardanoAddressType.POINTER, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], certificate_pointer=None, ), + # pointer_script address - pointer is None + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + certificate_pointer=None, + ), + # pointer_script address - script_payment_script is None + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + script_payment_hash=None, + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + ), + # enterprise_script address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE_SCRIPT, + script_payment_hash=None, + ), # reward address - non staking path CardanoAddressParametersType( address_type=CardanoAddressType.REWARD, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] ), + # reward_script address - script_staking_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD_SCRIPT, + script_staking_hash=None, + ), # Shelley addresses with Byron namespace CardanoAddressParametersType( address_type=CardanoAddressType.BASE, - address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), + ), + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), ), CardanoAddressParametersType( address_type=CardanoAddressType.POINTER, diff --git a/core/tests/test_apps.cardano.credential.py b/core/tests/test_apps.cardano.credential.py new file mode 100644 index 000000000..73dc556e2 --- /dev/null +++ b/core/tests/test_apps.cardano.credential.py @@ -0,0 +1,267 @@ +from common import * + +from apps.cardano.helpers.credential import Credential +from apps.common.paths import HARDENED +from trezor.enums import CardanoAddressType +from trezor.messages import CardanoAddressParametersType, CardanoBlockchainPointerType + + +CERTIFICATE_POINTER = CardanoBlockchainPointerType( + block_index=24157, + tx_index=177, + certificate_index=42, +) + + +def _create_flags( + is_reward: bool = False, + is_no_staking: bool = False, + is_mismatch: bool = False, + is_unusual_path: bool = False, + is_other_warning: bool = False, +) -> tuple[bool, ...]: + return (is_reward, is_no_staking, is_mismatch, is_unusual_path, is_other_warning) + + +ADDRESS_PARAMETERS_CASES = [ + # base + ( + 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], + ), + _create_flags(), + _create_flags(), + ), + # base mismatch + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 1 | HARDENED, 2, 0], + ), + _create_flags(), + _create_flags(is_mismatch=True), + ), + # base payment unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + _create_flags(is_unusual_path=True), + _create_flags(is_mismatch=True), + ), + # base staking unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 2, 0], + ), + _create_flags(is_unusual_path=True), + _create_flags(is_unusual_path=True), + ), + # base both unusual and mismatch + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 102 | HARDENED, 2, 0], + ), + _create_flags(is_unusual_path=True), + _create_flags(is_mismatch=True, is_unusual_path=True), + ), + # base staking key hash + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(), + _create_flags(is_other_warning=True), + ), + # base key script + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(), + _create_flags(is_other_warning=True), + ), + # base key script unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + staking_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(is_unusual_path=True), + _create_flags(is_other_warning=True), + ), + # base script key + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + payment_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + _create_flags(is_other_warning=True), + _create_flags(), + ), + # base script key unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + payment_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 2, 0], + ), + _create_flags(is_other_warning=True), + _create_flags(is_unusual_path=True), + ), + # base script script + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + payment_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + staking_script_hash="2bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(is_other_warning=True), + _create_flags(is_other_warning=True), + ), + # pointer + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CERTIFICATE_POINTER, + ), + _create_flags(), + _create_flags(is_other_warning=True), + ), + # pointer unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + certificate_pointer=CERTIFICATE_POINTER, + ), + _create_flags(is_unusual_path=True), + _create_flags(is_other_warning=True), + ), + # pointer script + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + payment_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + certificate_pointer=CERTIFICATE_POINTER, + ), + _create_flags(is_other_warning=True), + _create_flags(is_other_warning=True), + ), + # enterprise + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + _create_flags(), + _create_flags(is_no_staking=True), + ), + # enterprise unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + ), + _create_flags(is_unusual_path=True), + _create_flags(is_no_staking=True), + ), + # enterprise script + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE_SCRIPT, + payment_script_hash="1bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(is_other_warning=True), + _create_flags(is_no_staking=True), + ), + # reward + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + _create_flags(is_reward=True), + _create_flags(), + ), + # reward unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 2, 0], + ), + _create_flags(is_reward=True), + _create_flags(is_unusual_path=True), + ), + # reward script + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD_SCRIPT, + staking_script_hash="2bc428e4720732ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff", + ), + _create_flags(is_reward=True), + _create_flags(is_other_warning=True), + ), + # byron + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + _create_flags(), + _create_flags(is_no_staking=True), + ), + # byron unusual + ( + CardanoAddressParametersType( + address_type=CardanoAddressType.BYRON, + address_n=[44 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], + ), + _create_flags(is_unusual_path=True), + _create_flags(is_no_staking=True), + ), +] + + +def _get_flags(credential: Credential) -> tuple[bool, ...]: + return ( + credential.is_reward, + credential.is_no_staking, + credential.is_mismatch, + credential.is_unusual_path, + credential.is_other_warning, + ) + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoCredential(unittest.TestCase): + def test_credential_flags(self): + for ( + address_parameters, + expected_payment_flags, + expected_stake_flags, + ) in ADDRESS_PARAMETERS_CASES: + payment_credential = Credential.payment_credential(address_parameters) + stake_credential = Credential.stake_credential(address_parameters) + self.assertEqual(_get_flags(payment_credential), expected_payment_flags) + self.assertEqual(_get_flags(stake_credential), expected_stake_flags) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/tests/test_apps.cardano.sign_tx.py b/core/tests/test_apps.cardano.sign_tx.py index c9584dc00..e69de29bb 100644 --- a/core/tests/test_apps.cardano.sign_tx.py +++ b/core/tests/test_apps.cardano.sign_tx.py @@ -1,42 +0,0 @@ -from common import * -from apps.common.paths import HARDENED - -if not utils.BITCOIN_ONLY: - from apps.cardano.sign_tx import _should_hide_output - - -@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") -class TestCardanoSignTransaction(unittest.TestCase): - def test_should_show_outputs(self): - outputs_to_hide = [ - # byron path - [44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - # shelley path - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - # path account is 2 - [1852 | HARDENED, 1815 | HARDENED, 2 | HARDENED, 0, 0], - # path index is 2 - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 2], - ] - outputs_to_show = [ - # path is not complete - [1852 | HARDENED, 1815 | HARDENED], - # path is not complete - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED], - # staking output path - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0,], - # max safe account number exceeded - [1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], - # output address too large - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 1000001], - ] - - for output_path in outputs_to_hide: - self.assertTrue(_should_hide_output(output_path)) - - for output_path in outputs_to_show: - self.assertFalse(_should_hide_output(output_path)) - - -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 index 50b7c3799..e69de29bb 100644 --- a/core/tests/test_apps.cardano.staking_use_cases.py +++ b/core/tests/test_apps.cardano.staking_use_cases.py @@ -1,100 +0,0 @@ -from ubinascii import unhexlify - -from common import * - -from apps.common.paths import HARDENED -from trezor.crypto import bip32 -from trezor.enums import CardanoAddressType -from trezor.messages import CardanoAddressParametersType -from trezor.messages 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/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index 8e5c921fb..e046fced0 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -55,14 +55,6 @@ INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY = "The output's token_bundle entry is invalid" -ADDRESS_TYPES = ( - messages.CardanoAddressType.BYRON, - messages.CardanoAddressType.BASE, - messages.CardanoAddressType.POINTER, - messages.CardanoAddressType.ENTERPRISE, - messages.CardanoAddressType.REWARD, -) - InputWithPath = Tuple[messages.CardanoTxInput, List[int]] AssetGroupWithTokens = Tuple[messages.CardanoAssetGroup, List[messages.CardanoToken]] OutputWithAssetGroups = Tuple[messages.CardanoTxOutput, List[AssetGroupWithTokens]] @@ -102,13 +94,15 @@ def create_address_parameters( block_index: int = None, tx_index: int = None, certificate_index: int = None, + script_payment_hash: bytes = None, + script_staking_hash: bytes = None, ) -> messages.CardanoAddressParametersType: certificate_pointer = None - if address_type not in ADDRESS_TYPES: - raise ValueError("Unknown address type") - - if address_type == messages.CardanoAddressType.POINTER: + if address_type in ( + messages.CardanoAddressType.POINTER, + messages.CardanoAddressType.POINTER_SCRIPT, + ): certificate_pointer = _create_certificate_pointer( block_index, tx_index, certificate_index ) @@ -119,6 +113,8 @@ def create_address_parameters( address_n_staking=address_n_staking, staking_key_hash=staking_key_hash, certificate_pointer=certificate_pointer, + script_payment_hash=script_payment_hash, + script_staking_hash=script_staking_hash, ) @@ -220,21 +216,31 @@ def _parse_tokens(tokens) -> List[messages.CardanoToken]: def _parse_address_parameters( address_parameters, ) -> messages.CardanoAddressParametersType: - if "path" not in address_parameters: + if "addressType" not in address_parameters: raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) - staking_key_hash_bytes = None - if "stakingKeyHash" in address_parameters: - staking_key_hash_bytes = bytes.fromhex(address_parameters.get("stakingKeyHash")) + path = tools.parse_path(address_parameters.get("path")) + staking_path = tools.parse_path(address_parameters.get("stakingPath")) + staking_key_hash_bytes = parse_optional_bytes( + address_parameters.get("stakingKeyHash") + ) + script_payment_hash = parse_optional_bytes( + address_parameters.get("scriptPaymentHash") + ) + script_staking_hash = parse_optional_bytes( + address_parameters.get("scriptStakingHash") + ) return create_address_parameters( int(address_parameters["addressType"]), - tools.parse_path(address_parameters["path"]), - tools.parse_path(address_parameters.get("stakingPath")), + path, + staking_path, staking_key_hash_bytes, address_parameters.get("blockIndex"), address_parameters.get("txIndex"), address_parameters.get("certificateIndex"), + script_payment_hash, + script_staking_hash, ) @@ -248,7 +254,7 @@ def parse_native_script(native_script) -> messages.CardanoNativeScript: for sub_script in native_script.get("scripts", ()) ] - key_hash = _parse_optional_bytes(native_script.get("key_hash")) + key_hash = parse_optional_bytes(native_script.get("key_hash")) key_path = tools.parse_path(native_script.get("key_path")) required_signatures_count = parse_optional_int( native_script.get("required_signatures_count") diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 11d165a83..8b40de4c7 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -23,14 +23,6 @@ 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(): @@ -115,14 +107,21 @@ def sign_tx(client, file, signing_mode, protocol_magic, network_id, testnet): @cli.command() -@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-n", "--address", type=str, default=None, help=PATH_HELP) @click.option("-d", "--show-display", is_flag=True) -@click.option("-t", "--address-type", type=ChoiceType(ADDRESS_TYPES), default="base") +@click.option( + "-t", + "--address-type", + type=ChoiceType({m.name: m for m in messages.CardanoAddressType}), + 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("--script-payment-hash", type=str, default=None) +@click.option("--script-staking-hash", type=str, default=None) @click.option( "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] ) @@ -138,6 +137,8 @@ def get_address( block_index, tx_index, certificate_index, + script_payment_hash, + script_staking_hash, protocol_magic, network_id, show_display, @@ -161,9 +162,9 @@ def get_address( 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) + staking_key_hash_bytes = cardano.parse_optional_bytes(staking_key_hash) + script_payment_hash_bytes = cardano.parse_optional_bytes(script_payment_hash) + script_staking_hash_bytes = cardano.parse_optional_bytes(script_staking_hash) address_parameters = cardano.create_address_parameters( address_type, @@ -173,6 +174,8 @@ def get_address( block_index, tx_index, certificate_index, + script_payment_hash_bytes, + script_staking_hash_bytes, ) return cardano.get_address( diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 1e8057223..384ec090d 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -1962,6 +1962,8 @@ class CardanoAddressParametersType(protobuf.MessageType): 3: protobuf.Field("address_n_staking", "uint32", repeated=True, required=False), 4: protobuf.Field("staking_key_hash", "bytes", repeated=False, required=False), 5: protobuf.Field("certificate_pointer", "CardanoBlockchainPointerType", repeated=False, required=False), + 6: protobuf.Field("script_payment_hash", "bytes", repeated=False, required=False), + 7: protobuf.Field("script_staking_hash", "bytes", repeated=False, required=False), } def __init__( @@ -1972,12 +1974,16 @@ class CardanoAddressParametersType(protobuf.MessageType): address_n_staking: Optional[List["int"]] = None, staking_key_hash: Optional["bytes"] = None, certificate_pointer: Optional["CardanoBlockchainPointerType"] = None, + script_payment_hash: Optional["bytes"] = None, + script_staking_hash: Optional["bytes"] = None, ) -> None: 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.address_type = address_type self.staking_key_hash = staking_key_hash self.certificate_pointer = certificate_pointer + self.script_payment_hash = script_payment_hash + self.script_staking_hash = script_staking_hash class CardanoGetAddress(protobuf.MessageType): diff --git a/tests/device_tests/cardano/test_address_public_key.py b/tests/device_tests/cardano/test_address_public_key.py index 56337549a..59d468eb1 100644 --- a/tests/device_tests/cardano/test_address_public_key.py +++ b/tests/device_tests/cardano/test_address_public_key.py @@ -16,7 +16,12 @@ import pytest -from trezorlib.cardano import create_address_parameters, get_address, get_public_key +from trezorlib.cardano import ( + create_address_parameters, + get_address, + get_public_key, + parse_optional_bytes, +) from trezorlib.messages import CardanoAddressType from trezorlib.tools import parse_path @@ -34,6 +39,7 @@ pytestmark = [ "cardano/get_address_byron.slip39.json", "cardano/get_base_address.json", "cardano/get_base_address_with_staking_key_hash.json", + "cardano/get_base_address_with_script_hashes.json", "cardano/get_enterprise_address.json", "cardano/get_pointer_address.json", "cardano/get_reward_address.json", @@ -45,16 +51,22 @@ def test_cardano_get_address(client, parameters, result): address_type=getattr( CardanoAddressType, parameters["address_type"].upper() ), - address_n=parse_path(parameters["path"]), + address_n=parse_path(parameters.get("path")) + if "path" in parameters + else None, address_n_staking=parse_path(parameters.get("staking_path")) if "staking_path" in parameters else None, - staking_key_hash=bytes.fromhex(parameters.get("staking_key_hash")) - if "staking_key_hash" in parameters - else None, + staking_key_hash=parse_optional_bytes(parameters.get("staking_key_hash")), block_index=parameters.get("block_index"), tx_index=parameters.get("tx_index"), certificate_index=parameters.get("certificate_index"), + script_payment_hash=parse_optional_bytes( + parameters.get("script_payment_hash") + ), + script_staking_hash=parse_optional_bytes( + parameters.get("script_staking_hash") + ), ), protocol_magic=parameters["protocol_magic"], network_id=parameters["network_id"],