From dd9652cd071cc56bfa259f6abfbb1d8ae3d51b04 Mon Sep 17 00:00:00 2001 From: gabrielkerekes Date: Fri, 23 Jul 2021 11:42:38 +0200 Subject: [PATCH] feat(cardano): add get-native-script-hash call --- common/protob/messages-cardano.proto | 48 +++ common/protob/messages.proto | 3 +- .../cardano/get_native_script_hash.json | 321 ++++++++++++++++++ core/src/all_modules.py | 8 + .../apps/cardano/get_native_script_hash.py | 24 ++ core/src/apps/cardano/helpers/__init__.py | 2 + core/src/apps/cardano/helpers/bech32.py | 3 + core/src/apps/cardano/helpers/utils.py | 16 +- core/src/apps/cardano/layout.py | 118 ++++++- core/src/apps/cardano/native_script.py | 171 ++++++++++ core/src/apps/workflow_handlers.py | 2 + .../CardanoNativeScriptHashDisplayFormat.py | 7 + .../trezor/enums/CardanoNativeScriptType.py | 10 + core/src/trezor/enums/MessageType.py | 2 + core/src/trezor/enums/__init__.py | 15 + core/src/trezor/messages.py | 58 ++++ core/tests/test_apps.cardano.native_script.py | 301 ++++++++++++++++ python/src/trezorlib/cardano.py | 55 ++- python/src/trezorlib/cli/cardano.py | 17 + python/src/trezorlib/messages.py | 80 +++++ .../cardano/test_get_native_script_hash.py | 28 ++ 21 files changed, 1283 insertions(+), 6 deletions(-) create mode 100644 common/tests/fixtures/cardano/get_native_script_hash.json create mode 100644 core/src/apps/cardano/get_native_script_hash.py create mode 100644 core/src/apps/cardano/native_script.py create mode 100644 core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py create mode 100644 core/src/trezor/enums/CardanoNativeScriptType.py create mode 100644 core/tests/test_apps.cardano.native_script.py create mode 100644 tests/device_tests/cardano/test_get_native_script_hash.py diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 9aa5c60b5..b31b00ab8 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -25,6 +25,21 @@ enum CardanoAddressType { REWARD_SCRIPT = 15; } +enum CardanoNativeScriptType { + PUB_KEY = 0; + ALL = 1; + ANY = 2; + N_OF_K = 3; + INVALID_BEFORE = 4; + INVALID_HEREAFTER = 5; +} + +enum CardanoNativeScriptHashDisplayFormat { + HIDE = 0; + BECH32 = 1; + POLICY_ID = 2; +} + enum CardanoCertificateType { STAKE_REGISTRATION = 0; STAKE_DEREGISTRATION = 1; @@ -64,6 +79,39 @@ message CardanoBlockchainPointerType { required uint32 certificate_index = 3; } +/* + * @embed + */ +message CardanoNativeScript { + required CardanoNativeScriptType type = 1; + repeated CardanoNativeScript scripts = 2; + + optional bytes key_hash = 3; + repeated uint32 key_path = 4; + optional uint32 required_signatures_count = 5; + optional uint64 invalid_before = 6; + optional uint64 invalid_hereafter = 7; +} + +/** + * Request: Ask device for Cardano native script hash + * @start + * @next CardanoNativeScriptHash + * @next Failure + */ +message CardanoGetNativeScriptHash { + required CardanoNativeScript script = 1; + required CardanoNativeScriptHashDisplayFormat display_format = 2; // display hash as bech32 or policy id +} + +/** + * Request: Ask device for Cardano native script hash + * @end + */ +message CardanoNativeScriptHash { + required bytes script_hash = 1; +} + /** * Structure to represent address parameters so they can be * reused in CardanoGetAddress and CardanoTxOutputType. diff --git a/common/protob/messages.proto b/common/protob/messages.proto index ef540934b..b61f2428b 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -263,7 +263,8 @@ enum MessageType { MessageType_CardanoTxAuxiliaryData = 327 [(wire_in) = true]; MessageType_CardanoPoolOwner = 328 [(wire_in) = true]; MessageType_CardanoPoolRelayParameters = 329 [(wire_in) = true]; - + MessageType_CardanoGetNativeScriptHash = 330 [(wire_in) = true]; + MessageType_CardanoNativeScriptHash = 331 [(wire_in) = true, (wire_out) = true]; // Ripple MessageType_RippleGetAddress = 400 [(wire_in) = true]; diff --git a/common/tests/fixtures/cardano/get_native_script_hash.json b/common/tests/fixtures/cardano/get_native_script_hash.json new file mode 100644 index 000000000..b6d08a3b9 --- /dev/null +++ b/common/tests/fixtures/cardano/get_native_script_hash.json @@ -0,0 +1,321 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "PUB_KEY script", + "parameters": { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + "result": { + "expected_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + }, + { + "description": "PUB_KEY script containing a path", + "parameters": { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + "result": { + "expected_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + }, + { + "description": "ALL script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6" + } + }, + { + "description": "ALL script containing a path", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6" + } + }, + { + "description": "ALL script containing a 1855 path", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_path": "m/1855'/1815'/0'" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "fbf6672eb655c29b0f148fa1429be57c2174b067a7b3e3942e967fe8" + } + }, + { + "description": "ANY script", + "parameters": { + "type": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db" + } + }, + { + "description": "ANY script with a nested script containing a path", + "parameters": { + "type": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db" + } + }, + { + "description": "N_OF_K script", + "parameters": { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + "result": { + "expected_hash": "2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9" + } + }, + { + "description": "N_OF_K script containing a path", + "parameters": { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + "result": { + "expected_hash": "2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9" + } + }, + { + "description": "INVALID_BEFORE script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 4, + "invalid_before": 100 + } + ] + }, + "result": { + "expected_hash": "c6262ef9bb2b1291c058d93b46dabf458e2d135f803f60713f84b0b7" + } + }, + { + "description": "INVALID_HEREAFTER script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "b12ac304f89f4cd4d23f59a2b90d2b2697f7540b8f470d6aa05851b5" + } + }, + { + "description": "Nested script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + { + "type": 4, + "invalid_before": 100 + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "4a6b4288459bf34668c0b281f922691460caf0c7c09caee3a726c27a" + } + }, + { + "description": "Nested script without paths and shared with Ledger", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + { + "type": 4, + "invalid_before": 100 + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "0d63e8d2c5a00cbcffbdf9112487c443466e1ea7d8c834df5ac5c425" + } + } + ] +} diff --git a/core/src/all_modules.py b/core/src/all_modules.py index d54c0b018..b25dae6b0 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -390,6 +390,10 @@ if not utils.BITCOIN_ONLY: import trezor.enums.CardanoAddressType trezor.enums.CardanoCertificateType import trezor.enums.CardanoCertificateType + trezor.enums.CardanoNativeScriptHashDisplayFormat + import trezor.enums.CardanoNativeScriptHashDisplayFormat + trezor.enums.CardanoNativeScriptType + import trezor.enums.CardanoNativeScriptType trezor.enums.CardanoPoolRelayType import trezor.enums.CardanoPoolRelayType trezor.enums.CardanoTxAuxiliaryDataSupplementType @@ -448,6 +452,8 @@ if not utils.BITCOIN_ONLY: import apps.cardano.certificates apps.cardano.get_address import apps.cardano.get_address + apps.cardano.get_native_script_hash + import apps.cardano.get_native_script_hash apps.cardano.get_public_key import apps.cardano.get_public_key apps.cardano.helpers @@ -470,6 +476,8 @@ if not utils.BITCOIN_ONLY: import apps.cardano.helpers.utils apps.cardano.layout import apps.cardano.layout + apps.cardano.native_script + import apps.cardano.native_script apps.cardano.seed import apps.cardano.seed apps.cardano.sign_tx diff --git a/core/src/apps/cardano/get_native_script_hash.py b/core/src/apps/cardano/get_native_script_hash.py new file mode 100644 index 000000000..b6a4c6096 --- /dev/null +++ b/core/src/apps/cardano/get_native_script_hash.py @@ -0,0 +1,24 @@ +from trezor import wire +from trezor.enums import CardanoNativeScriptHashDisplayFormat +from trezor.messages import CardanoNativeScriptHash + +from . import native_script, seed +from .layout import show_native_script, show_script_hash + +if False: + from trezor.messages import CardanoGetNativeScriptHash + + +@seed.with_keychain +async def get_native_script_hash( + ctx: wire.Context, msg: CardanoGetNativeScriptHash, keychain: seed.Keychain +) -> CardanoNativeScriptHash: + native_script.validate_native_script(msg.script) + + script_hash = native_script.get_native_script_hash(keychain, msg.script) + + if msg.display_format != CardanoNativeScriptHashDisplayFormat.HIDE: + await show_native_script(ctx, msg.script) + await show_script_hash(ctx, script_hash, msg.display_format) + + return CardanoNativeScriptHash(script_hash=script_hash) diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py index 3d16e01a9..8cb30147f 100644 --- a/core/src/apps/cardano/helpers/__init__.py +++ b/core/src/apps/cardano/helpers/__init__.py @@ -15,6 +15,8 @@ INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES = wire.ProcessError( "Stakepool registration transaction can only contain staking witnesses" ) INVALID_WITNESS_REQUEST = wire.ProcessError("Invalid witness request") +INVALID_NATIVE_SCRIPT = wire.ProcessError("Invalid native script") LOVELACE_MAX_SUPPLY = 45_000_000_000 * 1_000_000 ADDRESS_KEY_HASH_SIZE = 28 +SCRIPT_HASH_SIZE = 28 diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py index 4a35e29be..cb710fb72 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -9,6 +9,9 @@ HRP_REWARD_ADDRESS = "stake" HRP_TESTNET_REWARD_ADDRESS = "stake_test" # Jormungandr public key prefix - https://github.com/input-output-hk/voting-tools-lib/blob/18dae637e80db72444476606ab264b973bcf1a9d/src/Cardano/API/Extended.hs#L226 HRP_JORMUN_PUBLIC_KEY = "ed25519_pk" +HRP_SCRIPT_HASH = "script" +HRP_KEY_HASH = "addr_vkh" +HRP_SHARED_KEY_HASH = "addr_shared_vkh" def encode(hrp: str, data: bytes) -> str: diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index e0af3ada0..1e38f2c06 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -3,7 +3,7 @@ from trezor.crypto import hashlib from apps.cardano.helpers.paths import ACCOUNT_PATH_INDEX, unharden from apps.common.seed import remove_ed25519_prefix -from . import bech32 +from . import ADDRESS_KEY_HASH_SIZE, bech32 if False: from .. import seed @@ -59,6 +59,20 @@ def format_asset_fingerprint(policy_id: bytes, asset_name_bytes: bytes) -> str: return bech32.encode("asset", fingerprint) +def format_script_hash(script_hash: bytes) -> str: + return bech32.encode(bech32.HRP_SCRIPT_HASH, script_hash) + + +def format_key_hash(key_hash: bytes, is_shared_key: bool) -> str: + hrp = bech32.HRP_SHARED_KEY_HASH if is_shared_key else bech32.HRP_KEY_HASH + return bech32.encode(hrp, key_hash) + + +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_public_key( keychain: seed.Keychain, path: list[int], extended: bool = False ) -> bytes: diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index 52c59696d..cbdfeab83 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -1,7 +1,14 @@ from trezor import ui -from trezor.enums import ButtonRequestType, CardanoAddressType, CardanoCertificateType +from trezor.enums import ( + ButtonRequestType, + CardanoAddressType, + CardanoCertificateType, + CardanoNativeScriptHashDisplayFormat, + CardanoNativeScriptType, +) from trezor.strings import format_amount from trezor.ui.layouts import ( + confirm_blob, confirm_metadata, confirm_output, confirm_path_warning, @@ -20,7 +27,9 @@ from .helpers import protocol_magics from .helpers.utils import ( format_account_number, format_asset_fingerprint, + format_key_hash, format_optional_int, + format_script_hash, format_stake_pool_id, to_account_path, ) @@ -29,6 +38,7 @@ if False: from trezor import wire from trezor.messages import ( CardanoBlockchainPointerType, + CardanoNativeScript, CardanoTxCertificate, CardanoTxWithdrawal, CardanoPoolParametersType, @@ -48,6 +58,15 @@ ADDRESS_TYPE_NAMES = { CardanoAddressType.REWARD: "Reward", } +SCRIPT_TYPE_NAMES = { + CardanoNativeScriptType.PUB_KEY: "Key", + CardanoNativeScriptType.ALL: "All", + CardanoNativeScriptType.ANY: "Any", + CardanoNativeScriptType.N_OF_K: "N of K", + CardanoNativeScriptType.INVALID_BEFORE: "Invalid before", + CardanoNativeScriptType.INVALID_HEREAFTER: "Invalid hereafter", +} + CERTIFICATE_TYPE_NAMES = { CardanoCertificateType.STAKE_REGISTRATION: "Stake key registration", CardanoCertificateType.STAKE_DEREGISTRATION: "Stake key deregistration", @@ -64,6 +83,103 @@ def is_printable_ascii_bytestring(bytestr: bytes) -> bool: return all((32 < b < 127) for b in bytestr) +async def show_native_script( + ctx: wire.Context, + script: CardanoNativeScript, + indices: list[int] = [], +) -> None: + indices_str = ".".join([str(i) for i in indices]) + + script_type_name_suffix = "" + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_path: + script_type_name_suffix = "path" + elif script.key_hash: + script_type_name_suffix = "hash" + + props: list[PropertyType] = [ + ( + "Script%s - %s %s:" + % ( + (" " + indices_str if indices_str else ""), + SCRIPT_TYPE_NAMES[script.type], + script_type_name_suffix, + ), + None, + ) + ] + + if script.type == CardanoNativeScriptType.PUB_KEY: + assert script.key_hash is not None or script.key_path # validate_script + if script.key_hash: + props.append((None, format_key_hash(script.key_hash, True))) + elif script.key_path: + props.append((address_n_to_str(script.key_path), None)) + elif script.type == CardanoNativeScriptType.N_OF_K: + assert script.required_signatures_count is not None # validate_script + props.append( + ( + "Requires %s out of %s signatures." + % (script.required_signatures_count, len(script.scripts)), + None, + ) + ) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + assert script.invalid_before is not None # validate_script + props.append((str(script.invalid_before), None)) + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + assert script.invalid_hereafter is not None # validate_script + props.append((str(script.invalid_hereafter), None)) + + if script.type in ( + CardanoNativeScriptType.ALL, + CardanoNativeScriptType.ANY, + CardanoNativeScriptType.N_OF_K, + ): + assert script.scripts # validate_script + props.append(("Contains %i nested scripts." % len(script.scripts), None)) + + await confirm_properties( + ctx, + "verify_script", + title="Verify script", + props=props, + br_code=ButtonRequestType.Other, + ) + + for i, sub_script in enumerate(script.scripts): + await show_native_script(ctx, sub_script, indices + [(i + 1)]) + + +async def show_script_hash( + ctx: wire.Context, + script_hash: bytes, + display_format: CardanoNativeScriptHashDisplayFormat, +) -> None: + assert display_format in ( + CardanoNativeScriptHashDisplayFormat.BECH32, + CardanoNativeScriptHashDisplayFormat.POLICY_ID, + ) + + if display_format == CardanoNativeScriptHashDisplayFormat.BECH32: + await confirm_properties( + ctx, + "verify_script", + title="Verify script", + props=[("Script hash:", format_script_hash(script_hash))], + br_code=ButtonRequestType.Other, + ) + elif display_format == CardanoNativeScriptHashDisplayFormat.POLICY_ID: + await confirm_blob( + ctx, + "verify_script", + title="Verify script", + data=script_hash, + description="Policy ID:", + br_code=ButtonRequestType.Other, + ) + + async def confirm_sending( ctx: wire.Context, ada_amount: int, diff --git a/core/src/apps/cardano/native_script.py b/core/src/apps/cardano/native_script.py new file mode 100644 index 000000000..a0cb5788c --- /dev/null +++ b/core/src/apps/cardano/native_script.py @@ -0,0 +1,171 @@ +from trezor.crypto import hashlib +from trezor.enums import CardanoAddressType, CardanoNativeScriptType + +from apps.cardano.helpers import ( + ADDRESS_KEY_HASH_SIZE, + INVALID_NATIVE_SCRIPT, + SCRIPT_HASH_SIZE, +) +from apps.common import cbor + +from .helpers.paths import SCHEMA_MINT +from .helpers.utils import get_public_key_hash +from .seed import Keychain, is_multisig_path + +if False: + from typing import Any + + from trezor.messages import CardanoNativeScript + + from apps.common.cbor import CborSequence + +SCRIPT_ADDRESS_TYPES = ( + CardanoAddressType.BASE_SCRIPT_KEY, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.POINTER_SCRIPT, + CardanoAddressType.ENTERPRISE_SCRIPT, + CardanoAddressType.REWARD_SCRIPT, +) + + +def validate_native_script(script: CardanoNativeScript | None) -> None: + if not script: + raise INVALID_NATIVE_SCRIPT + + _validate_native_script_structure(script) + + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_hash and script.key_path: + raise INVALID_NATIVE_SCRIPT + if script.key_hash: + if len(script.key_hash) != ADDRESS_KEY_HASH_SIZE: + raise INVALID_NATIVE_SCRIPT + elif script.key_path: + if not is_multisig_path(script.key_path) and not SCHEMA_MINT.match( + script.key_path + ): + raise INVALID_NATIVE_SCRIPT + else: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.ALL: + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.ANY: + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.N_OF_K: + if script.required_signatures_count is None: + raise INVALID_NATIVE_SCRIPT + if script.required_signatures_count > len(script.scripts): + raise INVALID_NATIVE_SCRIPT + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + if script.invalid_before is None: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + if script.invalid_hereafter is None: + raise INVALID_NATIVE_SCRIPT + + +def _validate_native_script_structure(script: CardanoNativeScript) -> None: + key_hash = script.key_hash + key_path = script.key_path + scripts = script.scripts + required_signatures_count = script.required_signatures_count + invalid_before = script.invalid_before + invalid_hereafter = script.invalid_hereafter + + fields_to_be_empty: dict[CardanoNativeScriptType, tuple[Any, ...]] = { + CardanoNativeScriptType.PUB_KEY: ( + scripts, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.ALL: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.ANY: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.N_OF_K: ( + key_hash, + key_path, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.INVALID_BEFORE: ( + key_hash, + key_path, + required_signatures_count, + invalid_hereafter, + ), + CardanoNativeScriptType.INVALID_HEREAFTER: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + ), + } + + if script.type not in fields_to_be_empty or any(fields_to_be_empty[script.type]): + raise INVALID_NATIVE_SCRIPT + + +def get_native_script_hash(keychain: Keychain, script: CardanoNativeScript) -> bytes: + script_cbor = cbor.encode(cborize_native_script(keychain, script)) + prefixed_script_cbor = b"\00" + script_cbor + return hashlib.blake2b(data=prefixed_script_cbor, outlen=SCRIPT_HASH_SIZE).digest() + + +def cborize_native_script( + keychain: Keychain, script: CardanoNativeScript +) -> CborSequence: + script_content: CborSequence + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_hash: + script_content = (script.key_hash,) + elif script.key_path: + script_content = (get_public_key_hash(keychain, script.key_path),) + else: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.ALL: + script_content = ( + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.ANY: + script_content = ( + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.N_OF_K: + script_content = ( + script.required_signatures_count, + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + script_content = (script.invalid_before,) + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + script_content = (script.invalid_hereafter,) + else: + raise INVALID_NATIVE_SCRIPT + + return (script.type,) + script_content diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index b81a6426b..db983ebe0 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -152,6 +152,8 @@ def find_message_handler_module(msg_type: int) -> str: return "apps.cardano.get_public_key" elif msg_type == MessageType.CardanoSignTxInit: return "apps.cardano.sign_tx" + elif msg_type == MessageType.CardanoGetNativeScriptHash: + return "apps.cardano.get_native_script_hash" # tezos elif msg_type == MessageType.TezosGetAddress: diff --git a/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py b/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py new file mode 100644 index 000000000..3a9c5502d --- /dev/null +++ b/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +HIDE = 0 +BECH32 = 1 +POLICY_ID = 2 diff --git a/core/src/trezor/enums/CardanoNativeScriptType.py b/core/src/trezor/enums/CardanoNativeScriptType.py new file mode 100644 index 000000000..72d6e6647 --- /dev/null +++ b/core/src/trezor/enums/CardanoNativeScriptType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +PUB_KEY = 0 +ALL = 1 +ANY = 2 +N_OF_K = 3 +INVALID_BEFORE = 4 +INVALID_HEREAFTER = 5 diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index fbe67c573..9d2c51037 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -150,6 +150,8 @@ if not utils.BITCOIN_ONLY: CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 5876771de..7233d968a 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -155,6 +155,8 @@ if TYPE_CHECKING: CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 @@ -328,6 +330,19 @@ if TYPE_CHECKING: REWARD = 14 REWARD_SCRIPT = 15 + class CardanoNativeScriptType(IntEnum): + PUB_KEY = 0 + ALL = 1 + ANY = 2 + N_OF_K = 3 + INVALID_BEFORE = 4 + INVALID_HEREAFTER = 5 + + class CardanoNativeScriptHashDisplayFormat(IntEnum): + HIDE = 0 + BECH32 = 1 + POLICY_ID = 2 + class CardanoCertificateType(IntEnum): STAKE_REGISTRATION = 0 STAKE_DEREGISTRATION = 1 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index d9b9d0637..c8ce73a64 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -27,6 +27,8 @@ if TYPE_CHECKING: from trezor.enums import Capability # noqa: F401 from trezor.enums import CardanoAddressType # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401 + from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 + from trezor.enums import CardanoNativeScriptType # noqa: F401 from trezor.enums import CardanoPoolRelayType # noqa: F401 from trezor.enums import CardanoTxAuxiliaryDataSupplementType # noqa: F401 from trezor.enums import CardanoTxSigningMode # noqa: F401 @@ -1059,6 +1061,62 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoBlockchainPointerType"]: return isinstance(msg, cls) + class CardanoNativeScript(protobuf.MessageType): + type: "CardanoNativeScriptType" + scripts: "list[CardanoNativeScript]" + key_hash: "bytes | None" + key_path: "list[int]" + required_signatures_count: "int | None" + invalid_before: "int | None" + invalid_hereafter: "int | None" + + def __init__( + self, + *, + type: "CardanoNativeScriptType", + scripts: "list[CardanoNativeScript] | None" = None, + key_path: "list[int] | None" = None, + key_hash: "bytes | None" = None, + required_signatures_count: "int | None" = None, + invalid_before: "int | None" = None, + invalid_hereafter: "int | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoNativeScript"]: + return isinstance(msg, cls) + + class CardanoGetNativeScriptHash(protobuf.MessageType): + script: "CardanoNativeScript" + display_format: "CardanoNativeScriptHashDisplayFormat" + + def __init__( + self, + *, + script: "CardanoNativeScript", + display_format: "CardanoNativeScriptHashDisplayFormat", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoGetNativeScriptHash"]: + return isinstance(msg, cls) + + class CardanoNativeScriptHash(protobuf.MessageType): + script_hash: "bytes" + + def __init__( + self, + *, + script_hash: "bytes", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoNativeScriptHash"]: + return isinstance(msg, cls) + class CardanoAddressParametersType(protobuf.MessageType): address_type: "CardanoAddressType" address_n: "list[int]" diff --git a/core/tests/test_apps.cardano.native_script.py b/core/tests/test_apps.cardano.native_script.py new file mode 100644 index 000000000..0a344fee8 --- /dev/null +++ b/core/tests/test_apps.cardano.native_script.py @@ -0,0 +1,301 @@ +from common import * +from trezor import wire +from trezor.crypto import bip32 +from trezor.enums import CardanoNativeScriptType +from trezor.messages import CardanoNativeScript + +if not utils.BITCOIN_ONLY: + from apps.cardano.seed import Keychain + from apps.cardano.native_script import get_native_script_hash, validate_native_script + +VALID_NATIVE_SCRIPTS = [ + # PUB_KEY + [ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + b"29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + ], + # PUB_KEY with path + [ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + b"29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + ], + # ALL + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6", + ], + # ALL with 1855 path + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1855 | HARDENED, 1815 | HARDENED, 0 | HARDENED], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"fbf6672eb655c29b0f148fa1429be57c2174b067a7b3e3942e967fe8", + ], + # ALL scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.ALL, scripts=[]), + b"d441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf" + ], + # ANY + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ANY, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db", + ], + # ANY scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.ANY, scripts=[]), + b"52dc3d43b6d2465e96109ce75ab61abe5e9c1d8a3c9ce6ff8a3af528" + ], + # N OF K + [ + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + ), + ), + ], + ), + b"2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9", + ], + # N_OF_K scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.N_OF_K, required_signatures_count=0, scripts=[]), + b"3530cc9ae7f2895111a99b7a02184dd7c0cea7424f1632d73951b1d7" + ], + # INVALID BEFORE + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_BEFORE, invalid_before=100 + ), + ], + ), + b"c6262ef9bb2b1291c058d93b46dabf458e2d135f803f60713f84b0b7", + ], + # INVALID HEREAFTER + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_HEREAFTER, + invalid_hereafter=200, + ), + ], + ), + b"b12ac304f89f4cd4d23f59a2b90d2b2697f7540b8f470d6aa05851b5", + ], + # NESTED SCRIPT + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.ANY, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + ), + ), + ], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_BEFORE, invalid_before=100 + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_HEREAFTER, + invalid_hereafter=200, + ), + ], + ), + b"4a6b4288459bf34668c0b281f922691460caf0c7c09caee3a726c27a", + ], +] + +INVALID_SCRIPTS = [ + # PUB_KEY key_hash has invalid length + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify("3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0f"), + ), + # PUB_KEY key_path is not multisig or mint + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + # PUB_KEY mint key_path is too long + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1855 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0], + ), + # N_OF_K required_signatures_count is not set + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0fa9" + ), + ), + ], + ), + # N_OF_K N is larger than K + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0fa9" + ), + ), + ], + ), + # INVALID_BEFORE invalid_before is not set + CardanoNativeScript(type=CardanoNativeScriptType.INVALID_BEFORE), + # INVALID_HEREAFTER invalid_hereafter is not set + CardanoNativeScript(type=CardanoNativeScriptType.INVALID_HEREAFTER), +] + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoNativeScript(unittest.TestCase): + def test_get_native_script_hash(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) + + for script, expected_hash in VALID_NATIVE_SCRIPTS: + actual_hash = get_native_script_hash(keychain, script) + self.assertEqual(hexlify(actual_hash), expected_hash) + + def test_validate_native_script(self): + for script, _ in VALID_NATIVE_SCRIPTS: + validate_native_script(script) + + def test_validate_native_script_invalid(self): + for script in INVALID_SCRIPTS: + with self.assertRaises(wire.ProcessError): + validate_native_script(script) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index a388d16db..8e5c921fb 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -86,6 +86,14 @@ AuxiliaryDataSupplement = Dict[str, Union[int, bytes]] SignTxResponse = Dict[str, Union[bytes, List[Witness], AuxiliaryDataSupplement]] +def parse_optional_bytes(value: Optional[str]) -> Optional[bytes]: + return bytes.fromhex(value) if value is not None else None + + +def parse_optional_int(value) -> Optional[int]: + return int(value) if value is not None else None + + def create_address_parameters( address_type: messages.CardanoAddressType, address_n: List[int], @@ -230,6 +238,35 @@ def _parse_address_parameters( ) +def parse_native_script(native_script) -> messages.CardanoNativeScript: + if "type" not in native_script: + raise ValueError("Script is missing some fields") + + type = native_script["type"] + scripts = [ + parse_native_script(sub_script) + for sub_script in native_script.get("scripts", ()) + ] + + 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") + ) + invalid_before = parse_optional_int(native_script.get("invalid_before")) + invalid_hereafter = parse_optional_int(native_script.get("invalid_hereafter")) + + return messages.CardanoNativeScript( + type=type, + scripts=scripts, + key_hash=key_hash, + key_path=key_path, + required_signatures_count=required_signatures_count, + invalid_before=invalid_before, + invalid_hereafter=invalid_hereafter, + ) + + def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: CERTIFICATE_MISSING_FIELDS_ERROR = ValueError( "The certificate is missing some fields" @@ -380,9 +417,7 @@ def parse_auxiliary_data(auxiliary_data) -> messages.CardanoTxAuxiliaryData: ) # include all provided fields so we can test validation in FW - hash = None - if "hash" in auxiliary_data: - hash = bytes.fromhex(auxiliary_data["hash"]) + hash = parse_optional_bytes(auxiliary_data.get("hash")) catalyst_registration_parameters = None if "catalyst_registration_parameters" in auxiliary_data: @@ -493,6 +528,20 @@ def get_public_key(client, address_n: List[int]) -> messages.CardanoPublicKey: return client.call(messages.CardanoGetPublicKey(address_n=address_n)) +@expect(messages.CardanoNativeScriptHash) +def get_native_script_hash( + client, + native_script: messages.CardanoNativeScript, + display_format: messages.CardanoNativeScriptHashDisplayFormat = messages.CardanoNativeScriptHashDisplayFormat.HIDE, +) -> messages.CardanoNativeScriptHash: + return client.call( + messages.CardanoGetNativeScriptHash( + script=native_script, + display_format=display_format, + ) + ) + + def sign_tx( client, signing_mode: messages.CardanoTxSigningMode, diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 26b0468e2..11d165a83 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -187,3 +187,20 @@ def get_public_key(client, address): """Get Cardano public key.""" address_n = tools.parse_path(address) return cardano.get_public_key(client, address_n) + + +@cli.command() +@click.argument("file", type=click.File("r")) +@click.option( + "-d", + "--display-format", + type=ChoiceType({m.name: m for m in messages.CardanoNativeScriptHashDisplayFormat}), + default="HIDE", +) +@with_client +def get_native_script_hash(client, file, display_format): + """Get Cardano native script hash.""" + native_script_json = json.load(file) + native_script = cardano.parse_native_script(native_script_json) + + return cardano.get_native_script_hash(client, native_script, display_format) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 7304ceff7..1e8057223 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -176,6 +176,8 @@ class MessageType(IntEnum): CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 @@ -342,6 +344,21 @@ class CardanoAddressType(IntEnum): REWARD_SCRIPT = 15 +class CardanoNativeScriptType(IntEnum): + PUB_KEY = 0 + ALL = 1 + ANY = 2 + N_OF_K = 3 + INVALID_BEFORE = 4 + INVALID_HEREAFTER = 5 + + +class CardanoNativeScriptHashDisplayFormat(IntEnum): + HIDE = 0 + BECH32 = 1 + POLICY_ID = 2 + + class CardanoCertificateType(IntEnum): STAKE_REGISTRATION = 0 STAKE_DEREGISTRATION = 1 @@ -1874,6 +1891,69 @@ class CardanoBlockchainPointerType(protobuf.MessageType): self.certificate_index = certificate_index +class CardanoNativeScript(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("type", "CardanoNativeScriptType", repeated=False, required=True), + 2: protobuf.Field("scripts", "CardanoNativeScript", repeated=True, required=False), + 3: protobuf.Field("key_hash", "bytes", repeated=False, required=False), + 4: protobuf.Field("key_path", "uint32", repeated=True, required=False), + 5: protobuf.Field("required_signatures_count", "uint32", repeated=False, required=False), + 6: protobuf.Field("invalid_before", "uint64", repeated=False, required=False), + 7: protobuf.Field("invalid_hereafter", "uint64", repeated=False, required=False), + } + + def __init__( + self, + *, + type: "CardanoNativeScriptType", + scripts: Optional[List["CardanoNativeScript"]] = None, + key_path: Optional[List["int"]] = None, + key_hash: Optional["bytes"] = None, + required_signatures_count: Optional["int"] = None, + invalid_before: Optional["int"] = None, + invalid_hereafter: Optional["int"] = None, + ) -> None: + self.scripts = scripts if scripts is not None else [] + self.key_path = key_path if key_path is not None else [] + self.type = type + self.key_hash = key_hash + self.required_signatures_count = required_signatures_count + self.invalid_before = invalid_before + self.invalid_hereafter = invalid_hereafter + + +class CardanoGetNativeScriptHash(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 330 + FIELDS = { + 1: protobuf.Field("script", "CardanoNativeScript", repeated=False, required=True), + 2: protobuf.Field("display_format", "CardanoNativeScriptHashDisplayFormat", repeated=False, required=True), + } + + def __init__( + self, + *, + script: "CardanoNativeScript", + display_format: "CardanoNativeScriptHashDisplayFormat", + ) -> None: + self.script = script + self.display_format = display_format + + +class CardanoNativeScriptHash(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 331 + FIELDS = { + 1: protobuf.Field("script_hash", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + script_hash: "bytes", + ) -> None: + self.script_hash = script_hash + + class CardanoAddressParametersType(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { diff --git a/tests/device_tests/cardano/test_get_native_script_hash.py b/tests/device_tests/cardano/test_get_native_script_hash.py new file mode 100644 index 000000000..a6f4f8855 --- /dev/null +++ b/tests/device_tests/cardano/test_get_native_script_hash.py @@ -0,0 +1,28 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from trezorlib.cardano import get_native_script_hash, parse_native_script + +from ...common import parametrize_using_common_fixtures + + +@parametrize_using_common_fixtures( + "cardano/get_native_script_hash.json", +) +def test_cardano_get_native_script_hash(client, parameters, result): + native_script = parse_native_script(parameters) + native_script_hash = get_native_script_hash(client, native_script).script_hash + assert native_script_hash.hex() == result["expected_hash"]