From 9f9535abb3e1577000e9f6832f3201445d178257 Mon Sep 17 00:00:00 2001 From: David Misiak Date: Fri, 5 Nov 2021 18:30:35 +0100 Subject: [PATCH] feat(cardano): add key hash stake credentials --- common/protob/messages-cardano.proto | 2 + .../fixtures/cardano/sign_tx.failed.json | 162 +++++++++++++ .../cardano/sign_tx.multisig.failed.json | 225 +++++++++++++++++- .../fixtures/cardano/sign_tx.plutus.json | 160 +++++++++++++ core/src/apps/cardano/certificates.py | 34 ++- core/src/apps/cardano/helpers/bech32.py | 2 + core/src/apps/cardano/helpers/credential.py | 38 +-- core/src/apps/cardano/helpers/utils.py | 34 +-- core/src/apps/cardano/layout.py | 74 +++--- core/src/apps/cardano/sign_tx.py | 16 +- core/src/trezor/messages.py | 4 + core/tests/test_apps.cardano.certificate.py | 97 ++++++++ python/src/trezorlib/cardano.py | 22 +- python/src/trezorlib/messages.py | 6 + 14 files changed, 774 insertions(+), 102 deletions(-) diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 8409bb3611..dcf33583e1 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -311,6 +311,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 } /** @@ -321,6 +322,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 054bf8b3e0..87999b53ef 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -692,6 +692,46 @@ "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": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid certificate" + } + }, { "description": "Certificate has both path and script_hash", "parameters": { @@ -733,6 +773,47 @@ "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": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid certificate" + } + }, { "description": "Certificate has invalid pool size", "parameters": { @@ -894,6 +975,46 @@ "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": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, { "description": "Withdrawal amount is too large", "parameters": { @@ -975,6 +1096,47 @@ "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": [], + "include_network_id": false + }, + "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 c43d2a22d0..d6a384abe4 100644 --- a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json @@ -14,7 +14,50 @@ "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" + } + ], + "include_network_id": false + }, + "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": [], @@ -57,7 +100,53 @@ "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" + } + ], + "include_network_id": false + }, + "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": [], @@ -103,7 +192,7 @@ "certificates": [ { "type": 2, - "path": "m/1852'/1815'/0'/0/0", + "path": "m/1852'/1815'/0'/2/0", "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" } ], @@ -137,6 +226,136 @@ "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" + } + ], + "include_network_id": false + }, + "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" + } + ], + "include_network_id": false + }, + "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" + } + ], + "include_network_id": false + }, + "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 ced8565a02..e1b127e93d 100644 --- a/common/tests/fixtures/cardano/sign_tx.plutus.json +++ b/common/tests/fixtures/cardano/sign_tx.plutus.json @@ -731,6 +731,166 @@ ] } }, + { + "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": [], + "include_network_id": false + }, + "result": { + "tx_hash": "717e9b63b08fda304bf7625d5df4149200b28b740db9b66082961a1d2f938ccd", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "f92cc207742fd8303112edaffbac243dfc433778a6711d90f8eaad22b207253c0268226c4895311df975354804d3351403cbc06a01886e954903e71af3d15d06", + "chain_code": null + }, + { + "type": 1, + "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", + "signature": "f606a56f775ed61b67d89be664d2111841251f141cfdc4995567dd6f355d79d77f5160f1053ba74b541d52f12360ae1747b4991c34d1228f47cdef3e72384a05", + "chain_code": null + }, + { + "type": 1, + "pub_key": "bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e", + "signature": "c776b7ec00819b9e37501013309b41ecb10e0235db064e0f7b22d8c230d56cfc14a48330b1ee60675578c30cb79466fa4ade86d049670601fc9dd5f7e310df07", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "1697e336ca218344e9f9f82c19ddbdba6009eebee76b9602ecd43a7d79824c7030d80cb31a363c39d0e520888dc4135bd9b1d647ebef76ba3a816a0e1b45ad07", + "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": [], + "include_network_id": false + }, + "result": { + "tx_hash": "baabb4e6dced60de330a089590ea38b7bbe505bbf9c785ef88078242f0ea9860", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "38b39e71cf45c0bfa0754ab4fa24443fc09cd936f0180a37b4bca9ef72d5a431680d50e02afd93518caa868428d784bbf30b01bfcb55fc0d51e7d2616006510b", + "chain_code": null + }, + { + "type": 1, + "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", + "signature": "c0f3c3c3c8034041e9273bd856f33f2523c6b347b996da58ef7732a0a780a4696846fc6de08662d36581027a0a72d9981c41f0e7cf69afd52990991b69468e07", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "14251ec24c089371cecafd2a54e430b560a0bc029cdb8579368730d1376f911a653722d850e6ce3fb4ae820d02b8771d905d13aea8b5ea23b5045d3b2685f507", + "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 a2eca64889..c66dadc52c 100644 --- a/core/src/apps/cardano/certificates.py +++ b/core/src/apps/cardano/certificates.py @@ -65,7 +65,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: @@ -83,8 +87,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 @@ -92,7 +94,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( @@ -111,14 +118,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, ) @@ -127,10 +140,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/bech32.py b/core/src/apps/cardano/helpers/bech32.py index fdfecdbe8f..3b6321fb26 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -12,6 +12,8 @@ HRP_JORMUN_PUBLIC_KEY = "ed25519_pk" HRP_SCRIPT_HASH = "script" HRP_KEY_HASH = "addr_vkh" HRP_SHARED_KEY_HASH = "addr_shared_vkh" +HRP_STAKE_KEY_HASH = "stake_vkh" +HRP_STAKE_SHARED_KEY_HASH = "stake_shared_vkh" HRP_REQUIRED_SIGNER_KEY_HASH = "req_signer_vkh" HRP_OUTPUT_DATUM_HASH = "datum" HRP_SCRIPT_DATA_HASH = "script_data" diff --git a/core/src/apps/cardano/helpers/credential.py b/core/src/apps/cardano/helpers/credential.py index 3417e4c84a..47c5220fb7 100644 --- a/core/src/apps/cardano/helpers/credential.py +++ b/core/src/apps/cardano/helpers/credential.py @@ -4,7 +4,7 @@ from trezor.enums import CardanoAddressType from ...common.paths import address_n_to_str from .paths import CHAIN_STAKING_KEY, SCHEMA_PAYMENT, SCHEMA_STAKING -from .utils import format_key_hash, format_script_hash, to_account_path +from .utils import bech32, to_account_path if TYPE_CHECKING: from trezor.messages import ( @@ -13,6 +13,9 @@ if TYPE_CHECKING: ) from trezor.ui.layouts import PropertyType +CREDENTIAL_TYPE_PAYMENT: str = "payment" +CREDENTIAL_TYPE_STAKE: str = "stake" + class Credential: """ @@ -57,12 +60,12 @@ class Credential: ) -> "Credential": address_type = address_params.address_type credential = cls( - "payment", - address_type, - address_params.address_n, - None, - address_params.script_payment_hash, - None, + type_name=CREDENTIAL_TYPE_PAYMENT, + address_type=address_type, + path=address_params.address_n, + key_hash=None, + script_hash=address_params.script_payment_hash, + pointer=None, ) if address_type in ( @@ -100,12 +103,12 @@ class Credential: ) -> "Credential": address_type = address_params.address_type credential = cls( - "stake", - address_type, - address_params.address_n_staking, - address_params.staking_key_hash, - address_params.script_staking_hash, - address_params.certificate_pointer, + type_name=CREDENTIAL_TYPE_STAKE, + address_type=address_type, + path=address_params.address_n_staking, + key_hash=address_params.staking_key_hash, + script_hash=address_params.script_staking_hash, + pointer=address_params.certificate_pointer, ) if address_type == CardanoAddressType.BASE: @@ -185,9 +188,14 @@ class Credential: if self.path: return [(None, address_n_to_str(self.path))] elif self.key_hash: - return [(None, format_key_hash(self.key_hash, False))] + hrp = ( + bech32.HRP_KEY_HASH + if self.type_name == CREDENTIAL_TYPE_PAYMENT + else bech32.HRP_STAKE_KEY_HASH + ) + return [(None, bech32.encode(hrp, self.key_hash))] elif self.script_hash: - return [(None, format_script_hash(self.script_hash))] + return [(None, bech32.encode(bech32.HRP_SCRIPT_HASH, self.script_hash))] elif self.pointer: return [ (f"Block: {self.pointer.block_index}", None), diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index a0ddfdc7d8..d5721ed884 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -67,27 +67,6 @@ def format_asset_fingerprint(policy_id: bytes, asset_name_bytes: bytes) -> str: return bech32.encode("asset", fingerprint) -def format_script_hash(script_hash: bytes) -> str: - return bech32.encode(bech32.HRP_SCRIPT_HASH, script_hash) - - -def format_key_hash(key_hash: bytes, is_shared_key: bool) -> str: - hrp = bech32.HRP_SHARED_KEY_HASH if is_shared_key else bech32.HRP_KEY_HASH - return bech32.encode(hrp, key_hash) - - -def format_required_signer_key_hash(required_signer_key_hash: bytes) -> str: - return bech32.encode(bech32.HRP_REQUIRED_SIGNER_KEY_HASH, required_signer_key_hash) - - -def format_output_datum_hash(output_datum_hash: bytes) -> str: - return bech32.encode(bech32.HRP_OUTPUT_DATUM_HASH, output_datum_hash) - - -def format_script_data_hash(script_data_hash: bytes) -> str: - return bech32.encode(bech32.HRP_SCRIPT_DATA_HASH, script_data_hash) - - def get_public_key_hash(keychain: seed.Keychain, path: list[int]) -> bytes: public_key = derive_public_key(keychain, path) return hashlib.blake2b(data=public_key, outlen=ADDRESS_KEY_HASH_SIZE).digest() @@ -104,14 +83,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 @@ -123,5 +106,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 1f1583e185..806b829a15 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -25,16 +25,11 @@ from apps.common.paths import address_n_to_str from . import seed from .address import derive_human_readable_address -from .helpers import protocol_magics +from .helpers import bech32, protocol_magics from .helpers.utils import ( format_account_number, format_asset_fingerprint, - format_key_hash, format_optional_int, - format_output_datum_hash, - format_required_signer_key_hash, - format_script_data_hash, - format_script_hash, format_stake_pool_id, to_account_path, ) @@ -126,7 +121,9 @@ async def show_native_script( if script.type == CardanoNativeScriptType.PUB_KEY: assert script.key_hash is not None or script.key_path # validate_script if script.key_hash: - props.append((None, format_key_hash(script.key_hash, True))) + props.append( + (None, bech32.encode(bech32.HRP_SHARED_KEY_HASH, script.key_hash)) + ) elif script.key_path: props.append((address_n_to_str(script.key_path), None)) elif script.type == CardanoNativeScriptType.N_OF_K: @@ -179,7 +176,9 @@ async def show_script_hash( ctx, "verify_script", title="Verify script", - props=[("Script hash:", format_script_hash(script_hash))], + props=[ + ("Script hash:", bech32.encode(bech32.HRP_SCRIPT_HASH, script_hash)) + ], br_code=ButtonRequestType.Other, ) elif display_format == CardanoNativeScriptHashDisplayFormat.POLICY_ID: @@ -380,7 +379,7 @@ async def show_warning_tx_output_contains_datum_hash( props=[ ( "The following transaction output contains datum hash:", - format_output_datum_hash(datum_hash), + bech32.encode(bech32.HRP_OUTPUT_DATUM_HASH, datum_hash), ), ("\nContinue?", None), ], @@ -476,19 +475,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))) @@ -632,21 +623,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", @@ -656,6 +638,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:", bech32.encode(bech32.HRP_STAKE_KEY_HASH, key_hash)) + elif script_hash: + return ("for script:", bech32.encode(bech32.HRP_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, @@ -734,7 +733,12 @@ async def confirm_script_data_hash(ctx: wire.Context, script_data_hash: bytes) - ctx, "confirm_script_data_hash", title="Confirm transaction", - props=[("Script data hash:", format_script_data_hash(script_data_hash))], + props=[ + ( + "Script data hash:", + bech32.encode(bech32.HRP_SCRIPT_DATA_HASH, script_data_hash), + ) + ], br_code=ButtonRequestType.Other, ) @@ -761,7 +765,7 @@ async def confirm_required_signer( required_signer.key_hash is not None or required_signer.key_path ) # _validate_required_signer formatted_signer = ( - format_required_signer_key_hash(required_signer.key_hash) + bech32.encode(bech32.HRP_REQUIRED_SIGNER_KEY_HASH, required_signer.key_hash) if required_signer.key_hash is not None else address_n_to_str(required_signer.key_path) ) diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 816c82b8de..2c64df3041 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -524,9 +524,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 @@ -1021,7 +1018,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) @@ -1039,15 +1035,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 ) @@ -1093,7 +1090,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( @@ -1101,6 +1098,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 86556d0ab4..4b5f3d050e 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -1528,6 +1528,7 @@ if TYPE_CHECKING: pool: "bytes | None" pool_parameters: "CardanoPoolParametersType | None" script_hash: "bytes | None" + key_hash: "bytes | None" def __init__( self, @@ -1537,6 +1538,7 @@ if TYPE_CHECKING: pool: "bytes | None" = None, pool_parameters: "CardanoPoolParametersType | None" = None, script_hash: "bytes | None" = None, + key_hash: "bytes | None" = None, ) -> None: pass @@ -1548,6 +1550,7 @@ if TYPE_CHECKING: path: "list[int]" amount: "int" script_hash: "bytes | None" + key_hash: "bytes | None" def __init__( self, @@ -1555,6 +1558,7 @@ if TYPE_CHECKING: 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 69ebc233d9..6940285218 100644 --- a/core/tests/test_apps.cardano.certificate.py +++ b/core/tests/test_apps.cardano.certificate.py @@ -40,6 +40,15 @@ class TestCardanoCertificate(unittest.TestCase): ), CardanoTxSigningMode.PLUTUS_TRANSACTION, ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + key_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.PLUTUS_TRANSACTION, + ), ( CardanoTxCertificate( type=CardanoCertificateType.STAKE_DELEGATION, @@ -74,6 +83,18 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 @@ class TestCardanoCertificate(unittest.TestCase): ), 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 b4bf7b8e4e..cef8f94248 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -330,7 +330,7 @@ def parse_certificate(certificate: dict) -> 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 ) @@ -340,6 +340,7 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: path=path, pool=bytes.fromhex(certificate["pool"]), script_hash=script_hash, + key_hash=key_hash, ), None, ) @@ -347,13 +348,16 @@ def parse_certificate(certificate: dict) -> 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, ) @@ -406,16 +410,17 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: raise ValueError("Unknown certificate type") -def _parse_path_or_script_hash( +def _parse_credential( obj: dict, 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: dict) -> messages.CardanoPoolOwner: @@ -473,7 +478,7 @@ def parse_withdrawal(withdrawal: dict) -> 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 ) @@ -481,6 +486,7 @@ def parse_withdrawal(withdrawal: dict) -> 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 e1305b8490..6f44b425fc 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -2490,6 +2490,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__( @@ -2500,12 +2501,14 @@ class CardanoTxCertificate(protobuf.MessageType): pool: Optional["bytes"] = None, pool_parameters: Optional["CardanoPoolParametersType"] = None, script_hash: Optional["bytes"] = None, + key_hash: Optional["bytes"] = None, ) -> None: self.path: Sequence["int"] = 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): @@ -2514,6 +2517,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__( @@ -2522,10 +2526,12 @@ class CardanoTxWithdrawal(protobuf.MessageType): amount: "int", path: Optional[Sequence["int"]] = None, script_hash: Optional["bytes"] = None, + key_hash: Optional["bytes"] = None, ) -> None: self.path: Sequence["int"] = path if path is not None else [] self.amount = amount self.script_hash = script_hash + self.key_hash = key_hash class CardanoCatalystRegistrationParametersType(protobuf.MessageType):