diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 266224aaf93..fe7c04d92f3 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -310,6 +310,7 @@ message CardanoPoolParametersType { optional bytes pool = 3; // pool hash optional CardanoPoolParametersType pool_parameters = 4; // used for stake pool registration certificate optional bytes script_hash = 5; // stake credential script hash + optional bytes key_hash = 6; // stake credential key hash } /** @@ -320,6 +321,7 @@ message CardanoTxWithdrawal { repeated uint32 path = 1; // stake credential key path required uint64 amount = 2; optional bytes script_hash = 3; // stake credential script hash + optional bytes key_hash = 4; // stake credential key hash } /** diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index d4d0f3d8116..aa4ab4ce0a6 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -673,6 +673,45 @@ "error_message": "Invalid certificate" } }, + { + "description": "Certificate has key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid certificate" + } + }, { "description": "Certificate has both path and script_hash", "parameters": { @@ -713,6 +752,46 @@ "error_message": "Invalid certificate" } }, + { + "description": "Certificate has both path and key_hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "path": "m/1852'/1815'/0'/0/0", + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid certificate" + } + }, { "description": "Certificate has invalid pool size", "parameters": { @@ -870,6 +949,45 @@ "error_message": "Invalid withdrawal" } }, + { + "description": "Withdrawal has key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, { "description": "Withdrawal amount is too large", "parameters": { @@ -949,6 +1067,46 @@ "error_message": "Invalid withdrawal" } }, + { + "description": "Withdrawal contains both path and key_hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "path": "m/1852'/1815'/0'/2/0", + "amount": "1000", + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, { "description": "Auxiliary data hash has incorrect length", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json index 3ce4ce42992..cee6bbe0ee2 100644 --- a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json @@ -14,7 +14,49 @@ "certificates": [ { "type": 0, - "path": "m/1852'/1815'/0'/0/0" + "path": "m/1852'/1815'/0'/2/0" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Multisig transaction with stake registration certificate containing a key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" } ], "withdrawals": [], @@ -56,7 +98,52 @@ "certificates": [ { "type": 1, - "path": "m/1852'/1815'/0'/0/0" + "path": "m/1852'/1815'/0'/2/0" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1854'/1815'/2'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Multisig transaction with stake deregistration certificate containing a key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" } ], "withdrawals": [], @@ -101,7 +188,7 @@ "certificates": [ { "type": 2, - "path": "m/1852'/1815'/0'/0/0", + "path": "m/1852'/1815'/0'/2/0", "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" } ], @@ -134,6 +221,133 @@ "error_message": "Invalid certificate" } }, + { + "description": "Multisig transaction with stake delegation certificate containing a key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 2, + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Multisig transaction with withdrawal containing a path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "path": "m/1852'/1815'/0'/2/0", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, + { + "description": "Multisig transaction with withdrawal containing a key hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, { "description": "Multisig transaction with repeated withdrawal", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx.plutus.json b/common/tests/fixtures/cardano/sign_tx.plutus.json index 6d33b822fa0..0d906a7eb4f 100644 --- a/common/tests/fixtures/cardano/sign_tx.plutus.json +++ b/common/tests/fixtures/cardano/sign_tx.plutus.json @@ -721,6 +721,164 @@ ] } }, + { + "description": "Plutus transaction with stake credentials given as key paths", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "path": "m/1852'/1815'/0'/2/0" + } + ], + "withdrawals": [ + { + "amount": "1000", + "path": "m/1852'/1815'/0'/2/0" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": "d593fd793c377ac50a3169bb8378ffc257c944da31aa8f355dfa5a4f6ff89e02", + "collateral_inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0 + } + ], + "required_signers": [ + { + "key_path": "m/1852'/1815'/0'/0/1" + }, + { + "key_path": "m/1854'/1815'/0'/2/0" + } + ], + "signing_mode": "PLUTUS_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "tx_hash": "81c254cf9af40f9785128eb825c5544af86748d67e79a8e70d4216a230a00f72", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "003e2c30e01bdd1630f80850944dab6e35665952408f2bc65b1d6ce8d84ecba80f8a683914e71cf583cd542179ccf9f041765e50b1d2c456ecf7a32675c50d02", + "chain_code": null + }, + { + "type": 1, + "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", + "signature": "f2573c5538a9d27f3a7c87df0c97107c5b8ff7c3fcde8d3d3ca93719ea0c2957429236fb77d79ab4d6638944896741eb863fdad0983920f88b96cf5b916b2b01", + "chain_code": null + }, + { + "type": 1, + "pub_key": "bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e", + "signature": "d4f0a6380d6246694e3451450dc0eb143a1336ff148193fb817f3d27c18bb09c41191bac498effcb0722f625e6c962c94203b13ff1a68a41902dfa95f5bc640b", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "d6516805600fcdde33399ad26df35bb50d9e33be4fb5de70877c36ed659ad54d21bd2cb475e77640e4df770981e5f6ac69eca1bf28dc2bcb4a520b7e0362ce06", + "chain_code": null + } + ] + } + }, + { + "description": "Plutus transaction with stake credentials given as key hashes", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [ + { + "amount": "1000", + "key_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": "d593fd793c377ac50a3169bb8378ffc257c944da31aa8f355dfa5a4f6ff89e02", + "collateral_inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0 + } + ], + "required_signers": [ + { + "key_path": "m/1852'/1815'/0'/0/1" + }, + { + "key_path": "m/1854'/1815'/0'/2/0" + } + ], + "signing_mode": "PLUTUS_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "tx_hash": "1223d1192249b003c5c9b3e4f7ff8705c68abfbba6a04ccbee7368a2b9205692", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "f200af7b376c0b01d944ee2ce8a45a4af4d2b66b958b24bd3c9d9ee6ad873975cc5fb7497d59b54c5751f4d9909f9e730752cded8464c9e3fa68c33df8436907", + "chain_code": null + }, + { + "type": 1, + "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", + "signature": "44625e416fd5554cd1ef64e0d6bd4d305282746def5320b38dabeede04a17b3cafdb0bc119143092d5d5385f60b2eed234b9632c819ffa81df7c53153f5c8c0e", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "0bd87578a02ba23d78ea9777b3bc2dc19c59eb12b2a95297f5130e2c59871ec0a1166b8249b27dec80338cbc35c90c21b2c3c989b754523adaa69e98660d9f04", + "chain_code": null + } + ] + } + }, { "description": "Plutus transaction with output datum hash", "parameters": { diff --git a/core/src/apps/cardano/certificates.py b/core/src/apps/cardano/certificates.py index 522d2f70e36..926e93f7e05 100644 --- a/core/src/apps/cardano/certificates.py +++ b/core/src/apps/cardano/certificates.py @@ -63,7 +63,11 @@ def validate_certificate( CardanoCertificateType.STAKE_DEREGISTRATION, ): validate_stake_credential( - certificate.path, certificate.script_hash, signing_mode, INVALID_CERTIFICATE + certificate.path, + certificate.script_hash, + certificate.key_hash, + signing_mode, + INVALID_CERTIFICATE, ) if certificate.type == CardanoCertificateType.STAKE_DELEGATION: @@ -81,8 +85,6 @@ def validate_certificate( def _validate_certificate_structure(certificate: CardanoTxCertificate) -> None: - path = certificate.path - script_hash = certificate.script_hash pool = certificate.pool pool_parameters = certificate.pool_parameters @@ -90,7 +92,12 @@ def _validate_certificate_structure(certificate: CardanoTxCertificate) -> None: CardanoCertificateType.STAKE_REGISTRATION: (pool, pool_parameters), CardanoCertificateType.STAKE_DELEGATION: (pool_parameters,), CardanoCertificateType.STAKE_DEREGISTRATION: (pool, pool_parameters), - CardanoCertificateType.STAKE_POOL_REGISTRATION: (path, script_hash, pool), + CardanoCertificateType.STAKE_POOL_REGISTRATION: ( + certificate.path, + certificate.script_hash, + certificate.key_hash, + pool, + ), } if certificate.type not in fields_to_be_empty or any( @@ -109,14 +116,20 @@ def cborize_certificate( return ( certificate.type, cborize_certificate_stake_credential( - keychain, certificate.path, certificate.script_hash + keychain, + certificate.path, + certificate.script_hash, + certificate.key_hash, ), ) elif certificate.type == CardanoCertificateType.STAKE_DELEGATION: return ( certificate.type, cborize_certificate_stake_credential( - keychain, certificate.path, certificate.script_hash + keychain, + certificate.path, + certificate.script_hash, + certificate.key_hash, ), certificate.pool, ) @@ -125,10 +138,13 @@ def cborize_certificate( def cborize_certificate_stake_credential( - keychain: seed.Keychain, path: list[int], script_hash: bytes | None + keychain: seed.Keychain, + path: list[int], + script_hash: bytes | None, + key_hash: bytes | None, ) -> tuple[int, bytes]: - if path: - return 0, get_public_key_hash(keychain, path) + if key_hash or path: + return 0, key_hash or get_public_key_hash(keychain, path) if script_hash: return 1, script_hash diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index f41bb79d680..4b6ccebea18 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -90,14 +90,18 @@ def derive_public_key( def validate_stake_credential( path: list[int], script_hash: bytes | None, + key_hash: bytes | None, signing_mode: CardanoTxSigningMode, error: wire.ProcessError, ) -> None: - if path and script_hash: + if sum(bool(k) for k in (path, script_hash, key_hash)) != 1: raise error if path: - if signing_mode != CardanoTxSigningMode.ORDINARY_TRANSACTION: + if signing_mode not in ( + CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ): raise error if not SCHEMA_STAKING_ANY_ACCOUNT.match(path): raise error @@ -109,5 +113,10 @@ def validate_stake_credential( 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: raise error diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index 4f08ec7523d..57880f1ba2a 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -457,19 +457,11 @@ async def confirm_certificate( props: list[PropertyType] = [ ("Confirm:", CERTIFICATE_TYPE_NAMES[certificate.type]), + _format_stake_credential( + certificate.path, certificate.script_hash, certificate.key_hash + ), ] - if certificate.path: - props.append( - ( - f"for account {format_account_number(certificate.path)}:", - address_n_to_str(to_account_path(certificate.path)), - ), - ) - else: - assert certificate.script_hash is not None # validate_certificate - props.append(("for script:", format_script_hash(certificate.script_hash))) - if certificate.type == CardanoCertificateType.STAKE_DELEGATION: assert certificate.pool is not None # validate_certificate props.append(("to pool:", format_stake_pool_id(certificate.pool))) @@ -613,21 +605,12 @@ async def confirm_withdrawal( ) -> None: props: list[PropertyType] = [ ("Confirm withdrawal", None), + _format_stake_credential( + withdrawal.path, withdrawal.script_hash, withdrawal.key_hash + ), + ("Amount:", format_coin_amount(withdrawal.amount)), ] - if withdrawal.path: - props.append( - ( - f"for account {format_account_number(withdrawal.path)}:", - address_n_to_str(to_account_path(withdrawal.path)), - ) - ) - else: - assert withdrawal.script_hash is not None # validate_withdrawal - props.append(("for script:", format_script_hash(withdrawal.script_hash))) - - props.append(("Amount:", format_coin_amount(withdrawal.amount))) - await confirm_properties( ctx, "confirm_withdrawal", @@ -637,6 +620,23 @@ async def confirm_withdrawal( ) +def _format_stake_credential( + path: list[int], script_hash: bytes | None, key_hash: bytes | None +) -> tuple[str, str]: + if path: + return ( + f"for account {format_account_number(path)}:", + address_n_to_str(to_account_path(path)), + ) + elif key_hash: + return ("for key hash:", format_key_hash(key_hash, False)) + elif script_hash: + return ("for script:", format_script_hash(script_hash)) + else: + # should be unreachable unless there's a bug in validation + raise ValueError + + async def confirm_catalyst_registration( ctx: wire.Context, public_key: str, diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 75acf9e164d..bcea1f0cafd 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -512,9 +512,6 @@ async def _process_certificates( account_path_checker: AccountPathChecker, ) -> None: """Read, validate, confirm and serialize the certificates.""" - if certificates_count == 0: - return - for _ in range(certificates_count): certificate: CardanoTxCertificate = await ctx.call( CardanoTxItemAck(), CardanoTxCertificate @@ -1001,7 +998,6 @@ async def _show_certificate( CardanoTxSigningMode.MULTISIG_TRANSACTION, CardanoTxSigningMode.PLUTUS_TRANSACTION, ): - assert certificate.script_hash # validate_certificate await confirm_certificate(ctx, certificate) elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: await _show_stake_pool_registration_certificate(ctx, certificate) @@ -1019,15 +1015,16 @@ def _validate_withdrawal( previous_reward_address: bytes, ) -> None: validate_stake_credential( - withdrawal.path, withdrawal.script_hash, signing_mode, INVALID_WITHDRAWAL + withdrawal.path, + withdrawal.script_hash, + withdrawal.key_hash, + signing_mode, + INVALID_WITHDRAWAL, ) if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: raise INVALID_WITHDRAWAL - credential = tuple(withdrawal.path) if withdrawal.path else withdrawal.script_hash - assert credential # validate_stake_credential - reward_address = _derive_withdrawal_reward_address_bytes( keychain, withdrawal, protocol_magic, network_id ) @@ -1073,7 +1070,7 @@ def _derive_withdrawal_reward_address_bytes( ) -> bytes: reward_address_type = ( CardanoAddressType.REWARD - if withdrawal.path + if withdrawal.path or withdrawal.key_hash else CardanoAddressType.REWARD_SCRIPT ) return derive_address_bytes( @@ -1081,6 +1078,7 @@ def _derive_withdrawal_reward_address_bytes( 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, diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 256f6f55df1..591d14af3ad 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -1431,6 +1431,7 @@ class CardanoTxCertificate(protobuf.MessageType): pool: "bytes | None" pool_parameters: "CardanoPoolParametersType | None" script_hash: "bytes | None" + key_hash: "bytes | None" def __init__( self, @@ -1440,6 +1441,7 @@ def __init__( pool: "bytes | None" = None, pool_parameters: "CardanoPoolParametersType | None" = None, script_hash: "bytes | None" = None, + key_hash: "bytes | None" = None, ) -> None: pass @@ -1451,6 +1453,7 @@ class CardanoTxWithdrawal(protobuf.MessageType): path: "list[int]" amount: "int" script_hash: "bytes | None" + key_hash: "bytes | None" def __init__( self, @@ -1458,6 +1461,7 @@ def __init__( amount: "int", path: "list[int] | None" = None, script_hash: "bytes | None" = None, + key_hash: "bytes | None" = None, ) -> None: pass diff --git a/core/tests/test_apps.cardano.certificate.py b/core/tests/test_apps.cardano.certificate.py index 69ebc233d93..69402852186 100644 --- a/core/tests/test_apps.cardano.certificate.py +++ b/core/tests/test_apps.cardano.certificate.py @@ -40,6 +40,15 @@ def test_validate_certificate(self): ), CardanoTxSigningMode.PLUTUS_TRANSACTION, ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), ( CardanoTxCertificate( type=CardanoCertificateType.STAKE_DELEGATION, @@ -74,6 +83,18 @@ def test_validate_certificate(self): ), CardanoTxSigningMode.PLUTUS_TRANSACTION, ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), ( CardanoTxCertificate( type=CardanoCertificateType.STAKE_DEREGISTRATION, @@ -99,6 +120,15 @@ def test_validate_certificate(self): ), CardanoTxSigningMode.PLUTUS_TRANSACTION, ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), ( CardanoTxCertificate( type=CardanoCertificateType.STAKE_POOL_REGISTRATION, @@ -142,6 +172,19 @@ def test_validate_certificate(self): ), 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" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), # STAKE_REGISTRATION pool is set ( CardanoTxCertificate( @@ -200,6 +243,22 @@ def test_validate_certificate(self): ), 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" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), # STAKE_DELEGATION pool parameters are set ( CardanoTxCertificate( @@ -244,6 +303,19 @@ def test_validate_certificate(self): ), 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" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), # STAKE_DEREGISTRATION pool is set ( CardanoTxCertificate( @@ -333,6 +405,31 @@ def test_validate_certificate(self): ), CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, ), + # STAKE_POOL_REGISTRATION key hash is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + 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 is set ( CardanoTxCertificate( diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index 52565449d0b..6d570bd9b23 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -312,7 +312,7 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: if "pool" not in certificate: raise CERTIFICATE_MISSING_FIELDS_ERROR - path, script_hash = _parse_path_or_script_hash( + path, script_hash, key_hash = _parse_credential( certificate, CERTIFICATE_MISSING_FIELDS_ERROR ) @@ -322,6 +322,7 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: path=path, pool=bytes.fromhex(certificate["pool"]), script_hash=script_hash, + key_hash=key_hash, ), None, ) @@ -329,13 +330,16 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: messages.CardanoCertificateType.STAKE_REGISTRATION, messages.CardanoCertificateType.STAKE_DEREGISTRATION, ): - path, script_hash = _parse_path_or_script_hash( + path, script_hash, key_hash = _parse_credential( certificate, CERTIFICATE_MISSING_FIELDS_ERROR ) return ( messages.CardanoTxCertificate( - type=certificate_type, path=path, script_hash=script_hash + type=certificate_type, + path=path, + script_hash=script_hash, + key_hash=key_hash, ), None, ) @@ -387,16 +391,17 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: raise ValueError("Unknown certificate type") -def _parse_path_or_script_hash( +def _parse_credential( obj, error: ValueError -) -> Tuple[List[int], Optional[bytes]]: - if "path" not in obj and "script_hash" not in obj: +) -> Tuple[List[int], Optional[bytes], Optional[bytes]]: + if not any(k in obj for k in ("path", "script_hash", "key_hash")): raise error path = tools.parse_path(obj.get("path")) script_hash = parse_optional_bytes(obj.get("script_hash")) + key_hash = parse_optional_bytes(obj.get("key_hash")) - return path, script_hash + return path, script_hash, key_hash def _parse_pool_owner(pool_owner) -> messages.CardanoPoolOwner: @@ -454,7 +459,7 @@ def parse_withdrawal(withdrawal) -> messages.CardanoTxWithdrawal: if "amount" not in withdrawal: raise WITHDRAWAL_MISSING_FIELDS_ERROR - path, script_hash = _parse_path_or_script_hash( + path, script_hash, key_hash = _parse_credential( withdrawal, WITHDRAWAL_MISSING_FIELDS_ERROR ) @@ -462,6 +467,7 @@ def parse_withdrawal(withdrawal) -> messages.CardanoTxWithdrawal: path=path, amount=int(withdrawal["amount"]), script_hash=script_hash, + key_hash=key_hash, ) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 75cb1979f29..ee9e5ae625d 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -2364,6 +2364,7 @@ class CardanoTxCertificate(protobuf.MessageType): 3: protobuf.Field("pool", "bytes", repeated=False, required=False), 4: protobuf.Field("pool_parameters", "CardanoPoolParametersType", repeated=False, required=False), 5: protobuf.Field("script_hash", "bytes", repeated=False, required=False), + 6: protobuf.Field("key_hash", "bytes", repeated=False, required=False), } def __init__( @@ -2374,12 +2375,14 @@ def __init__( pool: Optional["bytes"] = None, pool_parameters: Optional["CardanoPoolParametersType"] = None, script_hash: Optional["bytes"] = None, + key_hash: Optional["bytes"] = None, ) -> None: self.path = path if path is not None else [] self.type = type self.pool = pool self.pool_parameters = pool_parameters self.script_hash = script_hash + self.key_hash = key_hash class CardanoTxWithdrawal(protobuf.MessageType): @@ -2388,6 +2391,7 @@ class CardanoTxWithdrawal(protobuf.MessageType): 1: protobuf.Field("path", "uint32", repeated=True, required=False), 2: protobuf.Field("amount", "uint64", repeated=False, required=True), 3: protobuf.Field("script_hash", "bytes", repeated=False, required=False), + 4: protobuf.Field("key_hash", "bytes", repeated=False, required=False), } def __init__( @@ -2396,10 +2400,12 @@ def __init__( amount: "int", path: Optional[List["int"]] = None, script_hash: Optional["bytes"] = None, + key_hash: Optional["bytes"] = None, ) -> None: self.path = path if path is not None else [] self.amount = amount self.script_hash = script_hash + self.key_hash = key_hash class CardanoCatalystRegistrationParametersType(protobuf.MessageType):