diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index ee7ff5cb2..888d9bebf 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -69,6 +69,11 @@ enum CardanoTxAuxiliaryDataSupplementType { CATALYST_REGISTRATION_SIGNATURE = 1; } +enum CardanoCatalystRegistrationFormat { + CIP15 = 0; + CIP36 = 1; +} + enum CardanoTxSigningMode { ORDINARY_TRANSACTION = 0; POOL_REGISTRATION_AS_OWNER = 1; @@ -332,7 +337,7 @@ message CardanoPoolParametersType { * Request: Transaction certificate data * @next CardanoTxItemAck */ - message CardanoTxCertificate { +message CardanoTxCertificate { required CardanoCertificateType type = 1; // certificate type repeated uint32 path = 2; // stake credential key path optional bytes pool = 3; // pool hash @@ -355,11 +360,22 @@ message CardanoTxWithdrawal { /** * @embed */ -message CardanoCatalystRegistrationParametersType { +message CardanoCatalystRegistrationDelegation { required bytes voting_public_key = 1; + required uint32 weight = 2; +} + +/** + * @embed + */ +message CardanoCatalystRegistrationParametersType { + optional bytes voting_public_key = 1; repeated uint32 staking_path = 2; required CardanoAddressParametersType reward_address_parameters = 3; required uint64 nonce = 4; + optional CardanoCatalystRegistrationFormat format = 5 [default=CIP15]; + repeated CardanoCatalystRegistrationDelegation delegations = 6; // mutually exclusive with voting_public_key; max 32 delegations + optional uint64 voting_purpose = 7; } /** diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index 6034e47aa..e2deef165 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -1398,6 +1398,63 @@ "error_message": "Invalid auxiliary data" } }, + { + "description": "transaction with both voting public key and delegations in catalyst registration", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "catalyst_registration_parameters": { + "voting_public_key": "38DA0B509D45BF6C87BD55594B92F97081D3923B8C1334B9B8D0BF13FC1C12D0", + "staking_path": "m/1852'/1815'/0'/2/0", + "reward_address_parameters": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0" + }, + "nonce": 140, + "format": 1, + "delegations": [ + { + "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 1 + } + ] + } + }, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid auxiliary data" + } + }, { "description": "Output datum hash has incorrect length", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx.json b/common/tests/fixtures/cardano/sign_tx.json index 7bb2bc3fb..8b9dc6858 100644 --- a/common/tests/fixtures/cardano/sign_tx.json +++ b/common/tests/fixtures/cardano/sign_tx.json @@ -1002,7 +1002,7 @@ } }, { - "description": "transaction with catalyst registration", + "description": "transaction with CIP15 catalyst registration", "parameters": { "protocol_magic": 764824073, "network_id": 1, @@ -1064,6 +1064,149 @@ } } }, + { + "description": "transaction with CIP36 catalyst registration and voting purpose not specified", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "catalyst_registration_parameters": { + "staking_path": "m/1852'/1815'/0'/2/0", + "reward_address_parameters": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0" + }, + "nonce": 22634813, + "format": 1, + "delegations": [ + { + "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 1 + }, + { + "voting_public_key": "2af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 2 + } + ] + } + }, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "15e4e382d913a743776b93d730fee3ca39bfa3ee203801205333bc9aad249612", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "c984c65a5d6ee16c9cdd9fd332a5f64907f25438ef2d1e6d625bdd5c76d15acdf3e5700338b6b5c0ca30d25dd604e1b33ab5ee3459ff8ce3ca5a11e774a18605", + "chain_code": null + } + ], + "auxiliary_data_supplement": { + "type": 1, + "auxiliary_data_hash": "9d4c00f5b5b67760931fd7ed9850ff8e14dcdf957685191ab4bc755c52f0ed56", + "catalyst_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001" + } + } + }, + { + "description": "transaction with CIP36 catalyst registration and OTHER voting purpose", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "catalyst_registration_parameters": { + "staking_path": "m/1852'/1815'/0'/2/0", + "reward_address_parameters": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0" + }, + "nonce": 22634813, + "format": 1, + "delegations": [ + { + "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 1 + } + ], + "voting_purpose": 1 + } + }, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "98357cec961c4c2bfef747bb204a06945ab55077166ec4367b644882136b8b39", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "9ac45a56c7002a8bca2121b9f0bae52a7201336b7528495c22d49b845b514d93a70ca1571e8a4dd418fbf4c260018c264843e54fbd2a8c6486e8f00f93cd5103", + "chain_code": null + } + ], + "auxiliary_data_supplement": { + "type": 1, + "auxiliary_data_hash": "28b7ffa6800833bdfe5421739eaa21d4a49cde1d84e762b147001169f7c0a385", + "catalyst_signature": "ebc00c615f988c6fc2e132d4419a719f04bbec56fe2569a00746a9e9b0d6e5bdd0809515cb2522c773c991c5ae39834403654d36b37e70b14897c0e98c8c0a0c" + } + } + }, { "description": "Testnet transaction", "parameters": { @@ -1592,14 +1735,21 @@ ], "auxiliary_data": { "catalyst_registration_parameters": { - "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", "staking_path": "m/1852'/1815'/0'/2/0", "reward_address_parameters": { "addressType": 0, "path": "m/1852'/1815'/0'/0/0", "stakingPath": "m/1852'/1815'/0'/2/0" }, - "nonce": 22634813 + "nonce": 22634813, + "format": 1, + "delegations": [ + { + "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 1 + } + ], + "voting_purpose": 0 } }, "mint": [], @@ -1614,37 +1764,37 @@ "include_network_id": false }, "result": { - "tx_hash": "ee0dfef8b97857ebe7aa8935af50e9f8f608ff4054c0c034600750d722d90631", + "tx_hash": "f98e1b5edfd376356eb211103bfae679380929bf7fbc40b3355a68e98111d091", "witnesses": [ { "type": 1, "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", - "signature": "7d17407e4e8f8b89f8794c022408a84e6f7ef163957d9d7e8ebee4cf9b5c87750c7c559f3a2663441535eec88ebce8540e7d7ea30897de984b1053b818374007", + "signature": "448d2e063f1dbc8662a9f6dea887549cbee7d8e4254124dd1aed08330f4ce165531a846b4ebc42e9944d85b99e878b4255860b960c5f4bd94d4feeb42295d402", "chain_code": null }, { "type": 1, "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", - "signature": "df62ec013a32d137c86931cec726d104cbc3193776026ec36d10450d9cbd289abc4c2d44311878b3aba035a8aec2c076522183027f9da046b586b5de5c460504", + "signature": "5ba01fe1a043d3851236395a22982bfdf9d58d80ee963c042e2aa3bc0f8b35b99be18319710ade92edcf49b7185b5e8d91710f3acaa8d9e0f41bad1e3271a801", "chain_code": null }, { "type": 1, "pub_key": "e90d7b0a6cf831b0042d37961dd528842860e77914e715bcece676c75353b812", - "signature": "e249396d227f1d0540e58b64610bdb990eb1f1db9b3bae4a3d4a8088679af4a3bab464a5c912f7041a5fabc37e3009b3e1f4d76e2406429a0ebed85b880ecd0c", + "signature": "5595ab117629c0a3743e7081b315d937451d546525db43b7253a76662a24100d23baeaf232dc2cccfbdd624ec3439a20a3ca0914b71df0a766ba08f444d1a60d", "chain_code": null }, { "type": 1, "pub_key": "bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e", - "signature": "0dfd139ce3e255664a77de7d199ce5e4f1a1238ec17a6acec4aaae79be2ccd9b1d21127164c059c8aea2c4b91292aaf352c824550db7594b59e4eca6455d3f03", + "signature": "a130822ccf92dee7a9c357432c7e4b4c6f21fc6efac9c548d00162569bc748b19384ccdf6c132d68b04526658c3766e40cef7b45f73f5398b0db946469343005", "chain_code": null } ], "auxiliary_data_supplement": { "type": 1, - "auxiliary_data_hash": "a943e9166f1bb6d767b175384d3bd7d23645170df36fc1861fbf344135d8e120", - "catalyst_signature": "74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409" + "auxiliary_data_hash": "544c9ae849c82e31224865ff936decc6160047409eee4a6b4178b729fe3d286c", + "catalyst_signature": "3064949c9f186138f95e228075d0119dd5cb50e1b7e75d24d569fa547e018a597615da7c79a39ca8e394ee1ba8acb83e70be80f37e69aef3b86e7c4a6bd44903" } } }, diff --git a/common/tests/fixtures/cardano/sign_tx.show_details.json b/common/tests/fixtures/cardano/sign_tx.show_details.json index 7bd8b88c4..0dfbe6c38 100644 --- a/common/tests/fixtures/cardano/sign_tx.show_details.json +++ b/common/tests/fixtures/cardano/sign_tx.show_details.json @@ -455,6 +455,79 @@ } ] } + }, + { + "description": "transaction with CIP36 catalyst registration and voting purpose not specified", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "catalyst_registration_parameters": { + "staking_path": "m/1852'/1815'/0'/2/0", + "reward_address_parameters": { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0" + }, + "nonce": 22634813, + "format": 1, + "delegations": [ + { + "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 1 + }, + { + "voting_public_key": "2af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "weight": 2 + } + ] + } + }, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "15e4e382d913a743776b93d730fee3ca39bfa3ee203801205333bc9aad249612", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "c984c65a5d6ee16c9cdd9fd332a5f64907f25438ef2d1e6d625bdd5c76d15acdf3e5700338b6b5c0ca30d25dd604e1b33ab5ee3459ff8ce3ca5a11e774a18605", + "chain_code": null + } + ], + "auxiliary_data_supplement": { + "type": 1, + "auxiliary_data_hash": "9d4c00f5b5b67760931fd7ed9850ff8e14dcdf957685191ab4bc755c52f0ed56", + "catalyst_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001" + } + } } ] } diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 6f99efaee..481e9d54d 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -419,6 +419,8 @@ if not utils.BITCOIN_ONLY: import trezor.enums.BinanceTimeInForce trezor.enums.CardanoAddressType import trezor.enums.CardanoAddressType + trezor.enums.CardanoCatalystRegistrationFormat + import trezor.enums.CardanoCatalystRegistrationFormat trezor.enums.CardanoCertificateType import trezor.enums.CardanoCertificateType trezor.enums.CardanoDerivationType diff --git a/core/src/apps/cardano/auxiliary_data.py b/core/src/apps/cardano/auxiliary_data.py index ecce5265e..44fef54c1 100644 --- a/core/src/apps/cardano/auxiliary_data.py +++ b/core/src/apps/cardano/auxiliary_data.py @@ -3,18 +3,22 @@ from typing import TYPE_CHECKING from trezor import messages, wire from trezor.crypto import hashlib from trezor.crypto.curve import ed25519 -from trezor.enums import CardanoAddressType, CardanoTxAuxiliaryDataSupplementType +from trezor.enums import ( + CardanoAddressType, + CardanoCatalystRegistrationFormat, + CardanoTxAuxiliaryDataSupplementType, +) from apps.common import cbor -from . import addresses +from . import addresses, layout from .helpers import bech32 from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT from .helpers.utils import derive_public_key -from .layout import confirm_catalyst_registration, show_auxiliary_data_hash if TYPE_CHECKING: - CatalystRegistrationPayload = dict[int, bytes | int] + Delegations = list[tuple[bytes, int]] + CatalystRegistrationPayload = dict[int, Delegations | bytes | int] SignedCatalystRegistrationPayload = tuple[CatalystRegistrationPayload, bytes] CatalystRegistrationSignature = dict[int, bytes] CatalystRegistration = dict[ @@ -30,6 +34,14 @@ CATALYST_REGISTRATION_HASH_SIZE = 32 METADATA_KEY_CATALYST_REGISTRATION = 61284 METADATA_KEY_CATALYST_REGISTRATION_SIGNATURE = 61285 +MAX_DELEGATION_COUNT = 32 +DEFAULT_VOTING_PURPOSE = 0 + + +def assert_cond(condition: bool) -> None: + if not condition: + raise wire.ProcessError("Invalid auxiliary data") + def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None: fields_provided = 0 @@ -41,31 +53,57 @@ def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None: _validate_catalyst_registration_parameters( auxiliary_data.catalyst_registration_parameters ) - - if fields_provided != 1: - raise wire.ProcessError("Invalid auxiliary data") + assert_cond(fields_provided == 1) def _validate_hash(auxiliary_data_hash: bytes) -> None: - if len(auxiliary_data_hash) != AUXILIARY_DATA_HASH_SIZE: - raise wire.ProcessError("Invalid auxiliary data") + assert_cond(len(auxiliary_data_hash) == AUXILIARY_DATA_HASH_SIZE) def _validate_catalyst_registration_parameters( parameters: messages.CardanoCatalystRegistrationParametersType, ) -> None: - if len(parameters.voting_public_key) != CATALYST_VOTING_PUBLIC_KEY_LENGTH: - raise wire.ProcessError("Invalid auxiliary data") + voting_key_fields_provided = 0 + if parameters.voting_public_key is not None: + voting_key_fields_provided += 1 + _validate_voting_public_key(parameters.voting_public_key) + if parameters.delegations: + voting_key_fields_provided += 1 + assert_cond(parameters.format == CardanoCatalystRegistrationFormat.CIP36) + _validate_delegations(parameters.delegations) + assert_cond(voting_key_fields_provided == 1) - if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path): - raise wire.ProcessError("Invalid auxiliary data") + assert_cond(SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path)) address_parameters = parameters.reward_address_parameters - if address_parameters.address_type == CardanoAddressType.BYRON: - raise wire.ProcessError("Invalid auxiliary data") - + assert_cond(address_parameters.address_type != CardanoAddressType.BYRON) addresses.validate_address_parameters(address_parameters) + if parameters.voting_purpose is not None: + assert_cond(parameters.format == CardanoCatalystRegistrationFormat.CIP36) + + +def _validate_voting_public_key(key: bytes) -> None: + assert_cond(len(key) == CATALYST_VOTING_PUBLIC_KEY_LENGTH) + + +def _validate_delegations( + delegations: list[messages.CardanoCatalystDelegation], +) -> None: + assert_cond(len(delegations) <= MAX_DELEGATION_COUNT) + for delegation in delegations: + _validate_voting_public_key(delegation.voting_public_key) + + +def _get_voting_purpose_to_serialize( + parameters: messages.CardanoCatalystRegistrationParametersType, +) -> int | None: + if parameters.format == CardanoCatalystRegistrationFormat.CIP15: + return None + if parameters.voting_purpose is None: + return DEFAULT_VOTING_PURPOSE + return parameters.voting_purpose + async def show( ctx: wire.Context, @@ -83,10 +121,11 @@ async def show( parameters, protocol_magic, network_id, + should_show_details, ) if should_show_details: - await show_auxiliary_data_hash(ctx, auxiliary_data_hash) + await layout.show_auxiliary_data_hash(ctx, auxiliary_data_hash) async def _show_catalyst_registration( @@ -95,9 +134,22 @@ async def _show_catalyst_registration( parameters: messages.CardanoCatalystRegistrationParametersType, protocol_magic: int, network_id: int, + should_show_details: bool, ) -> None: - public_key = parameters.voting_public_key - encoded_public_key = bech32.encode(bech32.HRP_JORMUN_PUBLIC_KEY, public_key) + for delegation in parameters.delegations: + encoded_public_key = bech32.encode( + bech32.HRP_JORMUN_PUBLIC_KEY, delegation.voting_public_key + ) + await layout.confirm_catalyst_registration_delegation( + ctx, encoded_public_key, delegation.weight + ) + + encoded_public_key: str | None = None + if parameters.voting_public_key: + encoded_public_key = bech32.encode( + bech32.HRP_JORMUN_PUBLIC_KEY, parameters.voting_public_key + ) + reward_address = addresses.derive_human_readable( keychain, parameters.reward_address_parameters, @@ -105,12 +157,17 @@ async def _show_catalyst_registration( network_id, ) - await confirm_catalyst_registration( + voting_purpose: int | None = ( + _get_voting_purpose_to_serialize(parameters) if should_show_details else None + ) + + await layout.confirm_catalyst_registration( ctx, encoded_public_key, parameters.staking_path, reward_address, parameters.nonce, + voting_purpose, ) @@ -172,19 +229,36 @@ def _get_signed_catalyst_registration_payload( protocol_magic: int, network_id: int, ) -> SignedCatalystRegistrationPayload: + delegations_or_key: Delegations | bytes + if len(parameters.delegations) > 0: + delegations_or_key = [ + (delegation.voting_public_key, delegation.weight) + for delegation in parameters.delegations + ] + elif parameters.voting_public_key: + delegations_or_key = parameters.voting_public_key + else: + raise RuntimeError # should not be reached - _validate_governance_registration_parameters + staking_key = derive_public_key(keychain, parameters.staking_path) + reward_address = addresses.derive_bytes( + keychain, + parameters.reward_address_parameters, + protocol_magic, + network_id, + ) + + voting_purpose = _get_voting_purpose_to_serialize(parameters) + payload: CatalystRegistrationPayload = { - 1: parameters.voting_public_key, + 1: delegations_or_key, 2: staking_key, - 3: addresses.derive_bytes( - keychain, - parameters.reward_address_parameters, - protocol_magic, - network_id, - ), + 3: reward_address, 4: parameters.nonce, } + if voting_purpose is not None: + payload[5] = voting_purpose signature = _create_catalyst_registration_payload_signature( keychain, diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index ecfcc196d..70b1a9597 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -750,27 +750,61 @@ def _format_stake_credential( raise ValueError -async def confirm_catalyst_registration( +async def confirm_catalyst_registration_delegation( ctx: wire.Context, public_key: str, + weight: int, +) -> None: + props: list[PropertyType] = [ + ("Catalyst voting key registration", None), + ("Delegating to:", public_key), + ] + if weight is not None: + props.append(("Weight:", str(weight))) + + await confirm_properties( + ctx, + "confirm_catalyst_registration_delegation", + title="Confirm transaction", + props=props, + br_code=ButtonRequestType.Other, + ) + + +async def confirm_catalyst_registration( + ctx: wire.Context, + public_key: str | None, staking_path: list[int], reward_address: str, nonce: int, + voting_purpose: int | None, ) -> None: - await confirm_properties( - ctx, - "confirm_catalyst_registration", - title="Confirm transaction", - props=[ - ("Catalyst voting key registration", None), - ("Voting public key:", public_key), + props: list[PropertyType] = [("Catalyst voting key registration", None)] + if public_key is not None: + props.append(("Voting public key:", public_key)) + props.extend( + [ ( f"Staking key for account {format_account_number(staking_path)}:", address_n_to_str(staking_path), ), ("Rewards go to:", reward_address), ("Nonce:", str(nonce)), - ], + ] + ) + if voting_purpose is not None: + props.append( + ( + "Voting purpose:", + "Catalyst" if voting_purpose == 0 else f"{voting_purpose} (other)", + ) + ) + + await confirm_properties( + ctx, + "confirm_catalyst_registration", + title="Confirm transaction", + props=props, br_code=ButtonRequestType.Other, ) diff --git a/core/src/trezor/enums/CardanoCatalystRegistrationFormat.py b/core/src/trezor/enums/CardanoCatalystRegistrationFormat.py new file mode 100644 index 000000000..be0471af0 --- /dev/null +++ b/core/src/trezor/enums/CardanoCatalystRegistrationFormat.py @@ -0,0 +1,6 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +CIP15 = 0 +CIP36 = 1 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index def49dd99..2ecd581dc 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -383,6 +383,10 @@ if TYPE_CHECKING: NONE = 0 CATALYST_REGISTRATION_SIGNATURE = 1 + class CardanoCatalystRegistrationFormat(IntEnum): + CIP15 = 0 + CIP36 = 1 + class CardanoTxSigningMode(IntEnum): ORDINARY_TRANSACTION = 0 POOL_REGISTRATION_AS_OWNER = 1 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 9a5911112..5fd4368cd 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from trezor.enums import ButtonRequestType # noqa: F401 from trezor.enums import Capability # noqa: F401 from trezor.enums import CardanoAddressType # noqa: F401 + from trezor.enums import CardanoCatalystRegistrationFormat # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401 from trezor.enums import CardanoDerivationType # noqa: F401 from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 @@ -1636,19 +1637,41 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxWithdrawal"]: return isinstance(msg, cls) - class CardanoCatalystRegistrationParametersType(protobuf.MessageType): + class CardanoCatalystRegistrationDelegation(protobuf.MessageType): voting_public_key: "bytes" - staking_path: "list[int]" - reward_address_parameters: "CardanoAddressParametersType" - nonce: "int" + weight: "int" def __init__( self, *, voting_public_key: "bytes", + weight: "int", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoCatalystRegistrationDelegation"]: + return isinstance(msg, cls) + + class CardanoCatalystRegistrationParametersType(protobuf.MessageType): + voting_public_key: "bytes | None" + staking_path: "list[int]" + reward_address_parameters: "CardanoAddressParametersType" + nonce: "int" + format: "CardanoCatalystRegistrationFormat" + delegations: "list[CardanoCatalystRegistrationDelegation]" + voting_purpose: "int | None" + + def __init__( + self, + *, reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: "list[int] | None" = None, + delegations: "list[CardanoCatalystRegistrationDelegation] | None" = None, + voting_public_key: "bytes | None" = None, + format: "CardanoCatalystRegistrationFormat | None" = None, + voting_purpose: "int | None" = None, ) -> None: pass diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index f5fe27ad9..5bdecdd5c 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -57,11 +57,11 @@ REQUIRED_FIELDS_POOL_PARAMETERS = ( ) REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") REQUIRED_FIELDS_CATALYST_REGISTRATION = ( - "voting_public_key", "staking_path", "nonce", "reward_address_parameters", ) +REQUIRED_FIELDS_CATALYST_DELEGATION = ("voting_public_key", "weight") INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" @@ -566,10 +566,27 @@ def parse_auxiliary_data( ): raise AUXILIARY_DATA_MISSING_FIELDS_ERROR + serialization_format = catalyst_registration.get("format") + + delegations = [] + for delegation in catalyst_registration.get("delegations", []): + if not all(k in delegation for k in REQUIRED_FIELDS_CATALYST_DELEGATION): + raise AUXILIARY_DATA_MISSING_FIELDS_ERROR + delegations.append( + messages.CardanoCatalystRegistrationDelegation( + voting_public_key=bytes.fromhex(delegation["voting_public_key"]), + weight=int(delegation["weight"]), + ) + ) + + voting_purpose = None + if serialization_format == messages.CardanoCatalystRegistrationFormat.CIP36: + voting_purpose = catalyst_registration.get("voting_purpose") + catalyst_registration_parameters = ( messages.CardanoCatalystRegistrationParametersType( - voting_public_key=bytes.fromhex( - catalyst_registration["voting_public_key"] + voting_public_key=parse_optional_bytes( + catalyst_registration.get("voting_public_key") ), staking_path=tools.parse_path(catalyst_registration["staking_path"]), nonce=catalyst_registration["nonce"], @@ -577,6 +594,9 @@ def parse_auxiliary_data( catalyst_registration["reward_address_parameters"], str(AUXILIARY_DATA_MISSING_FIELDS_ERROR), ), + format=serialization_format, + delegations=delegations, + voting_purpose=voting_purpose, ) ) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 4e3be96e2..08c57a824 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -408,6 +408,11 @@ class CardanoTxAuxiliaryDataSupplementType(IntEnum): CATALYST_REGISTRATION_SIGNATURE = 1 +class CardanoCatalystRegistrationFormat(IntEnum): + CIP15 = 0 + CIP36 = 1 + + class CardanoTxSigningMode(IntEnum): ORDINARY_TRANSACTION = 0 POOL_REGISTRATION_AS_OWNER = 1 @@ -2626,27 +2631,53 @@ class CardanoTxWithdrawal(protobuf.MessageType): self.key_hash = key_hash -class CardanoCatalystRegistrationParametersType(protobuf.MessageType): +class CardanoCatalystRegistrationDelegation(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { 1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=True), - 2: protobuf.Field("staking_path", "uint32", repeated=True, required=False), - 3: protobuf.Field("reward_address_parameters", "CardanoAddressParametersType", repeated=False, required=True), - 4: protobuf.Field("nonce", "uint64", repeated=False, required=True), + 2: protobuf.Field("weight", "uint32", repeated=False, required=True), } def __init__( self, *, voting_public_key: "bytes", + weight: "int", + ) -> None: + self.voting_public_key = voting_public_key + self.weight = weight + + +class CardanoCatalystRegistrationParametersType(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=False), + 2: protobuf.Field("staking_path", "uint32", repeated=True, required=False), + 3: protobuf.Field("reward_address_parameters", "CardanoAddressParametersType", repeated=False, required=True), + 4: protobuf.Field("nonce", "uint64", repeated=False, required=True), + 5: protobuf.Field("format", "CardanoCatalystRegistrationFormat", repeated=False, required=False), + 6: protobuf.Field("delegations", "CardanoCatalystRegistrationDelegation", repeated=True, required=False), + 7: protobuf.Field("voting_purpose", "uint64", repeated=False, required=False), + } + + def __init__( + self, + *, reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: Optional[Sequence["int"]] = None, + delegations: Optional[Sequence["CardanoCatalystRegistrationDelegation"]] = None, + voting_public_key: Optional["bytes"] = None, + format: Optional["CardanoCatalystRegistrationFormat"] = CardanoCatalystRegistrationFormat.CIP15, + voting_purpose: Optional["int"] = None, ) -> None: self.staking_path: Sequence["int"] = staking_path if staking_path is not None else [] - self.voting_public_key = voting_public_key + self.delegations: Sequence["CardanoCatalystRegistrationDelegation"] = delegations if delegations is not None else [] self.reward_address_parameters = reward_address_parameters self.nonce = nonce + self.voting_public_key = voting_public_key + self.format = format + self.voting_purpose = voting_purpose class CardanoTxAuxiliaryData(protobuf.MessageType):