From 2724d29968d71e53dec219cbd64a49984ac5f7d7 Mon Sep 17 00:00:00 2001 From: David Misiak Date: Wed, 11 May 2022 14:40:00 +0200 Subject: [PATCH] refactor(cardano): introduce Signer and its subclasses --- core/src/all_modules.py | 10 + core/src/apps/cardano/certificates.py | 19 +- core/src/apps/cardano/get_address.py | 2 +- core/src/apps/cardano/helpers/utils.py | 45 +- core/src/apps/cardano/layout.py | 40 +- core/src/apps/cardano/sign_tx.py | 1316 ----------------- core/src/apps/cardano/sign_tx/__init__.py | 44 + .../apps/cardano/sign_tx/multisig_signer.py | 88 ++ .../apps/cardano/sign_tx/ordinary_signer.py | 102 ++ .../src/apps/cardano/sign_tx/plutus_signer.py | 112 ++ .../apps/cardano/sign_tx/pool_owner_signer.py | 92 ++ core/src/apps/cardano/sign_tx/signer.py | 926 ++++++++++++ core/tests/test_apps.cardano.certificate.py | 593 +++----- tests/ui_tests/fixtures.json | 4 +- 14 files changed, 1654 insertions(+), 1739 deletions(-) delete mode 100644 core/src/apps/cardano/sign_tx.py create mode 100644 core/src/apps/cardano/sign_tx/__init__.py create mode 100644 core/src/apps/cardano/sign_tx/multisig_signer.py create mode 100644 core/src/apps/cardano/sign_tx/ordinary_signer.py create mode 100644 core/src/apps/cardano/sign_tx/plutus_signer.py create mode 100644 core/src/apps/cardano/sign_tx/pool_owner_signer.py create mode 100644 core/src/apps/cardano/sign_tx/signer.py diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 52f5d3a6a..2519218e8 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -523,6 +523,16 @@ if not utils.BITCOIN_ONLY: import apps.cardano.seed apps.cardano.sign_tx import apps.cardano.sign_tx + apps.cardano.sign_tx.multisig_signer + import apps.cardano.sign_tx.multisig_signer + apps.cardano.sign_tx.ordinary_signer + import apps.cardano.sign_tx.ordinary_signer + apps.cardano.sign_tx.plutus_signer + import apps.cardano.sign_tx.plutus_signer + apps.cardano.sign_tx.pool_owner_signer + import apps.cardano.sign_tx.pool_owner_signer + apps.cardano.sign_tx.signer + import apps.cardano.sign_tx.signer apps.common.mnemonic import apps.common.mnemonic apps.eos diff --git a/core/src/apps/cardano/certificates.py b/core/src/apps/cardano/certificates.py index c66dadc52..3fa10b077 100644 --- a/core/src/apps/cardano/certificates.py +++ b/core/src/apps/cardano/certificates.py @@ -1,10 +1,6 @@ from typing import TYPE_CHECKING -from trezor.enums import ( - CardanoCertificateType, - CardanoPoolRelayType, - CardanoTxSigningMode, -) +from trezor.enums import CardanoCertificateType, CardanoPoolRelayType from apps.common import cbor @@ -41,22 +37,10 @@ MAX_PORT_NUMBER = 65535 def validate_certificate( certificate: CardanoTxCertificate, - signing_mode: CardanoTxSigningMode, protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, ) -> None: - if ( - signing_mode != CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER - and certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION - ): - raise INVALID_CERTIFICATE - elif ( - signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER - and certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION - ): - raise INVALID_CERTIFICATE - _validate_certificate_structure(certificate) if certificate.type in ( @@ -68,7 +52,6 @@ def validate_certificate( certificate.path, certificate.script_hash, certificate.key_hash, - signing_mode, INVALID_CERTIFICATE, ) diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index 641c35b01..184a4e94b 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -6,8 +6,8 @@ from trezor.messages import CardanoAddress from . import seed from .address import derive_human_readable_address, validate_address_parameters from .helpers.credential import Credential, should_show_address_credentials +from .helpers.utils import validate_network_info from .layout import show_address_credentials, show_cardano_address -from .sign_tx import validate_network_info if TYPE_CHECKING: from trezor.messages import ( diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index d5721ed88..2f0918559 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING +from trezor import wire from trezor.crypto import hashlib -from trezor.enums import CardanoTxSigningMode +from apps.cardano.helpers import network_ids, protocol_magics from apps.cardano.helpers.paths import ( ACCOUNT_PATH_INDEX, SCHEMA_STAKING_ANY_ACCOUNT, @@ -13,7 +14,6 @@ from apps.common.seed import remove_ed25519_prefix from . import ADDRESS_KEY_HASH_SIZE, SCRIPT_HASH_SIZE, bech32 if TYPE_CHECKING: - from trezor import wire from .. import seed @@ -84,32 +84,27 @@ def validate_stake_credential( path: list[int], script_hash: bytes | None, key_hash: bytes | None, - signing_mode: CardanoTxSigningMode, error: wire.ProcessError, ) -> None: if sum(bool(k) for k in (path, script_hash, key_hash)) != 1: raise error - if path: - if signing_mode not in ( - CardanoTxSigningMode.ORDINARY_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - raise error - if not SCHEMA_STAKING_ANY_ACCOUNT.match(path): - raise error - elif script_hash: - if signing_mode not in ( - CardanoTxSigningMode.MULTISIG_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - raise error - if len(script_hash) != SCRIPT_HASH_SIZE: - raise error - elif key_hash: - if signing_mode != CardanoTxSigningMode.PLUTUS_TRANSACTION: - raise error - if len(key_hash) != ADDRESS_KEY_HASH_SIZE: - raise error - else: + if path and not SCHEMA_STAKING_ANY_ACCOUNT.match(path): raise error + if script_hash and len(script_hash) != SCRIPT_HASH_SIZE: + raise error + if key_hash and len(key_hash) != ADDRESS_KEY_HASH_SIZE: + raise error + + +def validate_network_info(network_id: int, protocol_magic: int) -> None: + """ + We are only concerned about checking that both network_id and protocol_magic + belong to the mainnet or that both belong to a testnet. We don't need to check for + consistency between various testnets (at least for now). + """ + is_mainnet_network_id = network_ids.is_mainnet(network_id) + is_mainnet_protocol_magic = protocol_magics.is_mainnet(protocol_magic) + + if is_mainnet_network_id != is_mainnet_protocol_magic: + raise wire.ProcessError("Invalid network id/protocol magic combination!") diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index bb6687479..b168c2ef2 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -7,7 +7,6 @@ from trezor.enums import ( CardanoCertificateType, CardanoNativeScriptHashDisplayFormat, CardanoNativeScriptType, - CardanoTxSigningMode, ) from trezor.messages import CardanoAddressParametersType from trezor.strings import format_amount @@ -193,26 +192,25 @@ async def show_script_hash( ) -async def show_transaction_signing_mode( - ctx: wire.Context, signing_mode: CardanoTxSigningMode -) -> None: - if signing_mode == CardanoTxSigningMode.MULTISIG_TRANSACTION: - await confirm_metadata( - ctx, - "confirm_signing_mode", - title="Confirm transaction", - content="Confirming a multisig transaction.", - larger_vspace=True, - br_code=ButtonRequestType.Other, - ) - elif signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - await confirm_metadata( - ctx, - "confirm_signing_mode", - title="Confirm transaction", - content="Confirming a Plutus transaction - loss of collateral is possible. Check all items carefully.", - br_code=ButtonRequestType.Other, - ) +async def show_multisig_transaction(ctx: wire.Context) -> None: + await confirm_metadata( + ctx, + "confirm_signing_mode", + title="Confirm transaction", + content="Confirming a multisig transaction.", + larger_vspace=True, + br_code=ButtonRequestType.Other, + ) + + +async def show_plutus_transaction(ctx: wire.Context) -> None: + await confirm_metadata( + ctx, + "confirm_signing_mode", + title="Confirm transaction", + content="Confirming a Plutus transaction - loss of collateral is possible. Check all items carefully.", + br_code=ButtonRequestType.Other, + ) async def confirm_input(ctx: wire.Context, input: CardanoTxInput) -> None: diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py deleted file mode 100644 index c3ae8324c..000000000 --- a/core/src/apps/cardano/sign_tx.py +++ /dev/null @@ -1,1316 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import log, wire -from trezor.crypto import hashlib -from trezor.crypto.curve import ed25519 -from trezor.enums import ( - CardanoAddressType, - CardanoCertificateType, - CardanoTxSigningMode, - CardanoTxWitnessType, -) -from trezor.messages import ( - CardanoAddressParametersType, - CardanoAssetGroup, - CardanoPoolOwner, - CardanoPoolRelayParameters, - CardanoSignTxFinished, - CardanoSignTxInit, - CardanoToken, - CardanoTxAuxiliaryData, - CardanoTxBodyHash, - CardanoTxCertificate, - CardanoTxCollateralInput, - CardanoTxHostAck, - CardanoTxInput, - CardanoTxItemAck, - CardanoTxMint, - CardanoTxOutput, - CardanoTxRequiredSigner, - CardanoTxWithdrawal, - CardanoTxWitnessRequest, - CardanoTxWitnessResponse, -) - -from apps.common import cbor, safety_checks - -from . import seed -from .address import ( - ADDRESS_TYPES_PAYMENT_SCRIPT, - derive_address_bytes, - derive_human_readable_address, - get_address_bytes_unsafe, - get_address_type, - validate_output_address, - validate_output_address_parameters, -) -from .auxiliary_data import ( - get_auxiliary_data_hash_and_supplement, - show_auxiliary_data, - validate_auxiliary_data, -) -from .certificates import ( - assert_certificate_cond, - cborize_certificate, - cborize_initial_pool_registration_certificate_fields, - cborize_pool_metadata, - cborize_pool_owner, - cborize_pool_relay, - validate_certificate, - validate_pool_owner, - validate_pool_relay, -) -from .helpers import ( - ADDRESS_KEY_HASH_SIZE, - INPUT_PREV_HASH_SIZE, - INVALID_COLLATERAL_INPUT, - INVALID_INPUT, - INVALID_OUTPUT, - INVALID_OUTPUT_DATUM_HASH, - INVALID_REQUIRED_SIGNER, - INVALID_SCRIPT_DATA_HASH, - INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE, - INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES, - INVALID_TOKEN_BUNDLE_MINT, - INVALID_TOKEN_BUNDLE_OUTPUT, - INVALID_TX_SIGNING_REQUEST, - INVALID_WITHDRAWAL, - INVALID_WITNESS_REQUEST, - LOVELACE_MAX_SUPPLY, - OUTPUT_DATUM_HASH_SIZE, - SCRIPT_DATA_HASH_SIZE, - network_ids, - protocol_magics, -) -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, - CHANGE_OUTPUT_PATH_NAME, - CHANGE_OUTPUT_STAKING_PATH_NAME, - POOL_OWNER_STAKING_PATH_NAME, - SCHEMA_MINT, - SCHEMA_PAYMENT, - SCHEMA_STAKING, - SCHEMA_STAKING_ANY_ACCOUNT, - WITNESS_PATH_NAME, -) -from .helpers.utils import ( - derive_public_key, - get_public_key_hash, - validate_stake_credential, -) -from .layout import ( - confirm_certificate, - confirm_collateral_input, - confirm_input, - confirm_required_signer, - confirm_script_data_hash, - confirm_sending, - confirm_sending_token, - confirm_stake_pool_metadata, - confirm_stake_pool_owner, - confirm_stake_pool_parameters, - confirm_stake_pool_registration_final, - confirm_token_minting, - confirm_transaction, - confirm_withdrawal, - confirm_witness_request, - show_change_output_credentials, - show_device_owned_output_credentials, - show_transaction_signing_mode, - show_warning_no_collateral_inputs, - show_warning_no_script_data_hash, - show_warning_path, - show_warning_tx_contains_mint, - show_warning_tx_network_unverifiable, - show_warning_tx_output_contains_datum_hash, - show_warning_tx_output_contains_tokens, - show_warning_tx_output_no_datum_hash, -) -from .seed import is_byron_path, is_minting_path, is_multisig_path, is_shelley_path - -if TYPE_CHECKING: - from typing import Any - from apps.common.paths import PathSchema - - CardanoTxResponseType = CardanoTxItemAck | CardanoTxWitnessResponse - -MINTING_POLICY_ID_LENGTH = 28 -MAX_ASSET_NAME_LENGTH = 32 - -TX_BODY_KEY_INPUTS = const(0) -TX_BODY_KEY_OUTPUTS = const(1) -TX_BODY_KEY_FEE = const(2) -TX_BODY_KEY_TTL = const(3) -TX_BODY_KEY_CERTIFICATES = const(4) -TX_BODY_KEY_WITHDRAWALS = const(5) -TX_BODY_KEY_AUXILIARY_DATA = const(7) -TX_BODY_KEY_VALIDITY_INTERVAL_START = const(8) -TX_BODY_KEY_MINT = const(9) -TX_BODY_KEY_SCRIPT_DATA_HASH = const(11) -TX_BODY_KEY_COLLATERAL_INPUTS = const(13) -TX_BODY_KEY_REQUIRED_SIGNERS = const(14) -TX_BODY_KEY_NETWORK_ID = const(15) - -POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT = 10 - - -@seed.with_keychain -async def sign_tx( - ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain -) -> CardanoSignTxFinished: - await show_transaction_signing_mode(ctx, msg.signing_mode) - - is_network_id_verifiable = await _validate_tx_signing_request(ctx, msg) - - # inputs, outputs and fee are mandatory fields, count the number of optional fields present - tx_body_map_item_count = 3 + sum( - ( - msg.ttl is not None, - msg.certificates_count > 0, - msg.withdrawals_count > 0, - msg.has_auxiliary_data, - msg.validity_interval_start is not None, - msg.minting_asset_groups_count > 0, - msg.include_network_id, - msg.script_data_hash is not None, - msg.collateral_inputs_count > 0, - msg.required_signers_count > 0, - ) - ) - - account_path_checker = AccountPathChecker() - - hash_fn = hashlib.blake2b(outlen=32) - tx_dict: HashBuilderDict[int, Any] = HashBuilderDict( - tx_body_map_item_count, INVALID_TX_SIGNING_REQUEST - ) - tx_dict.start(hash_fn) - with tx_dict: - await _process_transaction(ctx, msg, keychain, tx_dict, account_path_checker) - - tx_hash = hash_fn.digest() - await _confirm_transaction(ctx, msg, is_network_id_verifiable, tx_hash) - - try: - response_after_witness_requests = await _process_witness_requests( - ctx, - keychain, - tx_hash, - msg.witness_requests_count, - msg.signing_mode, - msg.minting_asset_groups_count > 0, - account_path_checker, - ) - - await ctx.call(response_after_witness_requests, CardanoTxHostAck) - - await ctx.call(CardanoTxBodyHash(tx_hash=tx_hash), CardanoTxHostAck) - return CardanoSignTxFinished() - - except ValueError as e: - if __debug__: - log.exception(__name__, e) - raise wire.ProcessError("Signing failed") - - -async def _validate_tx_signing_request( - ctx: wire.Context, msg: CardanoSignTxInit -) -> bool: - """Validate the data in the signing request and return whether the provided network id is verifiable.""" - if msg.fee > LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Fee is out of range!") - validate_network_info(msg.network_id, msg.protocol_magic) - - is_network_id_verifiable = _is_network_id_verifiable(msg) - if not is_network_id_verifiable: - await show_warning_tx_network_unverifiable(ctx) - - if msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: - _validate_stake_pool_registration_tx_structure(msg) - - if msg.script_data_hash is not None and msg.signing_mode not in ( - CardanoTxSigningMode.ORDINARY_TRANSACTION, - CardanoTxSigningMode.MULTISIG_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - raise INVALID_TX_SIGNING_REQUEST - - if msg.signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - # these items should be present if a Plutus script should be executed - if msg.script_data_hash is None: - await show_warning_no_script_data_hash(ctx) - if msg.collateral_inputs_count == 0: - await show_warning_no_collateral_inputs(ctx) - else: - # these items are only allowed in PLUTUS_TRANSACTION - if msg.collateral_inputs_count != 0 or msg.required_signers_count != 0: - raise INVALID_TX_SIGNING_REQUEST - - return is_network_id_verifiable - - -async def _process_transaction( - ctx: wire.Context, - msg: CardanoSignTxInit, - keychain: seed.Keychain, - tx_dict: HashBuilderDict, - account_path_checker: AccountPathChecker, -) -> None: - inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList(msg.inputs_count) - with tx_dict.add(TX_BODY_KEY_INPUTS, inputs_list): - await _process_inputs(ctx, inputs_list, msg.inputs_count, msg.signing_mode) - - outputs_list: HashBuilderList = HashBuilderList(msg.outputs_count) - with tx_dict.add(TX_BODY_KEY_OUTPUTS, outputs_list): - await _process_outputs( - ctx, - keychain, - outputs_list, - msg.outputs_count, - msg.signing_mode, - msg.protocol_magic, - msg.network_id, - account_path_checker, - ) - - tx_dict.add(TX_BODY_KEY_FEE, msg.fee) - - if msg.ttl is not None: - tx_dict.add(TX_BODY_KEY_TTL, msg.ttl) - - if msg.certificates_count > 0: - certificates_list: HashBuilderList = HashBuilderList(msg.certificates_count) - with tx_dict.add(TX_BODY_KEY_CERTIFICATES, certificates_list): - await _process_certificates( - ctx, - keychain, - certificates_list, - msg.certificates_count, - msg.signing_mode, - msg.protocol_magic, - msg.network_id, - account_path_checker, - ) - - if msg.withdrawals_count > 0: - withdrawals_dict: HashBuilderDict[bytes, int] = HashBuilderDict( - msg.withdrawals_count, INVALID_WITHDRAWAL - ) - with tx_dict.add(TX_BODY_KEY_WITHDRAWALS, withdrawals_dict): - await _process_withdrawals( - ctx, - keychain, - withdrawals_dict, - msg.withdrawals_count, - msg.signing_mode, - msg.protocol_magic, - msg.network_id, - account_path_checker, - ) - - if msg.has_auxiliary_data: - await _process_auxiliary_data( - ctx, - keychain, - tx_dict, - msg.protocol_magic, - msg.network_id, - ) - - if msg.validity_interval_start is not None: - tx_dict.add(TX_BODY_KEY_VALIDITY_INTERVAL_START, msg.validity_interval_start) - - if msg.minting_asset_groups_count > 0: - minting_dict: HashBuilderDict[bytes, HashBuilderDict] = HashBuilderDict( - msg.minting_asset_groups_count, INVALID_TOKEN_BUNDLE_MINT - ) - with tx_dict.add(TX_BODY_KEY_MINT, minting_dict): - await _process_minting(ctx, minting_dict) - - if msg.script_data_hash is not None: - await _process_script_data_hash(ctx, tx_dict, msg.script_data_hash) - - if msg.collateral_inputs_count > 0: - collateral_inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList( - msg.collateral_inputs_count - ) - with tx_dict.add(TX_BODY_KEY_COLLATERAL_INPUTS, collateral_inputs_list): - await _process_collateral_inputs( - ctx, - collateral_inputs_list, - msg.collateral_inputs_count, - ) - - if msg.required_signers_count > 0: - required_signers_list: HashBuilderList[bytes] = HashBuilderList( - msg.required_signers_count - ) - with tx_dict.add(TX_BODY_KEY_REQUIRED_SIGNERS, required_signers_list): - await _process_required_signers( - ctx, - keychain, - required_signers_list, - msg.required_signers_count, - ) - - if msg.include_network_id: - tx_dict.add(TX_BODY_KEY_NETWORK_ID, msg.network_id) - - -async def _confirm_transaction( - ctx: wire.Context, - msg: CardanoSignTxInit, - is_network_id_verifiable: bool, - tx_hash: bytes, -) -> None: - if msg.signing_mode in ( - CardanoTxSigningMode.ORDINARY_TRANSACTION, - CardanoTxSigningMode.MULTISIG_TRANSACTION, - ): - await confirm_transaction( - ctx, - msg.fee, - msg.network_id, - msg.protocol_magic, - msg.ttl, - msg.validity_interval_start, - is_network_id_verifiable, - tx_hash=None, - ) - elif msg.signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - # we display tx hash so that experienced users can compare it to the tx hash computed by - # a trusted device (in case the tx contains many items which are tedious to check one by - # one on the Trezor screen) - await confirm_transaction( - ctx, - msg.fee, - msg.network_id, - msg.protocol_magic, - msg.ttl, - msg.validity_interval_start, - is_network_id_verifiable, - tx_hash, - ) - elif msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: - await confirm_stake_pool_registration_final( - ctx, msg.protocol_magic, msg.ttl, msg.validity_interval_start - ) - else: - raise RuntimeError # we didn't cover all signing modes - - -async def _process_inputs( - ctx: wire.Context, - inputs_list: HashBuilderList[tuple[bytes, int]], - inputs_count: int, - signing_mode: CardanoTxSigningMode, -) -> None: - """Read, validate and serialize the inputs.""" - for _ in range(inputs_count): - input: CardanoTxInput = await ctx.call(CardanoTxItemAck(), CardanoTxInput) - _validate_input(input) - if signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - await confirm_input(ctx, input) - - inputs_list.append((input.prev_hash, input.prev_index)) - - -async def _process_outputs( - ctx: wire.Context, - keychain: seed.Keychain, - outputs_list: HashBuilderList, - outputs_count: int, - signing_mode: CardanoTxSigningMode, - protocol_magic: int, - network_id: int, - account_path_checker: AccountPathChecker, -) -> None: - """Read, validate, confirm and serialize the outputs, return the total non-change output amount.""" - total_amount = 0 - for _ in range(outputs_count): - output: CardanoTxOutput = await ctx.call(CardanoTxItemAck(), CardanoTxOutput) - _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, - output, - protocol_magic, - network_id, - signing_mode, - ) - - output_address = _get_output_address( - keychain, protocol_magic, network_id, output - ) - - has_datum_hash = output.datum_hash is not None - output_list: HashBuilderList = HashBuilderList(2 + int(has_datum_hash)) - with outputs_list.append(output_list): - output_list.append(output_address) - if output.asset_groups_count == 0: - # output structure is: [address, amount, datum_hash?] - output_list.append(output.amount) - else: - # output structure is: [address, [amount, asset_groups], datum_hash?] - output_value_list: HashBuilderList = HashBuilderList(2) - with output_list.append(output_value_list): - output_value_list.append(output.amount) - asset_groups_dict: HashBuilderDict[ - bytes, HashBuilderDict[bytes, int] - ] = HashBuilderDict( - output.asset_groups_count, INVALID_TOKEN_BUNDLE_OUTPUT - ) - with output_value_list.append(asset_groups_dict): - await _process_asset_groups( - ctx, - asset_groups_dict, - output.asset_groups_count, - should_show_output, - ) - if has_datum_hash: - output_list.append(output.datum_hash) - - total_amount += output.amount - - if total_amount > LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Total transaction amount is out of range!") - - -async def _process_asset_groups( - ctx: wire.Context, - asset_groups_dict: HashBuilderDict[bytes, HashBuilderDict[bytes, int]], - asset_groups_count: int, - should_show_tokens: bool, -) -> None: - """Read, validate and serialize the asset groups of an output.""" - for _ in range(asset_groups_count): - asset_group: CardanoAssetGroup = await ctx.call( - CardanoTxItemAck(), CardanoAssetGroup - ) - _validate_asset_group(asset_group) - - tokens: HashBuilderDict[bytes, int] = HashBuilderDict( - asset_group.tokens_count, INVALID_TOKEN_BUNDLE_OUTPUT - ) - with asset_groups_dict.add(asset_group.policy_id, tokens): - await _process_tokens( - ctx, - tokens, - asset_group.policy_id, - asset_group.tokens_count, - should_show_tokens, - ) - - -async def _process_tokens( - ctx: wire.Context, - tokens_dict: HashBuilderDict[bytes, int], - policy_id: bytes, - tokens_count: int, - should_show_tokens: bool, -) -> None: - """Read, validate, confirm and serialize the tokens of an asset group.""" - for _ in range(tokens_count): - token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) - _validate_token(token) - if should_show_tokens: - await confirm_sending_token(ctx, policy_id, token) - - assert token.amount is not None # _validate_token - tokens_dict.add(token.asset_name_bytes, token.amount) - - -async def _process_certificates( - ctx: wire.Context, - keychain: seed.Keychain, - certificates_list: HashBuilderList, - certificates_count: int, - signing_mode: CardanoTxSigningMode, - protocol_magic: int, - network_id: int, - account_path_checker: AccountPathChecker, -) -> None: - """Read, validate, confirm and serialize the certificates.""" - for _ in range(certificates_count): - certificate: CardanoTxCertificate = await ctx.call( - CardanoTxItemAck(), CardanoTxCertificate - ) - validate_certificate( - certificate, signing_mode, protocol_magic, network_id, account_path_checker - ) - await _show_certificate(ctx, certificate, signing_mode, network_id) - - if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: - pool_parameters = certificate.pool_parameters - assert pool_parameters is not None # validate_certificate - - pool_items_list: HashBuilderList = HashBuilderList( - POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT - ) - with certificates_list.append(pool_items_list): - for item in cborize_initial_pool_registration_certificate_fields( - certificate - ): - pool_items_list.append(item) - - pool_owners_list: HashBuilderList[bytes] = HashBuilderList( - pool_parameters.owners_count - ) - with pool_items_list.append(pool_owners_list): - await _process_pool_owners( - ctx, - keychain, - pool_owners_list, - pool_parameters.owners_count, - protocol_magic, - network_id, - account_path_checker, - ) - - relays_list: HashBuilderList[cbor.CborSequence] = HashBuilderList( - pool_parameters.relays_count - ) - with pool_items_list.append(relays_list): - await _process_pool_relays( - ctx, relays_list, pool_parameters.relays_count - ) - - pool_items_list.append(cborize_pool_metadata(pool_parameters.metadata)) - else: - certificates_list.append(cborize_certificate(keychain, certificate)) - - -async def _process_pool_owners( - ctx: wire.Context, - keychain: seed.Keychain, - pool_owners_list: HashBuilderList[bytes], - owners_count: int, - protocol_magic: int, - network_id: int, - account_path_checker: AccountPathChecker, -) -> None: - owners_as_path_count = 0 - for _ in range(owners_count): - owner: CardanoPoolOwner = await ctx.call(CardanoTxItemAck(), CardanoPoolOwner) - validate_pool_owner(owner, account_path_checker) - await _show_pool_owner(ctx, keychain, owner, protocol_magic, network_id) - - pool_owners_list.append(cborize_pool_owner(keychain, owner)) - - if owner.staking_key_path: - owners_as_path_count += 1 - - assert_certificate_cond(owners_as_path_count == 1) - - -async def _process_pool_relays( - ctx: wire.Context, - relays_list: HashBuilderList[cbor.CborSequence], - relays_count: int, -) -> None: - for _ in range(relays_count): - relay: CardanoPoolRelayParameters = await ctx.call( - CardanoTxItemAck(), CardanoPoolRelayParameters - ) - validate_pool_relay(relay) - relays_list.append(cborize_pool_relay(relay)) - - -async def _process_withdrawals( - ctx: wire.Context, - keychain: seed.Keychain, - withdrawals_dict: HashBuilderDict[bytes, int], - withdrawals_count: int, - signing_mode: CardanoTxSigningMode, - protocol_magic: int, - network_id: int, - account_path_checker: AccountPathChecker, -) -> None: - """Read, validate, confirm and serialize the withdrawals.""" - if withdrawals_count == 0: - return - - for _ in range(withdrawals_count): - withdrawal: CardanoTxWithdrawal = await ctx.call( - CardanoTxItemAck(), CardanoTxWithdrawal - ) - _validate_withdrawal( - withdrawal, - signing_mode, - account_path_checker, - ) - reward_address_bytes = _derive_withdrawal_reward_address_bytes( - keychain, withdrawal, protocol_magic, network_id - ) - - await confirm_withdrawal(ctx, withdrawal, reward_address_bytes, network_id) - - withdrawals_dict.add(reward_address_bytes, withdrawal.amount) - - -async def _process_auxiliary_data( - ctx: wire.Context, - keychain: seed.Keychain, - tx_body_builder_dict: HashBuilderDict, - protocol_magic: int, - network_id: int, -) -> None: - """Read, validate, confirm and serialize the auxiliary data.""" - auxiliary_data: CardanoTxAuxiliaryData = await ctx.call( - CardanoTxItemAck(), CardanoTxAuxiliaryData - ) - validate_auxiliary_data(auxiliary_data) - - ( - auxiliary_data_hash, - auxiliary_data_supplement, - ) = get_auxiliary_data_hash_and_supplement( - keychain, auxiliary_data, protocol_magic, network_id - ) - - await show_auxiliary_data( - ctx, - keychain, - auxiliary_data_hash, - auxiliary_data.catalyst_registration_parameters, - protocol_magic, - network_id, - ) - - tx_body_builder_dict.add(TX_BODY_KEY_AUXILIARY_DATA, auxiliary_data_hash) - - await ctx.call(auxiliary_data_supplement, CardanoTxHostAck) - - -async def _process_minting( - ctx: wire.Context, minting_dict: HashBuilderDict[bytes, HashBuilderDict] -) -> None: - """Read, validate and serialize the asset groups of token minting.""" - token_minting: CardanoTxMint = await ctx.call(CardanoTxItemAck(), CardanoTxMint) - - await show_warning_tx_contains_mint(ctx) - - for _ in range(token_minting.asset_groups_count): - asset_group: CardanoAssetGroup = await ctx.call( - CardanoTxItemAck(), CardanoAssetGroup - ) - _validate_asset_group(asset_group, is_mint=True) - - tokens: HashBuilderDict[bytes, int] = HashBuilderDict( - asset_group.tokens_count, INVALID_TOKEN_BUNDLE_MINT - ) - with minting_dict.add(asset_group.policy_id, tokens): - await _process_minting_tokens( - ctx, - tokens, - asset_group.policy_id, - asset_group.tokens_count, - ) - - -async def _process_minting_tokens( - ctx: wire.Context, - tokens: HashBuilderDict[bytes, int], - policy_id: bytes, - tokens_count: int, -) -> None: - """Read, validate, confirm and serialize the tokens of an asset group.""" - for _ in range(tokens_count): - token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) - _validate_token(token, is_mint=True) - await confirm_token_minting(ctx, policy_id, token) - - assert token.mint_amount is not None # _validate_token - tokens.add(token.asset_name_bytes, token.mint_amount) - - -async def _process_script_data_hash( - ctx: wire.Context, - tx_body_builder_dict: HashBuilderDict, - script_data_hash: bytes, -) -> None: - """Validate, confirm and serialize the script data hash.""" - _validate_script_data_hash(script_data_hash) - - await confirm_script_data_hash(ctx, script_data_hash) - - tx_body_builder_dict.add(TX_BODY_KEY_SCRIPT_DATA_HASH, script_data_hash) - - -async def _process_collateral_inputs( - ctx: wire.Context, - collateral_inputs_list: HashBuilderList[tuple[bytes, int]], - collateral_inputs_count: int, -) -> None: - """Read, validate, show and serialize the collateral inputs.""" - for _ in range(collateral_inputs_count): - collateral_input: CardanoTxCollateralInput = await ctx.call( - CardanoTxItemAck(), CardanoTxCollateralInput - ) - _validate_collateral_input(collateral_input) - await confirm_collateral_input(ctx, collateral_input) - - collateral_inputs_list.append( - (collateral_input.prev_hash, collateral_input.prev_index) - ) - - -async def _process_required_signers( - ctx: wire.Context, - keychain: seed.Keychain, - required_signers_list: HashBuilderList[bytes], - required_signers_count: int, -) -> None: - """Read, validate, show and serialize the required signers.""" - for _ in range(required_signers_count): - required_signer: CardanoTxRequiredSigner = await ctx.call( - CardanoTxItemAck(), CardanoTxRequiredSigner - ) - _validate_required_signer(required_signer) - await confirm_required_signer(ctx, required_signer) - - key_hash = required_signer.key_hash or get_public_key_hash( - keychain, required_signer.key_path - ) - - required_signers_list.append(key_hash) - - -async def _process_witness_requests( - ctx: wire.Context, - keychain: seed.Keychain, - tx_hash: bytes, - witness_requests_count: int, - signing_mode: CardanoTxSigningMode, - transaction_has_token_minting: bool, - account_path_checker: AccountPathChecker, -) -> CardanoTxResponseType: - response: CardanoTxResponseType = CardanoTxItemAck() - - for _ in range(witness_requests_count): - witness_request = await ctx.call(response, CardanoTxWitnessRequest) - _validate_witness_request( - witness_request, - signing_mode, - transaction_has_token_minting, - account_path_checker, - ) - path = witness_request.path - await _show_witness_request(ctx, path, signing_mode) - if is_byron_path(path): - response = _get_byron_witness(keychain, path, tx_hash) - else: - response = _get_shelley_witness(keychain, path, tx_hash) - - return response - - -def _get_byron_witness( - keychain: seed.Keychain, - path: list[int], - tx_hash: bytes, -) -> CardanoTxWitnessResponse: - node = keychain.derive(path) - return CardanoTxWitnessResponse( - type=CardanoTxWitnessType.BYRON_WITNESS, - pub_key=derive_public_key(keychain, path), - signature=_sign_tx_hash(keychain, tx_hash, path), - chain_code=node.chain_code(), - ) - - -def _get_shelley_witness( - keychain: seed.Keychain, - path: list[int], - tx_hash: bytes, -) -> CardanoTxWitnessResponse: - return CardanoTxWitnessResponse( - type=CardanoTxWitnessType.SHELLEY_WITNESS, - pub_key=derive_public_key(keychain, path), - signature=_sign_tx_hash(keychain, tx_hash, path), - ) - - -def validate_network_info(network_id: int, protocol_magic: int) -> None: - """ - We are only concerned about checking that both network_id and protocol_magic - belong to the mainnet or that both belong to a testnet. We don't need to check for - consistency between various testnets (at least for now). - """ - is_mainnet_network_id = network_ids.is_mainnet(network_id) - is_mainnet_protocol_magic = protocol_magics.is_mainnet(protocol_magic) - - if is_mainnet_network_id != is_mainnet_protocol_magic: - raise wire.ProcessError("Invalid network id/protocol magic combination!") - - -def _validate_stake_pool_registration_tx_structure(msg: CardanoSignTxInit) -> None: - """Ensure that there is exactly one certificate, which is stake pool registration, and no withdrawals""" - if ( - msg.certificates_count != 1 - or msg.signing_mode != CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER - or msg.withdrawals_count != 0 - or msg.minting_asset_groups_count != 0 - ): - raise INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE - - -def _validate_input(input: CardanoTxInput) -> None: - if len(input.prev_hash) != INPUT_PREV_HASH_SIZE: - raise INVALID_INPUT - - -def _validate_output( - output: CardanoTxOutput, - signing_mode: CardanoTxSigningMode, - protocol_magic: int, - network_id: int, - account_path_checker: AccountPathChecker, -) -> None: - if output.address_parameters and output.address is not None: - raise INVALID_OUTPUT - - if address_parameters := output.address_parameters: - if signing_mode not in ( - CardanoTxSigningMode.ORDINARY_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - # Change outputs are allowed only in ORDINARY_TRANSACTION. - # In PLUTUS_TRANSACTION, we display device-owned outputs similarly to change outputs. - raise INVALID_OUTPUT - - validate_output_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: - raise INVALID_OUTPUT - - if output.datum_hash is not None: - if signing_mode not in ( - CardanoTxSigningMode.ORDINARY_TRANSACTION, - CardanoTxSigningMode.MULTISIG_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - raise INVALID_OUTPUT - if len(output.datum_hash) != OUTPUT_DATUM_HASH_SIZE: - raise INVALID_OUTPUT_DATUM_HASH - address_type = _get_output_address_type(output) - if address_type not in ADDRESS_TYPES_PAYMENT_SCRIPT: - raise INVALID_OUTPUT - - account_path_checker.add_output(output) - - -def _should_show_output( - output: CardanoTxOutput, - signing_mode: CardanoTxSigningMode, -) -> bool: - if output.datum_hash: - # none of the reasons for hiding below should be reachable when datum hash - # is present, but let's make sure - return True - - address_type = _get_output_address_type(output) - if output.datum_hash is None and address_type in ADDRESS_TYPES_PAYMENT_SCRIPT: - # Plutus script address without a datum hash is unspendable, we must show a warning - return True - - if signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - # In Plutus transactions, all outputs need to be shown (even device-owned), because they - # might influence the script evaluation. - return True - - 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, - output: CardanoTxOutput, - protocol_magic: int, - network_id: int, - signing_mode: CardanoTxSigningMode, -) -> None: - if output.datum_hash: - await show_warning_tx_output_contains_datum_hash(ctx, output.datum_hash) - - address_type = _get_output_address_type(output) - if output.datum_hash is None and address_type in ADDRESS_TYPES_PAYMENT_SCRIPT: - await show_warning_tx_output_no_datum_hash(ctx) - - if output.asset_groups_count > 0: - await show_warning_tx_output_contains_tokens(ctx) - - is_change_output = False - if address_parameters := output.address_parameters: - address = derive_human_readable_address( - keychain, address_parameters, protocol_magic, network_id - ) - payment_credential = Credential.payment_credential(address_parameters) - stake_credential = Credential.stake_credential(address_parameters) - - if signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - show_both_credentials = should_show_address_credentials(address_parameters) - # In ORDINARY_TRANSACTION, change outputs with matching payment and staking paths can - # be hidden, but we need to show them in PLUTUS_TRANSACTION because of the script - # evaluation. We at least hide the staking path if it matches the payment path. - await show_device_owned_output_credentials( - ctx, - payment_credential, - stake_credential, - show_both_credentials, - ) - else: - is_change_output = True - await show_change_output_credentials( - ctx, - payment_credential, - stake_credential, - ) - else: - assert output.address is not None # _validate_output - address = output.address - - await confirm_sending(ctx, output.amount, address, is_change_output, network_id) - - -def _validate_asset_group( - asset_group: CardanoAssetGroup, is_mint: bool = False -) -> None: - INVALID_TOKEN_BUNDLE = ( - INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT - ) - - if len(asset_group.policy_id) != MINTING_POLICY_ID_LENGTH: - raise INVALID_TOKEN_BUNDLE - if asset_group.tokens_count == 0: - raise INVALID_TOKEN_BUNDLE - - -def _validate_token(token: CardanoToken, is_mint: bool = False) -> None: - INVALID_TOKEN_BUNDLE = ( - INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT - ) - - if is_mint: - if token.mint_amount is None or token.amount is not None: - raise INVALID_TOKEN_BUNDLE - else: - if token.amount is None or token.mint_amount is not None: - raise INVALID_TOKEN_BUNDLE - - if len(token.asset_name_bytes) > MAX_ASSET_NAME_LENGTH: - raise INVALID_TOKEN_BUNDLE - - -async def _show_certificate( - ctx: wire.Context, - certificate: CardanoTxCertificate, - signing_mode: CardanoTxSigningMode, - network_id: int, -) -> None: - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: - assert certificate.path # validate_certificate - await _fail_or_warn_if_invalid_path( - ctx, SCHEMA_STAKING, certificate.path, CERTIFICATE_PATH_NAME - ) - await confirm_certificate(ctx, certificate) - elif signing_mode in ( - CardanoTxSigningMode.MULTISIG_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - await confirm_certificate(ctx, certificate) - elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: - await _show_stake_pool_registration_certificate(ctx, certificate, network_id) - else: - raise RuntimeError # we didn't cover all signing modes - - -def _validate_withdrawal( - withdrawal: CardanoTxWithdrawal, - signing_mode: CardanoTxSigningMode, - account_path_checker: AccountPathChecker, -) -> None: - validate_stake_credential( - withdrawal.path, - withdrawal.script_hash, - withdrawal.key_hash, - signing_mode, - INVALID_WITHDRAWAL, - ) - - if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: - raise INVALID_WITHDRAWAL - - account_path_checker.add_withdrawal(withdrawal) - - -def _validate_script_data_hash(script_data_hash: bytes) -> None: - if len(script_data_hash) != SCRIPT_DATA_HASH_SIZE: - raise INVALID_SCRIPT_DATA_HASH - - -def _validate_collateral_input(collateral_input: CardanoTxCollateralInput) -> None: - if len(collateral_input.prev_hash) != INPUT_PREV_HASH_SIZE: - raise INVALID_COLLATERAL_INPUT - - -def _validate_required_signer(required_signer: CardanoTxRequiredSigner) -> None: - if required_signer.key_hash and required_signer.key_path: - raise INVALID_REQUIRED_SIGNER - - if required_signer.key_hash: - if len(required_signer.key_hash) != ADDRESS_KEY_HASH_SIZE: - raise INVALID_REQUIRED_SIGNER - elif required_signer.key_path: - if not ( - is_shelley_path(required_signer.key_path) - or is_multisig_path(required_signer.key_path) - or is_minting_path(required_signer.key_path) - ): - raise INVALID_REQUIRED_SIGNER - else: - raise INVALID_REQUIRED_SIGNER - - -def _derive_withdrawal_reward_address_bytes( - keychain: seed.Keychain, - withdrawal: CardanoTxWithdrawal, - protocol_magic: int, - network_id: int, -) -> bytes: - reward_address_type = ( - CardanoAddressType.REWARD - if withdrawal.path or withdrawal.key_hash - else CardanoAddressType.REWARD_SCRIPT - ) - return derive_address_bytes( - keychain, - CardanoAddressParametersType( - address_type=reward_address_type, - address_n_staking=withdrawal.path, - staking_key_hash=withdrawal.key_hash, - script_staking_hash=withdrawal.script_hash, - ), - protocol_magic, - network_id, - ) - - -def _get_output_address( - keychain: seed.Keychain, - protocol_magic: int, - network_id: int, - output: CardanoTxOutput, -) -> bytes: - if output.address_parameters: - return derive_address_bytes( - keychain, output.address_parameters, protocol_magic, network_id - ) - else: - assert output.address is not None # _validate_output - return get_address_bytes_unsafe(output.address) - - -def _get_output_address_type(output: CardanoTxOutput) -> CardanoAddressType: - if output.address_parameters: - return output.address_parameters.address_type - assert output.address is not None # _validate_output - return get_address_type(get_address_bytes_unsafe(output.address)) - - -def _sign_tx_hash( - keychain: seed.Keychain, tx_body_hash: bytes, path: list[int] -) -> bytes: - node = keychain.derive(path) - return ed25519.sign_ext(node.private_key(), node.private_key_ext(), tx_body_hash) - - -async def _show_stake_pool_registration_certificate( - ctx: wire.Context, - stake_pool_registration_certificate: CardanoTxCertificate, - network_id: int, -) -> None: - pool_parameters = stake_pool_registration_certificate.pool_parameters - # _validate_stake_pool_registration_tx_structure ensures that there is only one - # certificate, and validate_certificate ensures that the structure is valid - assert pool_parameters is not None - - # display the transaction (certificate) in UI - await confirm_stake_pool_parameters(ctx, pool_parameters, network_id) - - await confirm_stake_pool_metadata(ctx, pool_parameters.metadata) - - -async def _show_pool_owner( - 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( - ctx, - SCHEMA_STAKING, - owner.staking_key_path, - POOL_OWNER_STAKING_PATH_NAME, - ) - - await confirm_stake_pool_owner(ctx, keychain, owner, protocol_magic, network_id) - - -def _validate_witness_request( - witness_request: CardanoTxWitnessRequest, - signing_mode: CardanoTxSigningMode, - transaction_has_token_minting: bool, - account_path_checker: AccountPathChecker, -) -> None: - # further witness path validation happens in _show_witness_request - is_minting = SCHEMA_MINT.match(witness_request.path) - - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: - if not ( - is_byron_path(witness_request.path) - or is_shelley_path(witness_request.path) - or is_minting - ): - raise INVALID_WITNESS_REQUEST - if is_minting and not transaction_has_token_minting: - raise INVALID_WITNESS_REQUEST - elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: - _ensure_only_staking_witnesses(witness_request) - elif signing_mode == CardanoTxSigningMode.MULTISIG_TRANSACTION: - if not is_multisig_path(witness_request.path) and not is_minting: - raise INVALID_WITNESS_REQUEST - if is_minting and not transaction_has_token_minting: - raise INVALID_WITNESS_REQUEST - elif signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: - if not ( - is_shelley_path(witness_request.path) - or is_multisig_path(witness_request.path) - or is_minting - ): - raise INVALID_WITNESS_REQUEST - # in PLUTUS_TRANSACTION, we allow minting witnesses even when transaction - # doesn't have token minting - else: - raise RuntimeError # we didn't cover all signing modes - - account_path_checker.add_witness_request(witness_request) - - -def _ensure_only_staking_witnesses(witness: CardanoTxWitnessRequest) -> None: - """ - We have a separate tx signing flow for stake pool registration because it's a - transaction where the witnessable entries (i.e. inputs, withdrawals, etc.) - in the transaction are not supposed to be controlled by the HW wallet, which - means the user is vulnerable to unknowingly supplying a witness for an UTXO - or other tx entry they think is external, resulting in the co-signers - gaining control over their funds (Something SLIP-0019 is dealing with for - BTC but no similar standard is currently available for Cardano). Hence we - completely forbid witnessing inputs and other entries of the transaction - except the stake pool certificate itself and we provide a witness only to the - user's staking key in the list of pool owners. - """ - if not SCHEMA_STAKING_ANY_ACCOUNT.match(witness.path): - raise INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES - - -async def _show_witness_request( - ctx: wire.Context, - witness_path: list[int], - signing_mode: CardanoTxSigningMode, -) -> None: - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: - # In an ordinary transaction we only allow payment, staking or minting paths. - # If the path is an unusual payment or staking path, we either fail or show the path to the user - # depending on Trezor's configuration. If it's a minting path, we always show it. - is_payment = SCHEMA_PAYMENT.match(witness_path) - is_staking = SCHEMA_STAKING.match(witness_path) - is_minting = SCHEMA_MINT.match(witness_path) - - if is_minting: - await confirm_witness_request(ctx, witness_path) - elif not is_payment and not is_staking: - await _fail_or_warn_path(ctx, witness_path, WITNESS_PATH_NAME) - elif signing_mode in ( - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, - CardanoTxSigningMode.MULTISIG_TRANSACTION, - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ): - await confirm_witness_request(ctx, witness_path) - else: - raise RuntimeError # we didn't cover all signing modes - - -def _is_network_id_verifiable(msg: CardanoSignTxInit) -> bool: - """ - Checks whether there is at least one element that contains - information about network ID, otherwise Trezor cannot - guarantee that the tx is actually meant for the given network. - - Note: Shelley addresses contain network id. The intended network - of Byron addresses can be determined based on whether they - contain the protocol magic. These checks are performed during - address validation. - """ - return ( - msg.include_network_id - or msg.outputs_count != 0 - or msg.withdrawals_count != 0 - or msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER - ) - - -async def _fail_or_warn_if_invalid_path( - ctx: wire.Context, schema: PathSchema, path: list[int], path_name: str -) -> None: - if not schema.match(path): - await _fail_or_warn_path(ctx, path, path_name) - - -async def _fail_or_warn_path( - ctx: wire.Context, path: list[int], path_name: str -) -> None: - if safety_checks.is_strict(): - raise wire.DataError(f"Invalid {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(f"Invalid {CHANGE_OUTPUT_PATH_NAME.lower()}") - - if Credential.stake_credential(address_parameters).is_unusual_path: - raise wire.DataError(f"Invalid {CHANGE_OUTPUT_STAKING_PATH_NAME.lower()}") diff --git a/core/src/apps/cardano/sign_tx/__init__.py b/core/src/apps/cardano/sign_tx/__init__.py new file mode 100644 index 000000000..cf4eda083 --- /dev/null +++ b/core/src/apps/cardano/sign_tx/__init__.py @@ -0,0 +1,44 @@ +from typing import Type + +from trezor import log, wire +from trezor.enums import CardanoTxSigningMode +from trezor.messages import CardanoSignTxFinished, CardanoSignTxInit + +from .. import seed +from .signer import Signer + + +@seed.with_keychain +async def sign_tx( + ctx: wire.Context, msg: messages.CardanoSignTxInit, keychain: seed.Keychain +) -> messages.CardanoSignTxFinished: + signer_type: Type[Signer] + if msg.signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + from .ordinary_signer import OrdinarySigner + + signer_type = OrdinarySigner + elif msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + from .pool_owner_signer import PoolOwnerSigner + + signer_type = PoolOwnerSigner + elif msg.signing_mode == CardanoTxSigningMode.MULTISIG_TRANSACTION: + from .multisig_signer import MultisigSigner + + signer_type = MultisigSigner + elif msg.signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: + from .plutus_signer import PlutusSigner + + signer_type = PlutusSigner + else: + raise RuntimeError # should be unreachable + + signer = signer_type(ctx, msg, keychain) + + try: + await signer.sign() + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Signing failed") + + return CardanoSignTxFinished() diff --git a/core/src/apps/cardano/sign_tx/multisig_signer.py b/core/src/apps/cardano/sign_tx/multisig_signer.py new file mode 100644 index 000000000..965d70455 --- /dev/null +++ b/core/src/apps/cardano/sign_tx/multisig_signer.py @@ -0,0 +1,88 @@ +from trezor import wire +from trezor.enums import CardanoCertificateType +from trezor.messages import ( + CardanoSignTxInit, + CardanoTxCertificate, + CardanoTxOutput, + CardanoTxWithdrawal, + CardanoTxWitnessRequest, +) + +from .. import seed +from ..helpers import ( + INVALID_CERTIFICATE, + INVALID_OUTPUT, + INVALID_TX_SIGNING_REQUEST, + INVALID_WITHDRAWAL, + INVALID_WITNESS_REQUEST, +) +from ..helpers.paths import SCHEMA_MINT +from ..layout import show_multisig_transaction +from ..seed import is_multisig_path +from .signer import Signer + + +class MultisigSigner(Signer): + """ + The multisig signing mode only allows signing with multisig (and minting) keys. + """ + + def __init__( + self, ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain + ) -> None: + super().__init__(ctx, msg, keychain) + + def _validate_tx_signing_request(self) -> None: + super()._validate_tx_signing_request() + if ( + self.msg.collateral_inputs_count != 0 + or self.msg.required_signers_count != 0 + ): + raise INVALID_TX_SIGNING_REQUEST + + async def _show_tx_signing_request(self) -> None: + await show_multisig_transaction(self.ctx) + await super()._show_tx_signing_request() + + async def _confirm_tx(self, tx_hash: bytes) -> None: + # super() omitted intentionally + is_network_id_verifiable = self._is_network_id_verifiable() + await layout.confirm_tx( + self.ctx, + self.msg.fee, + self.msg.network_id, + self.msg.protocol_magic, + self.msg.ttl, + self.msg.validity_interval_start, + is_network_id_verifiable, + tx_hash=None, + ) + + def _validate_output(self, output: messages.CardanoTxOutput) -> None: + super()._validate_output(output) + if output.address_parameters is not None: + raise INVALID_OUTPUT + + def _validate_certificate(self, certificate: CardanoTxCertificate) -> None: + super()._validate_certificate(certificate) + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + raise INVALID_CERTIFICATE + if certificate.path or certificate.key_hash: + raise INVALID_CERTIFICATE + + def _validate_withdrawal(self, withdrawal: CardanoTxWithdrawal) -> None: + super()._validate_withdrawal(withdrawal) + if withdrawal.path or withdrawal.key_hash: + raise INVALID_WITHDRAWAL + + def _validate_witness_request( + self, witness_request: CardanoTxWitnessRequest + ) -> None: + super()._validate_witness_request(witness_request) + is_minting = SCHEMA_MINT.match(witness_request.path) + transaction_has_token_minting = self.msg.minting_asset_groups_count > 0 + + if not is_multisig_path(witness_request.path) and not is_minting: + raise INVALID_WITNESS_REQUEST + if is_minting and not transaction_has_token_minting: + raise INVALID_WITNESS_REQUEST diff --git a/core/src/apps/cardano/sign_tx/ordinary_signer.py b/core/src/apps/cardano/sign_tx/ordinary_signer.py new file mode 100644 index 000000000..3fc74ceab --- /dev/null +++ b/core/src/apps/cardano/sign_tx/ordinary_signer.py @@ -0,0 +1,102 @@ +from trezor import wire +from trezor.enums import CardanoCertificateType +from trezor.messages import ( + CardanoSignTxInit, + CardanoTxCertificate, + CardanoTxWithdrawal, + CardanoTxWitnessRequest, +) + +from .. import seed +from ..helpers import ( + INVALID_CERTIFICATE, + INVALID_TX_SIGNING_REQUEST, + INVALID_WITHDRAWAL, + INVALID_WITNESS_REQUEST, +) +from ..helpers.paths import ( + SCHEMA_MINT, + SCHEMA_PAYMENT, + SCHEMA_STAKING, + WITNESS_PATH_NAME, +) +from ..layout import confirm_witness_request +from ..seed import is_byron_path, is_shelley_path +from .signer import Signer + + +class OrdinarySigner(Signer): + """ + Ordinary txs are meant for usual actions, such as sending funds from addresses + controlled by 1852' keys, dealing with staking and minting/burning tokens. + """ + + def __init__( + self, ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain + ) -> None: + super().__init__(ctx, msg, keychain) + + def _validate_tx_signing_request(self) -> None: + super()._validate_tx_signing_request() + if ( + self.msg.collateral_inputs_count != 0 + or self.msg.required_signers_count != 0 + ): + raise INVALID_TX_SIGNING_REQUEST + + async def _confirm_tx(self, tx_hash: bytes) -> None: + # super() omitted intentionally + is_network_id_verifiable = self._is_network_id_verifiable() + await layout.confirm_tx( + self.ctx, + self.msg.fee, + self.msg.network_id, + self.msg.protocol_magic, + self.msg.ttl, + self.msg.validity_interval_start, + is_network_id_verifiable, + tx_hash=None, + ) + + def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> None: + super()._validate_certificate(certificate) + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + raise INVALID_CERTIFICATE + if certificate.script_hash or certificate.key_hash: + raise INVALID_CERTIFICATE + + def _validate_withdrawal(self, withdrawal: CardanoTxWithdrawal) -> None: + super()._validate_withdrawal(withdrawal) + if withdrawal.script_hash or withdrawal.key_hash: + raise INVALID_WITHDRAWAL + + def _validate_witness_request( + self, witness_request: CardanoTxWitnessRequest + ) -> None: + super()._validate_witness_request(witness_request) + is_minting = SCHEMA_MINT.match(witness_request.path) + transaction_has_token_minting = self.msg.minting_asset_groups_count > 0 + + if not ( + is_byron_path(witness_request.path) + or is_shelley_path(witness_request.path) + or is_minting + ): + raise INVALID_WITNESS_REQUEST + if is_minting and not transaction_has_token_minting: + raise INVALID_WITNESS_REQUEST + + async def _show_witness_request(self, witness_path: list[int]) -> None: + # super() omitted intentionally + # We only allow payment, staking or minting paths. + # If the path is an unusual payment or staking path, we either fail or show the + # path to the user depending on Trezor's configuration. If it's a minting path, + # we always show it. + is_payment = SCHEMA_PAYMENT.match(witness_path) + is_staking = SCHEMA_STAKING.match(witness_path) + is_minting = SCHEMA_MINT.match(witness_path) + + if is_minting: + await confirm_witness_request(self.ctx, witness_path) + elif not is_payment and not is_staking: + await self._fail_or_warn_path(witness_path, WITNESS_PATH_NAME) diff --git a/core/src/apps/cardano/sign_tx/plutus_signer.py b/core/src/apps/cardano/sign_tx/plutus_signer.py new file mode 100644 index 000000000..3aeef3ebb --- /dev/null +++ b/core/src/apps/cardano/sign_tx/plutus_signer.py @@ -0,0 +1,112 @@ +from trezor import wire +from trezor.enums import CardanoCertificateType +from trezor.messages import ( + CardanoAddressParametersType, + CardanoSignTxInit, + CardanoTxCertificate, + CardanoTxInput, + CardanoTxOutput, + CardanoTxWitnessRequest, +) + +from .. import seed +from ..helpers import INVALID_CERTIFICATE, INVALID_WITNESS_REQUEST +from ..helpers.credential import Credential, should_show_address_credentials +from ..helpers.paths import SCHEMA_MINT +from ..layout import ( + confirm_input, + confirm_transaction, + show_device_owned_output_credentials, + show_plutus_transaction, + show_warning_no_collateral_inputs, + show_warning_no_script_data_hash, +) +from ..seed import is_multisig_path, is_shelley_path +from .signer import Signer + + +class PlutusSigner(Signer): + """ + The Plutus siging mode is meant for txs that involve Plutus script evaluation. The + validation rules are less strict, but more tx items/warnings are shown to the user. + """ + + def __init__( + self, ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain + ) -> None: + super().__init__(ctx, msg, keychain) + + async def _show_tx_signing_request(self) -> None: + await show_plutus_transaction(self.ctx) + await super()._show_tx_signing_request() + # These items should be present if a Plutus script is to be executed. + if self.msg.script_data_hash is None: + await show_warning_no_script_data_hash(self.ctx) + if self.msg.collateral_inputs_count == 0: + await show_warning_no_collateral_inputs(self.ctx) + + async def _confirm_transaction(self, tx_hash: bytes) -> None: + # super() omitted intentionally + # We display tx hash so that experienced users can compare it to the tx hash + # computed by a trusted device (in case the tx contains many items which are + # tedious to check one by one on the Trezor screen). + is_network_id_verifiable = self._is_network_id_verifiable() + await confirm_transaction( + self.ctx, + self.msg.fee, + self.msg.network_id, + self.msg.protocol_magic, + self.msg.ttl, + self.msg.validity_interval_start, + is_network_id_verifiable, + tx_hash, + ) + + async def _show_input(self, input: CardanoTxInput) -> None: + # super() omitted intentionally + # The inputs are not interchangeable (because of datums), so we must show them. + await confirm_input(self.ctx, input) + + async def _show_output_credentials( + self, address_parameters: CardanoAddressParametersType + ) -> None: + # In ordinary txs, change outputs with matching payment and staking paths can be + # hidden, but we need to show them in Plutus txs because of the script + # evaluation. We at least hide the staking path if it matches the payment path. + show_both_credentials = should_show_address_credentials(address_parameters) + await show_device_owned_output_credentials( + self.ctx, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), + show_both_credentials, + ) + + def _should_show_output(self, output: CardanoTxOutput) -> bool: + # super() omitted intentionally + # All outputs need to be shown (even device-owned), because they might influence + # the script evaluation. + return True + + def _is_change_output(self, output: CardanoTxOutput) -> bool: + # super() omitted intentionally + # In Plutus txs, we don't call device-owned outputs "change" outputs. + return False + + def _validate_certificate(self, certificate: CardanoTxCertificate) -> None: + super()._validate_certificate(certificate) + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + raise INVALID_CERTIFICATE + + def _validate_witness_request( + self, witness_request: CardanoTxWitnessRequest + ) -> None: + super()._validate_witness_request(witness_request) + is_minting = SCHEMA_MINT.match(witness_request.path) + + # In Plutus txs, we allow minting witnesses even when the tx doesn't have token minting. + if not ( + is_shelley_path(witness_request.path) + or is_multisig_path(witness_request.path) + or is_minting + ): + raise INVALID_WITNESS_REQUEST diff --git a/core/src/apps/cardano/sign_tx/pool_owner_signer.py b/core/src/apps/cardano/sign_tx/pool_owner_signer.py new file mode 100644 index 000000000..413ecc0d6 --- /dev/null +++ b/core/src/apps/cardano/sign_tx/pool_owner_signer.py @@ -0,0 +1,92 @@ +from trezor import wire +from trezor.enums import CardanoCertificateType +from trezor.messages import ( + CardanoSignTxInit, + CardanoTxCertificate, + CardanoTxOutput, + CardanoTxWitnessRequest, +) + +from .. import seed +from ..helpers import ( + INVALID_CERTIFICATE, + INVALID_OUTPUT, + INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE, + INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES, + INVALID_TX_SIGNING_REQUEST, +) +from ..helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT +from ..layout import confirm_stake_pool_registration_final +from .signer import Signer + + +class PoolOwnerSigner(Signer): + """ + We have a separate tx signing flow for stake pool registration because it's a + transaction where the witnessable entries (i.e. inputs, withdrawals, etc.) are not + supposed to be controlled by the HW wallet, which means the user is vulnerable to + unknowingly supplying a witness for an UTXO or other tx entry they think is external, + resulting in the co-signers gaining control over their funds (Something SLIP-0019 is + dealing with for BTC but no similar standard is currently available for Cardano). + Hence we completely forbid witnessing inputs and other entries of the transaction + except the stake pool certificate itself and we provide a witness only to the user's + staking key in the list of pool owners. + """ + + def __init__( + self, ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain + ) -> None: + super().__init__(ctx, msg, keychain) + + def _validate_tx_signing_request(self) -> None: + super()._validate_tx_signing_request() + if ( + self.msg.certificates_count != 1 + or self.msg.withdrawals_count != 0 + or self.msg.minting_asset_groups_count != 0 + ): + raise INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE + + if ( + self.msg.script_data_hash is not None + or self.msg.collateral_inputs_count != 0 + or self.msg.required_signers_count != 0 + ): + raise INVALID_TX_SIGNING_REQUEST + + async def _confirm_transaction(self, tx_hash: bytes) -> None: + # super() omitted intentionally + await confirm_stake_pool_registration_final( + self.ctx, + self.msg.protocol_magic, + self.msg.ttl, + self.msg.validity_interval_start, + ) + + def _validate_output(self, output: CardanoTxOutput) -> None: + super()._validate_output(output) + if output.address_parameters is not None: + raise INVALID_OUTPUT + if output.datum_hash is not None: + raise INVALID_OUTPUT + + def _should_show_output(self, output: CardanoTxOutput) -> bool: + # super() omitted intentionally + # There are no spending witnesses, it is thus safe to hide outputs. + return False + + def _validate_certificate(self, certificate: CardanoTxCertificate) -> None: + super()._validate_certificate(certificate) + if certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION: + raise INVALID_CERTIFICATE + + def _validate_witness_request( + self, witness_request: CardanoTxWitnessRequest + ) -> None: + super()._validate_witness_request(witness_request) + if not SCHEMA_STAKING_ANY_ACCOUNT.match(witness_request.path): + raise INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES + + def _is_network_id_verifiable(self) -> bool: + # super() omitted intentionally + return True diff --git a/core/src/apps/cardano/sign_tx/signer.py b/core/src/apps/cardano/sign_tx/signer.py new file mode 100644 index 000000000..d1714e89a --- /dev/null +++ b/core/src/apps/cardano/sign_tx/signer.py @@ -0,0 +1,926 @@ +from micropython import const +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto import hashlib +from trezor.crypto.curve import ed25519 +from trezor.enums import ( + CardanoAddressType, + CardanoCertificateType, + CardanoTxWitnessType, +) +from trezor.messages import ( + CardanoAddressParametersType, + CardanoAssetGroup, + CardanoPoolOwner, + CardanoPoolRelayParameters, + CardanoSignTxInit, + CardanoToken, + CardanoTxAuxiliaryData, + CardanoTxBodyHash, + CardanoTxCertificate, + CardanoTxCollateralInput, + CardanoTxHostAck, + CardanoTxInput, + CardanoTxItemAck, + CardanoTxMint, + CardanoTxOutput, + CardanoTxRequiredSigner, + CardanoTxWithdrawal, + CardanoTxWitnessRequest, + CardanoTxWitnessResponse, +) + +from apps.common import cbor, safety_checks + +from .. import seed +from ..address import ( + ADDRESS_TYPES_PAYMENT_SCRIPT, + derive_address_bytes, + derive_human_readable_address, + get_address_bytes_unsafe, + get_address_type, + validate_output_address, + validate_output_address_parameters, +) +from ..auxiliary_data import ( + get_auxiliary_data_hash_and_supplement, + show_auxiliary_data, + validate_auxiliary_data, +) +from ..certificates import ( + assert_certificate_cond, + cborize_certificate, + cborize_initial_pool_registration_certificate_fields, + cborize_pool_metadata, + cborize_pool_owner, + cborize_pool_relay, + validate_certificate, + validate_pool_owner, + validate_pool_relay, +) +from ..helpers import ( + ADDRESS_KEY_HASH_SIZE, + INPUT_PREV_HASH_SIZE, + INVALID_COLLATERAL_INPUT, + INVALID_INPUT, + INVALID_OUTPUT, + INVALID_OUTPUT_DATUM_HASH, + INVALID_REQUIRED_SIGNER, + INVALID_SCRIPT_DATA_HASH, + INVALID_TOKEN_BUNDLE_MINT, + INVALID_TOKEN_BUNDLE_OUTPUT, + INVALID_TX_SIGNING_REQUEST, + INVALID_WITHDRAWAL, + LOVELACE_MAX_SUPPLY, + OUTPUT_DATUM_HASH_SIZE, + SCRIPT_DATA_HASH_SIZE, +) +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, + CHANGE_OUTPUT_PATH_NAME, + CHANGE_OUTPUT_STAKING_PATH_NAME, + POOL_OWNER_STAKING_PATH_NAME, + SCHEMA_STAKING, +) +from ..helpers.utils import ( + derive_public_key, + get_public_key_hash, + validate_network_info, + validate_stake_credential, +) +from ..layout import ( + confirm_certificate, + confirm_collateral_input, + confirm_required_signer, + confirm_script_data_hash, + confirm_sending, + confirm_sending_token, + confirm_stake_pool_metadata, + confirm_stake_pool_owner, + confirm_stake_pool_parameters, + confirm_token_minting, + confirm_transaction, + confirm_withdrawal, + confirm_witness_request, + show_change_output_credentials, + show_warning_path, + show_warning_tx_contains_mint, + show_warning_tx_network_unverifiable, + show_warning_tx_output_contains_datum_hash, + show_warning_tx_output_contains_tokens, + show_warning_tx_output_no_datum_hash, +) +from ..seed import is_byron_path, is_minting_path, is_multisig_path, is_shelley_path + +if TYPE_CHECKING: + from typing import Any + from apps.common.paths import PathSchema + + CardanoTxResponseType = CardanoTxItemAck | CardanoTxWitnessResponse + +MINTING_POLICY_ID_LENGTH = 28 +MAX_ASSET_NAME_LENGTH = 32 + +TX_BODY_KEY_INPUTS = const(0) +TX_BODY_KEY_OUTPUTS = const(1) +TX_BODY_KEY_FEE = const(2) +TX_BODY_KEY_TTL = const(3) +TX_BODY_KEY_CERTIFICATES = const(4) +TX_BODY_KEY_WITHDRAWALS = const(5) +TX_BODY_KEY_AUXILIARY_DATA = const(7) +TX_BODY_KEY_VALIDITY_INTERVAL_START = const(8) +TX_BODY_KEY_MINT = const(9) +TX_BODY_KEY_SCRIPT_DATA_HASH = const(11) +TX_BODY_KEY_COLLATERAL_INPUTS = const(13) +TX_BODY_KEY_REQUIRED_SIGNERS = const(14) +TX_BODY_KEY_NETWORK_ID = const(15) + +POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT = 10 + + +class Signer: + """ + This class encapsulates the entire tx signing process. By default, most tx items are + allowed and shown to the user. For each signing mode, there is a subclass that + overrides some methods, usually to add more validation rules and show/hide some + items. Each tx item is processed in a _process_xyz() method which handles validation, + user confirmation and serialization of the tx item. + """ + + def __init__( + self, ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain + ) -> None: + self.ctx = ctx + self.msg = msg + self.keychain = keychain + + self.account_path_checker = AccountPathChecker() + + # Inputs, outputs and fee are mandatory, count the number of optional fields present. + tx_body_map_item_count = 3 + sum( + ( + msg.ttl is not None, + msg.certificates_count > 0, + msg.withdrawals_count > 0, + msg.has_auxiliary_data, + msg.validity_interval_start is not None, + msg.minting_asset_groups_count > 0, + msg.include_network_id, + msg.script_data_hash is not None, + msg.collateral_inputs_count > 0, + msg.required_signers_count > 0, + ) + ) + self.tx_dict: HashBuilderDict[int, Any] = HashBuilderDict( + tx_body_map_item_count, INVALID_TX_SIGNING_REQUEST + ) + + async def sign(self) -> None: + hash_fn = hashlib.blake2b(outlen=32) + self.tx_dict.start(hash_fn) + with self.tx_dict: + await self._processs_tx_signing_request() + + tx_hash = hash_fn.digest() + await self._confirm_transaction(tx_hash) + + response_after_witness_requests = await self._process_witness_requests(tx_hash) + await self.ctx.call(response_after_witness_requests, CardanoTxHostAck) + await self.ctx.call(CardanoTxBodyHash(tx_hash=tx_hash), CardanoTxHostAck) + + # signing request + + async def _processs_tx_signing_request(self) -> None: + self._validate_tx_signing_request() + await self._show_tx_signing_request() + + inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList( + self.msg.inputs_count + ) + with self.tx_dict.add(TX_BODY_KEY_INPUTS, inputs_list): + await self._process_inputs(inputs_list) + + outputs_list: HashBuilderList = HashBuilderList(self.msg.outputs_count) + with self.tx_dict.add(TX_BODY_KEY_OUTPUTS, outputs_list): + await self._process_outputs(outputs_list) + + self.tx_dict.add(TX_BODY_KEY_FEE, self.msg.fee) + + if self.msg.ttl is not None: + self.tx_dict.add(TX_BODY_KEY_TTL, self.msg.ttl) + + if self.msg.certificates_count > 0: + certificates_list: HashBuilderList = HashBuilderList( + self.msg.certificates_count + ) + with self.tx_dict.add(TX_BODY_KEY_CERTIFICATES, certificates_list): + await self._process_certificates(certificates_list) + + if self.msg.withdrawals_count > 0: + withdrawals_dict: HashBuilderDict[bytes, int] = HashBuilderDict( + self.msg.withdrawals_count, INVALID_WITHDRAWAL + ) + with self.tx_dict.add(TX_BODY_KEY_WITHDRAWALS, withdrawals_dict): + await self._process_withdrawals(withdrawals_dict) + + if self.msg.has_auxiliary_data: + await self._process_auxiliary_data() + + if self.msg.validity_interval_start is not None: + self.tx_dict.add( + TX_BODY_KEY_VALIDITY_INTERVAL_START, self.msg.validity_interval_start + ) + + if self.msg.minting_asset_groups_count > 0: + minting_dict: HashBuilderDict[bytes, HashBuilderDict] = HashBuilderDict( + self.msg.minting_asset_groups_count, INVALID_TOKEN_BUNDLE_MINT + ) + with self.tx_dict.add(TX_BODY_KEY_MINT, minting_dict): + await self._process_minting(minting_dict) + + if self.msg.script_data_hash is not None: + await self._process_script_data_hash() + + if self.msg.collateral_inputs_count > 0: + collateral_inputs_list: HashBuilderList[ + tuple[bytes, int] + ] = HashBuilderList(self.msg.collateral_inputs_count) + with self.tx_dict.add( + TX_BODY_KEY_COLLATERAL_INPUTS, collateral_inputs_list + ): + await self._process_collateral_inputs(collateral_inputs_list) + + if self.msg.required_signers_count > 0: + required_signers_list: HashBuilderList[bytes] = HashBuilderList( + self.msg.required_signers_count + ) + with self.tx_dict.add(TX_BODY_KEY_REQUIRED_SIGNERS, required_signers_list): + await self._process_required_signers(required_signers_list) + + if self.msg.include_network_id: + self.tx_dict.add(TX_BODY_KEY_NETWORK_ID, self.msg.network_id) + + def _validate_tx_signing_request(self) -> None: + if self.msg.fee > LOVELACE_MAX_SUPPLY: + raise wire.ProcessError("Fee is out of range!") + validate_network_info(self.msg.network_id, self.msg.protocol_magic) + + async def _show_tx_signing_request(self) -> None: + if not self._is_network_id_verifiable(): + await show_warning_tx_network_unverifiable(self.ctx) + + async def _confirm_tx(self, tx_hash: bytes) -> None: + # Final signing confirmation is handled separately in each signing mode. + raise NotImplementedError + + # inputs + + async def _process_inputs( + self, inputs_list: HashBuilderList[tuple[bytes, int]] + ) -> None: + for _ in range(self.msg.inputs_count): + input: CardanoTxInput = await self.ctx.call( + CardanoTxItemAck(), CardanoTxInput + ) + self._validate_input(input) + await self._show_input(input) + inputs_list.append((input.prev_hash, input.prev_index)) + + def _validate_input(self, input: CardanoTxInput) -> None: + if len(input.prev_hash) != INPUT_PREV_HASH_SIZE: + raise INVALID_INPUT + + async def _show_input(self, input: CardanoTxInput) -> None: + # We never show the inputs, except for Plutus txs. + pass + + # outputs + + async def _process_outputs(self, outputs_list: HashBuilderList) -> None: + total_amount = 0 + for _ in range(self.msg.outputs_count): + output: CardanoTxOutput = await self.ctx.call( + CardanoTxItemAck(), CardanoTxOutput + ) + self._validate_output(output) + await self._show_output(output) + + output_address = self._get_output_address(output) + + has_datum_hash = output.datum_hash is not None + output_list: HashBuilderList = HashBuilderList(2 + int(has_datum_hash)) + with outputs_list.append(output_list): + output_list.append(output_address) + if output.asset_groups_count == 0: + # output structure is: [address, amount, datum_hash?] + output_list.append(output.amount) + else: + # output structure is: [address, [amount, asset_groups], datum_hash?] + output_value_list: HashBuilderList = HashBuilderList(2) + with output_list.append(output_value_list): + output_value_list.append(output.amount) + asset_groups_dict: HashBuilderDict[ + bytes, HashBuilderDict[bytes, int] + ] = HashBuilderDict( + output.asset_groups_count, INVALID_TOKEN_BUNDLE_OUTPUT + ) + with output_value_list.append(asset_groups_dict): + await self._process_asset_groups( + asset_groups_dict, + output.asset_groups_count, + self._should_show_output(output), + ) + if has_datum_hash: + output_list.append(output.datum_hash) + + total_amount += output.amount + + if total_amount > LOVELACE_MAX_SUPPLY: + raise wire.ProcessError("Total transaction amount is out of range!") + + def _validate_output(self, output: CardanoTxOutput) -> None: + if output.address_parameters is not None and output.address is not None: + raise INVALID_OUTPUT + + if output.address_parameters is not None: + validate_output_address_parameters(output.address_parameters) + self._fail_if_strict_and_unusual(output.address_parameters) + elif output.address is not None: + validate_output_address( + output.address, self.msg.protocol_magic, self.msg.network_id + ) + else: + raise INVALID_OUTPUT + + if output.datum_hash is not None: + if len(output.datum_hash) != OUTPUT_DATUM_HASH_SIZE: + raise INVALID_OUTPUT_DATUM_HASH + address_type = self._get_output_address_type(output) + if address_type not in ADDRESS_TYPES_PAYMENT_SCRIPT: + raise INVALID_OUTPUT + + self.account_path_checker.add_output(output) + + async def _show_output(self, output: CardanoTxOutput) -> None: + if not self._should_show_output(output): + return + + if output.datum_hash is not None: + await show_warning_tx_output_contains_datum_hash( + self.ctx, output.datum_hash + ) + + address_type = self._get_output_address_type(output) + if output.datum_hash is None and address_type in ADDRESS_TYPES_PAYMENT_SCRIPT: + await show_warning_tx_output_no_datum_hash(self.ctx) + + if output.asset_groups_count > 0: + await show_warning_tx_output_contains_tokens(self.ctx) + + if output.address_parameters is not None: + address = derive_human_readable_address( + self.keychain, + output.address_parameters, + self.msg.protocol_magic, + self.msg.network_id, + ) + await self._show_output_credentials(output.address_parameters) + else: + assert output.address is not None # _validate_output + address = output.address + + await confirm_sending( + self.ctx, + output.amount, + address, + self._is_change_output(output), + self.msg.network_id, + ) + + async def _show_output_credentials( + self, address_parameters: CardanoAddressParametersType + ) -> None: + await show_change_output_credentials( + self.ctx, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), + ) + + def _should_show_output(self, output: CardanoTxOutput) -> bool: + """ + Determines whether the output should be shown. Extracted from _show_output because + of readability and because the same decision is made when displaying output tokens. + """ + if output.datum_hash is not None: + # The `return False` case below should not be reachable when datum hash is + # present, but let's make it explicit. + return True + + address_type = self._get_output_address_type(output) + if output.datum_hash is None and address_type in ADDRESS_TYPES_PAYMENT_SCRIPT: + # Plutus script address without a datum hash is unspendable, we must show a warning. + return True + + if output.address_parameters is not None: # change output + if not should_show_address_credentials(output.address_parameters): + # We don't need to display simple address outputs. + return False + + return True + + def _is_change_output(self, output: CardanoTxOutput) -> bool: + """Used only to determine what message to show to the user when confirming sending.""" + return output.address_parameters is not None + + # asset groups + + async def _process_asset_groups( + self, + asset_groups_dict: HashBuilderDict[bytes, HashBuilderDict[bytes, int]], + asset_groups_count: int, + should_show_tokens: bool, + ) -> None: + for _ in range(asset_groups_count): + asset_group: CardanoAssetGroup = await self.ctx.call( + CardanoTxItemAck(), CardanoAssetGroup + ) + self._validate_asset_group(asset_group) + + tokens: HashBuilderDict[bytes, int] = HashBuilderDict( + asset_group.tokens_count, INVALID_TOKEN_BUNDLE_OUTPUT + ) + with asset_groups_dict.add(asset_group.policy_id, tokens): + await self._process_tokens( + tokens, + asset_group.policy_id, + asset_group.tokens_count, + should_show_tokens, + ) + + def _validate_asset_group( + self, asset_group: CardanoAssetGroup, is_mint: bool = False + ) -> None: + INVALID_TOKEN_BUNDLE = ( + INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT + ) + + if len(asset_group.policy_id) != MINTING_POLICY_ID_LENGTH: + raise INVALID_TOKEN_BUNDLE + if asset_group.tokens_count == 0: + raise INVALID_TOKEN_BUNDLE + + # tokens + + async def _process_tokens( + self, + tokens_dict: HashBuilderDict[bytes, int], + policy_id: bytes, + tokens_count: int, + should_show_tokens: bool, + ) -> None: + for _ in range(tokens_count): + token: CardanoToken = await self.ctx.call(CardanoTxItemAck(), CardanoToken) + self._validate_token(token) + if should_show_tokens: + await confirm_sending_token(self.ctx, policy_id, token) + + assert token.amount is not None # _validate_token + tokens_dict.add(token.asset_name_bytes, token.amount) + + def _validate_token(self, token: CardanoToken, is_mint: bool = False) -> None: + INVALID_TOKEN_BUNDLE = ( + INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT + ) + + if is_mint: + if token.mint_amount is None or token.amount is not None: + raise INVALID_TOKEN_BUNDLE + else: + if token.amount is None or token.mint_amount is not None: + raise INVALID_TOKEN_BUNDLE + + if len(token.asset_name_bytes) > MAX_ASSET_NAME_LENGTH: + raise INVALID_TOKEN_BUNDLE + + # certificates + + async def _process_certificates(self, certificates_list: HashBuilderList) -> None: + for _ in range(self.msg.certificates_count): + certificate: CardanoTxCertificate = await self.ctx.call( + CardanoTxItemAck(), CardanoTxCertificate + ) + self._validate_certificate(certificate) + await self._show_certificate(certificate) + + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + pool_parameters = certificate.pool_parameters + assert pool_parameters is not None # _validate_certificate + + pool_items_list: HashBuilderList = HashBuilderList( + POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT + ) + with certificates_list.append(pool_items_list): + for item in cborize_initial_pool_registration_certificate_fields( + certificate + ): + pool_items_list.append(item) + + pool_owners_list: HashBuilderList[bytes] = HashBuilderList( + pool_parameters.owners_count + ) + with pool_items_list.append(pool_owners_list): + await self._process_pool_owners( + pool_owners_list, pool_parameters.owners_count + ) + + relays_list: HashBuilderList[cbor.CborSequence] = HashBuilderList( + pool_parameters.relays_count + ) + with pool_items_list.append(relays_list): + await self._process_pool_relays( + relays_list, pool_parameters.relays_count + ) + + pool_items_list.append( + cborize_pool_metadata(pool_parameters.metadata) + ) + else: + certificates_list.append( + cborize_certificate(self.keychain, certificate) + ) + + def _validate_certificate(self, certificate: CardanoTxCertificate) -> None: + validate_certificate( + certificate, + self.msg.protocol_magic, + self.msg.network_id, + self.account_path_checker, + ) + + async def _show_certificate(self, certificate: CardanoTxCertificate) -> None: + if certificate.path: + await self._fail_or_warn_if_invalid_path( + SCHEMA_STAKING, certificate.path, CERTIFICATE_PATH_NAME + ) + + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + assert certificate.pool_parameters is not None + await confirm_stake_pool_parameters( + self.ctx, certificate.pool_parameters, self.msg.network_id + ) + await confirm_stake_pool_metadata( + self.ctx, certificate.pool_parameters.metadata + ) + else: + await confirm_certificate(self.ctx, certificate) + + # pool owners + + async def _process_pool_owners( + self, pool_owners_list: HashBuilderList[bytes], owners_count: int + ) -> None: + owners_as_path_count = 0 + for _ in range(owners_count): + owner: CardanoPoolOwner = await self.ctx.call( + CardanoTxItemAck(), CardanoPoolOwner + ) + validate_pool_owner(owner, self.account_path_checker) + await self._show_pool_owner(owner) + pool_owners_list.append(cborize_pool_owner(self.keychain, owner)) + + if owner.staking_key_path: + owners_as_path_count += 1 + + assert_certificate_cond(owners_as_path_count == 1) + + async def _show_pool_owner(self, owner: CardanoPoolOwner) -> None: + if owner.staking_key_path: + await self._fail_or_warn_if_invalid_path( + SCHEMA_STAKING, owner.staking_key_path, POOL_OWNER_STAKING_PATH_NAME + ) + + await confirm_stake_pool_owner( + self.ctx, self.keychain, owner, self.msg.protocol_magic, self.msg.network_id + ) + + # pool relays + + async def _process_pool_relays( + self, + relays_list: HashBuilderList[cbor.CborSequence], + relays_count: int, + ) -> None: + for _ in range(relays_count): + relay: CardanoPoolRelayParameters = await self.ctx.call( + CardanoTxItemAck(), CardanoPoolRelayParameters + ) + validate_pool_relay(relay) + relays_list.append(cborize_pool_relay(relay)) + + # withdrawals + + async def _process_withdrawals( + self, withdrawals_dict: HashBuilderDict[bytes, int] + ) -> None: + for _ in range(self.msg.withdrawals_count): + withdrawal: CardanoTxWithdrawal = await self.ctx.call( + CardanoTxItemAck(), CardanoTxWithdrawal + ) + self._validate_withdrawal(withdrawal) + reward_address_bytes = self._derive_withdrawal_reward_address_bytes( + withdrawal + ) + await confirm_withdrawal( + self.ctx, withdrawal, reward_address_bytes, self.msg.network_id + ) + withdrawals_dict.add(reward_address_bytes, withdrawal.amount) + + def _validate_withdrawal(self, withdrawal: CardanoTxWithdrawal) -> None: + validate_stake_credential( + withdrawal.path, + withdrawal.script_hash, + withdrawal.key_hash, + INVALID_WITHDRAWAL, + ) + + if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: + raise INVALID_WITHDRAWAL + + self.account_path_checker.add_withdrawal(withdrawal) + + # auxiliary data + + async def _process_auxiliary_data(self) -> None: + auxiliary_data: CardanoTxAuxiliaryData = await self.ctx.call( + CardanoTxItemAck(), CardanoTxAuxiliaryData + ) + validate_auxiliary_data(auxiliary_data) + + ( + auxiliary_data_hash, + auxiliary_data_supplement, + ) = get_auxiliary_data_hash_and_supplement( + self.keychain, auxiliary_data, self.msg.protocol_magic, self.msg.network_id + ) + await show_auxiliary_data( + self.ctx, + self.keychain, + auxiliary_data_hash, + auxiliary_data.catalyst_registration_parameters, + self.msg.protocol_magic, + self.msg.network_id, + ) + self.tx_dict.add(TX_BODY_KEY_AUXILIARY_DATA, auxiliary_data_hash) + + await self.ctx.call(auxiliary_data_supplement, CardanoTxHostAck) + + # minting + + async def _process_minting( + self, minting_dict: HashBuilderDict[bytes, HashBuilderDict] + ) -> None: + token_minting: CardanoTxMint = await self.ctx.call( + CardanoTxItemAck(), CardanoTxMint + ) + + await show_warning_tx_contains_mint(self.ctx) + + for _ in range(token_minting.asset_groups_count): + asset_group: CardanoAssetGroup = await self.ctx.call( + CardanoTxItemAck(), CardanoAssetGroup + ) + self._validate_asset_group(asset_group, is_mint=True) + + tokens: HashBuilderDict[bytes, int] = HashBuilderDict( + asset_group.tokens_count, INVALID_TOKEN_BUNDLE_MINT + ) + with minting_dict.add(asset_group.policy_id, tokens): + await self._process_minting_tokens( + tokens, + asset_group.policy_id, + asset_group.tokens_count, + ) + + # minting tokens + + async def _process_minting_tokens( + self, + tokens: HashBuilderDict[bytes, int], + policy_id: bytes, + tokens_count: int, + ) -> None: + for _ in range(tokens_count): + token: CardanoToken = await self.ctx.call(CardanoTxItemAck(), CardanoToken) + self._validate_token(token, is_mint=True) + await confirm_token_minting(self.ctx, policy_id, token) + + assert token.mint_amount is not None # _validate_token + tokens.add(token.asset_name_bytes, token.mint_amount) + + # script data hash + + async def _process_script_data_hash(self) -> None: + assert self.msg.script_data_hash is not None + self._validate_script_data_hash() + await confirm_script_data_hash(self.ctx, self.msg.script_data_hash) + self.tx_dict.add(TX_BODY_KEY_SCRIPT_DATA_HASH, self.msg.script_data_hash) + + def _validate_script_data_hash(self) -> None: + assert self.msg.script_data_hash is not None + if len(self.msg.script_data_hash) != SCRIPT_DATA_HASH_SIZE: + raise INVALID_SCRIPT_DATA_HASH + + # collateral inputs + + async def _process_collateral_inputs( + self, collateral_inputs_list: HashBuilderList[tuple[bytes, int]] + ) -> None: + for _ in range(self.msg.collateral_inputs_count): + collateral_input: CardanoTxCollateralInput = await self.ctx.call( + CardanoTxItemAck(), CardanoTxCollateralInput + ) + self._validate_collateral_input(collateral_input) + await confirm_collateral_input(self.ctx, collateral_input) + collateral_inputs_list.append( + (collateral_input.prev_hash, collateral_input.prev_index) + ) + + def _validate_collateral_input( + self, collateral_input: CardanoTxCollateralInput + ) -> None: + if len(collateral_input.prev_hash) != INPUT_PREV_HASH_SIZE: + raise INVALID_COLLATERAL_INPUT + + # required signers + + async def _process_required_signers( + self, required_signers_list: HashBuilderList[bytes] + ) -> None: + for _ in range(self.msg.required_signers_count): + required_signer: CardanoTxRequiredSigner = await self.ctx.call( + CardanoTxItemAck(), CardanoTxRequiredSigner + ) + self._validate_required_signer(required_signer) + await confirm_required_signer(self.ctx, required_signer) + + key_hash = required_signer.key_hash or get_public_key_hash( + self.keychain, required_signer.key_path + ) + required_signers_list.append(key_hash) + + def _validate_required_signer( + self, required_signer: CardanoTxRequiredSigner + ) -> None: + if required_signer.key_hash and required_signer.key_path: + raise INVALID_REQUIRED_SIGNER + + if required_signer.key_hash: + if len(required_signer.key_hash) != ADDRESS_KEY_HASH_SIZE: + raise INVALID_REQUIRED_SIGNER + elif required_signer.key_path: + if not ( + is_shelley_path(required_signer.key_path) + or is_multisig_path(required_signer.key_path) + or is_minting_path(required_signer.key_path) + ): + raise INVALID_REQUIRED_SIGNER + else: + raise INVALID_REQUIRED_SIGNER + + # witness requests + + async def _process_witness_requests(self, tx_hash: bytes) -> CardanoTxResponseType: + response: CardanoTxResponseType = CardanoTxItemAck() + + for _ in range(self.msg.witness_requests_count): + witness_request = await self.ctx.call(response, CardanoTxWitnessRequest) + self._validate_witness_request(witness_request) + path = witness_request.path + await self._show_witness_request(path) + if is_byron_path(path): + response = self._get_byron_witness(path, tx_hash) + else: + response = self._get_shelley_witness(path, tx_hash) + + return response + + def _validate_witness_request( + self, witness_request: CardanoTxWitnessRequest + ) -> None: + self.account_path_checker.add_witness_request(witness_request) + + async def _show_witness_request( + self, + witness_path: list[int], + ) -> None: + await confirm_witness_request(self.ctx, witness_path) + + # helpers + + def _is_network_id_verifiable(self) -> bool: + """ + Checks whether there is at least one element that contains information about + network ID, otherwise Trezor cannot guarantee that the tx is actually meant for + the given network. + + Note: Shelley addresses contain network id. The intended network of Byron + addresses can be determined based on whether they contain the protocol magic. + These checks are performed during address validation. + """ + return ( + self.msg.include_network_id + or self.msg.outputs_count != 0 + or self.msg.withdrawals_count != 0 + ) + + def _get_output_address(self, output: CardanoTxOutput) -> bytes: + if output.address_parameters: + return derive_address_bytes( + self.keychain, + output.address_parameters, + self.msg.protocol_magic, + self.msg.network_id, + ) + else: + assert output.address is not None # _validate_output + return get_address_bytes_unsafe(output.address) + + def _get_output_address_type(self, output: CardanoTxOutput) -> CardanoAddressType: + if output.address_parameters: + return output.address_parameters.address_type + assert output.address is not None # _validate_output + return get_address_type(get_address_bytes_unsafe(output.address)) + + def _derive_withdrawal_reward_address_bytes( + self, withdrawal: CardanoTxWithdrawal + ) -> bytes: + reward_address_type = ( + CardanoAddressType.REWARD + if withdrawal.path or withdrawal.key_hash + else CardanoAddressType.REWARD_SCRIPT + ) + return derive_address_bytes( + self.keychain, + CardanoAddressParametersType( + address_type=reward_address_type, + address_n_staking=withdrawal.path, + staking_key_hash=withdrawal.key_hash, + script_staking_hash=withdrawal.script_hash, + ), + self.msg.protocol_magic, + self.msg.network_id, + ) + + def _get_byron_witness( + self, path: list[int], tx_hash: bytes + ) -> CardanoTxWitnessResponse: + node = self.keychain.derive(path) + return CardanoTxWitnessResponse( + type=CardanoTxWitnessType.BYRON_WITNESS, + pub_key=derive_public_key(self.keychain, path), + signature=self._sign_tx_hash(tx_hash, path), + chain_code=node.chain_code(), + ) + + def _get_shelley_witness( + self, path: list[int], tx_hash: bytes + ) -> CardanoTxWitnessResponse: + return CardanoTxWitnessResponse( + type=CardanoTxWitnessType.SHELLEY_WITNESS, + pub_key=derive_public_key(self.keychain, path), + signature=self._sign_tx_hash(tx_hash, path), + ) + + def _sign_tx_hash(self, tx_body_hash: bytes, path: list[int]) -> bytes: + node = self.keychain.derive(path) + return ed25519.sign_ext( + node.private_key(), node.private_key_ext(), tx_body_hash + ) + + async def _fail_or_warn_if_invalid_path( + self, schema: PathSchema, path: list[int], path_name: str + ) -> None: + if not schema.match(path): + await self._fail_or_warn_path(path, path_name) + + async def _fail_or_warn_path(self, path: list[int], path_name: str) -> None: + if safety_checks.is_strict(): + raise wire.DataError(f"Invalid {path_name.lower()}") + else: + await show_warning_path(self.ctx, path, path_name) + + def _fail_if_strict_and_unusual( + self, address_parameters: CardanoAddressParametersType + ) -> None: + if not safety_checks.is_strict(): + return + + if Credential.payment_credential(address_parameters).is_unusual_path: + raise wire.DataError(f"Invalid {CHANGE_OUTPUT_PATH_NAME.lower()}") + + if Credential.stake_credential(address_parameters).is_unusual_path: + raise wire.DataError(f"Invalid {CHANGE_OUTPUT_STAKING_PATH_NAME.lower()}") diff --git a/core/tests/test_apps.cardano.certificate.py b/core/tests/test_apps.cardano.certificate.py index 694028521..abddee727 100644 --- a/core/tests/test_apps.cardano.certificate.py +++ b/core/tests/test_apps.cardano.certificate.py @@ -1,6 +1,6 @@ from common import * from trezor import wire -from trezor.enums import CardanoCertificateType, CardanoTxSigningMode +from trezor.enums import CardanoCertificateType from trezor.messages import CardanoTxCertificate, CardanoPoolParametersType from apps.common.paths import HARDENED @@ -15,465 +15,346 @@ if not utils.BITCOIN_ONLY: class TestCardanoCertificate(unittest.TestCase): def test_validate_certificate(self): valid_test_vectors = [ - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.MULTISIG_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, - ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.MULTISIG_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.MULTISIG_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, - ), - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, - metadata=None, + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + metadata=None, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), ] invalid_test_vectors = [ # STAKE_REGISTRATION neither path or script_hash is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, ), # STAKE_REGISTRATION both path and script_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_REGISTRATION both script_hash and key_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), # STAKE_REGISTRATION pool is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_REGISTRATION pool parameters are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_REGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_DELEGATION neither path or script_hash is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_DELEGATION both path and script_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_DELEGATION both script_hash and key_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), # STAKE_DELEGATION pool parameters are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DELEGATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool=unhexlify( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_DEREGISTRATION neither path or script_hash is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, ), # STAKE_DEREGISTRATION both path and script_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_DEREGISTRATION both script_hash and key_hash are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" ), - CardanoTxSigningMode.PLUTUS_TRANSACTION, ), # STAKE_DEREGISTRATION pool is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, ), # STAKE_DEREGISTRATION pool parameters are set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_DEREGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_POOL_REGISTRATION pool parameters are not set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - ), - CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, ), # STAKE_POOL_REGISTRATION path is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_POOL_REGISTRATION script hash is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_POOL_REGISTRATION key hash is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - key_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), # STAKE_POOL_REGISTRATION pool is set - ( - CardanoTxCertificate( - type=CardanoCertificateType.STAKE_POOL_REGISTRATION, - script_hash=unhexlify( - "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" - ), - pool=unhexlify( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" ), - pool_parameters=CardanoPoolParametersType( - pool_id=unhexlify( - "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" - ), - vrf_key_hash=unhexlify( - "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" - ), - pledge=500000000, - cost=340000000, - margin_numerator=1, - margin_denominator=2, - reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", - owners_count=1, - relays_count=1, + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, ), - CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), ] - for certificate, signing_mode in valid_test_vectors: + for certificate in valid_test_vectors: validate_certificate( certificate, - signing_mode, protocol_magics.MAINNET, network_ids.MAINNET, AccountPathChecker(), ) - for certificate, signing_mode in invalid_test_vectors: + for certificate in invalid_test_vectors: with self.assertRaises(wire.ProcessError): validate_certificate( certificate, - signing_mode, protocol_magics.MAINNET, network_ids.MAINNET, AccountPathChecker(), diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index a14c59667..17ff0533f 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1184,8 +1184,8 @@ "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[margin_higher_than_1]": "9e1f554bb74f847e8f09201dda808fd1e0cdb68737515f34d6ad435957671c77", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[missing_owner_with_path]": "2dede6a914015cb606124904268abc3cb9034a8e716beb380c62dee29e8a48a4", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_1852_multisi-b7679330": "de16c51797b0e28e766553a31c8ad849c7eb247b0101a1996dba2eea1d59ed2d", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_a_collateral_input]": "875ec83057c301a416c4ea3e77b931132cef45fafc7272cd95cd04bb6d8c4e86", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_a_required_signer]": "875ec83057c301a416c4ea3e77b931132cef45fafc7272cd95cd04bb6d8c4e86", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_a_collateral_input]": "9e1f554bb74f847e8f09201dda808fd1e0cdb68737515f34d6ad435957671c77", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_a_required_signer]": "9e1f554bb74f847e8f09201dda808fd1e0cdb68737515f34d6ad435957671c77", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_long_token_m-9fb3cfe5": "31f7a5a1d32c997c61649ace37fdf3aab3d7bf1cb7c73ffa777ba03ff52470ec", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_output_conta-e3b36436": "875ec83057c301a416c4ea3e77b931132cef45fafc7272cd95cd04bb6d8c4e86", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_repeated_withdrawal]": "691048e2f57f12bd216e1803237f8ce738a0bc66c546601ed2e26fa76ee831dc",