diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 9aa5c60b52f..8e0acb1ff23 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -25,6 +25,21 @@ enum CardanoAddressType { REWARD_SCRIPT = 15; } +enum CardanoNativeScriptType { + PUB_KEY = 0; + ALL = 1; + ANY = 2; + N_OF_K = 3; + INVALID_BEFORE = 4; + INVALID_HEREAFTER = 5; +} + +enum CardanoNativeScriptHashDisplayFormat { + HIDE = 0; + BECH32 = 1; + POLICY_ID = 2; +} + enum CardanoCertificateType { STAKE_REGISTRATION = 0; STAKE_DEREGISTRATION = 1; @@ -46,6 +61,7 @@ enum CardanoTxAuxiliaryDataSupplementType { enum CardanoTxSigningMode { ORDINARY_TRANSACTION = 0; POOL_REGISTRATION_AS_OWNER = 1; + SCRIPT_TRANSACTION = 2; } enum CardanoTxWitnessType { @@ -64,6 +80,39 @@ message CardanoBlockchainPointerType { required uint32 certificate_index = 3; } +/* + * @embed + */ +message CardanoNativeScript { + required CardanoNativeScriptType type = 1; + repeated CardanoNativeScript scripts = 2; + + optional bytes key_hash = 3; + repeated uint32 key_path = 4; + optional uint32 required_signatures_count = 5; + optional uint64 invalid_before = 6; + optional uint64 invalid_hereafter = 7; +} + +/** + * Request: Ask device for Cardano native script hash + * @start + * @next CardanoNativeScriptHash + * @next Failure + */ +message CardanoGetNativeScriptHash { + required CardanoNativeScript script = 1; + required CardanoNativeScriptHashDisplayFormat display_format = 2; // display hash as bech32 or policy id +} + +/** + * Request: Ask device for Cardano native script hash + * @end + */ +message CardanoNativeScriptHash { + required bytes script_hash = 1; +} + /** * Structure to represent address parameters so they can be * reused in CardanoGetAddress and CardanoTxOutputType. @@ -80,6 +129,8 @@ message CardanoAddressParametersType { // can be sent directly e.g. if it doesn't belong to // the same account as address_n optional CardanoBlockchainPointerType certificate_pointer = 5; // a pointer to the staking key registration certificate + optional bytes script_payment_hash = 6; + optional bytes script_staking_hash = 7; } /** @@ -143,6 +194,7 @@ message CardanoSignTxInit { required bool has_auxiliary_data = 10; optional uint64 validity_interval_start = 11; required uint32 witness_requests_count = 12; + required uint32 minting_asset_groups_count = 13; } /** @@ -180,7 +232,8 @@ message CardanoAssetGroup { */ message CardanoToken { required bytes asset_name_bytes = 1; // asset name as bytestring (may be either ascii string or hash) - required uint64 amount = 2; // asset amount + optional uint64 amount = 2; // asset amount + optional sint64 mint_amount = 3; // mint amount (can also be negative in which case the tokens are burnt) } /** @@ -238,9 +291,10 @@ message CardanoPoolParametersType { */ message CardanoTxCertificate { required CardanoCertificateType type = 1; // certificate type - repeated uint32 path = 2; // BIP-32 path to derive (staking) key + repeated uint32 path = 2; // stake credential key path 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 } /** @@ -248,8 +302,9 @@ message CardanoPoolParametersType { * @next CardanoTxItemAck */ message CardanoTxWithdrawal { - repeated uint32 path = 1; + repeated uint32 path = 1; // stake credential key path required uint64 amount = 2; + optional bytes script_hash = 3; // stake credential script hash } /** @@ -272,6 +327,14 @@ message CardanoTxAuxiliaryData { optional bytes hash = 2; } +/** + * Request: Transaction mint + * @next CardanoTxItemAck + */ +message CardanoTxMint { + required uint32 asset_groups_count = 1; +} + /** * Response: Acknowledgement of the last transaction item received * @next CardanoTxInput @@ -284,6 +347,7 @@ message CardanoTxAuxiliaryData { * @next CardanoTxWithdrawal * @next CardanoTxAuxiliaryData * @next CardanoTxWitnessRequest + * @next CardanoTxMint */ message CardanoTxItemAck { } @@ -392,7 +456,7 @@ message CardanoSignTx { message CardanoTokenType { required bytes asset_name_bytes = 1; // asset name as bytestring (may be either ascii string or hash) - required uint64 amount = 2; // asset amount + required uint64 amount = 2; // asset amount } /** diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 995e4c17d1e..32fb336dda1 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -258,7 +258,9 @@ enum MessageType { MessageType_CardanoTxAuxiliaryData = 327 [(wire_in) = true]; MessageType_CardanoPoolOwner = 328 [(wire_in) = true]; MessageType_CardanoPoolRelayParameters = 329 [(wire_in) = true]; - + MessageType_CardanoGetNativeScriptHash = 330 [(wire_in) = true]; + MessageType_CardanoNativeScriptHash = 331 [(wire_out) = true]; + MessageType_CardanoTxMint = 332 [(wire_in) = true]; // Ripple MessageType_RippleGetAddress = 400 [(wire_in) = true]; diff --git a/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json b/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json new file mode 100644 index 00000000000..42699345b05 --- /dev/null +++ b/common/tests/fixtures/cardano/get_base_address_with_script_hashes.json @@ -0,0 +1,80 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "parameters": { + "address_type": "base_script_key", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "staking_path": "m/1852'/1815'/0'/2/0", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1zyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsf42dkl" + } + }, + { + "parameters": { + "address_type": "base_script_key", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "staking_path": "m/1852'/1815'/0'/2/0", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1zqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms2rhd6q" + } + }, + { + "parameters": { + "address_type": "base_key_script", + "path": "m/1852'/1815'/0'/0/0", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1yxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s8vnrtt" + } + }, + { + "parameters": { + "address_type": "base_key_script", + "path": "m/1852'/1815'/0'/0/0", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1yzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sy6wr85" + } + }, + { + "parameters": { + "address_type": "base_script_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1xyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s3gftll" + } + }, + { + "parameters": { + "address_type": "base_script_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1xqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sj75tnq" + } + } + ] +} diff --git a/common/tests/fixtures/cardano/get_enterprise_address.json b/common/tests/fixtures/cardano/get_enterprise_address.json index 3b9a8c6de91..91da4711b4c 100644 --- a/common/tests/fixtures/cardano/get_enterprise_address.json +++ b/common/tests/fixtures/cardano/get_enterprise_address.json @@ -25,6 +25,28 @@ "result": { "expected_address": "addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47" } + }, + { + "parameters": { + "address_type": "enterprise_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr1wyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsqee7sp" + } + }, + { + "parameters": { + "address_type": "enterprise_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test1wqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsm3dzly" + } } ] } diff --git a/common/tests/fixtures/cardano/get_native_script_hash.json b/common/tests/fixtures/cardano/get_native_script_hash.json new file mode 100644 index 00000000000..b6d08a3b99b --- /dev/null +++ b/common/tests/fixtures/cardano/get_native_script_hash.json @@ -0,0 +1,321 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "PUB_KEY script", + "parameters": { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + "result": { + "expected_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + }, + { + "description": "PUB_KEY script containing a path", + "parameters": { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + "result": { + "expected_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + }, + { + "description": "ALL script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6" + } + }, + { + "description": "ALL script containing a path", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6" + } + }, + { + "description": "ALL script containing a 1855 path", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_path": "m/1855'/1815'/0'" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "fbf6672eb655c29b0f148fa1429be57c2174b067a7b3e3942e967fe8" + } + }, + { + "description": "ANY script", + "parameters": { + "type": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db" + } + }, + { + "description": "ANY script with a nested script containing a path", + "parameters": { + "type": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + "result": { + "expected_hash": "d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db" + } + }, + { + "description": "N_OF_K script", + "parameters": { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + "result": { + "expected_hash": "2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9" + } + }, + { + "description": "N_OF_K script containing a path", + "parameters": { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + "result": { + "expected_hash": "2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9" + } + }, + { + "description": "INVALID_BEFORE script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 4, + "invalid_before": 100 + } + ] + }, + "result": { + "expected_hash": "c6262ef9bb2b1291c058d93b46dabf458e2d135f803f60713f84b0b7" + } + }, + { + "description": "INVALID_HEREAFTER script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "b12ac304f89f4cd4d23f59a2b90d2b2697f7540b8f470d6aa05851b5" + } + }, + { + "description": "Nested script", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_path": "m/1854'/1815'/0'/0/0" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + { + "type": 4, + "invalid_before": 100 + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "4a6b4288459bf34668c0b281f922691460caf0c7c09caee3a726c27a" + } + }, + { + "description": "Nested script without paths and shared with Ledger", + "parameters": { + "type": 1, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + } + ] + }, + { + "type": 3, + "required_signatures_count": 2, + "scripts": [ + { + "type": 0, + "key_hash": "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + }, + { + "type": 0, + "key_hash": "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + }, + { + "type": 0, + "key_hash": "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + } + ] + }, + { + "type": 4, + "invalid_before": 100 + }, + { + "type": 5, + "invalid_hereafter": 200 + } + ] + }, + "result": { + "expected_hash": "0d63e8d2c5a00cbcffbdf9112487c443466e1ea7d8c834df5ac5c425" + } + } + ] +} diff --git a/common/tests/fixtures/cardano/get_pointer_address.json b/common/tests/fixtures/cardano/get_pointer_address.json index 18ac59bccce..6da57286c5a 100644 --- a/common/tests/fixtures/cardano/get_pointer_address.json +++ b/common/tests/fixtures/cardano/get_pointer_address.json @@ -31,6 +31,34 @@ "result": { "expected_address": "addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t" } + }, + { + "parameters": { + "address_type": "pointer_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "block_index": 24157, + "tx_index": 177, + "certificate_index": 42, + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "addr12yx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2zmd4yp" + } + }, + { + "parameters": { + "address_type": "pointer_script", + "script_payment_hash": "0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe", + "block_index": 24157, + "tx_index": 177, + "certificate_index": 42, + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "addr_test12qx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2d4sugn" + } } ] } diff --git a/common/tests/fixtures/cardano/get_public_key.json b/common/tests/fixtures/cardano/get_public_key.json index f115ba5cc52..620e97e202d 100644 --- a/common/tests/fixtures/cardano/get_public_key.json +++ b/common/tests/fixtures/cardano/get_public_key.json @@ -57,6 +57,78 @@ "public_key": "be81ace1f63f4f0cae74dd274a72d7818f238bc764ab3e0dc0beb1945b756dca", "chain_code": "29034f036a162ac4f9f9f397b2d1f289754bb6633915f26b199e156f81d05c88" } + }, + { + "parameters": { + "path": "m/1854'/1815'/0'" + }, + "result": { + "public_key": "99694f772a1cffa68ad4a8d911e649cfe990aee720d26893211a8818c4566a42", + "chain_code": "9c3372fcecd832bfe2418319e473b981cdd641e0e12bd878b5e94d93fa0b1552" + } + }, + { + "parameters": { + "path": "m/1854'/1815'/1'" + }, + "result": { + "public_key": "1b66eb903dc15131c0f39c1ac41ca659115ab790c3e2aef129e3095e406abffe", + "chain_code": "0fcb6522d3aa8ad78370c48e62c97eb1315f4234ded8d8fdd9b941c4e5fda0c0" + } + }, + { + "parameters": { + "path": "m/1854'/1815'/2'" + }, + "result": { + "public_key": "29f3535b40978eda8038de3fe85cf08efecff5e565a3dc745660a43a172aaa0a", + "chain_code": "b82dbc91a252a93ec6d3f9e418a4e779c848162777367f729468ec23f409304e" + } + }, + { + "parameters": { + "path": "m/1854'/1815'/3'" + }, + "result": { + "public_key": "bdc40afecb357a08538cd98878b11d0afa7a0a36a8c5f71be1b591ae09088149", + "chain_code": "c4e0b579c132de4204d19f12260ac6bede8ae67f0fe15b9098d4f32a42314924" + } + }, + { + "parameters": { + "path": "m/1855'/1815'/0'" + }, + "result": { + "public_key": "b75258e4f61eb7b313d8554c2fe10673cf214ca2d762bfd53ec3b7846e2ee872", + "chain_code": "9ad37dc23fe1cda7b0ac5574c6f16171cdb2bb723496954770a2bf0e08334e8f" + } + }, + { + "parameters": { + "path": "m/1855'/1815'/1'" + }, + "result": { + "public_key": "a54627d6d16724032172541d4261e7aa87c06395724f1d18975a21d56650bda9", + "chain_code": "bccfd881a3bb7eefbfb885a80909c30892a9a2151f7530c10c68ec6e7a89b28a" + } + }, + { + "parameters": { + "path": "m/1855'/1815'/2'" + }, + "result": { + "public_key": "5307953b17d4195e0b87d61731339dba2c7d543f621d87c1bd73466328372345", + "chain_code": "0e3314c9499b6efec4fa48cd5068dd549ed5ee524c300e754ebc0a63c3bc06ff" + } + }, + { + "parameters": { + "path": "m/1855'/1815'/3'" + }, + "result": { + "public_key": "b404bb5bf674a38ebd6141416df8e1aea9ed04c2ebea659c29145d24db7e67db", + "chain_code": "2dc07d8eda29c2a806295ee42dc1f3bf63814d0c9ee978a24827575dcb5a53bb" + } } ] } diff --git a/common/tests/fixtures/cardano/get_reward_address.json b/common/tests/fixtures/cardano/get_reward_address.json index 69054dddc91..5ea9454a930 100644 --- a/common/tests/fixtures/cardano/get_reward_address.json +++ b/common/tests/fixtures/cardano/get_reward_address.json @@ -6,8 +6,8 @@ "tests": [ { "parameters": { - "path": "m/1852'/1815'/0'/2/0", "address_type": "reward", + "staking_path": "m/1852'/1815'/0'/2/0", "network_id": 1, "protocol_magic": 764824073 }, @@ -17,14 +17,36 @@ }, { "parameters": { - "path": "m/1852'/1815'/0'/2/0", "address_type": "reward", + "staking_path": "m/1852'/1815'/0'/2/0", "network_id": 0, "protocol_magic": 42 }, "result": { "expected_address": "stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq" } + }, + { + "parameters": { + "address_type": "reward_script", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 1, + "protocol_magic": 764824073 + }, + "result": { + "expected_address": "stake17xxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gt5mad4" + } + }, + { + "parameters": { + "address_type": "reward_script", + "script_staking_hash": "8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9", + "network_id": 0, + "protocol_magic": 42 + }, + "result": { + "expected_address": "stake_test17zxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gv73lfg" + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index 1980d0a629d..aa00c88c7ca 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -27,7 +27,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -56,7 +58,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -85,7 +89,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -114,7 +120,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -143,7 +151,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -172,7 +182,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Fee is out of range" @@ -206,7 +218,9 @@ "amount": "1000000" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Total transaction amount is out of range!" @@ -235,7 +249,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Output address network mismatch" @@ -264,7 +280,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Output address network mismatch" @@ -293,7 +311,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -322,7 +342,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -351,7 +373,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid network id/protocol magic combination!" @@ -380,7 +404,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid network id/protocol magic combination!" @@ -409,7 +435,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid address" @@ -441,7 +469,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid output" @@ -475,7 +505,118 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Certificate has multisig path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "path": "m/1854'/1815'/0'/0/0" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Certificate has script hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "script_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": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Certificate has both path and script_hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "path": "m/1852'/1815'/0'/0/0", + "script_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": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -510,7 +651,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -544,7 +687,81 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, + { + "description": "Withdrawal has multisig path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "path": "m/1854'/1815'/0'/0/0", + "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": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, + { + "description": "Withdrawal has script hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "script_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": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid withdrawal" @@ -578,7 +795,46 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, + { + "description": "Withdrawal contains both path and script_hash", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "path": "m/1852'/1815'/0'/2/0", + "amount": "1000", + "script_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": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid withdrawal" @@ -616,7 +872,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Duplicate withdrawals" @@ -647,7 +905,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid auxiliary data" @@ -686,7 +946,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid auxiliary data" @@ -727,7 +989,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid auxiliary data" @@ -762,7 +1026,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid change output path" @@ -797,7 +1063,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid change output staking path" @@ -831,7 +1099,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate path" @@ -875,7 +1145,9 @@ ] } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid token bundle in output" @@ -924,12 +1196,323 @@ ] } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid token bundle in output" } }, + { + "description": "Additional witness requests in ORDINARY_TRANSACTION", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "1854 input path in ORDINARY_TRANSACTION", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1854'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "1854 change output path in ORDINARY_TRANSACTION", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "addressType": 0, + "path": "m/1854'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ], + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid address parameters" + } + }, + { + "description": "Ordinary transaction with token minting with 1854 path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1234", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "100" + } + ] + }, + { + "policy_id": "a5a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "100" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "100" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-100" + } + ] + }, + { + "policy_id": "a5a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "100" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-100" + } + ] + } + ], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "Ordinary transaction without token minting but with a 1855 additional witness request", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1234" + } + ], + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "Ordinary transaction with long token minting path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-1234" + } + ] + } + ], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, { "description": "Input and change output account mismatch", "parameters": { @@ -959,7 +1542,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid witness request" @@ -993,7 +1578,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid witness request" @@ -1027,7 +1614,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid witness request" @@ -1067,7 +1656,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -1107,7 +1698,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid withdrawal" @@ -1146,7 +1739,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid withdrawal" @@ -1179,7 +1774,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid witness request" @@ -1209,7 +1806,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid witness request" diff --git a/common/tests/fixtures/cardano/sign_tx.json b/common/tests/fixtures/cardano/sign_tx.json index e4f3e0ed6c7..e2a22f0fb7b 100644 --- a/common/tests/fixtures/cardano/sign_tx.json +++ b/common/tests/fixtures/cardano/sign_tx.json @@ -27,7 +27,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6", @@ -69,7 +71,9 @@ "amount": "1000000" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "81b14b7e62972127eb33c0b1198de6430540ad3a98eec621a3194f2baac43a43", @@ -112,7 +116,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "16fe72bb198be423677577e6326f1f648ec5fc11263b072006382d8125a6edda", @@ -164,7 +170,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "7e16a0b47bdfc37abf4ddd3143f7481af07ffe7abd68f752676f5b0b2890d05b", @@ -225,7 +233,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "5ddbb530b8a89e2b08fc91db03950c876c4a9c1c3fb6e628c4cab638b1c97648", @@ -269,7 +279,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "1fc82ce2420c173a0947eaf49af76fcd6f4e400e2bfb5fa152a482ea12dde24b", @@ -313,7 +325,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "abd1b24ac0638251398444ee136110f952738df32a512ce35894f8453d0e8edf", @@ -356,7 +370,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "d1610bb89bece22ed3158738bc1fbb31c6af0685053e2993361e3380f49afad9", @@ -401,7 +417,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "40535fa8f88515f1da008d3cdf544cf9dbf1675c3cb0adb13b74b9293f1b7096", @@ -443,7 +461,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "d3570557b197604109481a80aeb66cd2cfabc57f802ad593bacc12eb658e5d72", @@ -485,7 +505,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "1a3a295908afd8b2afc368071272d6964be6ee0af062bb765aea65ca454dc0c9", @@ -522,7 +544,9 @@ } ], "outputs": [], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "03535791d04fc1b4457fada025f1c1f7778b5c2d7fa580bbac8abd53b85d3255", @@ -569,7 +593,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "439764b5f7e08839881536a3191faeaf111e75d9f00f83b102c5c1c6fa9fcaf9", @@ -617,7 +643,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "3aca1784d151dc75bdbb80fae71bda3f4b26af3f5fd71bd5e9e9bbcdd2b64ad1", @@ -670,7 +698,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "22c67f12e6f6aa0f2f09fd27d472b19c7208ccd7c3af4b09604fd5d462c1de2b", @@ -719,7 +749,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "cc068a25994ef6a90cdab8adfbe302d6f742de9901ba2495dd64a09f2ef951f5", @@ -764,7 +796,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "1875f1d59a53f1cb4c43949867d72bcfd857fa3b64feb88f41b78ddaa1a21cbf", @@ -812,7 +846,9 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "839a587109358e0aa81b8fb3d5fa74665fac303425ec544a4db7f6ba4e882dff", @@ -863,7 +899,9 @@ "amount": "1000000" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "47cf79f20c6c62edb4162b3b232a57afc1bd0b57c7fd8389555276408a004776", @@ -918,7 +956,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "b7269ddc59e4094a6581c653e0d5dc1e553e3a5fb6ffae47d3d094dff1cfe87b", @@ -986,7 +1026,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "0b929def7bd9f44f5602f809bc0f9be30521f6b93d625525cf33b956993bfb22", @@ -1024,7 +1066,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "b621e22f7cb9aac1a70a3362fde88bdfd31fc100e20f3f3c24a7b853536b4f50", @@ -1071,7 +1115,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "00d393f7fc9a8c17b3efccb44dad9d7e15fdaf2d942a3a455b52b5be016066dd", @@ -1120,7 +1166,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "f4b7315ec080d05024d1f7bf6795dd234c6624970d8e272a245702de539feaa2", @@ -1164,7 +1212,9 @@ "amount": "7120787" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "cabc87a76ad8944e8a97a7cbf9c893a77ed7d1bd963c428c3786d663adb7f0dd", @@ -1299,7 +1349,9 @@ "nonce": 22634813 } }, - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "ee0dfef8b97857ebe7aa8935af50e9f8f608ff4054c0c034600750d722d90631", @@ -1335,6 +1387,107 @@ "catalyst_signature": "74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409" } } + }, + { + "description": "Ordinary transaction with token minting", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-1234" + } + ] + } + ], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'" + } + ] + }, + "result": { + "tx_hash": "042c1d3a6eab693d2ea6b186a88aed038159e7eb581da80464bca7339fb9afe0", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "ff10637250efa74970675169585720dd5b663c49ecf523ac6214e11a74858f80ec6ef4c86ea66666ec7102fe78c92bcc4e76d50a7bff1fd9660757e94863ba09", + "chain_code": null + }, + { + "type": 1, + "pub_key": "b75258e4f61eb7b313d8554c2fe10673cf214ca2d762bfd53ec3b7846e2ee872", + "signature": "d42665ef7855bfe6898b440476ec8967f8ce786a30865a27e0c091b912b8fd87cad2f7d2f1adeb0e2a7201f2ca020a41f48fb982cb3b7f278dab848192d42e0d", + "chain_code": null + } + ] + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json new file mode 100644 index 00000000000..9a245b5c752 --- /dev/null +++ b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json @@ -0,0 +1,319 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "Multisig transaction with stake registration certificate containing a path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "path": "m/1852'/1815'/0'/0/0" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Multisig transaction with stake deregistration certificate containing a path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "path": "m/1852'/1815'/0'/0/0" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_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 delegation certificate containing a path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 2, + "path": "m/1852'/1815'/0'/0/0", + "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Multisig transaction with 1852 multisig witness request", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1852'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "Multisig transaction with output containing address parameters", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid output" + } + }, + { + "description": "Multisig transaction without minting but with a 1855 additional witness request", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + }, + { + "description": "Multisig transaction with long token minting path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-1234" + } + ] + } + ], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Invalid witness request" + } + } + ] +} diff --git a/common/tests/fixtures/cardano/sign_tx.multisig.json b/common/tests/fixtures/cardano/sign_tx.multisig.json new file mode 100644 index 00000000000..61a392a5720 --- /dev/null +++ b/common/tests/fixtures/cardano/sign_tx.multisig.json @@ -0,0 +1,443 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "Multisig transaction with token minting", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-1234" + } + ] + } + ], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1855'/1815'/0'" + } + ] + }, + "result": { + "tx_hash": "042c1d3a6eab693d2ea6b186a88aed038159e7eb581da80464bca7339fb9afe0", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "ef08436c998df4fd4aade2ce240d92d8851783b688a949c167aa070e885ffb592943767ddae0b826265a307405cf9865b6f66fbfa2e5a39797950104b7b13d0d", + "chain_code": null + }, + { + "type": 1, + "pub_key": "b75258e4f61eb7b313d8554c2fe10673cf214ca2d762bfd53ec3b7846e2ee872", + "signature": "d42665ef7855bfe6898b440476ec8967f8ce786a30865a27e0c091b912b8fd87cad2f7d2f1adeb0e2a7201f2ca020a41f48fb982cb3b7f278dab848192d42e0d", + "chain_code": null + } + ] + } + }, + { + "description": "Multisig transaction with stake registration certificate", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "tx_hash": "ed9fc2755091fa72b58e9dd06db05cce87c0c6f3962f587d5fc348fe478f0752", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "dccfcce8a2a17673c0e465a60a334eabbe326127d3dd04b727702ea486ed7c231259353c0890cfcb8209169eda7a139aeec42c77ce87231b0b9c250efb64450e", + "chain_code": null + } + ] + } + }, + { + "description": "Multisig transaction with stake registration and stake delegation certificates", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 0, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + }, + { + "type": 2, + "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1854'/1815'/0'/2/0" + } + ] + }, + "result": { + "tx_hash": "26fb07b23368898665829283985ffe6c4cb2ec13758e83f467b78e5061f9619b", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "c3fc7aae0a78b3b888f68775da3b9ba1e5478f2003e8c1f0b558172acd23205f2652e7e021f5041a4a1a785fad4f711ca80a9b39afd2939644d4da47d86f7b05", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "982247b7a3a3625eaae74d4710f0d9a9b4bae6f0e201c31544f056ad3d7e5940e477cedf3f83fa0e37152e5f97585d910296e95395677dee047e204864187f09", + "chain_code": null + } + ] + } + }, + { + "description": "Multisig transaction with stake deregistration", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1854'/1815'/0'/2/0" + } + ] + }, + "result": { + "tx_hash": "c4e70484c964eca910219047542632ac9a9ac81f11f5d5afd8bb1b0ef4366d69", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "059fa17fb8e8302083d110ec4587d6ce80b3bc15baa75e0a2d449df190ce462d0e6ebc67d96f74fa6ce0b149714d1ef24f40c24846fef9d58405c6e2287e540b", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "dc51848d3257f8f6783d6a53736ba638bc62c7098e5ec6d4d2b313520c78c689942f6e2542ba2b6b9749b7a57d4c8658c84fbc5b1e2847159eb0c256298bcd01", + "chain_code": null + } + ] + } + }, + { + "description": "Multisig transaction with stake deregistration and withdrawal", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 1, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "withdrawals": [ + { + "amount": "1000", + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1854'/1815'/0'/2/0" + } + ] + }, + "result": { + "tx_hash": "e02d252c5cad2a4d8f163069cd7f0822c7876d16af9ad8ac2d461655812b2d1b", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "882994b27b1886a2f7ae3b42e08f3ce2c9c5b7d82e467135e0069f396a18f89696e882dbeadce0b3af8a10edbfb55057e6909e8232ac0107cc4fbf647493720b", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "cc119eb4e7f27d5c316a5d1301850a2f3e4d08c267d5422cae8e4f00178a55d053a2288ed0a55fc8ec05bd8c1cd5fee5a713da85d489a2a02ac273866e36ae06", + "chain_code": null + } + ] + } + }, + { + "description": "Multisig transaction with most elements filled and shared with Ledger", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [ + { + "type": 0, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + }, + { + "type": 1, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + }, + { + "type": 2, + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "pool": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + } + ], + "withdrawals": [ + { + "amount": "1000", + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + } + ], + "auxiliary_data": { + "hash": "58ec01578fcdfdc376f09631a7b2adc608eaf57e3720484c7ff37c13cff90fdf" + }, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "0d63e8d2c5a00cbcffbdf9112487c443466e1ea7d8c834df5ac5c425", + "tokens": [ + { + "asset_name_bytes": "74657374436f696e", + "amount": "7878754" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "0d63e8d2c5a00cbcffbdf9112487c443466e1ea7d8c834df5ac5c425", + "tokens": [ + { + "asset_name_bytes": "74657374436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "75657374436f696e", + "mint_amount": "-7878754" + } + ] + } + ], + "signing_mode": "SCRIPT_TRANSACTION", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + }, + { + "path": "m/1854'/1815'/0'/2/0" + }, + { + "path": "m/1855'/1815'/0'" + } + ] + }, + "result": { + "tx_hash": "2be64c04ea3f5bac3c224ec47a4157ade91fc6ab4fd6b83ce3d57b2e9186720b", + "witnesses": [ + { + "type": 1, + "pub_key": "b10be5c0d11ad8292bbe69e220ca0cfbe154610b3041a8e72f9d515c226ab3b1", + "signature": "38a56a46b21caef91742ffafdec202ed96809c3070c9bfd51db5c750d77edbfb8514d9cd2255ab5a857dd8a63706ae0ca29e390fba6af7a906b186aed117b809", + "chain_code": null + }, + { + "type": 1, + "pub_key": "f2ef4ecd21ad28a8d270ca7be7e96c87f60dc821e13c0d0c5870344e9693637c", + "signature": "0c9071c421fe207ac1d9102643eac8ddf5ff29238782956b5706b9f1f084dfc5c087b4ceda6d079f8bb6438d3b556d3ac97565a87a8ec33f11856408b0480400", + "chain_code": null + }, + { + "type": 1, + "pub_key": "b75258e4f61eb7b313d8554c2fe10673cf214ca2d762bfd53ec3b7846e2ee872", + "signature": "85bf1bc71c04c72ae8184885b9d5eadd49b2c27bd332a42bc42c35b49429509350795bbdb716a95946b7c30cb62f20e1d39e4be3df5625a141f3e3c2e3526e02", + "chain_code": null + } + ] + } + } + ] +} diff --git a/common/tests/fixtures/cardano/sign_tx.slip39.json b/common/tests/fixtures/cardano/sign_tx.slip39.json index 2879e90696d..fcf613f0404 100644 --- a/common/tests/fixtures/cardano/sign_tx.slip39.json +++ b/common/tests/fixtures/cardano/sign_tx.slip39.json @@ -31,7 +31,9 @@ "amount": "3003112" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6", @@ -73,7 +75,9 @@ "amount": "1000000" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "4c43ce4c72f145b145ae7add414722735e250d048f61c4585a5becafcbffa6ae", @@ -115,7 +119,9 @@ "amount": "1000000" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "tx_hash": "93a2c3cfb67ef1e4bae167b0f443c3370664bdb9171bc9cd41bad98e5cc049b2", diff --git a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json index 10529d00505..7b3640099ea 100644 --- a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json @@ -48,7 +48,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -101,7 +103,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -151,7 +155,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -201,7 +207,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -272,10 +280,12 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { - "error_message": "Stakepool registration transaction cannot contain other certificates nor withdrawals" + "error_message": "Stakepool registration transaction cannot contain other certificates, withdrawals or minting" } }, { @@ -305,7 +315,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" @@ -360,10 +372,12 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { - "error_message": "Stakepool registration transaction cannot contain other certificates nor withdrawals" + "error_message": "Stakepool registration transaction cannot contain other certificates, withdrawals or minting" } }, { @@ -410,10 +424,12 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { - "error_message": "Stakepool registration transaction can only contain staking witnesses" + "error_message": "Stakepool registration transaction can only contain sinlge staking witnesses with the same path as in the certificate" } }, { @@ -459,7 +475,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "ProcessError: Invalid address" @@ -508,7 +526,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "ProcessError: Invalid address" @@ -591,7 +611,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid pool owner staking path" @@ -674,11 +696,483 @@ "amount": "1" } ], - "signing_mode": "ORDINARY_TRANSACTION" + "mint": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [] }, "result": { "error_message": "Invalid certificate" } + }, + { + "description": "Sample stake pool registration certificate with 1854 additional witness request", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [ + { + "path": "m/1854'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction can only contain sinlge staking witnesses with the same path as in the certificate" + } + }, + { + "description": "Sample stake pool registration certificate with 1855 additional witness request", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction can only contain sinlge staking witnesses with the same path as in the certificate" + } + }, + { + "description": "Sample stake pool registration certificate with additional witness request with different staking path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [ + { + "path": "m/1852'/1815'/0'/2/1" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction can only contain sinlge staking witnesses with the same path as in the certificate" + } + }, + { + "description": "Sample stake pool registration certificate with token minting", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "100" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-100" + } + ] + }, + { + "policy_id": "a5a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "100" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-100" + } + ] + } + ], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [ + { + "path": "m/1855'/1815'/0'/0/0" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction cannot contain other certificates, withdrawals or minting" + } + }, + { + "description": "Sample stake pool registration certificate with change output", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ], + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] + }, + "result": { + "error_message": "Invalid output" + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json index 085326b49e3..a324262e9d2 100644 --- a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json @@ -81,7 +81,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "e3b9a5657bf62609465a930c8359d774c73944973cfc5a104a0f0ed1e1e8db21", @@ -172,7 +174,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "c0d944db15446cf05e8db014685414c928d4d9a3e96aea229234be56eeae34c5", @@ -264,7 +268,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "f3d62758ff2f520e7256e65be9d8165da60c7979a97202c19d625709412411fd", @@ -322,7 +328,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "504f9214142996e0b7e315103b25d88a4afa3d01dd5be22376921b52b01483c3", @@ -380,7 +388,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "12921b4f8e77f815e0c8ed97c541fbd5ba38a6d3323f4ff1af0cb934b8ac6b39", @@ -473,7 +483,9 @@ "amount": "1" } ], - "signing_mode": "POOL_REGISTRATION_AS_OWNER" + "mint": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [] }, "result": { "tx_hash": "880fafab19a36407e9af300c2905e2f6bc8a8debd8b625005f56994d242ba460", diff --git a/core/src/all_modules.py b/core/src/all_modules.py index c59b9b18c5c..3f9efd54b7c 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -392,6 +392,10 @@ import trezor.enums.CardanoAddressType trezor.enums.CardanoCertificateType import trezor.enums.CardanoCertificateType + trezor.enums.CardanoNativeScriptHashDisplayFormat + import trezor.enums.CardanoNativeScriptHashDisplayFormat + trezor.enums.CardanoNativeScriptType + import trezor.enums.CardanoNativeScriptType trezor.enums.CardanoPoolRelayType import trezor.enums.CardanoPoolRelayType trezor.enums.CardanoTxAuxiliaryDataSupplementType @@ -442,28 +446,36 @@ import apps.cardano.certificates apps.cardano.get_address import apps.cardano.get_address + apps.cardano.get_native_script_hash + import apps.cardano.get_native_script_hash apps.cardano.get_public_key import apps.cardano.get_public_key apps.cardano.helpers import apps.cardano.helpers apps.cardano.helpers.account_path_check import apps.cardano.helpers.account_path_check + apps.cardano.helpers.address_credential_policy + import apps.cardano.helpers.address_credential_policy apps.cardano.helpers.bech32 import apps.cardano.helpers.bech32 + apps.cardano.helpers.credential_params + import apps.cardano.helpers.credential_params apps.cardano.helpers.hash_builder_collection import apps.cardano.helpers.hash_builder_collection apps.cardano.helpers.network_ids import apps.cardano.helpers.network_ids apps.cardano.helpers.paths import apps.cardano.helpers.paths + apps.cardano.helpers.pool_owner_path_check + import apps.cardano.helpers.pool_owner_path_check apps.cardano.helpers.protocol_magics import apps.cardano.helpers.protocol_magics - apps.cardano.helpers.staking_use_cases - import apps.cardano.helpers.staking_use_cases apps.cardano.helpers.utils import apps.cardano.helpers.utils apps.cardano.layout import apps.cardano.layout + apps.cardano.native_script + import apps.cardano.native_script apps.cardano.seed import apps.cardano.seed apps.cardano.sign_tx diff --git a/core/src/apps/cardano/address.py b/core/src/apps/cardano/address.py index 2445c1d973b..a4e2d8bccc0 100644 --- a/core/src/apps/cardano/address.py +++ b/core/src/apps/cardano/address.py @@ -1,4 +1,4 @@ -from trezor.crypto import base58, hashlib +from trezor.crypto import base58 from trezor.enums import CardanoAddressType from .byron_address import derive_byron_address, validate_byron_address @@ -7,17 +7,20 @@ INVALID_ADDRESS, INVALID_ADDRESS_PARAMETERS, NETWORK_MISMATCH, + SCRIPT_HASH_SIZE, bech32, network_ids, ) from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT -from .helpers.utils import derive_public_key, variable_length_encode +from .helpers.utils import get_public_key_hash, variable_length_encode from .seed import is_byron_path, is_shelley_path if False: + from typing import Any + from trezor.messages import ( - CardanoBlockchainPointerType, CardanoAddressParametersType, + CardanoBlockchainPointerType, ) from . import seed @@ -33,6 +36,7 @@ CardanoAddressType.REWARD, CardanoAddressType.REWARD_SCRIPT, ) + MIN_ADDRESS_BYTES_LENGTH = 29 MAX_ADDRESS_BYTES_LENGTH = 65 @@ -43,20 +47,56 @@ def validate_address_parameters(parameters: CardanoAddressParametersType) -> Non if parameters.address_type == CardanoAddressType.BYRON: if not is_byron_path(parameters.address_n): raise INVALID_ADDRESS_PARAMETERS - elif parameters.address_type in ADDRESS_TYPES_SHELLEY: + + elif parameters.address_type == CardanoAddressType.BASE: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + _validate_base_address_staking_info( + parameters.address_n_staking, parameters.staking_key_hash + ) + + elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_KEY: + _validate_script_hash(parameters.script_payment_hash) + _validate_base_address_staking_info( + parameters.address_n_staking, parameters.staking_key_hash + ) + + elif parameters.address_type == CardanoAddressType.BASE_KEY_SCRIPT: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + _validate_script_hash(parameters.script_staking_hash) + + elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + _validate_script_hash(parameters.script_staking_hash) + + elif parameters.address_type == CardanoAddressType.POINTER: + if not is_shelley_path(parameters.address_n): + raise INVALID_ADDRESS_PARAMETERS + if parameters.certificate_pointer is None: + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.POINTER_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + if parameters.certificate_pointer is None: + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.ENTERPRISE: if not is_shelley_path(parameters.address_n): raise INVALID_ADDRESS_PARAMETERS - if parameters.address_type == CardanoAddressType.BASE: - _validate_base_address_staking_info( - parameters.address_n_staking, parameters.staking_key_hash - ) - elif parameters.address_type == CardanoAddressType.POINTER: - if parameters.certificate_pointer is None: - raise INVALID_ADDRESS_PARAMETERS - elif parameters.address_type == CardanoAddressType.REWARD: - if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.address_n): - raise INVALID_ADDRESS_PARAMETERS + elif parameters.address_type == CardanoAddressType.ENTERPRISE_SCRIPT: + _validate_script_hash(parameters.script_payment_hash) + + elif parameters.address_type == CardanoAddressType.REWARD: + if not is_shelley_path(parameters.address_n_staking): + raise INVALID_ADDRESS_PARAMETERS + if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.address_n_staking): + raise INVALID_ADDRESS_PARAMETERS + + elif parameters.address_type == CardanoAddressType.REWARD_SCRIPT: + _validate_script_hash(parameters.script_staking_hash) + else: raise INVALID_ADDRESS_PARAMETERS @@ -64,23 +104,86 @@ def validate_address_parameters(parameters: CardanoAddressParametersType) -> Non def _validate_address_parameters_structure( parameters: CardanoAddressParametersType, ) -> None: + address_n = parameters.address_n address_n_staking = parameters.address_n_staking staking_key_hash = parameters.staking_key_hash certificate_pointer = parameters.certificate_pointer - - fields_to_be_empty: tuple = () - if parameters.address_type in ( - CardanoAddressType.BYRON, - CardanoAddressType.REWARD, - CardanoAddressType.ENTERPRISE, + script_payment_hash = parameters.script_payment_hash + script_staking_hash = parameters.script_staking_hash + + fields_to_be_empty: dict[CardanoAddressType, tuple[Any, ...]] = { + CardanoAddressType.BASE: ( + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.BASE_KEY_SCRIPT: ( + address_n_staking, + certificate_pointer, + script_payment_hash, + ), + CardanoAddressType.BASE_SCRIPT_KEY: ( + address_n, + certificate_pointer, + script_staking_hash, + ), + CardanoAddressType.BASE_SCRIPT_SCRIPT: ( + address_n, + address_n_staking, + certificate_pointer, + ), + CardanoAddressType.POINTER: ( + address_n_staking, + staking_key_hash, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.POINTER_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + script_staking_hash, + ), + CardanoAddressType.ENTERPRISE: ( + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.ENTERPRISE_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + certificate_pointer, + script_staking_hash, + ), + CardanoAddressType.BYRON: ( + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.REWARD: ( + address_n, + staking_key_hash, + certificate_pointer, + script_payment_hash, + script_staking_hash, + ), + CardanoAddressType.REWARD_SCRIPT: ( + address_n, + address_n_staking, + staking_key_hash, + certificate_pointer, + script_payment_hash, + ), + } + + if parameters.address_type not in fields_to_be_empty or any( + fields_to_be_empty[parameters.address_type] ): - fields_to_be_empty = (address_n_staking, staking_key_hash, certificate_pointer) - elif parameters.address_type == CardanoAddressType.BASE: - fields_to_be_empty = (certificate_pointer,) - elif parameters.address_type == CardanoAddressType.POINTER: - fields_to_be_empty = (address_n_staking, staking_key_hash) - - if any(fields_to_be_empty): raise INVALID_ADDRESS_PARAMETERS @@ -101,6 +204,11 @@ def _validate_base_address_staking_info( raise INVALID_ADDRESS_PARAMETERS +def _validate_script_hash(script_hash: bytes | None) -> None: + if not script_hash or len(script_hash) != SCRIPT_HASH_SIZE: + raise INVALID_ADDRESS_PARAMETERS + + def _validate_address_and_get_type( address: str, protocol_magic: int, network_id: int ) -> int: @@ -189,7 +297,7 @@ def _get_bech32_hrp_for_address( # Byron address uses base58 encoding raise ValueError - if address_type == CardanoAddressType.REWARD: + if address_type in (CardanoAddressType.REWARD, CardanoAddressType.REWARD_SCRIPT): if network_ids.is_mainnet(network_id): return bech32.HRP_REWARD_ADDRESS else: @@ -210,11 +318,6 @@ def _get_address_network_id(address: bytes) -> int: return address[0] & 0x0F -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() - - def derive_human_readable_address( keychain: seed.Keychain, parameters: CardanoAddressParametersType, @@ -258,36 +361,14 @@ def derive_address_bytes( def _derive_shelley_address( - keychain: seed.Keychain, - parameters: CardanoAddressParametersType, - network_id: int, + keychain: seed.Keychain, parameters: CardanoAddressParametersType, network_id: int ) -> bytes: - if parameters.address_type == CardanoAddressType.BASE: - address = _derive_base_address( - keychain, - parameters.address_n, - parameters.address_n_staking, - parameters.staking_key_hash, - network_id, - ) - elif parameters.address_type == CardanoAddressType.POINTER: - # ensured by validate_address_parameters - assert parameters.certificate_pointer is not None - - address = _derive_pointer_address( - keychain, - parameters.address_n, - parameters.certificate_pointer, - network_id, - ) - elif parameters.address_type == CardanoAddressType.ENTERPRISE: - address = _derive_enterprise_address(keychain, parameters.address_n, network_id) - elif parameters.address_type == CardanoAddressType.REWARD: - address = _derive_reward_address(keychain, parameters.address_n, network_id) - else: - raise INVALID_ADDRESS_PARAMETERS + header = _create_address_header(parameters.address_type, network_id) - return address + payment_part = _get_address_payment_part(keychain, parameters) + staking_part = _get_address_staking_part(keychain, parameters) + + return header + payment_part + staking_part def _create_address_header(address_type: CardanoAddressType, network_id: int) -> bytes: @@ -295,33 +376,30 @@ def _create_address_header(address_type: CardanoAddressType, network_id: int) -> return header.to_bytes(1, "little") -def _derive_base_address( - keychain: seed.Keychain, - path: list[int], - staking_path: list[int], - staking_key_hash: bytes | None, - network_id: int, +def _get_address_payment_part( + keychain: seed.Keychain, parameters: CardanoAddressParametersType ) -> bytes: - header = _create_address_header(CardanoAddressType.BASE, network_id) - spending_key_hash = get_public_key_hash(keychain, path) - - if staking_key_hash is None: - staking_key_hash = get_public_key_hash(keychain, staking_path) - - return header + spending_key_hash + staking_key_hash + if parameters.address_n: + return get_public_key_hash(keychain, parameters.address_n) + elif parameters.script_payment_hash: + return parameters.script_payment_hash + else: + return bytes() -def _derive_pointer_address( - keychain: seed.Keychain, - path: list[int], - pointer: CardanoBlockchainPointerType, - network_id: int, +def _get_address_staking_part( + keychain: seed.Keychain, parameters: CardanoAddressParametersType ) -> bytes: - header = _create_address_header(CardanoAddressType.POINTER, network_id) - spending_key_hash = get_public_key_hash(keychain, path) - encoded_pointer = _encode_certificate_pointer(pointer) - - return header + spending_key_hash + encoded_pointer + if parameters.staking_key_hash: + return parameters.staking_key_hash + elif parameters.address_n_staking: + return get_public_key_hash(keychain, parameters.address_n_staking) + elif parameters.script_staking_hash: + return parameters.script_staking_hash + elif parameters.certificate_pointer: + return _encode_certificate_pointer(parameters.certificate_pointer) + else: + return bytes() def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes: @@ -330,36 +408,3 @@ def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes: certificate_index_encoded = variable_length_encode(pointer.certificate_index) return bytes(block_index_encoded + tx_index_encoded + certificate_index_encoded) - - -def _derive_enterprise_address( - keychain: seed.Keychain, - path: list[int], - network_id: int, -) -> bytes: - header = _create_address_header(CardanoAddressType.ENTERPRISE, network_id) - spending_key_hash = get_public_key_hash(keychain, path) - - return header + spending_key_hash - - -def _derive_reward_address( - keychain: seed.Keychain, - path: list[int], - network_id: int, -) -> bytes: - staking_key_hash = get_public_key_hash(keychain, path) - - return pack_reward_address_bytes(staking_key_hash, network_id) - - -def pack_reward_address_bytes( - staking_key_hash: bytes, - network_id: int, -) -> bytes: - """ - Helper function to transform raw staking key hash into reward address - """ - header = _create_address_header(CardanoAddressType.REWARD, network_id) - - return header + staking_key_hash diff --git a/core/src/apps/cardano/certificates.py b/core/src/apps/cardano/certificates.py index e2fe8b54271..b4247cd69db 100644 --- a/core/src/apps/cardano/certificates.py +++ b/core/src/apps/cardano/certificates.py @@ -13,8 +13,11 @@ ) from .helpers import ADDRESS_KEY_HASH_SIZE, INVALID_CERTIFICATE, LOVELACE_MAX_SUPPLY from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT +from .helpers.utils import validate_stake_credential if False: + from typing import Any + from trezor.messages import ( CardanoPoolMetadataType, CardanoPoolOwner, @@ -56,13 +59,16 @@ def validate_certificate( ): raise INVALID_CERTIFICATE + _validate_certificate_structure(certificate) + if certificate.type in ( CardanoCertificateType.STAKE_DELEGATION, CardanoCertificateType.STAKE_REGISTRATION, CardanoCertificateType.STAKE_DEREGISTRATION, ): - if not SCHEMA_STAKING_ANY_ACCOUNT.match(certificate.path): - raise INVALID_CERTIFICATE + validate_stake_credential( + certificate.path, certificate.script_hash, signing_mode, INVALID_CERTIFICATE + ) if certificate.type == CardanoCertificateType.STAKE_DELEGATION: if not certificate.pool or len(certificate.pool) != POOL_HASH_SIZE: @@ -78,6 +84,25 @@ def validate_certificate( account_path_checker.add_certificate(certificate) +def _validate_certificate_structure(certificate: CardanoTxCertificate) -> None: + path = certificate.path + script_hash = certificate.script_hash + pool = certificate.pool + pool_parameters = certificate.pool_parameters + + fields_to_be_empty: dict[CardanoCertificateType, tuple[Any, ...]] = { + 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), + } + + if certificate.type not in fields_to_be_empty or any( + fields_to_be_empty[certificate.type] + ): + raise INVALID_CERTIFICATE + + def cborize_certificate( keychain: seed.Keychain, certificate: CardanoTxCertificate ) -> CborSequence: @@ -87,18 +112,35 @@ def cborize_certificate( ): return ( certificate.type, - (0, get_public_key_hash(keychain, certificate.path)), + cborize_certificate_stake_credential( + keychain, certificate.path, certificate.script_hash + ), ) elif certificate.type == CardanoCertificateType.STAKE_DELEGATION: return ( certificate.type, - (0, get_public_key_hash(keychain, certificate.path)), + cborize_certificate_stake_credential( + keychain, certificate.path, certificate.script_hash + ), certificate.pool, ) else: raise INVALID_CERTIFICATE +def cborize_certificate_stake_credential( + keychain: seed.Keychain, path: list[int], script_hash: bytes | None +) -> tuple[int, bytes]: + if path: + return 0, get_public_key_hash(keychain, path) + + if script_hash: + return 1, script_hash + + # should be unreachable unless there's a bug in validation + raise INVALID_CERTIFICATE + + def cborize_initial_pool_registration_certificate_fields( certificate: CardanoTxCertificate, ) -> CborSequence: diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index 8f29e009339..ec03c902bec 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -1,19 +1,16 @@ from trezor import log, wire from trezor.messages import CardanoAddress -from trezor.ui.layouts import show_address - -from apps.common import paths from . import seed from .address import derive_human_readable_address, validate_address_parameters -from .helpers import protocol_magics, staking_use_cases -from .helpers.paths import SCHEMA_PAYMENT, SCHEMA_STAKING -from .helpers.utils import to_account_path -from .layout import ( - ADDRESS_TYPE_NAMES, - show_warning_address_foreign_staking_key, - show_warning_address_pointer, +from .helpers.address_credential_policy import ( + ADDRESS_POLICY_SHOW_SPLIT, + get_address_policy, + get_payment_credential_policy, + get_stake_credential_policy, ) +from .helpers.credential_params import CredentialParams +from .layout import show_cardano_address, show_credential from .sign_tx import validate_network_info if False: @@ -29,15 +26,6 @@ async def get_address( ) -> CardanoAddress: address_parameters = msg.address_parameters - await paths.validate_path( - ctx, - keychain, - address_parameters.address_n, - # path must match the PAYMENT or STAKING schema - SCHEMA_PAYMENT.match(address_parameters.address_n) - or SCHEMA_STAKING.match(address_parameters.address_n), - ) - validate_network_info(msg.network_id, msg.protocol_magic) validate_address_parameters(address_parameters) @@ -65,37 +53,17 @@ async def _display_address( address: str, protocol_magic: int, ) -> None: - await _show_staking_warnings(ctx, keychain, address_parameters) - - network_name = None - if not protocol_magics.is_mainnet(protocol_magic): - network_name = protocol_magics.to_ui_string(protocol_magic) - - address_n = paths.address_n_to_str(address_parameters.address_n) - await show_address( - ctx, - address=address, - title="%s address" % ADDRESS_TYPE_NAMES[address_parameters.address_type], - network=network_name, - address_extra=address_n, - title_qr=address_n, - ) - - -async def _show_staking_warnings( - ctx: wire.Context, - keychain: seed.Keychain, - address_parameters: CardanoAddressParametersType, -) -> None: - staking_type = staking_use_cases.get(keychain, address_parameters) - if staking_type == staking_use_cases.MISMATCH: - await show_warning_address_foreign_staking_key( + address_policy = get_address_policy(keychain, address_parameters) + if address_policy == ADDRESS_POLICY_SHOW_SPLIT: + await show_credential( ctx, - to_account_path(address_parameters.address_n), - to_account_path(address_parameters.address_n_staking), - address_parameters.staking_key_hash, + CredentialParams(CredentialParams.TYPE_PAYMENT, address_parameters), + get_payment_credential_policy(address_parameters), ) - elif staking_type == staking_use_cases.POINTER_ADDRESS: - # ensured in _derive_shelley_address: - assert address_parameters.certificate_pointer is not None - await show_warning_address_pointer(ctx, address_parameters.certificate_pointer) + await show_credential( + ctx, + CredentialParams(CredentialParams.TYPE_STAKE, address_parameters), + get_stake_credential_policy(keychain, address_parameters), + ) + + await show_cardano_address(ctx, address_parameters, address, protocol_magic) diff --git a/core/src/apps/cardano/get_native_script_hash.py b/core/src/apps/cardano/get_native_script_hash.py new file mode 100644 index 00000000000..c5d7d40b28a --- /dev/null +++ b/core/src/apps/cardano/get_native_script_hash.py @@ -0,0 +1,29 @@ +from trezor import log, wire +from trezor.enums import CardanoNativeScriptHashDisplayFormat +from trezor.messages import CardanoNativeScriptHash + +from . import native_script, seed +from .layout import show_native_script, show_script_hash + +if False: + from trezor.messages import CardanoGetNativeScriptHash + + +@seed.with_keychain +async def get_native_script_hash( + ctx: wire.Context, msg: CardanoGetNativeScriptHash, keychain: seed.Keychain +) -> CardanoNativeScriptHash: + native_script.validate_native_script(msg.script) + + try: + script_hash = native_script.get_native_script_hash(keychain, msg.script) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Getting native script hash failed") + + if msg.display_format != CardanoNativeScriptHashDisplayFormat.HIDE: + await show_native_script(ctx, msg.script) + await show_script_hash(ctx, script_hash, msg.display_format) + + return CardanoNativeScriptHash(script_hash=script_hash) diff --git a/core/src/apps/cardano/get_public_key.py b/core/src/apps/cardano/get_public_key.py index 3246c41af13..cd43514458c 100644 --- a/core/src/apps/cardano/get_public_key.py +++ b/core/src/apps/cardano/get_public_key.py @@ -7,7 +7,7 @@ from apps.common import paths from . import seed -from .helpers.paths import SCHEMA_PUBKEY +from .helpers.paths import SCHEMA_MINT, SCHEMA_PUBKEY from .helpers.utils import derive_public_key if False: @@ -23,7 +23,7 @@ async def get_public_key( keychain, msg.address_n, # path must match the PUBKEY schema - SCHEMA_PUBKEY.match(msg.address_n), + SCHEMA_PUBKEY.match(msg.address_n) or SCHEMA_MINT.match(msg.address_n), ) try: diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py index 3d16e01a939..af8ec9c9575 100644 --- a/core/src/apps/cardano/helpers/__init__.py +++ b/core/src/apps/cardano/helpers/__init__.py @@ -3,18 +3,22 @@ INVALID_ADDRESS = wire.ProcessError("Invalid address") INVALID_ADDRESS_PARAMETERS = wire.ProcessError("Invalid address parameters") NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch") +INVALID_TX_SIGNING_REQUEST = wire.ProcessError("Invalid tx signing request") INVALID_OUTPUT = wire.ProcessError("Invalid output") INVALID_CERTIFICATE = wire.ProcessError("Invalid certificate") INVALID_WITHDRAWAL = wire.ProcessError("Invalid withdrawal") INVALID_TOKEN_BUNDLE_OUTPUT = wire.ProcessError("Invalid token bundle in output") INVALID_AUXILIARY_DATA = wire.ProcessError("Invalid auxiliary data") INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE = wire.ProcessError( - "Stakepool registration transaction cannot contain other certificates nor withdrawals" + "Stakepool registration transaction cannot contain other certificates, withdrawals or minting" ) -INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES = wire.ProcessError( - "Stakepool registration transaction can only contain staking witnesses" +INVALID_STAKEPOOL_REGISTRATION_TX_WITNESS = wire.ProcessError( + "Stakepool registration transaction can only contain sinlge staking witnesses with the same path as in the certificate" ) INVALID_WITNESS_REQUEST = wire.ProcessError("Invalid witness request") +INVALID_NATIVE_SCRIPT = wire.ProcessError("Invalid native script") +INVALID_TOKEN_BUNDLE_MINT = wire.ProcessError("Invalid mint token bundle") LOVELACE_MAX_SUPPLY = 45_000_000_000 * 1_000_000 ADDRESS_KEY_HASH_SIZE = 28 +SCRIPT_HASH_SIZE = 28 diff --git a/core/src/apps/cardano/helpers/account_path_check.py b/core/src/apps/cardano/helpers/account_path_check.py index eb75e631b35..d65bf3150e2 100644 --- a/core/src/apps/cardano/helpers/account_path_check.py +++ b/core/src/apps/cardano/helpers/account_path_check.py @@ -1,5 +1,5 @@ from ...common.paths import HARDENED -from ..seed import is_byron_path, is_shelley_path +from ..seed import is_byron_path, is_minting_path, is_multisig_path, is_shelley_path from . import ( INVALID_CERTIFICATE, INVALID_OUTPUT, @@ -33,6 +33,10 @@ def __init__(self) -> None: self.account_path: object | list[int] = self.UNDEFINED def _add(self, path: list[int], error: wire.ProcessError) -> None: + # multi-sig and minting paths are always shown and thus don't need to be checked + if is_multisig_path(path) or is_minting_path(path): + return + account_path = to_account_path(path) if self.account_path is self.UNDEFINED: self.account_path = account_path diff --git a/core/src/apps/cardano/helpers/address_credential_policy.py b/core/src/apps/cardano/helpers/address_credential_policy.py new file mode 100644 index 00000000000..07fb4a20a4f --- /dev/null +++ b/core/src/apps/cardano/helpers/address_credential_policy.py @@ -0,0 +1,196 @@ +from trezor.enums import CardanoAddressType + +from ..address import get_public_key_hash +from .paths import CHAIN_STAKING_KEY, SCHEMA_PAYMENT, SCHEMA_STAKING +from .utils import to_account_path + +if False: + from trezor.messages import CardanoAddressParametersType + from ..seed import Keychain + + +CREDENTIAL_POLICY_SHOW = 0 +CREDENTIAL_POLICY_SHOW_KEY_PATH = 1 << 1 +CREDENTIAL_POLICY_SHOW_CHANGE_OUTPUT = 1 << 2 +CREDENTIAL_POLICY_SHOW_ADDRESS = 1 << 3 +CREDENTIAL_POLICY_WARN_SCRIPT = 1 << 4 +CREDENTIAL_POLICY_WARN_POINTER = 1 << 5 +CREDENTIAL_POLICY_WARN_REWARD_ADDRESS = 1 << 6 +CREDENTIAL_POLICY_WARN_NO_STAKING = 1 << 7 +CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL = 1 << 8 +CREDENTIAL_POLICY_WARN_MISMATCH = 1 << 9 + +CREDENTIAL_POLICIES_WARN = ( + CREDENTIAL_POLICY_WARN_SCRIPT, + CREDENTIAL_POLICY_WARN_POINTER, + CREDENTIAL_POLICY_WARN_REWARD_ADDRESS, + CREDENTIAL_POLICY_WARN_NO_STAKING, + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL, + CREDENTIAL_POLICY_WARN_MISMATCH, +) + +ADDRESS_POLICY_SHOW_SPLIT = 0 +ADDRESS_POLICY_SHOW_SIMPLE = 1 + + +def get_address_policy( + keychain: Keychain, address_parameters: CardanoAddressParametersType +) -> int: + if ( + address_parameters.address_type == CardanoAddressType.BASE + and SCHEMA_PAYMENT.match(address_parameters.address_n) + and _do_base_address_credentials_match( + keychain, + address_parameters.address_n, + address_parameters.address_n_staking, + address_parameters.staking_key_hash, + ) + ): + return ADDRESS_POLICY_SHOW_SIMPLE + + return ADDRESS_POLICY_SHOW_SPLIT + + +def get_change_output_payment_credential_policy( + address_parameters: CardanoAddressParametersType, +) -> int: + credential_policy = get_payment_credential_policy(address_parameters) + return _policy_to_change_output_policy(credential_policy) + + +def get_change_output_stake_credential_policy( + keychain: Keychain, address_parameters: CardanoAddressParametersType +) -> int: + credential_policy = get_stake_credential_policy(keychain, address_parameters) + return _policy_to_change_output_policy(credential_policy) + + +def get_payment_credential_policy( + address_parameters: CardanoAddressParametersType, +) -> int: + if address_parameters.address_type in ( + CardanoAddressType.BASE, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.POINTER, + CardanoAddressType.ENTERPRISE, + CardanoAddressType.BYRON, + ): + if not SCHEMA_PAYMENT.match(address_parameters.address_n): + return CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL + + return CREDENTIAL_POLICY_SHOW_KEY_PATH + + elif address_parameters.address_type in ( + CardanoAddressType.BASE_SCRIPT_KEY, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.POINTER_SCRIPT, + CardanoAddressType.ENTERPRISE_SCRIPT, + ): + return CREDENTIAL_POLICY_WARN_SCRIPT + + elif address_parameters.address_type in ( + CardanoAddressType.REWARD, + CardanoAddressType.REWARD_SCRIPT, + ): + return CREDENTIAL_POLICY_WARN_REWARD_ADDRESS + + else: + raise ValueError("Invalid address type") + + +def get_stake_credential_policy( + keychain: Keychain, address_parameters: CardanoAddressParametersType +) -> int: + address_type = address_parameters.address_type + if address_type == CardanoAddressType.BASE: + is_unusual = address_parameters.address_n_staking and not SCHEMA_STAKING.match( + address_parameters.address_n_staking + ) + + if not _do_base_address_credentials_match( + keychain, + address_parameters.address_n, + address_parameters.address_n_staking, + address_parameters.staking_key_hash, + ): + return ( + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL | CREDENTIAL_POLICY_WARN_MISMATCH + if is_unusual + else CREDENTIAL_POLICY_WARN_MISMATCH + ) + + return ( + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL + if is_unusual + else CREDENTIAL_POLICY_SHOW + ) + + elif address_type == CardanoAddressType.BASE_SCRIPT_KEY: + is_unusual = address_parameters.address_n_staking and not SCHEMA_STAKING.match( + address_parameters.address_n_staking + ) + return ( + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL | CREDENTIAL_POLICY_WARN_MISMATCH + if is_unusual + else CREDENTIAL_POLICY_WARN_MISMATCH + ) + + elif address_type in ( + CardanoAddressType.POINTER, + CardanoAddressType.POINTER_SCRIPT, + ): + return CREDENTIAL_POLICY_WARN_POINTER + + elif address_type == CardanoAddressType.REWARD: + return CREDENTIAL_POLICY_SHOW_KEY_PATH + + elif address_type in ( + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.REWARD_SCRIPT, + ): + return CREDENTIAL_POLICY_WARN_SCRIPT + + elif address_type in ( + CardanoAddressType.ENTERPRISE, + CardanoAddressType.ENTERPRISE_SCRIPT, + CardanoAddressType.BYRON, + ): + return CREDENTIAL_POLICY_WARN_NO_STAKING + + else: + raise ValueError("Invalid address type") + + +def _do_base_address_credentials_match( + keychain: Keychain, + address_n: list[int], + address_n_staking: list[int], + staking_key_hash: bytes | None, +) -> bool: + assert address_n_staking or staking_key_hash is not None + + spending_account_staking_path = _path_to_staking_path(address_n) + if address_n_staking: + if address_n_staking != spending_account_staking_path: + return False + else: + spending_account_staking_key_hash = get_public_key_hash( + keychain, spending_account_staking_path + ) + if staking_key_hash != spending_account_staking_key_hash: + return False + return True + + +def _path_to_staking_path(path: list[int]) -> list[int]: + return to_account_path(path) + [CHAIN_STAKING_KEY, 0] + + +def _policy_to_change_output_policy( + credential_policy: int, +) -> int: + """ + Updates the policy to a tx change output policy. As a result, different text will be displayed to the user. + """ + return credential_policy | CREDENTIAL_POLICY_SHOW_CHANGE_OUTPUT diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py index 4a35e29be85..cb710fb726e 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -9,6 +9,9 @@ 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_SCRIPT_HASH = "script" +HRP_KEY_HASH = "addr_vkh" +HRP_SHARED_KEY_HASH = "addr_shared_vkh" def encode(hrp: str, data: bytes) -> str: diff --git a/core/src/apps/cardano/helpers/credential_params.py b/core/src/apps/cardano/helpers/credential_params.py new file mode 100644 index 00000000000..0414f3996e8 --- /dev/null +++ b/core/src/apps/cardano/helpers/credential_params.py @@ -0,0 +1,93 @@ +from trezor.enums import CardanoAddressType + +from ...common.paths import address_n_to_str +from .utils import format_key_hash, format_script_hash + +if False: + from trezor.messages import ( + CardanoBlockchainPointerType, + CardanoAddressParametersType, + ) + from trezor.ui.layouts import PropertyType + + +class CredentialParams: + """ + Serves mainly as a wrapper object for credential parameters so that they don't have to be passed into functions + separately. Also contains functions which simplify displaying the credential. + """ + + type: int + address_type: CardanoAddressType + path: list[int] + key_hash: bytes | None + script_hash: bytes | None + pointer: CardanoBlockchainPointerType | None + + TYPE_PAYMENT = 0 + TYPE_STAKE = 1 + + def __init__(self, type: int, address_parameters: CardanoAddressParametersType): + self.type = type + self.address_type = address_parameters.address_type + + if type == self.TYPE_PAYMENT: + self.path = address_parameters.address_n + self.key_hash = None + self.script_hash = address_parameters.script_payment_hash + self.pointer = None + elif type == self.TYPE_STAKE: + self.path = address_parameters.address_n_staking + self.key_hash = address_parameters.staking_key_hash + self.script_hash = address_parameters.script_staking_hash + self.pointer = address_parameters.certificate_pointer + else: + raise ValueError + + def get_credential(self) -> list[int] | bytes | CardanoBlockchainPointerType | None: + if self.path: + return self.path + elif self.key_hash: + return self.key_hash + elif self.script_hash: + return self.script_hash + elif self.pointer: + return self.pointer + else: + return None + + def get_title(self) -> str: + if self.path: + return "path" + elif self.key_hash: + return "key hash" + elif self.script_hash: + return "script" + elif self.pointer: + return "pointer" + else: + return "" + + def format(self) -> list[PropertyType]: + if self.path: + return [(None, address_n_to_str(self.path))] + elif self.key_hash: + return [(None, format_key_hash(self.key_hash, False))] + elif self.script_hash: + return [(None, format_script_hash(self.script_hash))] + elif self.pointer: + return [ + ("Block: %s" % self.pointer.block_index, None), + ("Transaction: %s" % self.pointer.tx_index, None), + ("Certificate: %s" % self.pointer.certificate_index, None), + ] + else: + return [] + + def format_type(self) -> str: + if self.type == self.TYPE_PAYMENT: + return "payment" + elif self.type == self.TYPE_STAKE: + return "stake" + + raise ValueError diff --git a/core/src/apps/cardano/helpers/paths.py b/core/src/apps/cardano/helpers/paths.py index 98b4f2b68d8..443886a6f93 100644 --- a/core/src/apps/cardano/helpers/paths.py +++ b/core/src/apps/cardano/helpers/paths.py @@ -6,17 +6,22 @@ BYRON_ROOT = [44 | HARDENED, SLIP44_ID | HARDENED] SHELLEY_ROOT = [1852 | HARDENED, SLIP44_ID | HARDENED] +MULTISIG_ROOT = [1854 | HARDENED, SLIP44_ID | HARDENED] +MINTING_ROOT = [1855 | HARDENED, SLIP44_ID | HARDENED] # fmt: off -SCHEMA_PUBKEY = PathSchema.parse("m/[44,1852]'/coin_type'/account'/*", SLIP44_ID) +SCHEMA_PUBKEY = PathSchema.parse("m/[44,1852,1854]'/coin_type'/account'/*", SLIP44_ID) +# minting has a specific schema for key derivation - see CIP-1855 +SCHEMA_MINT = PathSchema.parse("m/1855'/coin_type'/[0-%s]'" % (HARDENED - 1), SLIP44_ID) SCHEMA_PAYMENT = PathSchema.parse("m/[44,1852]'/coin_type'/account'/[0,1]/address_index", SLIP44_ID) # staking is only allowed on Shelley paths with suffix /2/0 -SCHEMA_STAKING = PathSchema.parse("m/1852'/coin_type'/account'/2/0", SLIP44_ID) -SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse("m/1852'/coin_type'/[0-%s]'/2/0" % (HARDENED - 1), SLIP44_ID) +SCHEMA_STAKING = PathSchema.parse("m/[1852]'/coin_type'/account'/2/0", SLIP44_ID) +SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse("m/[1852]'/coin_type'/[0-%s]'/2/0" % (HARDENED - 1), SLIP44_ID) # fmt: on -ACCOUNT_PATH_LENGTH = const(3) ACCOUNT_PATH_INDEX = const(2) +ACCOUNT_PATH_LENGTH = const(3) +CHAIN_STAKING_KEY = const(2) CHANGE_OUTPUT_PATH_NAME = "Change output path" CHANGE_OUTPUT_STAKING_PATH_NAME = "Change output staking path" diff --git a/core/src/apps/cardano/helpers/pool_owner_path_check.py b/core/src/apps/cardano/helpers/pool_owner_path_check.py new file mode 100644 index 00000000000..8a44768faa0 --- /dev/null +++ b/core/src/apps/cardano/helpers/pool_owner_path_check.py @@ -0,0 +1,40 @@ +from . import INVALID_CERTIFICATE, INVALID_STAKEPOOL_REGISTRATION_TX_WITNESS +from .paths import SCHEMA_STAKING_ANY_ACCOUNT + + +class PoolOwnerPathChecker: + """ + We have a separate tx signing flow for stake pool registration because it's a + transaction where the witnessable entries (i.e. inputs, withdrawals, etc.) + in the transaction are not supposed to be controlled by the HW wallet, which + means the user is vulnerable to unknowingly supplying a witness for an UTXO + or other tx entry they think is external, resulting in the co-signers + gaining control over their funds (Something SLIP-0019 is dealing with for + BTC but no similar standard is currently available for Cardano). Hence we + completely forbid witnessing inputs and other entries of the transaction + except the stake pool certificate itself and we provide a witness only to the + user's staking key in the list of pool owners. + + This class ensures that: + - there is exactly one path in the pool owners list + - the witness request path is a staking path + - the two mentioned paths are the same + """ + + def __init__(self) -> None: + self.path: list[int] | None = None + + def add(self, path: list[int]) -> None: + if self.path is not None: + raise INVALID_CERTIFICATE + self.path = path + + def check_if_added(self) -> None: + if self.path is None: + raise INVALID_CERTIFICATE + + def check_witness_request(self, path: list[int]) -> None: + if not SCHEMA_STAKING_ANY_ACCOUNT.match(path): + raise INVALID_STAKEPOOL_REGISTRATION_TX_WITNESS + if self.path != path: + raise INVALID_STAKEPOOL_REGISTRATION_TX_WITNESS diff --git a/core/src/apps/cardano/helpers/staking_use_cases.py b/core/src/apps/cardano/helpers/staking_use_cases.py deleted file mode 100644 index cf413af6f94..00000000000 --- a/core/src/apps/cardano/helpers/staking_use_cases.py +++ /dev/null @@ -1,54 +0,0 @@ -from trezor.enums import CardanoAddressType - -from ..address import get_public_key_hash -from ..seed import is_shelley_path -from .utils import to_account_path - -if False: - from trezor.messages import CardanoAddressParametersType - from ..seed import Keychain - - -""" -Used as a helper when deciding what warnings we should -display to the user during get_address and sign_tx depending -on the type of address and its parameters. -""" - - -NO_STAKING = 0 -MATCH = 1 -MISMATCH = 2 -POINTER_ADDRESS = 3 - - -def get(keychain: Keychain, address_parameters: CardanoAddressParametersType) -> int: - address_type = address_parameters.address_type - if address_type == CardanoAddressType.BASE: - if not is_shelley_path(address_parameters.address_n): - return MISMATCH - - spending_account_staking_path = _path_to_staking_path( - address_parameters.address_n - ) - if address_parameters.address_n_staking: - if address_parameters.address_n_staking != spending_account_staking_path: - return MISMATCH - else: - staking_key_hash = get_public_key_hash( - keychain, spending_account_staking_path - ) - if address_parameters.staking_key_hash != staking_key_hash: - return MISMATCH - - return MATCH - elif address_type == CardanoAddressType.POINTER: - return POINTER_ADDRESS - elif address_type == CardanoAddressType.REWARD: - return MATCH - else: - return NO_STAKING - - -def _path_to_staking_path(path: list[int]) -> list[int]: - return to_account_path(path) + [2, 0] diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index e0af3ada0b1..8c7f190ac52 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -1,11 +1,17 @@ from trezor.crypto import hashlib +from trezor.enums import CardanoTxSigningMode -from apps.cardano.helpers.paths import ACCOUNT_PATH_INDEX, unharden +from apps.cardano.helpers.paths import ( + ACCOUNT_PATH_INDEX, + SCHEMA_STAKING_ANY_ACCOUNT, + unharden, +) from apps.common.seed import remove_ed25519_prefix -from . import bech32 +from . import ADDRESS_KEY_HASH_SIZE, SCRIPT_HASH_SIZE, bech32 if False: + from trezor import wire from .. import seed @@ -59,9 +65,46 @@ 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 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() + + def derive_public_key( keychain: seed.Keychain, path: list[int], extended: bool = False ) -> bytes: node = keychain.derive(path) public_key = remove_ed25519_prefix(node.public_key()) return public_key if not extended else public_key + node.chain_code() + + +def validate_stake_credential( + path: list[int], + script_hash: bytes | None, + signing_mode: CardanoTxSigningMode, + error: wire.ProcessError, +) -> None: + if path and script_hash: + raise error + + if path: + if signing_mode != CardanoTxSigningMode.ORDINARY_TRANSACTION: + raise error + if not SCHEMA_STAKING_ANY_ACCOUNT.match(path): + raise error + elif script_hash: + if signing_mode != CardanoTxSigningMode.SCRIPT_TRANSACTION: + raise error + if len(script_hash) != SCRIPT_HASH_SIZE: + raise error + else: + raise error diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index 52c59696d6e..7550cf12214 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -1,34 +1,50 @@ from trezor import ui -from trezor.enums import ButtonRequestType, CardanoAddressType, CardanoCertificateType +from trezor.enums import ( + ButtonRequestType, + CardanoAddressType, + CardanoCertificateType, + CardanoNativeScriptHashDisplayFormat, + CardanoNativeScriptType, + CardanoTxSigningMode, +) +from trezor.messages import CardanoAddressParametersType from trezor.strings import format_amount from trezor.ui.layouts import ( confirm_metadata, confirm_output, confirm_path_warning, confirm_properties, + show_address, ) from apps.common.paths import address_n_to_str from . import seed -from .address import ( - encode_human_readable_address, - get_public_key_hash, - pack_reward_address_bytes, -) +from .address import derive_human_readable_address from .helpers import protocol_magics +from .helpers.address_credential_policy import ( + CREDENTIAL_POLICIES_WARN, + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL, + CREDENTIAL_POLICY_SHOW_CHANGE_OUTPUT, + CREDENTIAL_POLICY_WARN_MISMATCH, + CREDENTIAL_POLICY_WARN_NO_STAKING, + CREDENTIAL_POLICY_WARN_REWARD_ADDRESS, +) from .helpers.utils import ( format_account_number, format_asset_fingerprint, + format_key_hash, format_optional_int, + format_script_hash, format_stake_pool_id, to_account_path, ) +from .seed import is_minting_path, is_multisig_path if False: from trezor import wire from trezor.messages import ( - CardanoBlockchainPointerType, + CardanoNativeScript, CardanoTxCertificate, CardanoTxWithdrawal, CardanoPoolParametersType, @@ -38,14 +54,30 @@ ) from trezor.ui.layouts import PropertyType + from .helpers.credential_params import CredentialParams ADDRESS_TYPE_NAMES = { CardanoAddressType.BYRON: "Legacy", CardanoAddressType.BASE: "Base", + CardanoAddressType.BASE_SCRIPT_KEY: "Base", + CardanoAddressType.BASE_KEY_SCRIPT: "Base", + CardanoAddressType.BASE_SCRIPT_SCRIPT: "Base", CardanoAddressType.POINTER: "Pointer", + CardanoAddressType.POINTER_SCRIPT: "Pointer", CardanoAddressType.ENTERPRISE: "Enterprise", + CardanoAddressType.ENTERPRISE_SCRIPT: "Enterprise", CardanoAddressType.REWARD: "Reward", + CardanoAddressType.REWARD_SCRIPT: "Reward", +} + +SCRIPT_TYPE_NAMES = { + CardanoNativeScriptType.PUB_KEY: "Key", + CardanoNativeScriptType.ALL: "All", + CardanoNativeScriptType.ANY: "Any", + CardanoNativeScriptType.N_OF_K: "N of K", + CardanoNativeScriptType.INVALID_BEFORE: "Invalid before", + CardanoNativeScriptType.INVALID_HEREAFTER: "Invalid hereafter", } CERTIFICATE_TYPE_NAMES = { @@ -64,17 +96,126 @@ def is_printable_ascii_bytestring(bytestr: bytes) -> bool: return all((32 < b < 127) for b in bytestr) +async def show_native_script( + ctx: wire.Context, + script: CardanoNativeScript, + indices: list[int] = [], +) -> None: + indices_str = ".".join([str(i) for i in indices]) + + script_type_name_suffix = "" + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_path: + script_type_name_suffix = "path" + elif script.key_hash: + script_type_name_suffix = "hash" + + props: list[PropertyType] = [ + ( + "Script%s - %s %s:" + % ( + (" " + indices_str if indices_str else ""), + SCRIPT_TYPE_NAMES[script.type], + script_type_name_suffix, + ), + None, + ) + ] + + 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))) + elif script.key_path: + props.append((address_n_to_str(script.key_path), None)) + elif script.type == CardanoNativeScriptType.N_OF_K: + assert script.required_signatures_count is not None # validate_script + props.append( + ( + "Requires %s out of %s signatures." + % (script.required_signatures_count, len(script.scripts)), + None, + ) + ) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + assert script.invalid_before is not None # validate_script + props.append((str(script.invalid_before), None)) + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + assert script.invalid_hereafter is not None # validate_script + props.append((str(script.invalid_hereafter), None)) + + if script.type in ( + CardanoNativeScriptType.ALL, + CardanoNativeScriptType.ANY, + CardanoNativeScriptType.N_OF_K, + ): + assert script.scripts # validate_script + props.append(("Contains %i nested scripts." % len(script.scripts), None)) + + await confirm_properties( + ctx, + "verify_script", + title="Verify script", + props=props, + br_code=ButtonRequestType.Other, + ) + + for i, sub_script in enumerate(script.scripts): + await show_native_script(ctx, sub_script, indices + [(i + 1)]) + + +async def show_script_hash( + ctx: wire.Context, + script_hash: bytes, + display_format: CardanoNativeScriptHashDisplayFormat, +) -> None: + assert display_format in ( + CardanoNativeScriptHashDisplayFormat.BECH32, + CardanoNativeScriptHashDisplayFormat.POLICY_ID, + ) + + props: list[PropertyType] = [] + if display_format == CardanoNativeScriptHashDisplayFormat.BECH32: + props = [("Script hash:", format_script_hash(script_hash))] + elif display_format == CardanoNativeScriptHashDisplayFormat.POLICY_ID: + props = [("Policy ID:", script_hash)] + + await confirm_properties( + ctx, + "verify_script", + title="Verify script", + props=props, + br_code=ButtonRequestType.Other, + ) + + +async def show_transaction_signing_mode( + ctx: wire.Context, signing_mode: CardanoTxSigningMode +) -> None: + if signing_mode == CardanoTxSigningMode.SCRIPT_TRANSACTION: + await confirm_metadata( + ctx, + "confirm_signing_mode", + title="Confirm transaction", + content="Confirming a script transaction.", + larger_vspace=True, + br_code=ButtonRequestType.Other, + ) + + async def confirm_sending( ctx: wire.Context, ada_amount: int, to: str, + is_change_output: bool, ) -> None: + subtitle = "Change amount:" if is_change_output else "Confirm sending:" await confirm_output( ctx, to, format_coin_amount(ada_amount), title="Confirm transaction", - subtitle="Confirm sending:", + subtitle=subtitle, font_amount=ui.BOLD, width_paginated=17, to_str="\nto\n", @@ -86,6 +227,8 @@ async def confirm_sending( async def confirm_sending_token( ctx: wire.Context, policy_id: bytes, token: CardanoToken ) -> None: + assert token.amount is not None # _validate_token + await confirm_properties( ctx, "confirm_token", @@ -104,13 +247,67 @@ async def confirm_sending_token( ) -async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: - await confirm_metadata( +async def show_credential( + ctx: wire.Context, + credential_params: CredentialParams, + credential_policy: int, +) -> None: + if (credential_policy & CREDENTIAL_POLICY_SHOW_CHANGE_OUTPUT) != 0: + title = "Confirm transaction" + else: + title = "%s address" % ADDRESS_TYPE_NAMES[credential_params.address_type] + + props: list[PropertyType] = [] + + credential = credential_params.get_credential() + # Credential can be empty in case of enterprise address stake credential + # and reward address payment credential. In that case we don't want to + # show some of the "props". + if credential: + if (credential_policy & CREDENTIAL_POLICY_SHOW_CHANGE_OUTPUT) != 0: + address_usage = "Change address" + else: + address_usage = "Address" + + credential_title = credential_params.get_title() + props.append( + ( + "%s %s credential is a %s:" + % (address_usage, credential_params.format_type(), credential_title), + None, + ) + ) + props.extend(credential_params.format()) + + if (credential_policy & CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL) != 0: + props.append((None, "Path is unusual.")) + if (credential_policy & CREDENTIAL_POLICY_WARN_MISMATCH) != 0: + props.append((None, "Credential doesn't match payment credential.")) + if (credential_policy & CREDENTIAL_POLICY_WARN_REWARD_ADDRESS) != 0: + props.append(("Address is a reward address.", None)) + if (credential_policy & CREDENTIAL_POLICY_WARN_NO_STAKING) != 0: + props.append( + ( + "%s address - no staking rewards." + % ADDRESS_TYPE_NAMES[credential_params.address_type], + None, + ) + ) + + if credential_policy in CREDENTIAL_POLICIES_WARN: + icon = ui.ICON_WRONG + icon_color = ui.RED + else: + icon = ui.ICON_SEND + icon_color = ui.GREEN + + await confirm_properties( ctx, - "confirm_tokens", - title="Confirm transaction", - content="The following\ntransaction output\ncontains tokens.", - larger_vspace=True, + "confirm_credential", + title=title, + props=props, + icon=icon, + icon_color=icon_color, br_code=ButtonRequestType.Other, ) @@ -119,92 +316,42 @@ async def show_warning_path(ctx: wire.Context, path: list[int], title: str) -> N await confirm_path_warning(ctx, address_n_to_str(path), path_type=title) -async def show_warning_tx_no_staking_info( - ctx: wire.Context, address_type: CardanoAddressType, amount: int -) -> None: - atype = ADDRESS_TYPE_NAMES[address_type].lower() - content = "Change %s address has no stake rights.\nChange amount:\n{}" % atype +async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: await confirm_metadata( ctx, - "warning_staking", + "confirm_tokens", title="Confirm transaction", - content=content, - param=format_coin_amount(amount), - hide_continue=True, + content="The following\ntransaction output\ncontains tokens.", + larger_vspace=True, br_code=ButtonRequestType.Other, ) -async def show_warning_tx_pointer_address( +async def confirm_witness_request( ctx: wire.Context, - pointer: CardanoBlockchainPointerType, - amount: int, + witness_path: list[int], ) -> None: - await confirm_properties( - ctx, - "warning_pointer", - title="Confirm transaction", - props=[ - ("Change address has a\npointer with staking\nrights.\n\n\n", None), - ( - "Pointer:", - "%s, %s, %s" - % ( - pointer.block_index, - pointer.tx_index, - pointer.certificate_index, - ), - ), - ("Change amount:", format_coin_amount(amount)), - ], - br_code=ButtonRequestType.Other, - ) - + if is_multisig_path(witness_path): + path_title = "multi-sig path" + elif is_minting_path(witness_path): + path_title = "token minting path" + else: + path_title = "path" -async def show_warning_tx_different_staking_account( - ctx: wire.Context, - staking_account_path: list[int], - amount: int, -) -> None: await confirm_properties( ctx, - "warning_differentstaking", + "confirm_total", title="Confirm transaction", props=[ ( - "Change address staking rights do not match the current account.\n\n", - None, - ), - ( - "Staking account %s:" % format_account_number(staking_account_path), - address_n_to_str(staking_account_path), - ), - ("Change amount:", format_coin_amount(amount)), + "Sign transaction with %s:" % path_title, + address_n_to_str(witness_path), + ) ], br_code=ButtonRequestType.Other, ) -async def show_warning_tx_staking_key_hash( - ctx: wire.Context, - staking_key_hash: bytes, - amount: int, -) -> None: - props = [ - ("Change address staking rights do not match the current account.\n\n", None), - ("Staking key hash:", staking_key_hash), - ("Change amount:", format_coin_amount(amount)), - ] - - await confirm_properties( - ctx, - "confirm_different_stakingrights", - title="Confirm transaction", - props=props, - br_code=ButtonRequestType.Other, - ) - - async def confirm_transaction( ctx: wire.Context, fee: int, @@ -244,13 +391,21 @@ async def confirm_certificate( # in this call assert certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION - props = [ + props: list[PropertyType] = [ ("Confirm:", CERTIFICATE_TYPE_NAMES[certificate.type]), - ( - "for account %s:" % format_account_number(certificate.path), - address_n_to_str(to_account_path(certificate.path)), - ), ] + + if certificate.path: + props.append( + ( + "for account %s:" % 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))) @@ -298,6 +453,7 @@ async def confirm_stake_pool_owner( ctx: wire.Context, keychain: seed.Keychain, owner: CardanoPoolOwner, + protocol_magic: int, network_id: int, ) -> None: props: list[tuple[str, str | None]] = [] @@ -305,11 +461,14 @@ async def confirm_stake_pool_owner( props.append(("Pool owner:", address_n_to_str(owner.staking_key_path))) props.append( ( - encode_human_readable_address( - pack_reward_address_bytes( - get_public_key_hash(keychain, owner.staking_key_path), - network_id, - ) + derive_human_readable_address( + keychain, + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n=owner.staking_key_path, + ), + protocol_magic, + network_id, ), None, ) @@ -319,8 +478,14 @@ async def confirm_stake_pool_owner( props.append( ( "Pool owner:", - encode_human_readable_address( - pack_reward_address_bytes(owner.staking_key_hash, network_id) + derive_human_readable_address( + keychain, + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + staking_key_hash=owner.staking_key_hash, + ), + protocol_magic, + network_id, ), ) ) @@ -384,18 +549,28 @@ async def confirm_stake_pool_registration_final( async def confirm_withdrawal( ctx: wire.Context, withdrawal: CardanoTxWithdrawal ) -> None: + props: list[PropertyType] = [ + ("Confirm withdrawal", None), + ] + + if withdrawal.path: + props.append( + ( + "for account %s:" % 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", title="Confirm transaction", - props=[ - ( - "Confirm withdrawal\nfor account %s:" - % format_account_number(withdrawal.path), - address_n_to_str(to_account_path(withdrawal.path)), - ), - ("Amount:", format_coin_amount(withdrawal.amount)), - ], + props=props, br_code=ButtonRequestType.Other, ) @@ -437,43 +612,40 @@ async def show_auxiliary_data_hash( ) -async def show_warning_address_foreign_staking_key( - ctx: wire.Context, - account_path: list[int], - staking_account_path: list[int], - staking_key_hash: bytes | None, -) -> None: - # TODO: confirm_properties not appropriate here - # instead, presumably, this should be a flow: - # 1. show_warning: Mismatch! continue? - # 2. confirm_blob(mismatched_value) - props: list[PropertyType] = [ - ( - "Stake rights associated with this address do not match your account %s:" - % format_account_number(account_path), - address_n_to_str(account_path), - ) - ] +async def show_warning_tx_contains_mint(ctx: wire.Context) -> None: + await confirm_metadata( + ctx, + "confirm_tokens", + title="Confirm transaction", + content="The transaction contains\nminting or burning of\ntokens.", + larger_vspace=True, + br_code=ButtonRequestType.Other, + ) - if staking_account_path: - props.append( - ( - "Stake account %s:" % format_account_number(staking_account_path), - address_n_to_str(staking_account_path), - ) - ) - else: - assert staking_key_hash is not None # _validate_base_address_staking_info - props.append(("Staking key:", staking_key_hash)) - props.append(("Continue?", None)) + +async def confirm_token_minting( + ctx: wire.Context, policy_id: bytes, token: CardanoToken +) -> None: + assert token.mint_amount is not None # _validate_token + is_minting = token.mint_amount >= 0 await confirm_properties( ctx, - "warning_foreign_stakingkey", - title="Warning", - props=props, - icon=ui.ICON_WRONG, - icon_color=ui.RED, + "confirm_mint", + title="Confirm transaction", + props=[ + ( + "Asset fingerprint:", + format_asset_fingerprint( + policy_id=policy_id, + asset_name_bytes=token.asset_name_bytes, + ), + ), + ( + "Amount %s:" % ("minted" if is_minting else "burned"), + format_amount(token.mint_amount, 0), + ), + ], br_code=ButtonRequestType.Other, ) @@ -489,20 +661,39 @@ async def show_warning_tx_network_unverifiable(ctx: wire.Context) -> None: ) -async def show_warning_address_pointer( - ctx: wire.Context, pointer: CardanoBlockchainPointerType +async def show_cardano_address( + ctx: wire.Context, + address_parameters: CardanoAddressParametersType, + address: str, + protocol_magic: int, ) -> None: - content = "Pointer address:\nBlock: %s\nTransaction: %s\nCertificate: %s" % ( - pointer.block_index, - pointer.tx_index, - pointer.certificate_index, - ) - await confirm_metadata( + network_name = None + if not protocol_magics.is_mainnet(protocol_magic): + network_name = protocol_magics.to_ui_string(protocol_magic) + + title = "%s address" % ADDRESS_TYPE_NAMES[address_parameters.address_type] + address_extra = None + title_qr = title + if address_parameters.address_type in ( + CardanoAddressType.BYRON, + CardanoAddressType.BASE, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.POINTER, + CardanoAddressType.ENTERPRISE, + CardanoAddressType.REWARD, + ): + if address_parameters.address_n: + address_extra = address_n_to_str(address_parameters.address_n) + title_qr = address_n_to_str(address_parameters.address_n) + elif address_parameters.address_n_staking: + address_extra = address_n_to_str(address_parameters.address_n_staking) + title_qr = address_n_to_str(address_parameters.address_n_staking) + + await show_address( ctx, - "warning_pointer", - title="Warning", - icon=ui.ICON_WRONG, - icon_color=ui.RED, - content=content, - br_code=ButtonRequestType.Other, + address=address, + title=title, + network=network_name, + address_extra=address_extra, + title_qr=title_qr, ) diff --git a/core/src/apps/cardano/native_script.py b/core/src/apps/cardano/native_script.py new file mode 100644 index 00000000000..a0cb5788cfc --- /dev/null +++ b/core/src/apps/cardano/native_script.py @@ -0,0 +1,171 @@ +from trezor.crypto import hashlib +from trezor.enums import CardanoAddressType, CardanoNativeScriptType + +from apps.cardano.helpers import ( + ADDRESS_KEY_HASH_SIZE, + INVALID_NATIVE_SCRIPT, + SCRIPT_HASH_SIZE, +) +from apps.common import cbor + +from .helpers.paths import SCHEMA_MINT +from .helpers.utils import get_public_key_hash +from .seed import Keychain, is_multisig_path + +if False: + from typing import Any + + from trezor.messages import CardanoNativeScript + + from apps.common.cbor import CborSequence + +SCRIPT_ADDRESS_TYPES = ( + CardanoAddressType.BASE_SCRIPT_KEY, + CardanoAddressType.BASE_KEY_SCRIPT, + CardanoAddressType.BASE_SCRIPT_SCRIPT, + CardanoAddressType.POINTER_SCRIPT, + CardanoAddressType.ENTERPRISE_SCRIPT, + CardanoAddressType.REWARD_SCRIPT, +) + + +def validate_native_script(script: CardanoNativeScript | None) -> None: + if not script: + raise INVALID_NATIVE_SCRIPT + + _validate_native_script_structure(script) + + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_hash and script.key_path: + raise INVALID_NATIVE_SCRIPT + if script.key_hash: + if len(script.key_hash) != ADDRESS_KEY_HASH_SIZE: + raise INVALID_NATIVE_SCRIPT + elif script.key_path: + if not is_multisig_path(script.key_path) and not SCHEMA_MINT.match( + script.key_path + ): + raise INVALID_NATIVE_SCRIPT + else: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.ALL: + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.ANY: + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.N_OF_K: + if script.required_signatures_count is None: + raise INVALID_NATIVE_SCRIPT + if script.required_signatures_count > len(script.scripts): + raise INVALID_NATIVE_SCRIPT + for sub_script in script.scripts: + validate_native_script(sub_script) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + if script.invalid_before is None: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + if script.invalid_hereafter is None: + raise INVALID_NATIVE_SCRIPT + + +def _validate_native_script_structure(script: CardanoNativeScript) -> None: + key_hash = script.key_hash + key_path = script.key_path + scripts = script.scripts + required_signatures_count = script.required_signatures_count + invalid_before = script.invalid_before + invalid_hereafter = script.invalid_hereafter + + fields_to_be_empty: dict[CardanoNativeScriptType, tuple[Any, ...]] = { + CardanoNativeScriptType.PUB_KEY: ( + scripts, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.ALL: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.ANY: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.N_OF_K: ( + key_hash, + key_path, + invalid_before, + invalid_hereafter, + ), + CardanoNativeScriptType.INVALID_BEFORE: ( + key_hash, + key_path, + required_signatures_count, + invalid_hereafter, + ), + CardanoNativeScriptType.INVALID_HEREAFTER: ( + key_hash, + key_path, + required_signatures_count, + invalid_before, + ), + } + + if script.type not in fields_to_be_empty or any(fields_to_be_empty[script.type]): + raise INVALID_NATIVE_SCRIPT + + +def get_native_script_hash(keychain: Keychain, script: CardanoNativeScript) -> bytes: + script_cbor = cbor.encode(cborize_native_script(keychain, script)) + prefixed_script_cbor = b"\00" + script_cbor + return hashlib.blake2b(data=prefixed_script_cbor, outlen=SCRIPT_HASH_SIZE).digest() + + +def cborize_native_script( + keychain: Keychain, script: CardanoNativeScript +) -> CborSequence: + script_content: CborSequence + if script.type == CardanoNativeScriptType.PUB_KEY: + if script.key_hash: + script_content = (script.key_hash,) + elif script.key_path: + script_content = (get_public_key_hash(keychain, script.key_path),) + else: + raise INVALID_NATIVE_SCRIPT + elif script.type == CardanoNativeScriptType.ALL: + script_content = ( + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.ANY: + script_content = ( + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.N_OF_K: + script_content = ( + script.required_signatures_count, + tuple( + cborize_native_script(keychain, sub_script) + for sub_script in script.scripts + ), + ) + elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + script_content = (script.invalid_before,) + elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + script_content = (script.invalid_hereafter,) + else: + raise INVALID_NATIVE_SCRIPT + + return (script.type,) + script_content diff --git a/core/src/apps/cardano/seed.py b/core/src/apps/cardano/seed.py index 16bcb430129..c5290dba7b1 100644 --- a/core/src/apps/cardano/seed.py +++ b/core/src/apps/cardano/seed.py @@ -18,15 +18,20 @@ class Keychain: - """Cardano keychain hard-coded to Byron and Shelley seed namespaces.""" + """ + Cardano keychain hard-coded to 44 (Byron), 1852 (Shelley), 1854 (multi-sig) and 1855 (token minting) + seed namespaces. + """ def __init__(self, root: bip32.HDNode) -> None: self.byron_root = derive_path_cardano(root, paths.BYRON_ROOT) self.shelley_root = derive_path_cardano(root, paths.SHELLEY_ROOT) + self.multisig_root = derive_path_cardano(root, paths.MULTISIG_ROOT) + self.minting_root = derive_path_cardano(root, paths.MINTING_ROOT) root.__del__() def verify_path(self, path: Bip32Path) -> None: - if not is_byron_path(path) and not is_shelley_path(path): + if not self.is_in_keychain(path): raise wire.DataError("Forbidden key path") def _get_path_root(self, path: Bip32Path) -> bip32.HDNode: @@ -34,18 +39,31 @@ def _get_path_root(self, path: Bip32Path) -> bip32.HDNode: return self.byron_root elif is_shelley_path(path): return self.shelley_root + elif is_multisig_path(path): + return self.multisig_root + elif is_minting_path(path): + return self.minting_root else: raise wire.DataError("Forbidden key path") def is_in_keychain(self, path: Bip32Path) -> bool: - return is_byron_path(path) or is_shelley_path(path) + return ( + is_byron_path(path) + or is_shelley_path(path) + or is_multisig_path(path) + or is_minting_path(path) + ) def derive(self, node_path: Bip32Path) -> bip32.HDNode: self.verify_path(node_path) path_root = self._get_path_root(node_path) # this is true now, so for simplicity we don't branch on path type - assert len(paths.BYRON_ROOT) == len(paths.SHELLEY_ROOT) + assert ( + len(paths.BYRON_ROOT) == len(paths.SHELLEY_ROOT) + and len(paths.MULTISIG_ROOT) == len(paths.SHELLEY_ROOT) + and len(paths.MINTING_ROOT) == len(paths.SHELLEY_ROOT) + ) suffix = node_path[len(paths.SHELLEY_ROOT) :] # derive child node from the root @@ -64,6 +82,14 @@ def is_shelley_path(path: Bip32Path) -> bool: return path[: len(paths.SHELLEY_ROOT)] == paths.SHELLEY_ROOT +def is_multisig_path(path: Bip32Path) -> bool: + return path[: len(paths.MULTISIG_ROOT)] == paths.MULTISIG_ROOT + + +def is_minting_path(path: Bip32Path) -> bool: + return path[: len(paths.MINTING_ROOT)] == paths.MINTING_ROOT + + def derive_path_cardano(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: node = root.clone() for i in path: diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 40fa16a6b14..b50e5544849 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -23,6 +23,7 @@ CardanoTxHostAck, CardanoTxInput, CardanoTxItemAck, + CardanoTxMint, CardanoTxOutput, CardanoTxWithdrawal, CardanoTxWitnessRequest, @@ -45,7 +46,6 @@ validate_auxiliary_data, ) from .certificates import ( - assert_certificate_cond, cborize_certificate, cborize_initial_pool_registration_certificate_fields, cborize_pool_metadata, @@ -58,27 +58,38 @@ from .helpers import ( INVALID_OUTPUT, INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE, - INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES, + INVALID_TOKEN_BUNDLE_MINT, INVALID_TOKEN_BUNDLE_OUTPUT, + INVALID_TX_SIGNING_REQUEST, INVALID_WITHDRAWAL, + INVALID_WITNESS_REQUEST, LOVELACE_MAX_SUPPLY, network_ids, protocol_magics, - staking_use_cases, ) from .helpers.account_path_check import AccountPathChecker +from .helpers.address_credential_policy import ( + ADDRESS_POLICY_SHOW_SIMPLE, + ADDRESS_POLICY_SHOW_SPLIT, + CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL, + get_address_policy, + get_change_output_payment_credential_policy, + get_change_output_stake_credential_policy, +) +from .helpers.credential_params import CredentialParams from .helpers.hash_builder_collection import HashBuilderDict, HashBuilderList from .helpers.paths import ( CERTIFICATE_PATH_NAME, CHANGE_OUTPUT_PATH_NAME, CHANGE_OUTPUT_STAKING_PATH_NAME, POOL_OWNER_STAKING_PATH_NAME, + SCHEMA_MINT, SCHEMA_PAYMENT, SCHEMA_STAKING, - SCHEMA_STAKING_ANY_ACCOUNT, WITNESS_PATH_NAME, ) -from .helpers.utils import derive_public_key, to_account_path +from .helpers.pool_owner_path_check import PoolOwnerPathChecker +from .helpers.utils import derive_public_key, validate_stake_credential from .layout import ( confirm_certificate, confirm_sending, @@ -87,17 +98,18 @@ confirm_stake_pool_owner, confirm_stake_pool_parameters, confirm_stake_pool_registration_final, + confirm_token_minting, confirm_transaction, confirm_withdrawal, + confirm_witness_request, + show_credential, + show_transaction_signing_mode, show_warning_path, - show_warning_tx_different_staking_account, + show_warning_tx_contains_mint, show_warning_tx_network_unverifiable, - show_warning_tx_no_staking_info, show_warning_tx_output_contains_tokens, - show_warning_tx_pointer_address, - show_warning_tx_staking_key_hash, ) -from .seed import is_byron_path +from .seed import is_byron_path, is_multisig_path, is_shelley_path if False: from typing import Any, Union @@ -116,6 +128,7 @@ TX_BODY_KEY_WITHDRAWALS = const(5) TX_BODY_KEY_AUXILIARY_DATA = const(7) TX_BODY_KEY_VALIDITY_INTERVAL_START = const(8) +TX_BODY_KEY_MINT = const(9) POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT = 10 @@ -126,6 +139,8 @@ async def sign_tx( ) -> CardanoSignTxFinished: is_network_id_verifiable = await _validate_tx_signing_request(ctx, msg) + await show_transaction_signing_mode(ctx, msg.signing_mode) + # inputs, outputs and fee are mandatory fields, count the number of optional fields present tx_body_map_item_count = 3 + sum( ( @@ -134,16 +149,20 @@ async def sign_tx( msg.withdrawals_count > 0, msg.has_auxiliary_data, msg.validity_interval_start is not None, + msg.minting_asset_groups_count > 0, ) ) account_path_checker = AccountPathChecker() + pool_owner_path_checker = PoolOwnerPathChecker() hash_fn = hashlib.blake2b(outlen=32) tx_dict: HashBuilderDict[int, Any] = HashBuilderDict(tx_body_map_item_count) tx_dict.start(hash_fn) with tx_dict: - await _process_transaction(ctx, msg, keychain, tx_dict, account_path_checker) + await _process_transaction( + ctx, msg, keychain, tx_dict, account_path_checker, pool_owner_path_checker + ) await _confirm_transaction(ctx, msg, is_network_id_verifiable) @@ -155,8 +174,11 @@ async def sign_tx( tx_hash, msg.witness_requests_count, msg.signing_mode, + msg.minting_asset_groups_count > 0, account_path_checker, + pool_owner_path_checker, ) + await ctx.call(response_after_witness_requests, CardanoTxHostAck) await ctx.call(CardanoTxBodyHash(tx_hash=tx_hash), CardanoTxHostAck) @@ -182,6 +204,11 @@ async def _validate_tx_signing_request( await show_warning_tx_network_unverifiable(ctx) elif msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: _validate_stake_pool_registration_tx_structure(msg) + elif msg.signing_mode == CardanoTxSigningMode.SCRIPT_TRANSACTION: + if not is_network_id_verifiable: + await show_warning_tx_network_unverifiable(ctx) + else: + raise INVALID_TX_SIGNING_REQUEST return is_network_id_verifiable @@ -192,6 +219,7 @@ async def _process_transaction( keychain: seed.Keychain, tx_dict: HashBuilderDict, account_path_checker: AccountPathChecker, + pool_owner_path_checker: PoolOwnerPathChecker, ) -> None: inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList(msg.inputs_count) with tx_dict.add(TX_BODY_KEY_INPUTS, inputs_list): @@ -227,6 +255,7 @@ async def _process_transaction( msg.protocol_magic, msg.network_id, account_path_checker, + pool_owner_path_checker, ) if msg.withdrawals_count > 0: @@ -239,6 +268,7 @@ async def _process_transaction( keychain, withdrawals_dict, msg.withdrawals_count, + msg.signing_mode, msg.protocol_magic, msg.network_id, account_path_checker, @@ -256,13 +286,23 @@ async def _process_transaction( if msg.validity_interval_start is not None: tx_dict.add(TX_BODY_KEY_VALIDITY_INTERVAL_START, msg.validity_interval_start) + if msg.minting_asset_groups_count > 0: + minting_dict: HashBuilderDict[bytes, HashBuilderDict] = HashBuilderDict( + msg.minting_asset_groups_count + ) + with tx_dict.add(TX_BODY_KEY_MINT, minting_dict): + await _process_minting(ctx, minting_dict) + async def _confirm_transaction( ctx: wire.Context, msg: CardanoSignTxInit, is_network_id_verifiable: bool, ) -> None: - if msg.signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + if msg.signing_mode in ( + CardanoTxSigningMode.ORDINARY_TRANSACTION, + CardanoTxSigningMode.SCRIPT_TRANSACTION, + ): await confirm_transaction( ctx, msg.fee, @@ -275,6 +315,8 @@ async def _confirm_transaction( await confirm_stake_pool_registration_final( ctx, msg.protocol_magic, msg.ttl, msg.validity_interval_start ) + else: + raise ValueError async def _process_inputs( @@ -302,13 +344,21 @@ async def _process_outputs( total_amount = 0 for _ in range(outputs_count): output: CardanoTxOutput = await ctx.call(CardanoTxItemAck(), CardanoTxOutput) - _validate_output(output, protocol_magic, network_id, account_path_checker) - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + _validate_output( + keychain, + output, + signing_mode, + protocol_magic, + network_id, + account_path_checker, + ) + + should_show_output = _should_show_output(keychain, output, signing_mode) + if should_show_output: await _show_output( ctx, keychain, output, - signing_mode, protocol_magic, network_id, ) @@ -335,7 +385,7 @@ async def _process_outputs( ctx, asset_groups_dict, output.asset_groups_count, - _should_show_tokens(output, signing_mode), + should_show_output, ) total_amount += output.amount @@ -358,8 +408,8 @@ async def _process_asset_groups( asset_group: CardanoAssetGroup = await ctx.call( CardanoTxItemAck(), CardanoAssetGroup ) - asset_group.policy_id = bytes(asset_group.policy_id) - _validate_asset_group(asset_group, seen_policy_ids) + asset_group.policy_id = asset_group.policy_id + _validate_output_asset_group(asset_group, seen_policy_ids) seen_policy_ids.add(asset_group.policy_id) tokens: HashBuilderDict[bytes, int] = HashBuilderDict(asset_group.tokens_count) @@ -386,12 +436,13 @@ async def _process_tokens( seen_asset_name_bytes: set[bytes] = set() for _ in range(tokens_count): token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) - token.asset_name_bytes = bytes(token.asset_name_bytes) - _validate_token(token, seen_asset_name_bytes) + token.asset_name_bytes = token.asset_name_bytes + _validate_output_token(token, seen_asset_name_bytes) seen_asset_name_bytes.add(token.asset_name_bytes) if should_show_tokens: await confirm_sending_token(ctx, policy_id, token) + assert token.amount is not None # _validate_token tokens_dict.add(token.asset_name_bytes, token.amount) @@ -404,6 +455,7 @@ async def _process_certificates( protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, + pool_owner_path_checker: PoolOwnerPathChecker, ) -> None: """Read, validate, confirm and serialize the certificates.""" if certificates_count == 0: @@ -440,8 +492,10 @@ async def _process_certificates( keychain, pool_owners_list, pool_parameters.owners_count, + protocol_magic, network_id, account_path_checker, + pool_owner_path_checker, ) relays_list: HashBuilderList[cbor.CborSequence] = HashBuilderList( @@ -462,21 +516,22 @@ async def _process_pool_owners( keychain: seed.Keychain, pool_owners_list: HashBuilderList[bytes], owners_count: int, + protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, + pool_owner_path_checker: PoolOwnerPathChecker, ) -> None: - owners_as_path_count = 0 for _ in range(owners_count): owner: CardanoPoolOwner = await ctx.call(CardanoTxItemAck(), CardanoPoolOwner) validate_pool_owner(owner, account_path_checker) - await _show_pool_owner(ctx, keychain, owner, network_id) + await _show_pool_owner(ctx, keychain, owner, protocol_magic, network_id) pool_owners_list.append(cborize_pool_owner(keychain, owner)) if owner.staking_key_path: - owners_as_path_count += 1 + pool_owner_path_checker.add(owner.staking_key_path) - assert_certificate_cond(owners_as_path_count == 1) + pool_owner_path_checker.check_if_added() async def _process_pool_relays( @@ -497,6 +552,7 @@ async def _process_withdrawals( keychain: seed.Keychain, withdrawals_dict: HashBuilderDict[bytes, int], withdrawals_count: int, + signing_mode: CardanoTxSigningMode, protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, @@ -507,18 +563,26 @@ async def _process_withdrawals( # until the CIP with canonical CBOR is finalized storing the seen_withdrawals is the only way we can check for # duplicate withdrawals - seen_withdrawals: set[tuple[int, ...]] = set() + seen_withdrawals: set[tuple[int, ...] | bytes] = set() for _ in range(withdrawals_count): withdrawal: CardanoTxWithdrawal = await ctx.call( CardanoTxItemAck(), CardanoTxWithdrawal ) - _validate_withdrawal(withdrawal, seen_withdrawals, account_path_checker) + _validate_withdrawal( + withdrawal, seen_withdrawals, signing_mode, account_path_checker + ) await confirm_withdrawal(ctx, withdrawal) + reward_address_type = ( + CardanoAddressType.REWARD + if withdrawal.path + else CardanoAddressType.REWARD_SCRIPT + ) reward_address = derive_address_bytes( keychain, CardanoAddressParametersType( - address_type=CardanoAddressType.REWARD, - address_n=withdrawal.path, + address_type=reward_address_type, + address_n_staking=withdrawal.path, + script_staking_hash=withdrawal.script_hash, ), protocol_magic, network_id, @@ -561,28 +625,95 @@ async def _process_auxiliary_data( await ctx.call(auxiliary_data_supplement, CardanoTxHostAck) +async def _process_minting( + ctx: wire.Context, minting_dict: HashBuilderDict[bytes, HashBuilderDict] +) -> None: + """Read, validate and serialize the asset groups of token minting.""" + token_minting: CardanoTxMint = await ctx.call(CardanoTxItemAck(), CardanoTxMint) + + await show_warning_tx_contains_mint(ctx) + + # until the CIP with canonical CBOR is finalized storing the seen_policy_ids is the only way we can check for + # duplicate policy_ids + seen_policy_ids: set[bytes] = set() + for _ in range(token_minting.asset_groups_count): + asset_group: CardanoAssetGroup = await ctx.call( + CardanoTxItemAck(), CardanoAssetGroup + ) + asset_group.policy_id = asset_group.policy_id + _validate_minting_asset_group(asset_group, seen_policy_ids) + seen_policy_ids.add(asset_group.policy_id) + + tokens: HashBuilderDict[bytes, int] = HashBuilderDict(asset_group.tokens_count) + with minting_dict.add(asset_group.policy_id, tokens): + await _process_minting_tokens( + ctx, + tokens, + asset_group.policy_id, + asset_group.tokens_count, + ) + + +async def _process_minting_tokens( + ctx: wire.Context, + tokens: HashBuilderDict[bytes, int], + policy_id: bytes, + tokens_count: int, +) -> None: + """Read, validate, confirm and serialize the tokens of an asset group.""" + # until the CIP with canonical CBOR is finalized storing the seen_asset_name_bytes is the only way we can check for + # duplicate tokens + seen_asset_name_bytes: set[bytes] = set() + for _ in range(tokens_count): + token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) + token.asset_name_bytes = token.asset_name_bytes + _validate_minting_token(token, seen_asset_name_bytes) + seen_asset_name_bytes.add(token.asset_name_bytes) + await confirm_token_minting(ctx, policy_id, token) + + assert token.mint_amount is not None # _validate_token + tokens.add(token.asset_name_bytes, token.mint_amount) + + async def _process_witness_requests( ctx: wire.Context, keychain: seed.Keychain, tx_hash: bytes, witness_requests_count: int, signing_mode: CardanoTxSigningMode, + transaction_has_token_minting: bool, account_path_checker: AccountPathChecker, + pool_owner_path_checker: PoolOwnerPathChecker, ) -> CardanoTxResponseType: response: CardanoTxResponseType = CardanoTxItemAck() + for _ in range(witness_requests_count): witness_request = await ctx.call(response, CardanoTxWitnessRequest) - _validate_witness_request(witness_request, signing_mode, account_path_checker) - await _show_witness(ctx, witness_request.path) - - response = ( - _get_byron_witness(keychain, witness_request.path, tx_hash) - if is_byron_path(witness_request.path) - else _get_shelley_witness(keychain, witness_request.path, tx_hash) + _validate_witness_request( + witness_request, + signing_mode, + transaction_has_token_minting, + account_path_checker, + pool_owner_path_checker, ) + await _show_witness_request(ctx, witness_request.path, signing_mode) + response = _get_witness(keychain, witness_request.path, tx_hash) + return response +def _get_witness( + keychain: seed.Keychain, + path: list[int], + tx_hash: bytes, +) -> CardanoTxWitnessResponse: + return ( + _get_byron_witness(keychain, path, tx_hash) + if is_byron_path(path) + else _get_shelley_witness(keychain, path, tx_hash) + ) + + def _get_byron_witness( keychain: seed.Keychain, path: list[int], @@ -628,12 +759,15 @@ def _validate_stake_pool_registration_tx_structure(msg: CardanoSignTxInit) -> No msg.certificates_count != 1 or msg.signing_mode != CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER or msg.withdrawals_count != 0 + or msg.minting_asset_groups_count != 0 ): raise INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE def _validate_output( + keychain: seed.Keychain, output: CardanoTxOutput, + signing_mode: CardanoTxSigningMode, protocol_magic: int, network_id: int, account_path_checker: AccountPathChecker, @@ -641,8 +775,12 @@ def _validate_output( if output.address_parameters and output.address is not None: raise INVALID_OUTPUT - if output.address_parameters: - validate_address_parameters(output.address_parameters) + if address_parameters := output.address_parameters: + if signing_mode != CardanoTxSigningMode.ORDINARY_TRANSACTION: + raise INVALID_OUTPUT + + validate_address_parameters(address_parameters) + _fail_if_strict_and_unusual(keychain, address_parameters) elif output.address is not None: validate_output_address(output.address, protocol_magic, network_id) else: @@ -651,59 +789,127 @@ def _validate_output( account_path_checker.add_output(output) +def _should_show_output( + keychain: seed.Keychain, + output: CardanoTxOutput, + signing_mode: CardanoTxSigningMode, +) -> bool: + if signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + return False + + if output.address_parameters: # is change output + address_policy = get_address_policy(keychain, output.address_parameters) + if address_policy == ADDRESS_POLICY_SHOW_SIMPLE: + # we don't need to display simple address outputs + return False + + return True + + async def _show_output( ctx: wire.Context, keychain: seed.Keychain, output: CardanoTxOutput, - signing_mode: CardanoTxSigningMode, protocol_magic: int, network_id: int, ) -> None: - if output.address_parameters: - await _fail_or_warn_if_invalid_path( - ctx, - SCHEMA_PAYMENT, - output.address_parameters.address_n, - CHANGE_OUTPUT_PATH_NAME, - ) + if output.asset_groups_count > 0: + await show_warning_tx_output_contains_tokens(ctx) + + is_change_output: bool + if address_parameters := output.address_parameters: + is_change_output = True + + address_policy = get_address_policy(keychain, address_parameters) + assert address_policy == ADDRESS_POLICY_SHOW_SPLIT # _should_show_output - await _show_change_output_staking_warnings( - ctx, keychain, output.address_parameters, output.amount + payment_credential_policy = get_change_output_payment_credential_policy( + address_parameters + ) + stake_credential_policy = get_change_output_stake_credential_policy( + keychain, address_parameters ) - if _should_hide_output(output.address_parameters.address_n): - return + await show_credential( + ctx, + CredentialParams(CredentialParams.TYPE_PAYMENT, address_parameters), + payment_credential_policy, + ) + await show_credential( + ctx, + CredentialParams(CredentialParams.TYPE_STAKE, address_parameters), + stake_credential_policy, + ) address = derive_human_readable_address( - keychain, output.address_parameters, protocol_magic, network_id + keychain, address_parameters, protocol_magic, network_id ) else: + is_change_output = False + assert output.address is not None # _validate_output address = output.address - if output.asset_groups_count > 0: - await show_warning_tx_output_contains_tokens(ctx) + await confirm_sending(ctx, output.amount, address, is_change_output) - if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: - await confirm_sending(ctx, output.amount, address) +def _validate_output_asset_group( + asset_group: CardanoAssetGroup, seen_policy_ids: set[bytes] +) -> None: + _validate_asset_group(asset_group, seen_policy_ids, False) -def _validate_asset_group( + +def _validate_minting_asset_group( asset_group: CardanoAssetGroup, seen_policy_ids: set[bytes] ) -> None: + _validate_asset_group(asset_group, seen_policy_ids, True) + + +def _validate_asset_group( + asset_group: CardanoAssetGroup, seen_policy_ids: set[bytes], is_mint: bool +) -> None: + INVALID_TOKEN_BUNDLE = ( + INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT + ) + if len(asset_group.policy_id) != MINTING_POLICY_ID_LENGTH: - raise INVALID_TOKEN_BUNDLE_OUTPUT + raise INVALID_TOKEN_BUNDLE if asset_group.tokens_count == 0: - raise INVALID_TOKEN_BUNDLE_OUTPUT + raise INVALID_TOKEN_BUNDLE if asset_group.policy_id in seen_policy_ids: - raise INVALID_TOKEN_BUNDLE_OUTPUT + raise INVALID_TOKEN_BUNDLE + + +def _validate_output_token( + token: CardanoToken, seen_asset_name_bytes: set[bytes] +) -> None: + _validate_token(token, seen_asset_name_bytes, False) -def _validate_token(token: CardanoToken, seen_asset_name_bytes: set[bytes]) -> None: +def _validate_minting_token( + token: CardanoToken, seen_asset_name_bytes: set[bytes] +) -> None: + _validate_token(token, seen_asset_name_bytes, True) + + +def _validate_token( + token: CardanoToken, seen_asset_name_bytes: set[bytes], is_mint: bool +) -> None: + INVALID_TOKEN_BUNDLE = ( + INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT + ) + + if is_mint: + if token.mint_amount is None or token.amount is not None: + raise INVALID_TOKEN_BUNDLE + else: + if token.amount is None or token.mint_amount is not None: + raise INVALID_TOKEN_BUNDLE + if len(token.asset_name_bytes) > MAX_ASSET_NAME_LENGTH: - raise INVALID_TOKEN_BUNDLE_OUTPUT + raise INVALID_TOKEN_BUNDLE if token.asset_name_bytes in seen_asset_name_bytes: - raise INVALID_TOKEN_BUNDLE_OUTPUT + raise INVALID_TOKEN_BUNDLE async def _show_certificate( @@ -712,30 +918,38 @@ async def _show_certificate( signing_mode: CardanoTxSigningMode, ) -> None: if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + assert certificate.path # validate_certificate await _fail_or_warn_if_invalid_path( ctx, SCHEMA_STAKING, certificate.path, CERTIFICATE_PATH_NAME ) await confirm_certificate(ctx, certificate) + elif signing_mode == CardanoTxSigningMode.SCRIPT_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) def _validate_withdrawal( withdrawal: CardanoTxWithdrawal, - seen_withdrawals: set[tuple[int, ...]], + seen_withdrawals: set[tuple[int, ...] | bytes], + signing_mode: CardanoTxSigningMode, account_path_checker: AccountPathChecker, ) -> None: - if not SCHEMA_STAKING_ANY_ACCOUNT.match(withdrawal.path): - raise INVALID_WITHDRAWAL + validate_stake_credential( + withdrawal.path, withdrawal.script_hash, signing_mode, INVALID_WITHDRAWAL + ) if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: raise INVALID_WITHDRAWAL - path_tuple = tuple(withdrawal.path) - if path_tuple in seen_withdrawals: + credential = tuple(withdrawal.path) if withdrawal.path else withdrawal.script_hash + assert credential # validate_stake_credential + + if credential in seen_withdrawals: raise wire.ProcessError("Duplicate withdrawals") else: - seen_withdrawals.add(path_tuple) + seen_withdrawals.add(credential) account_path_checker.add_withdrawal(withdrawal) @@ -777,7 +991,11 @@ async def _show_stake_pool_registration_certificate( async def _show_pool_owner( - ctx: wire.Context, keychain: seed.Keychain, owner: CardanoPoolOwner, network_id: int + ctx: wire.Context, + keychain: seed.Keychain, + owner: CardanoPoolOwner, + protocol_magic: int, + network_id: int, ) -> None: if owner.staking_key_path: await _fail_or_warn_if_invalid_path( @@ -787,115 +1005,60 @@ async def _show_pool_owner( POOL_OWNER_STAKING_PATH_NAME, ) - await confirm_stake_pool_owner(ctx, keychain, owner, network_id) + await confirm_stake_pool_owner(ctx, keychain, owner, protocol_magic, network_id) def _validate_witness_request( witness_request: CardanoTxWitnessRequest, signing_mode: CardanoTxSigningMode, + transaction_has_token_minting: bool, account_path_checker: AccountPathChecker, + pool_owner_path_checker: PoolOwnerPathChecker, ) -> None: - # witness path validation happens in _show_witness + # further witness path validation happens in _show_witness_request + is_minting = SCHEMA_MINT.match(witness_request.path) - if signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: - _ensure_no_payment_witness(witness_request) + if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + if not ( + is_byron_path(witness_request.path) + or is_shelley_path(witness_request.path) + or is_minting + ): + raise INVALID_WITNESS_REQUEST + if is_minting and not transaction_has_token_minting: + raise INVALID_WITNESS_REQUEST + elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + pool_owner_path_checker.check_witness_request(witness_request.path) + elif signing_mode == CardanoTxSigningMode.SCRIPT_TRANSACTION: + if not is_multisig_path(witness_request.path) and not is_minting: + raise INVALID_WITNESS_REQUEST + if is_minting and not transaction_has_token_minting: + raise INVALID_WITNESS_REQUEST account_path_checker.add_witness_request(witness_request) -def _ensure_no_payment_witness(witness: CardanoTxWitnessRequest) -> None: - """ - We have a separate tx signing flow for stake pool registration because it's a - transaction where the witnessable entries (i.e. inputs, withdrawals, etc.) - in the transaction are not supposed to be controlled by the HW wallet, which - means the user is vulnerable to unknowingly supplying a witness for an UTXO - or other tx entry they think is external, resulting in the co-signers - gaining control over their funds (Something SLIP-0019 is dealing with for - BTC but no similar standard is currently available for Cardano). Hence we - completely forbid witnessing inputs and other entries of the transaction - except the stake pool certificate itself and we provide a witness only to the - user's staking key in the list of pool owners. - """ - if not SCHEMA_STAKING_ANY_ACCOUNT.match(witness.path): - raise INVALID_STAKEPOOL_REGISTRATION_TX_WITNESSES - - -async def _show_witness( +async def _show_witness_request( ctx: wire.Context, witness_path: list[int], + signing_mode: CardanoTxSigningMode, ) -> None: - if not SCHEMA_PAYMENT.match(witness_path) and not SCHEMA_STAKING.match( - witness_path - ): - await _fail_or_warn_path( - ctx, - witness_path, - WITNESS_PATH_NAME, - ) - - -async def _show_change_output_staking_warnings( - ctx: wire.Context, - keychain: seed.Keychain, - address_parameters: CardanoAddressParametersType, - amount: int, -) -> None: - address_type = address_parameters.address_type - - if ( - address_type == CardanoAddressType.BASE - and not address_parameters.staking_key_hash - ): - await _fail_or_warn_if_invalid_path( - ctx, - SCHEMA_STAKING, - address_parameters.address_n_staking, - CHANGE_OUTPUT_STAKING_PATH_NAME, - ) - - staking_use_case = staking_use_cases.get(keychain, address_parameters) - if staking_use_case == staking_use_cases.NO_STAKING: - await show_warning_tx_no_staking_info(ctx, address_type, amount) - elif staking_use_case == staking_use_cases.POINTER_ADDRESS: - # ensured in _derive_shelley_address: - assert address_parameters.certificate_pointer is not None - await show_warning_tx_pointer_address( - ctx, - address_parameters.certificate_pointer, - amount, - ) - elif staking_use_case == staking_use_cases.MISMATCH: - if address_parameters.address_n_staking: - await show_warning_tx_different_staking_account( - ctx, - to_account_path(address_parameters.address_n_staking), - amount, - ) - else: - # ensured in _validate_base_address_staking_info: - assert address_parameters.staking_key_hash - await show_warning_tx_staking_key_hash( - ctx, - address_parameters.staking_key_hash, - amount, - ) - - -def _should_hide_output(path: list[int]) -> bool: - """Return whether the output address is from a safe path, so it could be hidden.""" - return SCHEMA_PAYMENT.match(path) - - -def _should_show_tokens( - output: CardanoTxOutput, signing_mode: CardanoTxSigningMode -) -> bool: - if signing_mode != CardanoTxSigningMode.ORDINARY_TRANSACTION: - return False - - if output.address_parameters: - return not _should_hide_output(output.address_parameters.address_n) - - return True + if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + # In an ordinary transaction we only allow payment, staking or minting paths. + # If the path is an unusual payment or staking path, we either fail or show the path to the user + # depending on Trezor's configuration. If it's a minting path, we always show it. + is_payment = SCHEMA_PAYMENT.match(witness_path) + is_staking = SCHEMA_STAKING.match(witness_path) + is_minting = SCHEMA_MINT.match(witness_path) + + if is_minting: + await confirm_witness_request(ctx, witness_path) + elif not is_payment and not is_staking: + await _fail_or_warn_path(ctx, witness_path, WITNESS_PATH_NAME) + elif signing_mode == CardanoTxSigningMode.SCRIPT_TRANSACTION: + await confirm_witness_request(ctx, witness_path) + elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + await confirm_witness_request(ctx, witness_path) def _is_network_id_verifiable(msg: CardanoSignTxInit) -> bool: @@ -925,3 +1088,22 @@ async def _fail_or_warn_path( raise wire.DataError("Invalid %s" % path_name.lower()) else: await show_warning_path(ctx, path, path_name) + + +def _fail_if_strict_and_unusual( + keychain: seed.Keychain, address_parameters: CardanoAddressParametersType +) -> None: + if not safety_checks.is_strict(): + return + + payment_credential_policy = get_change_output_payment_credential_policy( + address_parameters + ) + if (payment_credential_policy & CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL) != 0: + raise wire.DataError("Invalid %s" % CHANGE_OUTPUT_PATH_NAME.lower()) + + stake_credential_policy = get_change_output_stake_credential_policy( + keychain, address_parameters + ) + if (stake_credential_policy & CREDENTIAL_POLICY_FAIL_OR_WARN_UNUSUAL) != 0: + raise wire.DataError("Invalid %s" % CHANGE_OUTPUT_STAKING_PATH_NAME.lower()) diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index b81a6426b3e..db983ebe061 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -152,6 +152,8 @@ def find_message_handler_module(msg_type: int) -> str: return "apps.cardano.get_public_key" elif msg_type == MessageType.CardanoSignTxInit: return "apps.cardano.sign_tx" + elif msg_type == MessageType.CardanoGetNativeScriptHash: + return "apps.cardano.get_native_script_hash" # tezos elif msg_type == MessageType.TezosGetAddress: diff --git a/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py b/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py new file mode 100644 index 00000000000..3a9c5502d43 --- /dev/null +++ b/core/src/trezor/enums/CardanoNativeScriptHashDisplayFormat.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +HIDE = 0 +BECH32 = 1 +POLICY_ID = 2 diff --git a/core/src/trezor/enums/CardanoNativeScriptType.py b/core/src/trezor/enums/CardanoNativeScriptType.py new file mode 100644 index 00000000000..72d6e6647c2 --- /dev/null +++ b/core/src/trezor/enums/CardanoNativeScriptType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +PUB_KEY = 0 +ALL = 1 +ANY = 2 +N_OF_K = 3 +INVALID_BEFORE = 4 +INVALID_HEREAFTER = 5 diff --git a/core/src/trezor/enums/CardanoTxSigningMode.py b/core/src/trezor/enums/CardanoTxSigningMode.py index 64dd6589892..653ced80683 100644 --- a/core/src/trezor/enums/CardanoTxSigningMode.py +++ b/core/src/trezor/enums/CardanoTxSigningMode.py @@ -4,3 +4,4 @@ ORDINARY_TRANSACTION = 0 POOL_REGISTRATION_AS_OWNER = 1 +SCRIPT_TRANSACTION = 2 diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index fbe67c573d7..048f67941a1 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -150,6 +150,9 @@ CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 + CardanoTxMint = 332 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 2f5b42ab0d9..906a514dc5c 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -155,6 +155,9 @@ class MessageType(IntEnum): CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 + CardanoTxMint = 332 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 @@ -328,6 +331,19 @@ class CardanoAddressType(IntEnum): REWARD = 14 REWARD_SCRIPT = 15 + class CardanoNativeScriptType(IntEnum): + PUB_KEY = 0 + ALL = 1 + ANY = 2 + N_OF_K = 3 + INVALID_BEFORE = 4 + INVALID_HEREAFTER = 5 + + class CardanoNativeScriptHashDisplayFormat(IntEnum): + HIDE = 0 + BECH32 = 1 + POLICY_ID = 2 + class CardanoCertificateType(IntEnum): STAKE_REGISTRATION = 0 STAKE_DEREGISTRATION = 1 @@ -346,6 +362,7 @@ class CardanoTxAuxiliaryDataSupplementType(IntEnum): class CardanoTxSigningMode(IntEnum): ORDINARY_TRANSACTION = 0 POOL_REGISTRATION_AS_OWNER = 1 + SCRIPT_TRANSACTION = 2 class CardanoTxWitnessType(IntEnum): BYRON_WITNESS = 0 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 7d0fc5014d8..d35e9fa61db 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -27,6 +27,8 @@ def __getattr__(name: str) -> Any: from trezor.enums import Capability # noqa: F401 from trezor.enums import CardanoAddressType # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401 + from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 + from trezor.enums import CardanoNativeScriptType # noqa: F401 from trezor.enums import CardanoPoolRelayType # noqa: F401 from trezor.enums import CardanoTxAuxiliaryDataSupplementType # noqa: F401 from trezor.enums import CardanoTxSigningMode # noqa: F401 @@ -1056,12 +1058,70 @@ def __init__( def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoBlockchainPointerType"]: return isinstance(msg, cls) + class CardanoNativeScript(protobuf.MessageType): + type: "CardanoNativeScriptType" + scripts: "list[CardanoNativeScript]" + key_hash: "bytes | None" + key_path: "list[int]" + required_signatures_count: "int | None" + invalid_before: "int | None" + invalid_hereafter: "int | None" + + def __init__( + self, + *, + type: "CardanoNativeScriptType", + scripts: "list[CardanoNativeScript] | None" = None, + key_path: "list[int] | None" = None, + key_hash: "bytes | None" = None, + required_signatures_count: "int | None" = None, + invalid_before: "int | None" = None, + invalid_hereafter: "int | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoNativeScript"]: + return isinstance(msg, cls) + + class CardanoGetNativeScriptHash(protobuf.MessageType): + script: "CardanoNativeScript" + display_format: "CardanoNativeScriptHashDisplayFormat" + + def __init__( + self, + *, + script: "CardanoNativeScript", + display_format: "CardanoNativeScriptHashDisplayFormat", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoGetNativeScriptHash"]: + return isinstance(msg, cls) + + class CardanoNativeScriptHash(protobuf.MessageType): + script_hash: "bytes" + + def __init__( + self, + *, + script_hash: "bytes", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoNativeScriptHash"]: + return isinstance(msg, cls) + class CardanoAddressParametersType(protobuf.MessageType): address_type: "CardanoAddressType" address_n: "list[int]" address_n_staking: "list[int]" staking_key_hash: "bytes | None" certificate_pointer: "CardanoBlockchainPointerType | None" + script_payment_hash: "bytes | None" + script_staking_hash: "bytes | None" def __init__( self, @@ -1071,6 +1131,8 @@ def __init__( address_n_staking: "list[int] | None" = None, staking_key_hash: "bytes | None" = None, certificate_pointer: "CardanoBlockchainPointerType | None" = None, + script_payment_hash: "bytes | None" = None, + script_staking_hash: "bytes | None" = None, ) -> None: pass @@ -1157,6 +1219,7 @@ class CardanoSignTxInit(protobuf.MessageType): has_auxiliary_data: "bool" validity_interval_start: "int | None" witness_requests_count: "int" + minting_asset_groups_count: "int" def __init__( self, @@ -1171,6 +1234,7 @@ def __init__( withdrawals_count: "int", has_auxiliary_data: "bool", witness_requests_count: "int", + minting_asset_groups_count: "int", ttl: "int | None" = None, validity_interval_start: "int | None" = None, ) -> None: @@ -1234,13 +1298,15 @@ def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoAssetGroup"] class CardanoToken(protobuf.MessageType): asset_name_bytes: "bytes" - amount: "int" + amount: "int | None" + mint_amount: "int | None" def __init__( self, *, asset_name_bytes: "bytes", - amount: "int", + amount: "int | None" = None, + mint_amount: "int | None" = None, ) -> None: pass @@ -1339,6 +1405,7 @@ class CardanoTxCertificate(protobuf.MessageType): path: "list[int]" pool: "bytes | None" pool_parameters: "CardanoPoolParametersType | None" + script_hash: "bytes | None" def __init__( self, @@ -1347,6 +1414,7 @@ def __init__( path: "list[int] | None" = None, pool: "bytes | None" = None, pool_parameters: "CardanoPoolParametersType | None" = None, + script_hash: "bytes | None" = None, ) -> None: pass @@ -1357,12 +1425,14 @@ def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoTxCertificat class CardanoTxWithdrawal(protobuf.MessageType): path: "list[int]" amount: "int" + script_hash: "bytes | None" def __init__( self, *, amount: "int", path: "list[int] | None" = None, + script_hash: "bytes | None" = None, ) -> None: pass @@ -1406,6 +1476,20 @@ def __init__( def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoTxAuxiliaryData"]: return isinstance(msg, cls) + class CardanoTxMint(protobuf.MessageType): + asset_groups_count: "int" + + def __init__( + self, + *, + asset_groups_count: "int", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["CardanoTxMint"]: + return isinstance(msg, cls) + class CardanoTxItemAck(protobuf.MessageType): @classmethod diff --git a/core/tests/test_apps.cardano.address.py b/core/tests/test_apps.cardano.address.py index 30410df4336..b68d28c6ce6 100644 --- a/core/tests/test_apps.cardano.address.py +++ b/core/tests/test_apps.cardano.address.py @@ -285,142 +285,154 @@ def test_slip39_256(self): self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub) self.assertEqual(hexlify(n.chain_code()), chain) - def test_base_address(self): - mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + def test_testnet_byron_address(self): + mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) keychain = Keychain(node) - test_vectors = [ - # network id, account, expected result - # data generated with code under test - (network_ids.MAINNET, 4, "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"), - (network_ids.TESTNET, 4, "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"), + addresses = [ + "2657WMsDfac5F3zbgs9BwNWx3dhGAJERkAL93gPa68NJ2i8mbCHm2pLUHWSj8Mfea", + "2657WMsDfac6ezKWszxLFqJjSUgpg9NgxKc1koqi24sVpRaPhiwMaExk4useKn5HA", + "2657WMsDfac7hr1ioJGr6g7r6JRx4r1My8Rj91tcPTeVjJDpfBYKURrPG2zVLx2Sq", ] - for network_id, account, expected_address in test_vectors: + for i, expected in enumerate(addresses): + # 44'/1815'/0'/0/i' address_parameters = CardanoAddressParametersType( - address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0], - address_n_staking=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 2, 0] + address_type=CardanoAddressType.BYRON, + address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], ) - actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) - - self.assertEqual(actual_address, expected_address) + address = derive_human_readable_address(keychain, address_parameters, protocol_magics.TESTNET, 0) + self.assertEqual(expected, address) - def test_base_address_with_staking_key_hash(self): - mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" + def test_derive_address(self): + mnemonic = "all all all all all all all all all all all all" passphrase = "" node = bip32.from_mnemonic_cardano(mnemonic, passphrase) keychain = Keychain(node) - test_vectors = [ - # network id, account, staking key hash, expected result - # own staking key hash - # data generated with code under test - (network_ids.MAINNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"), - (network_ids.TESTNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"), - # staking key hash not owned - derived with "all all..." mnenomnic, data generated with code under test - (network_ids.MAINNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsxrrvc2"), - (network_ids.MAINNET, 0, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzersj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms6xjnst"), - (network_ids.TESTNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms947v54"), - ] - - for network_id, account, staking_key_hash, expected_address in test_vectors: - address_parameters = CardanoAddressParametersType( + address_parameters = { + "BASE": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 2, 0] + ), + "BASE_OWN_STAKING_KEY_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff") + ), + "BASE_OWN_STAKING_KEY_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff") + ), + # staking key hash not owned - derived with "all all..." mnenomnic + "BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 4 | HARDENED, 0, 0], + staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277") + ), + # staking key hash not owned - derived with "all all..." mnenomnic + "BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_0": CardanoAddressParametersType( address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0], - staking_key_hash=staking_key_hash, - ) - actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) - - self.assertEqual(actual_address, expected_address) - - def test_enterprise_address(self): - mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" - passphrase = "" - node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - keychain = Keychain(node) - - test_vectors = [ - # network id, expected result - (network_ids.MAINNET, "addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8"), - (network_ids.TESTNET, "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz") - ] - - for network_id, expected_address in test_vectors: - address_parameters = CardanoAddressParametersType( - address_type=CardanoAddressType.ENTERPRISE, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - ) - actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) - - self.assertEqual(actual_address, expected_address) - - def test_pointer_address(self): - mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" - passphrase = "" - node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - keychain = Keychain(node) - - test_vectors = [ - # network id, pointer, expected result - (network_ids.MAINNET, CardanoBlockchainPointerType(block_index=1, tx_index=2, certificate_index=3), "addr1gx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpse33frd"), - (network_ids.TESTNET, CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), "addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5ph3wczvf2pfz4ly") - ] - - for network_id, pointer, expected_address in test_vectors: - address_parameters = CardanoAddressParametersType( + staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277") + ), + "BASE_SCRIPT_KEY_SCRIPT_HASH":CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "BASE_KEY_SCRIPT_HASH":CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + "BASE_SCRIPT_SCRIPT_HASHES": CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + "POINTER1": CardanoAddressParametersType( address_type=CardanoAddressType.POINTER, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - certificate_pointer=pointer, - ) - actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) - - self.assertEqual(actual_address, expected_address) - - def test_reward_address(self): - mnemonic = "test walk nut penalty hip pave soap entry language right filter choice" - passphrase = "" - node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - keychain = Keychain(node) - + certificate_pointer=CardanoBlockchainPointerType(block_index=1, tx_index=2, certificate_index=3), + ), + "POINTER2": CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + ), + "POINTER_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "ENTERPRISE": CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + "ENTERPRISE_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + ), + "REWARD": CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + "REWARD_SCRIPT_HASH": CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD_SCRIPT, + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + } test_vectors = [ - # network id, expected result - (network_ids.MAINNET, "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz"), - (network_ids.TESTNET, "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl") + # base address + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqsx9990"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqnsc9fs"), + # base address with staking key hash + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_OWN_STAKING_KEY_HASH"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsydc62k"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE_OWN_STAKING_KEY_HASH"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hls8m96xf"), + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4"], "addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms06skxl"), + (network_ids.MAINNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_0"], "addr1qxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsl3s9zt"), + (network_ids.TESTNET, CardanoAddressType.BASE, address_parameters["BASE_FOREIGN_STAKING_KEY_HASH_ACCOUNT_4"], "addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsvvdk2q"), + # base_script_key address + (network_ids.MAINNET, CardanoAddressType.BASE_SCRIPT_KEY, address_parameters["BASE_SCRIPT_KEY_SCRIPT_HASH"], "addr1zyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsf42dkl"), + (network_ids.TESTNET, CardanoAddressType.BASE_SCRIPT_KEY, address_parameters["BASE_SCRIPT_KEY_SCRIPT_HASH"], "addr_test1zqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms2rhd6q"), + # base_key_script address + (network_ids.MAINNET, CardanoAddressType.BASE_KEY_SCRIPT, address_parameters["BASE_KEY_SCRIPT_HASH"], "addr1yxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s8vnrtt"), + (network_ids.TESTNET, CardanoAddressType.BASE_KEY_SCRIPT, address_parameters["BASE_KEY_SCRIPT_HASH"], "addr_test1yzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sy6wr85"), + # base_script_script address + (network_ids.MAINNET, CardanoAddressType.BASE_SCRIPT_SCRIPT, address_parameters["BASE_SCRIPT_SCRIPT_HASHES"], "addr1xyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5s3gftll"), + (network_ids.TESTNET, CardanoAddressType.BASE_SCRIPT_SCRIPT, address_parameters["BASE_SCRIPT_SCRIPT_HASHES"], "addr_test1xqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5d004u0fv0r3a4ld7f8yg8rmxnk5dsxf542ghcc42ngw5sj75tnq"), + # pointer address + (network_ids.MAINNET, CardanoAddressType.POINTER, address_parameters["POINTER1"], "addr1gxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92spqgpsl97q83"), + (network_ids.TESTNET, CardanoAddressType.POINTER, address_parameters["POINTER2"], "addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t"), + # pointer_script address + (network_ids.MAINNET, CardanoAddressType.POINTER_SCRIPT, address_parameters["POINTER_SCRIPT_HASH"], "addr12yx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2zmd4yp"), + (network_ids.TESTNET, CardanoAddressType.POINTER_SCRIPT, address_parameters["POINTER_SCRIPT_HASH"], "addr_test12qx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3l5ph3wczvf2d4sugn"), + # enterprise address + (network_ids.MAINNET, CardanoAddressType.ENTERPRISE, address_parameters["ENTERPRISE"], "addr1vxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92su77c6m"), + (network_ids.TESTNET, CardanoAddressType.ENTERPRISE, address_parameters["ENTERPRISE"], "addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47"), + # enterprise_script address + (network_ids.MAINNET, CardanoAddressType.ENTERPRISE_SCRIPT, address_parameters["ENTERPRISE_SCRIPT_HASH"], "addr1wyx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsqee7sp"), + (network_ids.TESTNET, CardanoAddressType.ENTERPRISE_SCRIPT, address_parameters["ENTERPRISE_SCRIPT_HASH"], "addr_test1wqx44jlk580mpjrjfesd7v2fsuc4ejlh3wmvp7dk702k3lsm3dzly"), + # reward address + (network_ids.MAINNET, CardanoAddressType.REWARD, address_parameters["REWARD"], "stake1uyfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yacalmqha"), + (network_ids.TESTNET, CardanoAddressType.REWARD, address_parameters["REWARD"], "stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq"), + # reward_script address + (network_ids.MAINNET, CardanoAddressType.REWARD_SCRIPT, address_parameters["REWARD_SCRIPT_HASH"], "stake17xxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gt5mad4"), + (network_ids.TESTNET, CardanoAddressType.REWARD_SCRIPT, address_parameters["REWARD_SCRIPT_HASH"], "stake_test17zxhh6785k83c76lklynjyr3anfm2xcry624ytuv24f582gv73lfg"), ] - for network_id, expected_address in test_vectors: - address_parameters = CardanoAddressParametersType( - address_type=CardanoAddressType.REWARD, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - ) + + for network_id, address_type, address_parameters, expected_address in test_vectors: + validate_address_parameters(address_parameters) actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id) self.assertEqual(actual_address, expected_address) - def test_testnet_byron_address(self): - mnemonic = "all all all all all all all all all all all all" - passphrase = "" - node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - keychain = Keychain(node) - - addresses = [ - "2657WMsDfac5F3zbgs9BwNWx3dhGAJERkAL93gPa68NJ2i8mbCHm2pLUHWSj8Mfea", - "2657WMsDfac6ezKWszxLFqJjSUgpg9NgxKc1koqi24sVpRaPhiwMaExk4useKn5HA", - "2657WMsDfac7hr1ioJGr6g7r6JRx4r1My8Rj91tcPTeVjJDpfBYKURrPG2zVLx2Sq", - ] - - for i, expected in enumerate(addresses): - # 44'/1815'/0'/0/i' - address_parameters = CardanoAddressParametersType( - address_type=CardanoAddressType.BYRON, - address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], - ) - address = derive_human_readable_address(keychain, address_parameters, protocol_magics.TESTNET, 0) - self.assertEqual(expected, address) - def test_validate_address_parameters(self): test_vectors = [ # base address - both address_n_staking and staking_key_hash are None @@ -451,22 +463,80 @@ def test_validate_address_parameters(self): address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], staking_key_hash=None, ), + # base_script_key address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + script_payment_hash=None, + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8d"), + ), + # base_script_key address - address_n_staking is not a staking path + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_KEY, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + # base_key_script address - script_staking_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=None, + ), + # base_script_script address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=None, + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), + ), + # base_script_script address - script_staking and script_staking_hash are None + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_SCRIPT_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + script_staking_hash=None, + ), # pointer address - pointer is None CardanoAddressParametersType( address_type=CardanoAddressType.POINTER, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], certificate_pointer=None, ), + # pointer_script address - pointer is None + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + script_payment_hash=unhexlify("0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe"), + certificate_pointer=None, + ), + # pointer_script address - script_payment_script is None + CardanoAddressParametersType( + address_type=CardanoAddressType.POINTER_SCRIPT, + script_payment_hash=None, + certificate_pointer=CardanoBlockchainPointerType(block_index=24157, tx_index=177, certificate_index=42), + ), + # enterprise_script address - script_payment_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.ENTERPRISE_SCRIPT, + script_payment_hash=None, + ), # reward address - non staking path CardanoAddressParametersType( address_type=CardanoAddressType.REWARD, address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] ), + # reward_script address - script_staking_hash is None + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD_SCRIPT, + script_staking_hash=None, + ), # Shelley addresses with Byron namespace CardanoAddressParametersType( address_type=CardanoAddressType.BASE, - address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0] + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + staking_key_hash=unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), + ), + CardanoAddressParametersType( + address_type=CardanoAddressType.BASE_KEY_SCRIPT, + address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + script_staking_hash=unhexlify("8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9"), ), CardanoAddressParametersType( address_type=CardanoAddressType.POINTER, diff --git a/core/tests/test_apps.cardano.certificate.py b/core/tests/test_apps.cardano.certificate.py new file mode 100644 index 00000000000..d43f7996078 --- /dev/null +++ b/core/tests/test_apps.cardano.certificate.py @@ -0,0 +1,357 @@ +from common import * +from trezor import wire +from trezor.enums import CardanoCertificateType, CardanoTxSigningMode +from trezor.messages import CardanoTxCertificate, CardanoPoolParametersType + +from apps.common.paths import HARDENED + +if not utils.BITCOIN_ONLY: + from apps.cardano.certificates import validate_certificate + from apps.cardano.helpers import protocol_magics, network_ids + from apps.cardano.helpers.account_path_check import AccountPathChecker + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoCertificate(unittest.TestCase): + def test_validate_certificate(self): + valid_test_vectors = [ + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.SCRIPT_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.SCRIPT_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.SCRIPT_TRANSACTION, + ), + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + metadata=None, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + ] + + invalid_test_vectors = [ + # STAKE_REGISTRATION neither path or script_hash is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_REGISTRATION both path and script_hash are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_REGISTRATION pool is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_REGISTRATION pool parameters are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + # STAKE_DELEGATION neither path or script_hash is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_DELEGATION both path and script_hash are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_DELEGATION pool parameters are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DELEGATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + # STAKE_DEREGISTRATION neither path or script_hash is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_DEREGISTRATION both path and script_hash are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_DEREGISTRATION pool is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_DEREGISTRATION pool parameters are set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_DEREGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + # STAKE_POOL_REGISTRATION pool parameters are not set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + ), + CardanoTxSigningMode.ORDINARY_TRANSACTION, + ), + # STAKE_POOL_REGISTRATION path is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + # STAKE_POOL_REGISTRATION script hash is set + ( + CardanoTxCertificate( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + script_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( + type=CardanoCertificateType.STAKE_POOL_REGISTRATION, + script_hash=unhexlify( + "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd" + ), + pool=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + pool_parameters=CardanoPoolParametersType( + pool_id=unhexlify( + "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973" + ), + vrf_key_hash=unhexlify( + "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640" + ), + pledge=500000000, + cost=340000000, + margin_numerator=1, + margin_denominator=2, + reward_account="stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + owners_count=1, + relays_count=1, + ), + ), + CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + ), + ] + + for certificate, signing_mode in valid_test_vectors: + validate_certificate( + certificate, + signing_mode, + protocol_magics.MAINNET, + network_ids.MAINNET, + AccountPathChecker(), + ) + + for certificate, signing_mode in invalid_test_vectors: + with self.assertRaises(wire.ProcessError): + validate_certificate( + certificate, + signing_mode, + protocol_magics.MAINNET, + network_ids.MAINNET, + AccountPathChecker(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/tests/test_apps.cardano.native_script.py b/core/tests/test_apps.cardano.native_script.py new file mode 100644 index 00000000000..0a344fee8f1 --- /dev/null +++ b/core/tests/test_apps.cardano.native_script.py @@ -0,0 +1,301 @@ +from common import * +from trezor import wire +from trezor.crypto import bip32 +from trezor.enums import CardanoNativeScriptType +from trezor.messages import CardanoNativeScript + +if not utils.BITCOIN_ONLY: + from apps.cardano.seed import Keychain + from apps.cardano.native_script import get_native_script_hash, validate_native_script + +VALID_NATIVE_SCRIPTS = [ + # PUB_KEY + [ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + b"29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + ], + # PUB_KEY with path + [ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + b"29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + ], + # ALL + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"af5c2ce476a6ede1c879f7b1909d6a0b96cb2081391712d4a355cef6", + ], + # ALL with 1855 path + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1855 | HARDENED, 1815 | HARDENED, 0 | HARDENED], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"fbf6672eb655c29b0f148fa1429be57c2174b067a7b3e3942e967fe8", + ], + # ALL scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.ALL, scripts=[]), + b"d441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf" + ], + # ANY + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ANY, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + b"d6428ec36719146b7b5fb3a2d5322ce702d32762b8c7eeeb797a20db", + ], + # ANY scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.ANY, scripts=[]), + b"52dc3d43b6d2465e96109ce75ab61abe5e9c1d8a3c9ce6ff8a3af528" + ], + # N OF K + [ + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + ), + ), + ], + ), + b"2b2b17fd18e18acae4601d4818a1dee00a917ff72e772fa8482e36c9", + ], + # N_OF_K scripts are empty + [ + CardanoNativeScript(type=CardanoNativeScriptType.N_OF_K, required_signatures_count=0, scripts=[]), + b"3530cc9ae7f2895111a99b7a02184dd7c0cea7424f1632d73951b1d7" + ], + # INVALID BEFORE + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_BEFORE, invalid_before=100 + ), + ], + ), + b"c6262ef9bb2b1291c058d93b46dabf458e2d135f803f60713f84b0b7", + ], + # INVALID HEREAFTER + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_HEREAFTER, + invalid_hereafter=200, + ), + ], + ), + b"b12ac304f89f4cd4d23f59a2b90d2b2697f7540b8f470d6aa05851b5", + ], + # NESTED SCRIPT + [ + CardanoNativeScript( + type=CardanoNativeScriptType.ALL, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "c4b9265645fde9536c0795adbcc5291767a0c61fd62448341d7e0386" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.ANY, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + ], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1854 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "0241f2d196f52a92fbd2183d03b370c30b6960cfdeae364ffabac889" + ), + ), + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "cecb1d427c4ae436d28cc0f8ae9bb37501a5b77bcc64cd1693e9ae20" + ), + ), + ], + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_BEFORE, invalid_before=100 + ), + CardanoNativeScript( + type=CardanoNativeScriptType.INVALID_HEREAFTER, + invalid_hereafter=200, + ), + ], + ), + b"4a6b4288459bf34668c0b281f922691460caf0c7c09caee3a726c27a", + ], +] + +INVALID_SCRIPTS = [ + # PUB_KEY key_hash has invalid length + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify("3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0f"), + ), + # PUB_KEY key_path is not multisig or mint + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], + ), + # PUB_KEY mint key_path is too long + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_path=[1855 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0], + ), + # N_OF_K required_signatures_count is not set + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0fa9" + ), + ), + ], + ), + # N_OF_K N is larger than K + CardanoNativeScript( + type=CardanoNativeScriptType.N_OF_K, + required_signatures_count=2, + scripts=[ + CardanoNativeScript( + type=CardanoNativeScriptType.PUB_KEY, + key_hash=unhexlify( + "3a55d9f68255dfbefa1efd711f82d005fae1be2e145d616c90cf0fa9" + ), + ), + ], + ), + # INVALID_BEFORE invalid_before is not set + CardanoNativeScript(type=CardanoNativeScriptType.INVALID_BEFORE), + # INVALID_HEREAFTER invalid_hereafter is not set + CardanoNativeScript(type=CardanoNativeScriptType.INVALID_HEREAFTER), +] + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestCardanoNativeScript(unittest.TestCase): + def test_get_native_script_hash(self): + mnemonic = "all all all all all all all all all all all all" + passphrase = "" + node = bip32.from_mnemonic_cardano(mnemonic, passphrase) + keychain = Keychain(node) + + for script, expected_hash in VALID_NATIVE_SCRIPTS: + actual_hash = get_native_script_hash(keychain, script) + self.assertEqual(hexlify(actual_hash), expected_hash) + + def test_validate_native_script(self): + for script, _ in VALID_NATIVE_SCRIPTS: + validate_native_script(script) + + def test_validate_native_script_invalid(self): + for script in INVALID_SCRIPTS: + with self.assertRaises(wire.ProcessError): + validate_native_script(script) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/tests/test_apps.cardano.sign_tx.py b/core/tests/test_apps.cardano.sign_tx.py index c9584dc0087..e69de29bb2d 100644 --- a/core/tests/test_apps.cardano.sign_tx.py +++ b/core/tests/test_apps.cardano.sign_tx.py @@ -1,42 +0,0 @@ -from common import * -from apps.common.paths import HARDENED - -if not utils.BITCOIN_ONLY: - from apps.cardano.sign_tx import _should_hide_output - - -@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") -class TestCardanoSignTransaction(unittest.TestCase): - def test_should_show_outputs(self): - outputs_to_hide = [ - # byron path - [44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - # shelley path - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - # path account is 2 - [1852 | HARDENED, 1815 | HARDENED, 2 | HARDENED, 0, 0], - # path index is 2 - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 2], - ] - outputs_to_show = [ - # path is not complete - [1852 | HARDENED, 1815 | HARDENED], - # path is not complete - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED], - # staking output path - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0,], - # max safe account number exceeded - [1852 | HARDENED, 1815 | HARDENED, 101 | HARDENED, 0, 0], - # output address too large - [1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 1000001], - ] - - for output_path in outputs_to_hide: - self.assertTrue(_should_hide_output(output_path)) - - for output_path in outputs_to_show: - self.assertFalse(_should_hide_output(output_path)) - - -if __name__ == "__main__": - unittest.main() diff --git a/core/tests/test_apps.cardano.staking_use_cases.py b/core/tests/test_apps.cardano.staking_use_cases.py index 50b7c379914..e69de29bb2d 100644 --- a/core/tests/test_apps.cardano.staking_use_cases.py +++ b/core/tests/test_apps.cardano.staking_use_cases.py @@ -1,100 +0,0 @@ -from ubinascii import unhexlify - -from common import * - -from apps.common.paths import HARDENED -from trezor.crypto import bip32 -from trezor.enums import CardanoAddressType -from trezor.messages import CardanoAddressParametersType -from trezor.messages import CardanoBlockchainPointerType - - -if not utils.BITCOIN_ONLY: - from apps.cardano.helpers import staking_use_cases - from apps.cardano.seed import Keychain - - -@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") -class TestCardanoStakingUseCases(unittest.TestCase): - def test_get(self): - mnemonic = ( - "test walk nut penalty hip pave soap entry language right filter choice" - ) - passphrase = "" - node = bip32.from_mnemonic_cardano(mnemonic, passphrase) - keychain = Keychain(node) - - expected_staking_use_cases = [ - # address parameters, expected staking use case - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - ), - staking_use_cases.MATCH, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 2 | HARDENED, 2, 0], - ), - staking_use_cases.MISMATCH, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - staking_key_hash=unhexlify("32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc"), - ), - staking_use_cases.MATCH, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.BASE, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), - ), - staking_use_cases.MISMATCH, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.POINTER, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - certificate_pointer=CardanoBlockchainPointerType( - block_index=1, tx_index=2, certificate_index=3 - ), - ), - staking_use_cases.POINTER_ADDRESS, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.REWARD, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0], - ), - staking_use_cases.MATCH, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.ENTERPRISE, - address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - ), - staking_use_cases.NO_STAKING, - ), - ( - CardanoAddressParametersType( - address_type=CardanoAddressType.BYRON, - address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0], - ), - staking_use_cases.NO_STAKING, - ), - ] - - for address_parameters, expected_staking_use_case in expected_staking_use_cases: - actual_staking_use_case = staking_use_cases.get(keychain, address_parameters) - self.assertEqual(actual_staking_use_case, expected_staking_use_case) - - -if __name__ == "__main__": - unittest.main() diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index a388d16db0e..61229f8d0a4 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -24,6 +24,7 @@ SIGNING_MODE_IDS = { "ORDINARY_TRANSACTION": messages.CardanoTxSigningMode.ORDINARY_TRANSACTION, "POOL_REGISTRATION_AS_OWNER": messages.CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER, + "SCRIPT_TRANSACTION": messages.CardanoTxSigningMode.SCRIPT_TRANSACTION, } PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 42} @@ -41,9 +42,7 @@ "reward_account", "owners", ) -REQUIRED_FIELDS_WITHDRAWAL = ("path", "amount") REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") -REQUIRED_FIELDS_TOKEN = ("asset_name_bytes", "amount") REQUIRED_FIELDS_CATALYST_REGISTRATION = ( "voting_public_key", "staking_path", @@ -54,13 +53,20 @@ INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY = "The output's token_bundle entry is invalid" +INVALID_MINT_TOKEN_BUNDLE_ENTRY = "The mint token_bundle entry is invalid" ADDRESS_TYPES = ( messages.CardanoAddressType.BYRON, messages.CardanoAddressType.BASE, + messages.CardanoAddressType.BASE_SCRIPT_KEY, + messages.CardanoAddressType.BASE_KEY_SCRIPT, + messages.CardanoAddressType.BASE_SCRIPT_SCRIPT, messages.CardanoAddressType.POINTER, + messages.CardanoAddressType.POINTER_SCRIPT, messages.CardanoAddressType.ENTERPRISE, + messages.CardanoAddressType.ENTERPRISE_SCRIPT, messages.CardanoAddressType.REWARD, + messages.CardanoAddressType.REWARD_SCRIPT, ) InputWithPath = Tuple[messages.CardanoTxInput, List[int]] @@ -74,6 +80,9 @@ messages.CardanoPoolOwner, messages.CardanoPoolRelayParameters, ] +MintItem = Union[ + messages.CardanoTxMint, messages.CardanoAssetGroup, messages.CardanoToken +] PoolOwnersAndRelays = Tuple[ List[messages.CardanoPoolOwner], List[messages.CardanoPoolRelayParameters] ] @@ -86,6 +95,14 @@ SignTxResponse = Dict[str, Union[bytes, List[Witness], AuxiliaryDataSupplement]] +def parse_optional_bytes(value: Optional[str]) -> Optional[bytes]: + return bytes.fromhex(value) if value else None + + +def parse_optional_int(value) -> Optional[int]: + return int(value) if value is not None else None + + def create_address_parameters( address_type: messages.CardanoAddressType, address_n: List[int], @@ -94,13 +111,18 @@ def create_address_parameters( block_index: int = None, tx_index: int = None, certificate_index: int = None, + script_payment_hash: bytes = None, + script_staking_hash: bytes = None, ) -> messages.CardanoAddressParametersType: certificate_pointer = None if address_type not in ADDRESS_TYPES: raise ValueError("Unknown address type") - if address_type == messages.CardanoAddressType.POINTER: + if address_type in ( + messages.CardanoAddressType.POINTER, + messages.CardanoAddressType.POINTER_SCRIPT, + ): certificate_pointer = _create_certificate_pointer( block_index, tx_index, certificate_index ) @@ -111,6 +133,8 @@ def create_address_parameters( address_n_staking=address_n_staking, staking_key_hash=staking_key_hash, certificate_pointer=certificate_pointer, + script_payment_hash=script_payment_hash, + script_staking_hash=script_staking_hash, ) @@ -156,10 +180,12 @@ def parse_output(output) -> OutputWithAssetGroups: address = output["address"] if contains_address_type: - address_parameters = _parse_address_parameters(output) + address_parameters = _parse_address_parameters( + output, INCOMPLETE_OUTPUT_ERROR_MESSAGE + ) if "token_bundle" in output: - token_bundle = _parse_token_bundle(output["token_bundle"]) + token_bundle = _parse_token_bundle(output["token_bundle"], is_mint=False) return ( messages.CardanoTxOutput( @@ -172,19 +198,26 @@ def parse_output(output) -> OutputWithAssetGroups: ) -def _parse_token_bundle(token_bundle) -> List[AssetGroupWithTokens]: +def _parse_token_bundle(token_bundle, is_mint: bool) -> List[AssetGroupWithTokens]: + error_message: str + if is_mint: + error_message = INVALID_MINT_TOKEN_BUNDLE_ENTRY + else: + error_message = INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY + result = [] for token_group in token_bundle: if not all(k in token_group for k in REQUIRED_FIELDS_TOKEN_GROUP): - raise ValueError(INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY) + raise ValueError(error_message) - tokens = _parse_tokens(token_group["tokens"]) + tokens = _parse_tokens(token_group["tokens"], is_mint) result.append( ( messages.CardanoAssetGroup( policy_id=bytes.fromhex(token_group["policy_id"]), tokens_count=len(tokens), + is_mint=is_mint, ), tokens, ) @@ -193,16 +226,34 @@ def _parse_token_bundle(token_bundle) -> List[AssetGroupWithTokens]: return result -def _parse_tokens(tokens) -> List[messages.CardanoToken]: +def _parse_tokens(tokens, is_mint: bool) -> List[messages.CardanoToken]: + error_message: str + if is_mint: + error_message = INVALID_MINT_TOKEN_BUNDLE_ENTRY + else: + error_message = INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY + result = [] for token in tokens: - if not all(k in token for k in REQUIRED_FIELDS_TOKEN): - raise ValueError(INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY) + if "asset_name_bytes" not in token: + raise ValueError(error_message) + + mint_amount = None + amount = None + if is_mint: + if "mint_amount" not in token: + raise ValueError(error_message) + mint_amount = int(token["mint_amount"]) + else: + if "amount" not in token: + raise ValueError(error_message) + amount = int(token["amount"]) result.append( messages.CardanoToken( asset_name_bytes=bytes.fromhex(token["asset_name_bytes"]), - amount=int(token["amount"]), + amount=amount, + mint_amount=mint_amount, ) ) @@ -210,23 +261,62 @@ def _parse_tokens(tokens) -> List[messages.CardanoToken]: def _parse_address_parameters( - address_parameters, + address_parameters, error_message: str ) -> messages.CardanoAddressParametersType: - if "path" not in address_parameters: - raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) + if "addressType" not in address_parameters: + raise ValueError(error_message) - staking_key_hash_bytes = None - if "stakingKeyHash" in address_parameters: - staking_key_hash_bytes = bytes.fromhex(address_parameters.get("stakingKeyHash")) + payment_path = tools.parse_path(address_parameters.get("path")) + staking_path = tools.parse_path(address_parameters.get("stakingPath")) + staking_key_hash_bytes = parse_optional_bytes( + address_parameters.get("stakingKeyHash") + ) + script_payment_hash = parse_optional_bytes( + address_parameters.get("scriptPaymentHash") + ) + script_staking_hash = parse_optional_bytes( + address_parameters.get("scriptStakingHash") + ) return create_address_parameters( int(address_parameters["addressType"]), - tools.parse_path(address_parameters["path"]), - tools.parse_path(address_parameters.get("stakingPath")), + payment_path, + staking_path, staking_key_hash_bytes, address_parameters.get("blockIndex"), address_parameters.get("txIndex"), address_parameters.get("certificateIndex"), + script_payment_hash, + script_staking_hash, + ) + + +def parse_native_script(native_script) -> messages.CardanoNativeScript: + if "type" not in native_script: + raise ValueError("Script is missing some fields") + + type = native_script["type"] + scripts = [ + parse_native_script(sub_script) + for sub_script in native_script.get("scripts", ()) + ] + + key_hash = parse_optional_bytes(native_script.get("key_hash")) + key_path = tools.parse_path(native_script.get("key_path")) + required_signatures_count = parse_optional_int( + native_script.get("required_signatures_count") + ) + invalid_before = parse_optional_int(native_script.get("invalid_before")) + invalid_hereafter = parse_optional_int(native_script.get("invalid_hereafter")) + + return messages.CardanoNativeScript( + type=type, + scripts=scripts, + key_hash=key_hash, + key_path=key_path, + required_signatures_count=required_signatures_count, + invalid_before=invalid_before, + invalid_hereafter=invalid_hereafter, ) @@ -244,11 +334,16 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: if "pool" not in certificate: raise CERTIFICATE_MISSING_FIELDS_ERROR + path, script_hash = _parse_path_or_script_hash( + certificate, CERTIFICATE_MISSING_FIELDS_ERROR + ) + return ( messages.CardanoTxCertificate( type=certificate_type, - path=tools.parse_path(certificate["path"]), + path=path, pool=bytes.fromhex(certificate["pool"]), + script_hash=script_hash, ), None, ) @@ -256,12 +351,13 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: messages.CardanoCertificateType.STAKE_REGISTRATION, messages.CardanoCertificateType.STAKE_DEREGISTRATION, ): - if "path" not in certificate: - raise CERTIFICATE_MISSING_FIELDS_ERROR + path, script_hash = _parse_path_or_script_hash( + certificate, CERTIFICATE_MISSING_FIELDS_ERROR + ) + return ( messages.CardanoTxCertificate( - type=certificate_type, - path=tools.parse_path(certificate["path"]), + type=certificate_type, path=path, script_hash=script_hash ), None, ) @@ -313,6 +409,18 @@ def parse_certificate(certificate) -> CertificateWithPoolOwnersAndRelays: raise ValueError("Unknown certificate type") +def _parse_path_or_script_hash( + obj, error: ValueError +) -> Tuple[List[int], Optional[bytes]]: + if "path" not in obj and "script_hash" not in obj: + raise error + + path = tools.parse_path(obj.get("path")) + script_hash = parse_optional_bytes(obj.get("script_hash")) + + return path, script_hash + + def _parse_pool_owner(pool_owner) -> messages.CardanoPoolOwner: if "staking_key_path" in pool_owner: return messages.CardanoPoolOwner( @@ -361,13 +469,21 @@ def _parse_pool_relay(pool_relay) -> messages.CardanoPoolRelayParameters: def parse_withdrawal(withdrawal) -> messages.CardanoTxWithdrawal: - if not all(k in withdrawal for k in REQUIRED_FIELDS_WITHDRAWAL): - raise ValueError("Withdrawal is missing some fields") + WITHDRAWAL_MISSING_FIELDS_ERROR = ValueError( + "The withdrawal is missing some fields" + ) + + if "amount" not in withdrawal: + raise WITHDRAWAL_MISSING_FIELDS_ERROR + + path, script_hash = _parse_path_or_script_hash( + withdrawal, WITHDRAWAL_MISSING_FIELDS_ERROR + ) - path = withdrawal["path"] return messages.CardanoTxWithdrawal( - path=tools.parse_path(path), + path=path, amount=int(withdrawal["amount"]), + script_hash=script_hash, ) @@ -400,7 +516,8 @@ def parse_auxiliary_data(auxiliary_data) -> messages.CardanoTxAuxiliaryData: staking_path=tools.parse_path(catalyst_registration["staking_path"]), nonce=catalyst_registration["nonce"], reward_address_parameters=_parse_address_parameters( - catalyst_registration["reward_address_parameters"] + catalyst_registration["reward_address_parameters"], + AUXILIARY_DATA_MISSING_FIELDS_ERROR, ), ) ) @@ -414,33 +531,62 @@ def parse_auxiliary_data(auxiliary_data) -> messages.CardanoTxAuxiliaryData: ) -def _get_witness_paths( +def parse_mint(mint) -> List[AssetGroupWithTokens]: + return _parse_token_bundle(mint, is_mint=True) + + +def parse_additional_witness_request( + additional_witness_request, +) -> Path: + if "path" not in additional_witness_request: + raise ValueError("Invalid additional witness request") + + return tools.parse_path(additional_witness_request["path"]) + + +def _get_witness_requests( inputs: List[InputWithPath], certificates: List[CertificateWithPoolOwnersAndRelays], withdrawals: List[messages.CardanoTxWithdrawal], -) -> List[Path]: + additional_witness_requests: List[Path], + signing_mode: messages.CardanoTxSigningMode, +) -> List[messages.CardanoTxWitnessRequest]: paths = set() - for _, path in inputs: - if path: - paths.add(tuple(path)) - for certificate, pool_owners_and_relays in certificates: - if certificate.type in ( - messages.CardanoCertificateType.STAKE_DEREGISTRATION, - messages.CardanoCertificateType.STAKE_DELEGATION, - ): - paths.add(tuple(certificate.path)) - elif ( - certificate.type == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION - and pool_owners_and_relays is not None - ): - owners, _ = pool_owners_and_relays - for pool_owner in owners: - if pool_owner.staking_key_path: - paths.add(tuple(pool_owner.staking_key_path)) - for withdrawal in withdrawals: - paths.add(tuple(withdrawal.path)) - return sorted([list(path) for path in paths]) + # don't gather paths from tx elements in SCRIPT_TRANSACTION signing mode + if signing_mode != messages.CardanoTxSigningMode.SCRIPT_TRANSACTION: + for _, path in inputs: + if path: + paths.add(tuple(path)) + for certificate, pool_owners_and_relays in certificates: + if ( + certificate.type + in ( + messages.CardanoCertificateType.STAKE_DEREGISTRATION, + messages.CardanoCertificateType.STAKE_DELEGATION, + ) + and certificate.path + ): + paths.add(tuple(certificate.path)) + elif ( + certificate.type + == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION + and pool_owners_and_relays is not None + ): + owners, _ = pool_owners_and_relays + for pool_owner in owners: + if pool_owner.staking_key_path: + paths.add(tuple(pool_owner.staking_key_path)) + for withdrawal in withdrawals: + if withdrawal.path: + paths.add(tuple(withdrawal.path)) + + # add additional_witness_requests in all cases + for additional_witness_request in additional_witness_requests: + paths.add(tuple(additional_witness_request)) + + sorted_paths = sorted([list(path) for path in paths]) + return [messages.CardanoTxWitnessRequest(path=path) for path in sorted_paths] def _get_input_items(inputs: List[InputWithPath]) -> Iterator[messages.CardanoTxInput]: @@ -467,6 +613,13 @@ def _get_certificate_items( yield from relays +def _get_mint_items(mint: List[AssetGroupWithTokens]) -> Iterator[MintItem]: + yield messages.CardanoTxMint(asset_groups_count=len(mint)) + for asset_group, tokens in mint: + yield asset_group + yield from tokens + + # ====== Client functions ====== # @@ -493,6 +646,20 @@ def get_public_key(client, address_n: List[int]) -> messages.CardanoPublicKey: return client.call(messages.CardanoGetPublicKey(address_n=address_n)) +@expect(messages.CardanoNativeScriptHash) +def get_native_script_hash( + client, + native_script: messages.CardanoNativeScript, + display_format: messages.CardanoNativeScriptHashDisplayFormat = messages.CardanoNativeScriptHashDisplayFormat.HIDE, +) -> messages.CardanoNativeScriptHash: + return client.call( + messages.CardanoGetNativeScriptHash( + script=native_script, + display_format=display_format, + ) + ) + + def sign_tx( client, signing_mode: messages.CardanoTxSigningMode, @@ -506,10 +673,14 @@ def sign_tx( protocol_magic: int = PROTOCOL_MAGICS["mainnet"], network_id: int = NETWORK_IDS["mainnet"], auxiliary_data: messages.CardanoTxAuxiliaryData = None, + mint: List[AssetGroupWithTokens] = (), + additional_witness_requests: List[Path] = (), ) -> SignTxResponse: UNEXPECTED_RESPONSE_ERROR = exceptions.TrezorException("Unexpected response") - witness_paths = _get_witness_paths(inputs, certificates, withdrawals) + witness_requests = _get_witness_requests( + inputs, certificates, withdrawals, additional_witness_requests, signing_mode + ) response = client.call( messages.CardanoSignTxInit( @@ -524,7 +695,8 @@ def sign_tx( protocol_magic=protocol_magic, network_id=network_id, has_auxiliary_data=auxiliary_data is not None, - witness_requests_count=len(witness_paths), + minting_asset_groups_count=len(mint), + witness_requests_count=len(witness_requests), ) ) if not isinstance(response, messages.CardanoTxItemAck): @@ -560,9 +732,15 @@ def sign_tx( if not isinstance(response, messages.CardanoTxItemAck): raise UNEXPECTED_RESPONSE_ERROR + if mint: + for mint_item in _get_mint_items(mint): + response = client.call(mint_item) + if not isinstance(response, messages.CardanoTxItemAck): + raise UNEXPECTED_RESPONSE_ERROR + sign_tx_response["witnesses"] = [] - for path in witness_paths: - response = client.call(messages.CardanoTxWitnessRequest(path=path)) + for witness_request in witness_requests: + response = client.call(witness_request) if not isinstance(response, messages.CardanoTxWitnessResponse): raise UNEXPECTED_RESPONSE_ERROR sign_tx_response["witnesses"].append( diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 26b0468e253..1a2db565d2f 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -23,14 +23,6 @@ PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" -ADDRESS_TYPES = { - "byron": messages.CardanoAddressType.BYRON, - "base": messages.CardanoAddressType.BASE, - "pointer": messages.CardanoAddressType.POINTER, - "enterprise": messages.CardanoAddressType.ENTERPRISE, - "reward": messages.CardanoAddressType.REWARD, -} - @click.group(name="cardano") def cli(): @@ -74,6 +66,11 @@ def sign_tx(client, file, signing_mode, protocol_magic, network_id, testnet): for withdrawal in transaction.get("withdrawals", ()) ] auxiliary_data = cardano.parse_auxiliary_data(transaction.get("auxiliary_data")) + mint = cardano.parse_mint(transaction.get("mint", ())) + additional_witness_requests = [ + cardano.parse_additional_witness_request(p) + for p in transaction["additional_witness_requests"] + ] sign_tx_response = cardano.sign_tx( client, @@ -88,6 +85,8 @@ def sign_tx(client, file, signing_mode, protocol_magic, network_id, testnet): protocol_magic, network_id, auxiliary_data, + mint, + additional_witness_requests, ) sign_tx_response["tx_hash"] = sign_tx_response["tx_hash"].hex() @@ -115,14 +114,21 @@ def sign_tx(client, file, signing_mode, protocol_magic, network_id, testnet): @cli.command() -@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-n", "--address", type=str, default=None, help=PATH_HELP) @click.option("-d", "--show-display", is_flag=True) -@click.option("-t", "--address-type", type=ChoiceType(ADDRESS_TYPES), default="base") +@click.option( + "-t", + "--address-type", + type=ChoiceType({m.name: m for m in messages.CardanoAddressType}), + default="BASE", +) @click.option("-s", "--staking-address", type=str, default=None) @click.option("-h", "--staking-key-hash", type=str, default=None) @click.option("-b", "--block_index", type=int, default=None) @click.option("-x", "--tx_index", type=int, default=None) @click.option("-c", "--certificate_index", type=int, default=None) +@click.option("--script-payment-hash", type=str, default=None) +@click.option("--script-staking-hash", type=str, default=None) @click.option( "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"] ) @@ -138,6 +144,8 @@ def get_address( block_index, tx_index, certificate_index, + script_payment_hash, + script_staking_hash, protocol_magic, network_id, show_display, @@ -161,9 +169,9 @@ def get_address( protocol_magic = cardano.PROTOCOL_MAGICS["testnet"] network_id = cardano.NETWORK_IDS["testnet"] - staking_key_hash_bytes = None - if staking_key_hash: - staking_key_hash_bytes = bytes.fromhex(staking_key_hash) + staking_key_hash_bytes = cardano.parse_optional_bytes(staking_key_hash) + script_payment_hash_bytes = cardano.parse_optional_bytes(script_payment_hash) + script_staking_hash_bytes = cardano.parse_optional_bytes(script_staking_hash) address_parameters = cardano.create_address_parameters( address_type, @@ -173,6 +181,8 @@ def get_address( block_index, tx_index, certificate_index, + script_payment_hash_bytes, + script_staking_hash_bytes, ) return cardano.get_address( @@ -187,3 +197,20 @@ def get_public_key(client, address): """Get Cardano public key.""" address_n = tools.parse_path(address) return cardano.get_public_key(client, address_n) + + +@cli.command() +@click.argument("file", type=click.File("r")) +@click.option("-f", "--file", "_ignore", is_flag=True, hidden=True, expose_value=False) +@click.option( + "-d", + "--display-format", + type=ChoiceType({m.name: m for m in messages.CardanoNativeScriptHashDisplayFormat}), + default="HIDE", +) +@with_client +def get_native_script_hash(client, file, display_format): + native_script_json = json.load(file) + native_script = cardano.parse_native_script(native_script_json) + + return cardano.get_native_script_hash(client, native_script, display_format) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 2307885b99c..11e46165393 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -176,6 +176,9 @@ class MessageType(IntEnum): CardanoTxAuxiliaryData = 327 CardanoPoolOwner = 328 CardanoPoolRelayParameters = 329 + CardanoGetNativeScriptHash = 330 + CardanoNativeScriptHash = 331 + CardanoTxMint = 332 RippleGetAddress = 400 RippleAddress = 401 RippleSignTx = 402 @@ -342,6 +345,21 @@ class CardanoAddressType(IntEnum): REWARD_SCRIPT = 15 +class CardanoNativeScriptType(IntEnum): + PUB_KEY = 0 + ALL = 1 + ANY = 2 + N_OF_K = 3 + INVALID_BEFORE = 4 + INVALID_HEREAFTER = 5 + + +class CardanoNativeScriptHashDisplayFormat(IntEnum): + HIDE = 0 + BECH32 = 1 + POLICY_ID = 2 + + class CardanoCertificateType(IntEnum): STAKE_REGISTRATION = 0 STAKE_DEREGISTRATION = 1 @@ -363,6 +381,7 @@ class CardanoTxAuxiliaryDataSupplementType(IntEnum): class CardanoTxSigningMode(IntEnum): ORDINARY_TRANSACTION = 0 POOL_REGISTRATION_AS_OWNER = 1 + SCRIPT_TRANSACTION = 2 class CardanoTxWitnessType(IntEnum): @@ -1854,6 +1873,69 @@ def __init__( self.certificate_index = certificate_index +class CardanoNativeScript(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("type", "CardanoNativeScriptType", repeated=False, required=True), + 2: protobuf.Field("scripts", "CardanoNativeScript", repeated=True, required=False), + 3: protobuf.Field("key_hash", "bytes", repeated=False, required=False), + 4: protobuf.Field("key_path", "uint32", repeated=True, required=False), + 5: protobuf.Field("required_signatures_count", "uint32", repeated=False, required=False), + 6: protobuf.Field("invalid_before", "uint64", repeated=False, required=False), + 7: protobuf.Field("invalid_hereafter", "uint64", repeated=False, required=False), + } + + def __init__( + self, + *, + type: "CardanoNativeScriptType", + scripts: Optional[List["CardanoNativeScript"]] = None, + key_path: Optional[List["int"]] = None, + key_hash: Optional["bytes"] = None, + required_signatures_count: Optional["int"] = None, + invalid_before: Optional["int"] = None, + invalid_hereafter: Optional["int"] = None, + ) -> None: + self.scripts = scripts if scripts is not None else [] + self.key_path = key_path if key_path is not None else [] + self.type = type + self.key_hash = key_hash + self.required_signatures_count = required_signatures_count + self.invalid_before = invalid_before + self.invalid_hereafter = invalid_hereafter + + +class CardanoGetNativeScriptHash(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 330 + FIELDS = { + 1: protobuf.Field("script", "CardanoNativeScript", repeated=False, required=True), + 2: protobuf.Field("display_format", "CardanoNativeScriptHashDisplayFormat", repeated=False, required=True), + } + + def __init__( + self, + *, + script: "CardanoNativeScript", + display_format: "CardanoNativeScriptHashDisplayFormat", + ) -> None: + self.script = script + self.display_format = display_format + + +class CardanoNativeScriptHash(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 331 + FIELDS = { + 1: protobuf.Field("script_hash", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + script_hash: "bytes", + ) -> None: + self.script_hash = script_hash + + class CardanoAddressParametersType(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { @@ -1862,6 +1944,8 @@ class CardanoAddressParametersType(protobuf.MessageType): 3: protobuf.Field("address_n_staking", "uint32", repeated=True, required=False), 4: protobuf.Field("staking_key_hash", "bytes", repeated=False, required=False), 5: protobuf.Field("certificate_pointer", "CardanoBlockchainPointerType", repeated=False, required=False), + 6: protobuf.Field("script_payment_hash", "bytes", repeated=False, required=False), + 7: protobuf.Field("script_staking_hash", "bytes", repeated=False, required=False), } def __init__( @@ -1872,12 +1956,16 @@ def __init__( address_n_staking: Optional[List["int"]] = None, staking_key_hash: Optional["bytes"] = None, certificate_pointer: Optional["CardanoBlockchainPointerType"] = None, + script_payment_hash: Optional["bytes"] = None, + script_staking_hash: Optional["bytes"] = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.address_n_staking = address_n_staking if address_n_staking is not None else [] self.address_type = address_type self.staking_key_hash = staking_key_hash self.certificate_pointer = certificate_pointer + self.script_payment_hash = script_payment_hash + self.script_staking_hash = script_staking_hash class CardanoGetAddress(protobuf.MessageType): @@ -1966,6 +2054,7 @@ class CardanoSignTxInit(protobuf.MessageType): 10: protobuf.Field("has_auxiliary_data", "bool", repeated=False, required=True), 11: protobuf.Field("validity_interval_start", "uint64", repeated=False, required=False), 12: protobuf.Field("witness_requests_count", "uint32", repeated=False, required=True), + 13: protobuf.Field("minting_asset_groups_count", "uint32", repeated=False, required=True), } def __init__( @@ -1981,6 +2070,7 @@ def __init__( withdrawals_count: "int", has_auxiliary_data: "bool", witness_requests_count: "int", + minting_asset_groups_count: "int", ttl: Optional["int"] = None, validity_interval_start: Optional["int"] = None, ) -> None: @@ -1994,6 +2084,7 @@ def __init__( self.withdrawals_count = withdrawals_count self.has_auxiliary_data = has_auxiliary_data self.witness_requests_count = witness_requests_count + self.minting_asset_groups_count = minting_asset_groups_count self.ttl = ttl self.validity_interval_start = validity_interval_start @@ -2059,17 +2150,20 @@ class CardanoToken(protobuf.MessageType): MESSAGE_WIRE_TYPE = 324 FIELDS = { 1: protobuf.Field("asset_name_bytes", "bytes", repeated=False, required=True), - 2: protobuf.Field("amount", "uint64", repeated=False, required=True), + 2: protobuf.Field("amount", "uint64", repeated=False, required=False), + 3: protobuf.Field("mint_amount", "sint64", repeated=False, required=False), } def __init__( self, *, asset_name_bytes: "bytes", - amount: "int", + amount: Optional["int"] = None, + mint_amount: Optional["int"] = None, ) -> None: self.asset_name_bytes = asset_name_bytes self.amount = amount + self.mint_amount = mint_amount class CardanoPoolOwner(protobuf.MessageType): @@ -2186,6 +2280,7 @@ class CardanoTxCertificate(protobuf.MessageType): 2: protobuf.Field("path", "uint32", repeated=True, required=False), 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), } def __init__( @@ -2195,11 +2290,13 @@ def __init__( path: Optional[List["int"]] = None, pool: Optional["bytes"] = None, pool_parameters: Optional["CardanoPoolParametersType"] = None, + script_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 class CardanoTxWithdrawal(protobuf.MessageType): @@ -2207,6 +2304,7 @@ class CardanoTxWithdrawal(protobuf.MessageType): FIELDS = { 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), } def __init__( @@ -2214,9 +2312,11 @@ def __init__( *, amount: "int", path: Optional[List["int"]] = None, + script_hash: Optional["bytes"] = None, ) -> None: self.path = path if path is not None else [] self.amount = amount + self.script_hash = script_hash class CardanoCatalystRegistrationParametersType(protobuf.MessageType): @@ -2259,6 +2359,20 @@ def __init__( self.hash = hash +class CardanoTxMint(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 332 + FIELDS = { + 1: protobuf.Field("asset_groups_count", "uint32", repeated=False, required=True), + } + + def __init__( + self, + *, + asset_groups_count: "int", + ) -> None: + self.asset_groups_count = asset_groups_count + + class CardanoTxItemAck(protobuf.MessageType): MESSAGE_WIRE_TYPE = 313 diff --git a/tests/device_tests/cardano/test_address_public_key.py b/tests/device_tests/cardano/test_address_public_key.py index 56337549a85..c0ade7fbfcb 100644 --- a/tests/device_tests/cardano/test_address_public_key.py +++ b/tests/device_tests/cardano/test_address_public_key.py @@ -34,6 +34,7 @@ "cardano/get_address_byron.slip39.json", "cardano/get_base_address.json", "cardano/get_base_address_with_staking_key_hash.json", + "cardano/get_base_address_with_script_hashes.json", "cardano/get_enterprise_address.json", "cardano/get_pointer_address.json", "cardano/get_reward_address.json", @@ -45,7 +46,9 @@ def test_cardano_get_address(client, parameters, result): address_type=getattr( CardanoAddressType, parameters["address_type"].upper() ), - address_n=parse_path(parameters["path"]), + address_n=parse_path(parameters.get("path")) + if "path" in parameters + else None, address_n_staking=parse_path(parameters.get("staking_path")) if "staking_path" in parameters else None, @@ -55,6 +58,12 @@ def test_cardano_get_address(client, parameters, result): block_index=parameters.get("block_index"), tx_index=parameters.get("tx_index"), certificate_index=parameters.get("certificate_index"), + script_payment_hash=bytes.fromhex(parameters.get("script_payment_hash")) + if "script_payment_hash" in parameters + else None, + script_staking_hash=bytes.fromhex(parameters.get("script_staking_hash")) + if "script_staking_hash" in parameters + else None, ), protocol_magic=parameters["protocol_magic"], network_id=parameters["network_id"], diff --git a/tests/device_tests/cardano/test_get_native_script_hash.py b/tests/device_tests/cardano/test_get_native_script_hash.py new file mode 100644 index 00000000000..a6f4f8855ad --- /dev/null +++ b/tests/device_tests/cardano/test_get_native_script_hash.py @@ -0,0 +1,28 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from trezorlib.cardano import get_native_script_hash, parse_native_script + +from ...common import parametrize_using_common_fixtures + + +@parametrize_using_common_fixtures( + "cardano/get_native_script_hash.json", +) +def test_cardano_get_native_script_hash(client, parameters, result): + native_script = parse_native_script(parameters) + native_script_hash = get_native_script_hash(client, native_script).script_hash + assert native_script_hash.hex() == result["expected_hash"] diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index d2c5590f04b..e73d4cde402 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -31,6 +31,7 @@ @parametrize_using_common_fixtures( "cardano/sign_tx_stake_pool_registration.json", "cardano/sign_tx.json", + "cardano/sign_tx.multisig.json", "cardano/sign_tx.slip39.json", ) def test_cardano_sign_tx(client, parameters, result): @@ -40,6 +41,11 @@ def test_cardano_sign_tx(client, parameters, result): certificates = [cardano.parse_certificate(c) for c in parameters["certificates"]] withdrawals = [cardano.parse_withdrawal(w) for w in parameters["withdrawals"]] auxiliary_data = cardano.parse_auxiliary_data(parameters["auxiliary_data"]) + mint = cardano.parse_mint(parameters["mint"]) + additional_witness_requests = [ + cardano.parse_additional_witness_request(p) + for p in parameters["additional_witness_requests"] + ] if parameters.get("security_checks") == "prompt": device.apply_settings( @@ -62,12 +68,16 @@ def test_cardano_sign_tx(client, parameters, result): protocol_magic=parameters["protocol_magic"], network_id=parameters["network_id"], auxiliary_data=auxiliary_data, + mint=mint, + additional_witness_requests=additional_witness_requests, ) assert response == _transform_expected_result(result) @parametrize_using_common_fixtures( - "cardano/sign_tx.failed.json", "cardano/sign_tx_stake_pool_registration.failed.json" + "cardano/sign_tx.failed.json", + "cardano/sign_tx.multisig.failed.json", + "cardano/sign_tx_stake_pool_registration.failed.json", ) def test_cardano_sign_tx_failed(client, parameters, result): signing_mode = messages.CardanoTxSigningMode.__members__[parameters["signing_mode"]] @@ -76,6 +86,18 @@ def test_cardano_sign_tx_failed(client, parameters, result): certificates = [cardano.parse_certificate(c) for c in parameters["certificates"]] withdrawals = [cardano.parse_withdrawal(w) for w in parameters["withdrawals"]] auxiliary_data = cardano.parse_auxiliary_data(parameters["auxiliary_data"]) + mint = cardano.parse_mint(parameters["mint"]) + additional_witness_requests = [ + cardano.parse_additional_witness_request(p) + for p in parameters["additional_witness_requests"] + ] + + if parameters.get("security_checks") == "prompt": + device.apply_settings( + client, safety_checks=messages.SafetyCheckLevel.PromptTemporarily + ) + else: + device.apply_settings(client, safety_checks=messages.SafetyCheckLevel.Strict) with client: with pytest.raises(TrezorFailure, match=result["error_message"]): @@ -92,6 +114,8 @@ def test_cardano_sign_tx_failed(client, parameters, result): protocol_magic=parameters["protocol_magic"], network_id=parameters["network_id"], auxiliary_data=auxiliary_data, + mint=mint, + additional_witness_requests=additional_witness_requests, )