diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index ee7ff5cb26a..05c37d5d78e 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -66,7 +66,12 @@ enum CardanoPoolRelayType { enum CardanoTxAuxiliaryDataSupplementType { NONE = 0; - CATALYST_REGISTRATION_SIGNATURE = 1; + GOVERNANCE_REGISTRATION_SIGNATURE = 1; +} + +enum CardanoGovernanceRegistrationFormat { + CIP15 = 0; + CIP36 = 1; } enum CardanoTxSigningMode { @@ -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 CardanoGovernanceRegistrationDelegation { required bytes voting_public_key = 1; + required uint32 weight = 2; +} + +/** + * @embed + */ +message CardanoGovernanceRegistrationParametersType { + optional bytes voting_public_key = 1; repeated uint32 staking_path = 2; required CardanoAddressParametersType reward_address_parameters = 3; required uint64 nonce = 4; + optional CardanoGovernanceRegistrationFormat format = 5 [default=CIP15]; + repeated CardanoGovernanceRegistrationDelegation delegations = 6; // mutually exclusive with voting_public_key; max 32 delegations + optional uint64 voting_purpose = 7; } /** @@ -368,7 +384,7 @@ message CardanoCatalystRegistrationParametersType { * @next CardanoTxAuxiliaryDataSupplement */ message CardanoTxAuxiliaryData { - optional CardanoCatalystRegistrationParametersType catalyst_registration_parameters = 1; + optional CardanoGovernanceRegistrationParametersType governance_registration_parameters = 1; optional bytes hash = 2; } @@ -436,7 +452,7 @@ message CardanoTxItemAck { message CardanoTxAuxiliaryDataSupplement { required CardanoTxAuxiliaryDataSupplementType type = 1; optional bytes auxiliary_data_hash = 2; - optional bytes catalyst_signature = 3; + optional bytes governance_signature = 3; } /** diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index 6034e47aa25..16de3a0d80d 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -1299,7 +1299,7 @@ } }, { - "description": "transaction with catalyst registration containing byron reward address", + "description": "transaction with governance registration containing byron reward address", "parameters": { "protocol_magic": 764824073, "network_id": 1, @@ -1309,7 +1309,7 @@ "certificates": [], "withdrawals": [], "auxiliary_data": { - "catalyst_registration_parameters": { + "governance_registration_parameters": { "voting_public_key": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", "staking_path": "m/1852'/1815'/0'/2/0", "nonce": 22634813, @@ -1348,7 +1348,7 @@ } }, { - "description": "transaction with both auxiliary data blob and catalyst registration", + "description": "transaction with both auxiliary data blob and governance registration", "parameters": { "protocol_magic": 764824073, "network_id": 1, @@ -1359,7 +1359,7 @@ "withdrawals": [], "auxiliary_data": { "hash": "ea4c91860dd5ec5449f8f985d227946ff39086b17f10b5afb93d12ee87050b6a", - "catalyst_registration_parameters": { + "governance_registration_parameters": { "voting_public_key": "38DA0B509D45BF6C87BD55594B92F97081D3923B8C1334B9B8D0BF13FC1C12D0", "staking_path": "m/1852'/1815'/0'/2/0", "reward_address_parameters": { @@ -1398,6 +1398,63 @@ "error_message": "Invalid auxiliary data" } }, + { + "description": "transaction with both voting public key and delegations in governance registration", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "governance_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 7bb2bc3fbc8..3cb71aa12ec 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 governance registration", "parameters": { "protocol_magic": 764824073, "network_id": 1, @@ -1012,7 +1012,7 @@ "certificates": [], "withdrawals": [], "auxiliary_data": { - "catalyst_registration_parameters": { + "governance_registration_parameters": { "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", "staking_path": "m/1852'/1815'/0'/2/0", "reward_address_parameters": { @@ -1060,7 +1060,150 @@ "auxiliary_data_supplement": { "type": 1, "auxiliary_data_hash": "a943e9166f1bb6d767b175384d3bd7d23645170df36fc1861fbf344135d8e120", - "catalyst_signature": "74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409" + "governance_signature": "74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409" + } + } + }, + { + "description": "transaction with CIP36 governance 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": { + "governance_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", + "governance_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001" + } + } + }, + { + "description": "transaction with CIP36 governance registration and OTHER voting purpose", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": { + "governance_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", + "governance_signature": "ebc00c615f988c6fc2e132d4419a719f04bbec56fe2569a00746a9e9b0d6e5bdd0809515cb2522c773c991c5ae39834403654d36b37e70b14897c0e98c8c0a0c" } } }, @@ -1591,15 +1734,22 @@ } ], "auxiliary_data": { - "catalyst_registration_parameters": { - "voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "governance_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 + "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", + "governance_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 7bd8b88c4ac..e84986be6cf 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 governance 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": { + "governance_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", + "governance_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001" + } + } } ] } diff --git a/core/.changelog.d/2561.added b/core/.changelog.d/2561.added new file mode 100644 index 00000000000..cedfbf82188 --- /dev/null +++ b/core/.changelog.d/2561.added @@ -0,0 +1 @@ +Support for Cardano CIP-36 governance registration format diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 6f99efaee80..2434e26ba1b 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -423,6 +423,8 @@ import trezor.enums.CardanoCertificateType trezor.enums.CardanoDerivationType import trezor.enums.CardanoDerivationType + trezor.enums.CardanoGovernanceRegistrationFormat + import trezor.enums.CardanoGovernanceRegistrationFormat trezor.enums.CardanoNativeScriptHashDisplayFormat import trezor.enums.CardanoNativeScriptHashDisplayFormat trezor.enums.CardanoNativeScriptType diff --git a/core/src/apps/cardano/README.md b/core/src/apps/cardano/README.md index 20d3accfca9..01b07cb67f9 100644 --- a/core/src/apps/cardano/README.md +++ b/core/src/apps/cardano/README.md @@ -291,9 +291,9 @@ Each transaction may contain auxiliary data. Auxiliary data format can be found Auxiliary data can be sent to Trezor as a hash or as an object with parameters. The hash will be included in the transaction body as is and will be shown to the user. -The only object currently supported is Catalyst voting key registration. To be in compliance with the CDDL and other Cardano tools, Catalyst voting key registration object is being wrapped in a tuple and an empty tuple follows it. The empty tuple represents `auxiliary_scripts` which are not yet supported on Trezor and are thus always empty. Byron addresses are not supported as Catalyst reward addresses. The Catalyst registration signature is returned in the form of `CardanoTxAuxiliaryDataSupplement` which also contains the auxiliary data hash calculated by Trezor. +The only object currently supported is governance voting key registration (currently, this is used only by Catalyst, but there may be other governance use cases in the future). To be in compliance with the CDDL and other Cardano tools, governance voting key registration object is being wrapped in a tuple and an empty tuple follows it. The empty tuple represents `auxiliary_scripts` which are not yet supported on Trezor and are thus always empty. Byron addresses are not supported as governance reward addresses. The governance registration signature is returned in the form of `CardanoTxAuxiliaryDataSupplement` which also contains the auxiliary data hash calculated by Trezor. -[Catalyst Registration Transaction Metadata Format](https://github.com/cardano-foundation/CIPs/blob/749f22eccd78e05fcdc4552c49639bb3bbd0a458/CIP-0015/CIP-0015.md) +[Governance Registration Transaction Metadata Format](https://cips.cardano.org/cips/cip36/) ### Native scripts diff --git a/core/src/apps/cardano/auxiliary_data.py b/core/src/apps/cardano/auxiliary_data.py index de5b4483f46..54001a9b620 100644 --- a/core/src/apps/cardano/auxiliary_data.py +++ b/core/src/apps/cardano/auxiliary_data.py @@ -3,32 +3,44 @@ 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, + CardanoGovernanceRegistrationFormat, + 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] - SignedCatalystRegistrationPayload = tuple[CatalystRegistrationPayload, bytes] - CatalystRegistrationSignature = dict[int, bytes] - CatalystRegistration = dict[ - int, CatalystRegistrationPayload | CatalystRegistrationSignature + Delegations = list[tuple[bytes, int]] + GovernanceRegistrationPayload = dict[int, Delegations | bytes | int] + SignedGovernanceRegistrationPayload = tuple[GovernanceRegistrationPayload, bytes] + GovernanceRegistrationSignature = dict[int, bytes] + GovernanceRegistration = dict[ + int, GovernanceRegistrationPayload | GovernanceRegistrationSignature ] from . import seed AUXILIARY_DATA_HASH_SIZE = 32 -CATALYST_VOTING_PUBLIC_KEY_LENGTH = 32 -CATALYST_REGISTRATION_HASH_SIZE = 32 +GOVERNANCE_VOTING_PUBLIC_KEY_LENGTH = 32 +GOVERNANCE_REGISTRATION_HASH_SIZE = 32 -METADATA_KEY_CATALYST_REGISTRATION = 61284 -METADATA_KEY_CATALYST_REGISTRATION_SIGNATURE = 61285 +METADATA_KEY_GOVERNANCE_REGISTRATION = 61284 +METADATA_KEY_GOVERNANCE_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: @@ -36,85 +48,126 @@ def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None: if auxiliary_data.hash: fields_provided += 1 _validate_hash(auxiliary_data.hash) - if auxiliary_data.catalyst_registration_parameters: + if auxiliary_data.governance_registration_parameters: fields_provided += 1 - _validate_catalyst_registration_parameters( - auxiliary_data.catalyst_registration_parameters + _validate_governance_registration_parameters( + auxiliary_data.governance_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( - catalyst_registration_parameters: messages.CardanoCatalystRegistrationParametersType, +def _validate_governance_registration_parameters( + parameters: messages.CardanoGovernanceRegistrationParametersType, ) -> None: - if ( - len(catalyst_registration_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 == CardanoGovernanceRegistrationFormat.CIP36) + _validate_delegations(parameters.delegations) + assert_cond(voting_key_fields_provided == 1) + + assert_cond(SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path)) + + address_parameters = parameters.reward_address_parameters + assert_cond(address_parameters.address_type != CardanoAddressType.BYRON) + addresses.validate_address_parameters(address_parameters) - if not SCHEMA_STAKING_ANY_ACCOUNT.match( - catalyst_registration_parameters.staking_path - ): - raise wire.ProcessError("Invalid auxiliary data") + if parameters.voting_purpose is not None: + assert_cond(parameters.format == CardanoGovernanceRegistrationFormat.CIP36) - address_parameters = catalyst_registration_parameters.reward_address_parameters - if address_parameters.address_type == CardanoAddressType.BYRON: - raise wire.ProcessError("Invalid auxiliary data") - addresses.validate_address_parameters(address_parameters) +def _validate_voting_public_key(key: bytes) -> None: + assert_cond(len(key) == GOVERNANCE_VOTING_PUBLIC_KEY_LENGTH) + + +def _validate_delegations( + delegations: list[messages.CardanoGovernanceDelegation], +) -> 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.CardanoGovernanceRegistrationParametersType, +) -> int | None: + if parameters.format == CardanoGovernanceRegistrationFormat.CIP15: + return None + if parameters.voting_purpose is None: + return DEFAULT_VOTING_PURPOSE + return parameters.voting_purpose async def show( ctx: wire.Context, keychain: seed.Keychain, auxiliary_data_hash: bytes, - catalyst_registration_parameters: messages.CardanoCatalystRegistrationParametersType - | None, + parameters: messages.CardanoGovernanceRegistrationParametersType | None, protocol_magic: int, network_id: int, should_show_details: bool, ) -> None: - if catalyst_registration_parameters: - await _show_catalyst_registration( + if parameters: + await _show_governance_registration( ctx, keychain, - catalyst_registration_parameters, + 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( +async def _show_governance_registration( ctx: wire.Context, keychain: seed.Keychain, - catalyst_registration_parameters: messages.CardanoCatalystRegistrationParametersType, + parameters: messages.CardanoGovernanceRegistrationParametersType, protocol_magic: int, network_id: int, + should_show_details: bool, ) -> None: - public_key = catalyst_registration_parameters.voting_public_key - encoded_public_key = bech32.encode(bech32.HRP_JORMUN_PUBLIC_KEY, public_key) - staking_path = catalyst_registration_parameters.staking_path + for delegation in parameters.delegations: + encoded_public_key = bech32.encode( + bech32.HRP_GOVERNANCE_PUBLIC_KEY, delegation.voting_public_key + ) + await layout.confirm_governance_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_GOVERNANCE_PUBLIC_KEY, parameters.voting_public_key + ) + reward_address = addresses.derive_human_readable( keychain, - catalyst_registration_parameters.reward_address_parameters, + parameters.reward_address_parameters, protocol_magic, network_id, ) - nonce = catalyst_registration_parameters.nonce - await confirm_catalyst_registration( - ctx, encoded_public_key, staking_path, reward_address, nonce + voting_purpose: int | None = ( + _get_voting_purpose_to_serialize(parameters) if should_show_details else None + ) + + await layout.confirm_governance_registration( + ctx, + encoded_public_key, + parameters.staking_path, + reward_address, + parameters.nonce, + voting_purpose, ) @@ -124,20 +177,20 @@ def get_hash_and_supplement( protocol_magic: int, network_id: int, ) -> tuple[bytes, messages.CardanoTxAuxiliaryDataSupplement]: - if parameters := auxiliary_data.catalyst_registration_parameters: + if parameters := auxiliary_data.governance_registration_parameters: ( - catalyst_registration_payload, - catalyst_signature, - ) = _get_signed_catalyst_registration_payload( + governance_registration_payload, + governance_signature, + ) = _get_signed_governance_registration_payload( keychain, parameters, protocol_magic, network_id ) - auxiliary_data_hash = _get_catalyst_registration_hash( - catalyst_registration_payload, catalyst_signature + auxiliary_data_hash = _get_governance_registration_hash( + governance_registration_payload, governance_signature ) auxiliary_data_supplement = messages.CardanoTxAuxiliaryDataSupplement( - type=CardanoTxAuxiliaryDataSupplementType.CATALYST_REGISTRATION_SIGNATURE, + type=CardanoTxAuxiliaryDataSupplementType.GOVERNANCE_REGISTRATION_SIGNATURE, auxiliary_data_hash=auxiliary_data_hash, - catalyst_signature=catalyst_signature, + governance_signature=governance_signature, ) return auxiliary_data_hash, auxiliary_data_supplement else: @@ -147,78 +200,93 @@ def get_hash_and_supplement( ) -def _get_catalyst_registration_hash( - catalyst_registration_payload: CatalystRegistrationPayload, - catalyst_registration_payload_signature: bytes, +def _get_governance_registration_hash( + governance_registration_payload: GovernanceRegistrationPayload, + governance_registration_payload_signature: bytes, ) -> bytes: - cborized_catalyst_registration = _cborize_catalyst_registration( - catalyst_registration_payload, - catalyst_registration_payload_signature, + cborized_governance_registration = _cborize_governance_registration( + governance_registration_payload, + governance_registration_payload_signature, ) - return _get_hash(cbor.encode(_wrap_metadata(cborized_catalyst_registration))) + return _get_hash(cbor.encode(_wrap_metadata(cborized_governance_registration))) -def _cborize_catalyst_registration( - catalyst_registration_payload: CatalystRegistrationPayload, - catalyst_registration_payload_signature: bytes, -) -> CatalystRegistration: - catalyst_registration_signature = {1: catalyst_registration_payload_signature} +def _cborize_governance_registration( + governance_registration_payload: GovernanceRegistrationPayload, + governance_registration_payload_signature: bytes, +) -> GovernanceRegistration: + governance_registration_signature = {1: governance_registration_payload_signature} return { - METADATA_KEY_CATALYST_REGISTRATION: catalyst_registration_payload, - METADATA_KEY_CATALYST_REGISTRATION_SIGNATURE: catalyst_registration_signature, + METADATA_KEY_GOVERNANCE_REGISTRATION: governance_registration_payload, + METADATA_KEY_GOVERNANCE_REGISTRATION_SIGNATURE: governance_registration_signature, } -def _get_signed_catalyst_registration_payload( +def _get_signed_governance_registration_payload( keychain: seed.Keychain, - catalyst_registration_parameters: messages.CardanoCatalystRegistrationParametersType, + parameters: messages.CardanoGovernanceRegistrationParametersType, protocol_magic: int, network_id: int, -) -> SignedCatalystRegistrationPayload: - staking_key = derive_public_key( - keychain, catalyst_registration_parameters.staking_path +) -> SignedGovernanceRegistrationPayload: + 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, ) - payload: CatalystRegistrationPayload = { - 1: catalyst_registration_parameters.voting_public_key, + voting_purpose = _get_voting_purpose_to_serialize(parameters) + + payload: GovernanceRegistrationPayload = { + 1: delegations_or_key, 2: staking_key, - 3: addresses.derive_bytes( - keychain, - catalyst_registration_parameters.reward_address_parameters, - protocol_magic, - network_id, - ), - 4: catalyst_registration_parameters.nonce, + 3: reward_address, + 4: parameters.nonce, } + if voting_purpose is not None: + payload[5] = voting_purpose - signature = _create_catalyst_registration_payload_signature( + signature = _create_governance_registration_payload_signature( keychain, payload, - catalyst_registration_parameters.staking_path, + parameters.staking_path, ) return payload, signature -def _create_catalyst_registration_payload_signature( +def _create_governance_registration_payload_signature( keychain: seed.Keychain, - catalyst_registration_payload: CatalystRegistrationPayload, + governance_registration_payload: GovernanceRegistrationPayload, path: list[int], ) -> bytes: node = keychain.derive(path) - encoded_catalyst_registration = cbor.encode( - {METADATA_KEY_CATALYST_REGISTRATION: catalyst_registration_payload} + encoded_governance_registration = cbor.encode( + {METADATA_KEY_GOVERNANCE_REGISTRATION: governance_registration_payload} ) - catalyst_registration_hash = hashlib.blake2b( - data=encoded_catalyst_registration, - outlen=CATALYST_REGISTRATION_HASH_SIZE, + governance_registration_hash = hashlib.blake2b( + data=encoded_governance_registration, + outlen=GOVERNANCE_REGISTRATION_HASH_SIZE, ).digest() return ed25519.sign_ext( - node.private_key(), node.private_key_ext(), catalyst_registration_hash + node.private_key(), node.private_key_ext(), governance_registration_hash ) diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py index 22a65a7ef29..63480271311 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -7,8 +7,7 @@ HRP_TESTNET_ADDRESS = "addr_test" HRP_REWARD_ADDRESS = "stake" HRP_TESTNET_REWARD_ADDRESS = "stake_test" -# Jormungandr public key prefix - https://github.com/input-output-hk/voting-tools-lib/blob/18dae637e80db72444476606ab264b973bcf1a9d/src/Cardano/API/Extended.hs#L226 -HRP_JORMUN_PUBLIC_KEY = "ed25519_pk" +HRP_GOVERNANCE_PUBLIC_KEY = "gov_vk" HRP_SCRIPT_HASH = "script" HRP_KEY_HASH = "addr_vkh" HRP_SHARED_KEY_HASH = "addr_shared_vkh" diff --git a/core/src/apps/cardano/helpers/protocol_magics.py b/core/src/apps/cardano/helpers/protocol_magics.py index cc5c567e8ad..e0a30fc9bca 100644 --- a/core/src/apps/cardano/helpers/protocol_magics.py +++ b/core/src/apps/cardano/helpers/protocol_magics.py @@ -1,9 +1,14 @@ +# https://book.world.dev.cardano.org/environments.html MAINNET = 764824073 -TESTNET = 1097911063 +TESTNET_PREPROD = 1 +TESTNET_PREVIEW = 2 +TESTNET_LEGACY = 1097911063 NAMES = { MAINNET: "Mainnet", - TESTNET: "Testnet", + TESTNET_PREPROD: "Preprod Testnet", + TESTNET_PREVIEW: "Preview Testnet", + TESTNET_LEGACY: "Legacy Testnet", } diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index ecfcc196d24..8adb370724c 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_governance_registration_delegation( ctx: wire.Context, public_key: str, - staking_path: list[int], - reward_address: str, - nonce: int, + weight: int, ) -> None: + props: list[PropertyType] = [ + ("Governance 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", + "confirm_governance_registration_delegation", title="Confirm transaction", - props=[ - ("Catalyst voting key registration", None), - ("Voting public key:", public_key), + props=props, + br_code=ButtonRequestType.Other, + ) + + +async def confirm_governance_registration( + ctx: wire.Context, + public_key: str | None, + staking_path: list[int], + reward_address: str, + nonce: int, + voting_purpose: int | None, +) -> None: + props: list[PropertyType] = [("Governance 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_governance_registration", + title="Confirm transaction", + props=props, br_code=ButtonRequestType.Other, ) diff --git a/core/src/apps/cardano/sign_tx/signer.py b/core/src/apps/cardano/sign_tx/signer.py index af1c9c28ec0..a46ea3af750 100644 --- a/core/src/apps/cardano/sign_tx/signer.py +++ b/core/src/apps/cardano/sign_tx/signer.py @@ -839,7 +839,7 @@ async def _process_auxiliary_data(self) -> None: self.ctx, self.keychain, auxiliary_data_hash, - data.catalyst_registration_parameters, + data.governance_registration_parameters, self.msg.protocol_magic, self.msg.network_id, self.should_show_details, diff --git a/core/src/trezor/enums/CardanoGovernanceRegistrationFormat.py b/core/src/trezor/enums/CardanoGovernanceRegistrationFormat.py new file mode 100644 index 00000000000..be0471af086 --- /dev/null +++ b/core/src/trezor/enums/CardanoGovernanceRegistrationFormat.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/CardanoTxAuxiliaryDataSupplementType.py b/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py index 8baae8ff09c..3033edca2c5 100644 --- a/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py +++ b/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py @@ -3,4 +3,4 @@ # isort:skip_file NONE = 0 -CATALYST_REGISTRATION_SIGNATURE = 1 +GOVERNANCE_REGISTRATION_SIGNATURE = 1 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index def49dd9910..224a1374e04 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -381,7 +381,11 @@ class CardanoPoolRelayType(IntEnum): class CardanoTxAuxiliaryDataSupplementType(IntEnum): NONE = 0 - CATALYST_REGISTRATION_SIGNATURE = 1 + GOVERNANCE_REGISTRATION_SIGNATURE = 1 + + class CardanoGovernanceRegistrationFormat(IntEnum): + CIP15 = 0 + CIP36 = 1 class CardanoTxSigningMode(IntEnum): ORDINARY_TRANSACTION = 0 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index ddc0700f631..fbdaaac5216 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -26,6 +26,7 @@ def __getattr__(name: str) -> Any: from trezor.enums import CardanoAddressType # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401 from trezor.enums import CardanoDerivationType # noqa: F401 + from trezor.enums import CardanoGovernanceRegistrationFormat # noqa: F401 from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 from trezor.enums import CardanoNativeScriptType # noqa: F401 from trezor.enums import CardanoPoolRelayType # noqa: F401 @@ -1610,34 +1611,56 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxWithdrawal"]: return isinstance(msg, cls) - class CardanoCatalystRegistrationParametersType(protobuf.MessageType): + class CardanoGovernanceRegistrationDelegation(protobuf.MessageType): voting_public_key: "bytes" + weight: "int" + + def __init__( + self, + *, + voting_public_key: "bytes", + weight: "int", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoGovernanceRegistrationDelegation"]: + return isinstance(msg, cls) + + class CardanoGovernanceRegistrationParametersType(protobuf.MessageType): + voting_public_key: "bytes | None" staking_path: "list[int]" reward_address_parameters: "CardanoAddressParametersType" nonce: "int" + format: "CardanoGovernanceRegistrationFormat" + delegations: "list[CardanoGovernanceRegistrationDelegation]" + voting_purpose: "int | None" def __init__( self, *, - voting_public_key: "bytes", reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: "list[int] | None" = None, + delegations: "list[CardanoGovernanceRegistrationDelegation] | None" = None, + voting_public_key: "bytes | None" = None, + format: "CardanoGovernanceRegistrationFormat | None" = None, + voting_purpose: "int | None" = None, ) -> None: pass @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["CardanoCatalystRegistrationParametersType"]: + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoGovernanceRegistrationParametersType"]: return isinstance(msg, cls) class CardanoTxAuxiliaryData(protobuf.MessageType): - catalyst_registration_parameters: "CardanoCatalystRegistrationParametersType | None" + governance_registration_parameters: "CardanoGovernanceRegistrationParametersType | None" hash: "bytes | None" def __init__( self, *, - catalyst_registration_parameters: "CardanoCatalystRegistrationParametersType | None" = None, + governance_registration_parameters: "CardanoGovernanceRegistrationParametersType | None" = None, hash: "bytes | None" = None, ) -> None: pass @@ -1717,14 +1740,14 @@ def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxItemAck"]: class CardanoTxAuxiliaryDataSupplement(protobuf.MessageType): type: "CardanoTxAuxiliaryDataSupplementType" auxiliary_data_hash: "bytes | None" - catalyst_signature: "bytes | None" + governance_signature: "bytes | None" def __init__( self, *, type: "CardanoTxAuxiliaryDataSupplementType", auxiliary_data_hash: "bytes | None" = None, - catalyst_signature: "bytes | None" = None, + governance_signature: "bytes | None" = None, ) -> None: pass diff --git a/core/tests/test_apps.cardano.address.py b/core/tests/test_apps.cardano.address.py index f74e4fa6a3c..61be57b659c 100644 --- a/core/tests/test_apps.cardano.address.py +++ b/core/tests/test_apps.cardano.address.py @@ -290,7 +290,7 @@ def test_testnet_byron_address(self): address_type=CardanoAddressType.BYRON, address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], ) - address = derive_human_readable(self.keychain, address_parameters, protocol_magics.TESTNET, 0) + address = derive_human_readable(self.keychain, address_parameters, protocol_magics.TESTNET_LEGACY, 0) self.assertEqual(expected, address) def test_derive_address(self): diff --git a/python/.changelog.d/2561.added b/python/.changelog.d/2561.added new file mode 100644 index 00000000000..cedfbf82188 --- /dev/null +++ b/python/.changelog.d/2561.added @@ -0,0 +1 @@ +Support for Cardano CIP-36 governance registration format diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index f5fe27ad9b5..70983e13d0f 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -38,7 +38,12 @@ from .client import TrezorClient from .protobuf import MessageType -PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 1097911063} +PROTOCOL_MAGICS = { + "mainnet": 764824073, + "testnet_preprod": 1, + "testnet_preview": 2, + "testnet_legacy": 1097911063, +} NETWORK_IDS = {"mainnet": 1, "testnet": 0} MAX_CHUNK_SIZE = 1024 @@ -56,12 +61,12 @@ "owners", ) REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") -REQUIRED_FIELDS_CATALYST_REGISTRATION = ( - "voting_public_key", +REQUIRED_FIELDS_GOVERNANCE_REGISTRATION = ( "staking_path", "nonce", "reward_address_parameters", ) +REQUIRED_FIELDS_GOVERNANCE_DELEGATION = ("voting_public_key", "weight") INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" @@ -558,34 +563,55 @@ def parse_auxiliary_data( # include all provided fields so we can test validation in FW hash = parse_optional_bytes(auxiliary_data.get("hash")) - catalyst_registration_parameters = None - if "catalyst_registration_parameters" in auxiliary_data: - catalyst_registration = auxiliary_data["catalyst_registration_parameters"] + governance_registration_parameters = None + if "governance_registration_parameters" in auxiliary_data: + governance_registration = auxiliary_data["governance_registration_parameters"] if not all( - k in catalyst_registration for k in REQUIRED_FIELDS_CATALYST_REGISTRATION + k in governance_registration + for k in REQUIRED_FIELDS_GOVERNANCE_REGISTRATION ): raise AUXILIARY_DATA_MISSING_FIELDS_ERROR - catalyst_registration_parameters = ( - messages.CardanoCatalystRegistrationParametersType( - voting_public_key=bytes.fromhex( - catalyst_registration["voting_public_key"] + serialization_format = governance_registration.get("format") + + delegations = [] + for delegation in governance_registration.get("delegations", []): + if not all(k in delegation for k in REQUIRED_FIELDS_GOVERNANCE_DELEGATION): + raise AUXILIARY_DATA_MISSING_FIELDS_ERROR + delegations.append( + messages.CardanoGovernanceRegistrationDelegation( + voting_public_key=bytes.fromhex(delegation["voting_public_key"]), + weight=int(delegation["weight"]), + ) + ) + + voting_purpose = None + if serialization_format == messages.CardanoGovernanceRegistrationFormat.CIP36: + voting_purpose = governance_registration.get("voting_purpose") + + governance_registration_parameters = ( + messages.CardanoGovernanceRegistrationParametersType( + voting_public_key=parse_optional_bytes( + governance_registration.get("voting_public_key") ), - staking_path=tools.parse_path(catalyst_registration["staking_path"]), - nonce=catalyst_registration["nonce"], + staking_path=tools.parse_path(governance_registration["staking_path"]), + nonce=governance_registration["nonce"], reward_address_parameters=_parse_address_parameters( - catalyst_registration["reward_address_parameters"], + governance_registration["reward_address_parameters"], str(AUXILIARY_DATA_MISSING_FIELDS_ERROR), ), + format=serialization_format, + delegations=delegations, + voting_purpose=voting_purpose, ) ) - if hash is None and catalyst_registration_parameters is None: + if hash is None and governance_registration_parameters is None: raise AUXILIARY_DATA_MISSING_FIELDS_ERROR return messages.CardanoTxAuxiliaryData( hash=hash, - catalyst_registration_parameters=catalyst_registration_parameters, + governance_registration_parameters=governance_registration_parameters, ) diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 29d76459092..216419e648e 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -27,6 +27,12 @@ PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +TESTNET_CHOICES = { + "preprod": "testnet_preprod", + "preview": "testnet_preview", + "legacy": "testnet_legacy", +} + @click.group(name="cardano") def cli() -> None: @@ -46,7 +52,7 @@ def cli() -> None: "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] ) @click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"]) -@click.option("-t", "--testnet", is_flag=True) +@click.option("-t", "--testnet", type=ChoiceType(TESTNET_CHOICES)) @click.option( "-D", "--derivation-type", @@ -61,7 +67,7 @@ def sign_tx( signing_mode: messages.CardanoTxSigningMode, protocol_magic: int, network_id: int, - testnet: bool, + testnet: str, derivation_type: messages.CardanoDerivationType, include_network_id: bool, ) -> cardano.SignTxResponse: @@ -69,7 +75,7 @@ def sign_tx( transaction = json.load(file) if testnet: - protocol_magic = cardano.PROTOCOL_MAGICS["testnet"] + protocol_magic = cardano.PROTOCOL_MAGICS[testnet] network_id = cardano.NETWORK_IDS["testnet"] inputs = [cardano.parse_input(input) for input in transaction["inputs"]] @@ -156,9 +162,11 @@ def sign_tx( auxiliary_data_supplement["auxiliary_data_hash"] = auxiliary_data_supplement[ "auxiliary_data_hash" ].hex() - catalyst_signature = auxiliary_data_supplement.get("catalyst_signature") - if catalyst_signature: - auxiliary_data_supplement["catalyst_signature"] = catalyst_signature.hex() + governance_signature = auxiliary_data_supplement.get("governance_signature") + if governance_signature: + auxiliary_data_supplement[ + "governance_signature" + ] = governance_signature.hex() sign_tx_response["auxiliary_data_supplement"] = auxiliary_data_supplement return sign_tx_response @@ -183,7 +191,7 @@ def sign_tx( "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] ) @click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"]) -@click.option("-e", "--testnet", is_flag=True) +@click.option("-e", "--testnet", type=ChoiceType(TESTNET_CHOICES)) @click.option( "-D", "--derivation-type", @@ -205,7 +213,7 @@ def get_address( protocol_magic: int, network_id: int, show_display: bool, - testnet: bool, + testnet: str, derivation_type: messages.CardanoDerivationType, ) -> str: """ @@ -223,7 +231,7 @@ def get_address( Byron, enterprise and reward addresses only require the general parameters. """ if testnet: - protocol_magic = cardano.PROTOCOL_MAGICS["testnet"] + protocol_magic = cardano.PROTOCOL_MAGICS[testnet] network_id = cardano.NETWORK_IDS["testnet"] staking_key_hash_bytes = cardano.parse_optional_bytes(staking_key_hash) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 2c08eb50d18..5a55c71edac 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -405,7 +405,12 @@ class CardanoPoolRelayType(IntEnum): class CardanoTxAuxiliaryDataSupplementType(IntEnum): NONE = 0 - CATALYST_REGISTRATION_SIGNATURE = 1 + GOVERNANCE_REGISTRATION_SIGNATURE = 1 + + +class CardanoGovernanceRegistrationFormat(IntEnum): + CIP15 = 0 + CIP36 = 1 class CardanoTxSigningMode(IntEnum): @@ -2591,43 +2596,69 @@ def __init__( self.key_hash = key_hash -class CardanoCatalystRegistrationParametersType(protobuf.MessageType): +class CardanoGovernanceRegistrationDelegation(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { 1: protobuf.Field("voting_public_key", "bytes", 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 CardanoGovernanceRegistrationParametersType(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", "CardanoGovernanceRegistrationFormat", repeated=False, required=False), + 6: protobuf.Field("delegations", "CardanoGovernanceRegistrationDelegation", repeated=True, required=False), + 7: protobuf.Field("voting_purpose", "uint64", repeated=False, required=False), } def __init__( self, *, - voting_public_key: "bytes", reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: Optional[Sequence["int"]] = None, + delegations: Optional[Sequence["CardanoGovernanceRegistrationDelegation"]] = None, + voting_public_key: Optional["bytes"] = None, + format: Optional["CardanoGovernanceRegistrationFormat"] = CardanoGovernanceRegistrationFormat.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["CardanoGovernanceRegistrationDelegation"] = 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): MESSAGE_WIRE_TYPE = 327 FIELDS = { - 1: protobuf.Field("catalyst_registration_parameters", "CardanoCatalystRegistrationParametersType", repeated=False, required=False), + 1: protobuf.Field("governance_registration_parameters", "CardanoGovernanceRegistrationParametersType", repeated=False, required=False), 2: protobuf.Field("hash", "bytes", repeated=False, required=False), } def __init__( self, *, - catalyst_registration_parameters: Optional["CardanoCatalystRegistrationParametersType"] = None, + governance_registration_parameters: Optional["CardanoGovernanceRegistrationParametersType"] = None, hash: Optional["bytes"] = None, ) -> None: - self.catalyst_registration_parameters = catalyst_registration_parameters + self.governance_registration_parameters = governance_registration_parameters self.hash = hash @@ -2705,7 +2736,7 @@ class CardanoTxAuxiliaryDataSupplement(protobuf.MessageType): FIELDS = { 1: protobuf.Field("type", "CardanoTxAuxiliaryDataSupplementType", repeated=False, required=True), 2: protobuf.Field("auxiliary_data_hash", "bytes", repeated=False, required=False), - 3: protobuf.Field("catalyst_signature", "bytes", repeated=False, required=False), + 3: protobuf.Field("governance_signature", "bytes", repeated=False, required=False), } def __init__( @@ -2713,11 +2744,11 @@ def __init__( *, type: "CardanoTxAuxiliaryDataSupplementType", auxiliary_data_hash: Optional["bytes"] = None, - catalyst_signature: Optional["bytes"] = None, + governance_signature: Optional["bytes"] = None, ) -> None: self.type = type self.auxiliary_data_hash = auxiliary_data_hash - self.catalyst_signature = catalyst_signature + self.governance_signature = governance_signature class CardanoTxWitnessRequest(protobuf.MessageType): diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index c7dfb4a72cc..1afe11d359b 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -159,8 +159,8 @@ def _transform_expected_result(result): "type": supplement["type"], "auxiliary_data_hash": bytes.fromhex(supplement["auxiliary_data_hash"]), } - if catalyst_signature := supplement.get("catalyst_signature"): + if governance_signature := supplement.get("governance_signature"): transformed_result["auxiliary_data_supplement"][ - "catalyst_signature" - ] = bytes.fromhex(catalyst_signature) + "governance_signature" + ] = bytes.fromhex(governance_signature) return transformed_result diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 06f75d90899..45590c6331d 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1171,8 +1171,10 @@ "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_auxiliary_data_hash]": "53b24d40fc3ed95b8f102e8c5f2b885836eb5c56dd8aa0fdf3116bfb7187813c", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_p-3c7243e1": "54266c3f100fae1303bb579bb3b731a24908eee78c634b1ba92af0dfbcf19d82", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_s-20438873": "b616c4005ee1a2728953ceb3a154e578d8ce53a908b6f11e8598290a3842f08a", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_catalyst_registration]": "3abfe7b0f3de4adacf9d07653eb0d3c80eb4c8876dc456f8c88b2e68ba14850c", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_everything_set_except_pool_r-1e1ef130": "9130bf5c2da6543833a3c841b18e649768f5c27108695a6227b0cb747a34be3d", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip15_governance_registration]": "c4236858bd8dd94f5db41ee3ff3a02e31b3b576c7a1c725dfdbf0f3b8a273305", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_governance_registratio-35e04232": "2651ec619d75e4380f088db746ae1f68fa4fe74fdfb5c566d0738187a9afb624", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_governance_registratio-f00c1b65": "e219985044bc8ef2e6c6971dc434c3120364d83aa863fd66cacff9652ee46311", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_everything_set_except_pool_r-1e1ef130": "33bf8795141702aba3b0360b95a99fac8fab36bda607cd37b1b7ed689f0fcb1d", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration]": "fea08104b2228ba952c9f0ccb64a88fee1c1fc91aa1fca951e812d1a50f7c771", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_and_withdrawal]": "fea08104b2228ba952c9f0ccb64a88fee1c1fc91aa1fca951e812d1a50f7c771", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_with_ac-9ca046f0": "a05ee17a405d989076f31a3aea3f83ffc9fa0ad266dfcd6e9106b064a7d3279b", @@ -1283,8 +1285,9 @@ "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_protocol_magic_with_mainnet_network_id]": "9e1f554bb74f847e8f09201dda808fd1e0cdb68737515f34d6ad435957671c77", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_transaction_with_mainnet_output]": "5a608b731c43231bcae0a856e74497612ccfeff809e5022dfb27798863e1bfb1", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[total_collateral_is_present]": "9e1f554bb74f847e8f09201dda808fd1e0cdb68737515f34d6ad435957671c77", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_auxiliary_data_b-64274ac4": "2b85ec100a491345e0084c4efc226f60645fb47c56fb84ff133b2cb850cdd1f2", -"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_catalyst_registration-11533421": "2b85ec100a491345e0084c4efc226f60645fb47c56fb84ff133b2cb850cdd1f2", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_auxiliary_data_b-d83df998": "2b85ec100a491345e0084c4efc226f60645fb47c56fb84ff133b2cb850cdd1f2", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_voting_public_ke-0ded7777": "2b85ec100a491345e0084c4efc226f60645fb47c56fb84ff133b2cb850cdd1f2", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_governance_registrati-efb78b43": "2b85ec100a491345e0084c4efc226f60645fb47c56fb84ff133b2cb850cdd1f2", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[two_owners_with_path]": "7b2acc6b455cc40edcacdd0806c04040241f3c21205cfd25a3420c5ef65f33c4", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[unsupported_address_type]": "5a608b731c43231bcae0a856e74497612ccfeff809e5022dfb27798863e1bfb1", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[with_multisig_transaction_signing_mode]": "d2ee9af7349df63db617a497532ed03a9fa4c26dac455348cc9e65ca7bc04de7", @@ -1304,6 +1307,7 @@ "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[ordinary_transaction_with_output-9ba7352d": "3190bb90a5a30681ee8854b1a1e68130b2399b411aff9cb100ef05a7aed9543d", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[plutus_transaction_with_reference_input]": "27e3cda4e4c30e4e26d8bbf05309b7242d2884b4435551242110de665d9d050a", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[plutus_transaction_with_total_co-e846c221": "90dc92ab19e76b77afae69a0580a10086b6bfaf42a10ed3623608430466546b0", +"TT_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[transaction_with_cip36_governanc-36bf9253": "4e0664cff1e6e6a34907889bb6f18d9991b1f8f751b940af2c50f20dc4608aed", "TT_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[transaction_with_stake_deregistr-6e84da2f": "ca96bc67bc62a581af51bae76f75be98068f1157b2be9f1579a631c6e2008591", "TT_eos-test_get_public_key.py::test_eos_get_public_key": "02f08c137210d095c604b3382a57167dcf51a5f45ec5f63f79818851154e100e", "TT_eos-test_signtx.py::test_eos_signtx_buyram": "d9dd2567542e4c6a954ea9c98f5df7f9fd63e08c3009a4d773be39f07f05afbf", @@ -2232,8 +2236,8 @@ "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_auxiliary_data_hash]": "64c54a2ab8597b5c61fdeb6f2ff0175f5bd670865789a616e663bbdc61f06475", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_p-3c7243e1": "9856b9732f1e5a6e988d0a6e77b70f641899aa417af0759c8790d0e8b5a25e63", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_s-20438873": "931413769637b60239c7c779da17f9ecf6cd1510b1062ed211219cfd200c9bda", -"TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_catalyst_registration]": "dd1cbea03a9a6d994372394c79b8b76a45654c4db23f3245eb0e85ba3fbe331b", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_everything_set_except_pool_r-1e1ef130": "efe16298308e9d22b5621d55444b34e037879a67c79bbcb32cd62b783149e9c7", +"TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_governance_registration]": "dd1cbea03a9a6d994372394c79b8b76a45654c4db23f3245eb0e85ba3fbe331b", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration]": "1ffbcf96b0cce80bb86b325d6ac3eebc7b8528197d20a53ef8eb25eb420cf7c1", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_and_withdrawal]": "1ffbcf96b0cce80bb86b325d6ac3eebc7b8528197d20a53ef8eb25eb420cf7c1", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_with_ac-9ca046f0": "1abe4090acc9b8bd0081da73cec37c99f35d2d75001931b051c3187b9872a4a4", @@ -2345,7 +2349,7 @@ "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_transaction_with_mainnet_output]": "cbfd7d957541a2ab43c47ccdcf2fa61893d817f888c2ce65af0af14b61c11425", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[total_collateral_is_present]": "6f59841dc5e3597d0940a7b4be0813b25555652180043634c9018272c5a22a3a", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_auxiliary_data_b-64274ac4": "f71fe6d2af5c42cd6c75af8249deb3f14494ffa33e523469037b49cb13311991", -"TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_catalyst_registration-11533421": "f71fe6d2af5c42cd6c75af8249deb3f14494ffa33e523469037b49cb13311991", +"TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_governance_registration-11533421": "f71fe6d2af5c42cd6c75af8249deb3f14494ffa33e523469037b49cb13311991", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[two_owners_with_path]": "4ce6a2c9ffd6e26da53fa0a303b5a60c5d1bc4b0ec390fe70f08250fd0635ec2", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[unsupported_address_type]": "cbfd7d957541a2ab43c47ccdcf2fa61893d817f888c2ce65af0af14b61c11425", "TTui2_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[with_multisig_transaction_signing_mode]": "1046e4587492a3ca32656c7f772db75462abd816d36f60e33022a783a8f155ea",