From b261f789f383d8e07dab500bd0aa2b7fe41cbaf4 Mon Sep 17 00:00:00 2001 From: Rafael Korbas Date: Tue, 29 Sep 2020 20:23:25 +0200 Subject: [PATCH] Add support for stakepool registration to Cardano --- common/protob/messages-cardano.proto | 60 ++- ...ign_tx_stake_pool_registration.failed.json | 459 ++++++++++++++++++ .../sign_tx_stake_pool_registration.json | 194 ++++++++ core/src/apps/cardano/address.py | 76 ++- core/src/apps/cardano/byron_address.py | 2 +- core/src/apps/cardano/certificates.py | 258 ++++++++++ core/src/apps/cardano/helpers/__init__.py | 8 + .../apps/cardano/helpers/staking_use_cases.py | 2 +- core/src/apps/cardano/layout.py | 129 ++++- core/src/apps/cardano/sign_tx.py | 219 ++++++--- .../trezor/messages/CardanoCertificateType.py | 1 + .../messages/CardanoPoolMetadataType.py | 29 ++ .../trezor/messages/CardanoPoolOwnerType.py | 29 ++ .../messages/CardanoPoolParametersType.py | 57 +++ .../CardanoPoolRelayParametersType.py | 39 ++ .../trezor/messages/CardanoPoolRelayType.py | 8 + .../messages/CardanoTxCertificateType.py | 9 +- python/src/trezorlib/cardano.py | 137 +++++- .../messages/CardanoCertificateType.py | 1 + .../messages/CardanoPoolMetadataType.py | 29 ++ .../messages/CardanoPoolOwnerType.py | 29 ++ .../messages/CardanoPoolParametersType.py | 57 +++ .../CardanoPoolRelayParametersType.py | 39 ++ .../messages/CardanoPoolRelayType.py | 8 + .../messages/CardanoTxCertificateType.py | 9 +- python/src/trezorlib/messages/__init__.py | 5 + tests/device_tests/cardano/test_sign_tx.py | 8 +- tests/ui_tests/fixtures.json | 174 +++---- 28 files changed, 1861 insertions(+), 214 deletions(-) create mode 100644 common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json create mode 100644 common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json create mode 100644 core/src/apps/cardano/certificates.py create mode 100644 core/src/trezor/messages/CardanoPoolMetadataType.py create mode 100644 core/src/trezor/messages/CardanoPoolOwnerType.py create mode 100644 core/src/trezor/messages/CardanoPoolParametersType.py create mode 100644 core/src/trezor/messages/CardanoPoolRelayParametersType.py create mode 100644 core/src/trezor/messages/CardanoPoolRelayType.py create mode 100644 python/src/trezorlib/messages/CardanoPoolMetadataType.py create mode 100644 python/src/trezorlib/messages/CardanoPoolOwnerType.py create mode 100644 python/src/trezorlib/messages/CardanoPoolParametersType.py create mode 100644 python/src/trezorlib/messages/CardanoPoolRelayParametersType.py create mode 100644 python/src/trezorlib/messages/CardanoPoolRelayType.py diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 3de19c88e..4651a404c 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -29,6 +29,13 @@ enum CardanoCertificateType { STAKE_REGISTRATION = 0; STAKE_DEREGISTRATION = 1; STAKE_DELEGATION = 2; + STAKE_POOL_REGISTRATION = 3; +} + +enum CardanoPoolRelayType { + SINGLE_HOST_IP = 0; + SINGLE_HOST_NAME = 1; + MULTIPLE_HOST_NAME = 2; } /** @@ -138,13 +145,58 @@ message CardanoSignTx { optional uint64 amount = 3; // amount to spend optional CardanoAddressParametersType address_parameters = 4; // parameters used to derive the address } + + /** + * Stake pool owner parameters + */ + message CardanoPoolOwnerType { + repeated uint32 staking_key_path = 1; // BIP-32-style path to derive staking key of the owner + optional bytes staking_key_hash = 2; // owner's staking key if it is an external owner + } + + /** + * Stake pool relay parameters + */ + message CardanoPoolRelayParametersType { + required CardanoPoolRelayType type = 1; // pool relay type + optional bytes ipv4_address = 2; // ipv4 address of the relay given as 4 bytes + optional bytes ipv6_address = 3; // ipv6 address of the relay given as 16 bytes + optional string host_name = 4; // relay host name given as URL, at most 64 characters + optional uint32 port = 5; // relay port number in the range 0-65535 + } + + /** + * Stake pool metadata parameters + */ + message CardanoPoolMetadataType { + required string url = 1; // stake pool url hosting metadata, at most 64 characters + required bytes hash = 2; // stake pool metadata hash + } + + /** + * Stake pool parameters + */ + message CardanoPoolParametersType { + required bytes pool_id = 1; // stake pool cold public key hash (28 bytes) + required bytes vrf_key_hash = 2; // VRF key hash (32 bytes) + required uint64 pledge = 3; // pledge amount in lovelace + required uint64 cost = 4; // cost in lovelace + required uint64 margin_numerator = 5; // pool margin numerator + required uint64 margin_denominator = 6; // pool margin denominator + required string reward_account = 7; // bech32 reward address where the pool receives rewards + repeated CardanoPoolOwnerType owners = 8; // pool owners list + repeated CardanoPoolRelayParametersType relays = 9; // pool relays list + optional CardanoPoolMetadataType metadata = 10; // pool metadata + } + /** * Structure representing cardano transaction certificate */ - message CardanoTxCertificateType { - optional CardanoCertificateType type = 1; // certificate type - repeated uint32 path = 2; // BIP-32 path to derive (staking) key - optional bytes pool = 3; // pool hash + message CardanoTxCertificateType { + optional CardanoCertificateType type = 1; // certificate type + repeated uint32 path = 2; // BIP-32 path to derive (staking) key + optional bytes pool = 3; // pool hash + optional CardanoPoolParametersType pool_parameters = 4; // used for stake pool registration certificate } /** * Structure representing cardano transaction withdrawals 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 new file mode 100644 index 000000000..51a4ef279 --- /dev/null +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json @@ -0,0 +1,459 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "Missing owner with 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_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [], + "metadata": null + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Two owners with 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_path": "m/1852'/1815'/0'/2/0" + } + ], + "relays": [], + "metadata": null + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Invalid pool id", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "deadbeef", + "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" + } + ], + "relays": [], + "metadata": null + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Margin higher than 1", + "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": 2, + "denominator": 1 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + } + ], + "relays": [], + "metadata": null + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Invalid certificate" + } + }, + { + "description": "Contains other certificates", + "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" + } + ], + "relays": [], + "metadata": null + } + }, + { + "type": 0, + "path": "m/1852'/1815'/0'/2/0" + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction cannot contain other certificates nor withdrawals" + } + }, + { + "description": "Contains withdrawal", + "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" + } + ], + "relays": [], + "metadata": null + } + } + ], + "withdrawals": [ + { + "path": "m/1852'/1815'/0'/2/0", + "amount": "1000" + } + ], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction cannot contain other certificates nor withdrawals" + } + }, + { + "description": "All tx inputs must be external (without 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" + } + ], + "relays": [] + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "Stakepool registration transaction can contain only external inputs" + } + }, + { + "description": "Pool reward address belongs to different network than the tx", + "parameters": { + "protocol_magic": 42, + "network_id": 0, + "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" + } + ], + "relays": [] + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr_test1vr9s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqtmut0e", + "amount": "1" + } + ] + }, + "result": { + "error_message": "ProcessError: Invalid address" + } + }, + { + "description": "Pool reward address is a base address", + "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": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + } + ], + "relays": [] + } + } + ], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "error_message": "ProcessError: Invalid address" + } + } + ] +} diff --git a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json new file mode 100644 index 000000000..2b94ca356 --- /dev/null +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json @@ -0,0 +1,194 @@ +{ + "setup": { + "mnemonic": "all all all all all all all all all all all all", + "passphrase": "" + }, + "tests": [ + { + "description": "Sample stake pool registration certificate", + "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": [], + "metadata": "", + "input_flow": [["SWIPE", "SWIPE", "SWIPE", "YES"], ["SWIPE", "YES"], ["SWIPE", "YES"], ["YES"]], + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "tx_hash": "e3b9a5657bf62609465a930c8359d774c73944973cfc5a104a0f0ed1e1e8db21", + "serialized_tx": "83a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff0102182a030a04818a03581cf61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb49735820198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d06401a1dcd65001a1443fd00d81e820102581de13a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c49071182581c122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277581c3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c4907118584001904d244c0a8000150b80d01200000a3852e8a00003473700384001904d2f650b80d01200000a3852e8a00003473700384001904d244c0a80001f683011904d26d7777772e746573742e7465737482026e7777772e74657374322e74657374827568747470733a2f2f7777772e746573742e746573745820914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6a10081825820bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e584006305b52f76d2d2da6925c02036a9a28456976009f8c6432513f273110d09ea26db79c696cec322b010e5cbb7d90a6b473b157e65df846a1487062569a5f5a04f6" + } + }, + { + "description": "Stake pool registration certificate with no pool metadata", + "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" + } + ], + "relays": [] + } + } + ], + "withdrawals": [], + "metadata": "", + "input_flow": [["SWIPE", "SWIPE", "SWIPE", "YES"], ["YES"], ["YES"], ["YES"]], + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ] + }, + "result": { + "tx_hash": "504f9214142996e0b7e315103b25d88a4afa3d01dd5be22376921b52b01483c3", + "serialized_tx": "83a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff0102182a030a04818a03581cf61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb49735820198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d06401a1dcd65001a1443fd00d81e820102581de13a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c49071181581c122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b427780f6a10081825820bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e5840aa2099208399fcc27c18d7ef0c7e873f9e22f0935b7e912cddd34b33b8cafd541a878dc01c042ce490e4c9bad3c62c2f59acaa009d336c9ff875c5f153d34900f6" + } + }, + { + "description": "Stake pool registration on testnet", + "parameters": { + "protocol_magic": 42, + "network_id": 0, + "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": "stake_test1uqa87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygfvlkaz", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + } + ], + "relays": [] + } + } + ], + "withdrawals": [], + "metadata": "", + "input_flow": [["SWIPE", "SWIPE", "SWIPE", "YES"], ["YES"], ["YES"], ["YES"]], + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr_test1vr9s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqtmut0e", + "amount": "1" + } + ] + }, + "result": { + "tx_hash": "12921b4f8e77f815e0c8ed97c541fbd5ba38a6d3323f4ff1af0cb934b8ac6b39", + "serialized_tx": "83a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182581d60cb03849e268f989b5a843107bad7fa2908246986a8f3d643f8c184800102182a030a04818a03581cf61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb49735820198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d06401a1dcd65001a1443fd00d81e820102581de03a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c49071181581c122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b427780f6a10081825820bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e584027cab81902d04b2491d7aa2bf57bd9db59d33c2df1502dae0412d5225c6b0b8f7b057de6a7e7eae25016ed6ea1f6e6239fb36a285216c6ee4a3cb3376287a300f6" + } + } + ] +} diff --git a/core/src/apps/cardano/address.py b/core/src/apps/cardano/address.py index df601f4de..31eebea91 100644 --- a/core/src/apps/cardano/address.py +++ b/core/src/apps/cardano/address.py @@ -4,7 +4,7 @@ from trezor.messages import CardanoAddressParametersType, CardanoAddressType from apps.common.seed import remove_ed25519_prefix -from .byron_address import derive_byron_address, validate_output_byron_address +from .byron_address import derive_byron_address, validate_byron_address from .helpers import INVALID_ADDRESS, NETWORK_MISMATCH, bech32, network_ids from .helpers.paths import SCHEMA_STAKING from .helpers.utils import variable_length_encode @@ -32,7 +32,13 @@ MIN_ADDRESS_BYTES_LENGTH = 29 MAX_ADDRESS_BYTES_LENGTH = 65 -def validate_output_address(address: str, protocol_magic: int, network_id: int) -> None: +def _validate_address_and_get_type( + address: str, protocol_magic: int, network_id: int +) -> int: + """ + Validates Cardano address and returns its type + for the convenience of outward-facing functions. + """ if address is None or len(address) == 0: raise INVALID_ADDRESS @@ -40,12 +46,31 @@ def validate_output_address(address: str, protocol_magic: int, network_id: int) address_type = _get_address_type(address_bytes) if address_type == CardanoAddressType.BYRON: - validate_output_byron_address(address_bytes, protocol_magic) + validate_byron_address(address_bytes, protocol_magic) elif address_type in ADDRESS_TYPES_SHELLEY: - _validate_output_shelley_address(address, address_bytes, network_id) + _validate_shelley_address(address, address_bytes, network_id) else: raise INVALID_ADDRESS + return address_type + + +def validate_output_address(address: str, protocol_magic: int, network_id: int) -> None: + address_type = _validate_address_and_get_type(address, protocol_magic, network_id) + + if address_type in (CardanoAddressType.REWARD, CardanoAddressType.REWARD_SCRIPT): + raise INVALID_ADDRESS + + +def validate_reward_address(address: str, protocol_magic: int, network_id: int) -> None: + address_type = _validate_address_and_get_type(address, protocol_magic, network_id) + + if address_type not in ( + CardanoAddressType.REWARD, + CardanoAddressType.REWARD_SCRIPT, + ): + raise INVALID_ADDRESS + def get_address_bytes_unsafe(address: str) -> bytes: try: @@ -63,19 +88,13 @@ def _get_address_type(address: bytes) -> int: return address[0] >> 4 -def _validate_output_shelley_address( +def _validate_shelley_address( address_str: str, address_bytes: bytes, network_id: int ) -> None: address_type = _get_address_type(address_bytes) - # reward address cannot be an output address - if ( - address_type == CardanoAddressType.REWARD - or address_type == CardanoAddressType.REWARD_SCRIPT - ): - raise INVALID_ADDRESS _validate_address_size(address_bytes, address_type) - _validate_output_address_bech32_hrp(address_str, address_type, network_id) + _validate_address_bech32_hrp(address_str, address_type, network_id) _validate_address_network_id(address_bytes, network_id) @@ -86,7 +105,7 @@ def _validate_address_size( raise INVALID_ADDRESS -def _validate_output_address_bech32_hrp( +def _validate_address_bech32_hrp( address_str: str, address_type: EnumTypeCardanoAddressType, network_id: int ) -> None: valid_hrp = _get_bech32_hrp_for_address(address_type, network_id) @@ -136,14 +155,22 @@ def derive_human_readable_address( protocol_magic: int, network_id: int, ) -> str: - address = derive_address_bytes(keychain, parameters, protocol_magic, network_id) + address_bytes = derive_address_bytes( + keychain, parameters, protocol_magic, network_id + ) + + return encode_human_readable_address(address_bytes) - address_type = _get_address_type(address) + +def encode_human_readable_address(address_bytes: bytes) -> str: + address_type = _get_address_type(address_bytes) if address_type == CardanoAddressType.BYRON: - return base58.encode(address) + return base58.encode(address_bytes) elif address_type in ADDRESS_TYPES_SHELLEY: - hrp = _get_bech32_hrp_for_address(_get_address_type(address), network_id) - return bech32.encode(hrp, address) + hrp = _get_bech32_hrp_for_address( + address_type, _get_address_network_id(address_bytes) + ) + return bech32.encode(hrp, address_bytes) else: raise ValueError @@ -293,7 +320,18 @@ def _derive_reward_address( if not SCHEMA_STAKING.match(path): raise wire.DataError("Invalid path for reward address!") - header = _create_address_header(CardanoAddressType.REWARD, network_id) 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/byron_address.py b/core/src/apps/cardano/byron_address.py index bd44f9ec9..47e46c190 100644 --- a/core/src/apps/cardano/byron_address.py +++ b/core/src/apps/cardano/byron_address.py @@ -52,7 +52,7 @@ def get_address_attributes(protocol_magic: int) -> dict: return address_attributes -def validate_output_byron_address(address: bytes, protocol_magic: int) -> None: +def validate_byron_address(address: bytes, protocol_magic: int) -> None: address_data_encoded = _decode_address_raw(address) _validate_address_data_protocol_magic(address_data_encoded, protocol_magic) diff --git a/core/src/apps/cardano/certificates.py b/core/src/apps/cardano/certificates.py new file mode 100644 index 000000000..af6acc5eb --- /dev/null +++ b/core/src/apps/cardano/certificates.py @@ -0,0 +1,258 @@ +from trezor.messages import CardanoCertificateType, CardanoPoolRelayType + +from apps.common import cbor + +from .address import ( + get_address_bytes_unsafe, + get_public_key_hash, + validate_reward_address, +) +from .helpers import INVALID_CERTIFICATE, LOVELACE_MAX_SUPPLY +from .helpers.paths import SCHEMA_STAKING + +if False: + from trezor.messages.CardanoTxCertificateType import CardanoTxCertificateType + from trezor.messages.CardanoPoolParametersType import CardanoPoolParametersType + from trezor.messages.CardanoPoolRelayParametersType import ( + CardanoPoolRelayParametersType, + ) + from trezor.messages.CardanoPoolOwnerType import CardanoPoolOwnerType + from trezor.messages.CardanoPoolMetadataType import CardanoPoolMetadataType + from typing import List, Optional, Union, Tuple, Any + from . import seed + + CborSequence = Union[List[Any], Tuple[Any, ...]] + +POOL_HASH_SIZE = 28 +VRF_KEY_HASH_SIZE = 32 +POOL_METADATA_HASH_SIZE = 32 +PUBLIC_KEY_HASH_SIZE = 28 +IPV4_ADDRESS_SIZE = 4 +IPV6_ADDRESS_SIZE = 16 + +MAX_URL_LENGTH = 64 +MAX_PORT_NUMBER = 65535 + + +def validate_certificate( + certificate: CardanoTxCertificateType, protocol_magic: int, network_id: int +) -> None: + if certificate.type in ( + CardanoCertificateType.STAKE_DELEGATION, + CardanoCertificateType.STAKE_REGISTRATION, + CardanoCertificateType.STAKE_DEREGISTRATION, + ): + if not SCHEMA_STAKING.match(certificate.path): + raise INVALID_CERTIFICATE + + if certificate.type == CardanoCertificateType.STAKE_DELEGATION: + if not certificate.pool or len(certificate.pool) != POOL_HASH_SIZE: + raise INVALID_CERTIFICATE + + if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + if certificate.pool_parameters is None: + raise INVALID_CERTIFICATE + _validate_pool_parameters( + certificate.pool_parameters, protocol_magic, network_id + ) + + +def cborize_certificate( + keychain: seed.Keychain, certificate: CardanoTxCertificateType +) -> CborSequence: + if certificate.type in ( + CardanoCertificateType.STAKE_REGISTRATION, + CardanoCertificateType.STAKE_DEREGISTRATION, + ): + return ( + certificate.type, + (0, get_public_key_hash(keychain, certificate.path)), + ) + elif certificate.type == CardanoCertificateType.STAKE_DELEGATION: + return ( + certificate.type, + (0, get_public_key_hash(keychain, certificate.path)), + certificate.pool, + ) + elif certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + pool_parameters = certificate.pool_parameters + + assert pool_parameters is not None + + return ( + certificate.type, + pool_parameters.pool_id, + pool_parameters.vrf_key_hash, + pool_parameters.pledge, + pool_parameters.cost, + cbor.Tagged( + 30, + ( + pool_parameters.margin_numerator, + pool_parameters.margin_denominator, + ), + ), + # this relies on pool_parameters.reward_account being validated beforehand + # in _validate_pool_parameters + get_address_bytes_unsafe(pool_parameters.reward_account), + _cborize_pool_owners(keychain, pool_parameters.owners), + _cborize_pool_relays(pool_parameters.relays), + _cborize_pool_metadata(pool_parameters.metadata), + ) + else: + raise INVALID_CERTIFICATE + + +def assert_certificate_cond(condition: bool) -> None: + if not condition: + raise INVALID_CERTIFICATE + + +def _validate_pool_parameters( + pool_parameters: CardanoPoolParametersType, protocol_magic: int, network_id: int +) -> None: + assert_certificate_cond(len(pool_parameters.pool_id) == POOL_HASH_SIZE) + assert_certificate_cond(len(pool_parameters.vrf_key_hash) == VRF_KEY_HASH_SIZE) + assert_certificate_cond(0 <= pool_parameters.pledge <= LOVELACE_MAX_SUPPLY) + assert_certificate_cond(0 <= pool_parameters.cost <= LOVELACE_MAX_SUPPLY) + assert_certificate_cond(pool_parameters.margin_numerator > 0) + assert_certificate_cond(pool_parameters.margin_denominator > 0) + assert_certificate_cond( + pool_parameters.margin_numerator <= pool_parameters.margin_denominator + ) + assert_certificate_cond(len(pool_parameters.owners) > 0) + + validate_reward_address(pool_parameters.reward_account, protocol_magic, network_id) + + for pool_relay in pool_parameters.relays: + _validate_pool_relay(pool_relay) + + _validate_pool_owners(pool_parameters.owners) + + if pool_parameters.metadata: + _validate_pool_metadata(pool_parameters.metadata) + + +def _validate_pool_owners(owners: List[CardanoPoolOwnerType]) -> None: + owners_as_path_count = 0 + for owner in owners: + assert_certificate_cond( + owner.staking_key_hash is not None or owner.staking_key_path is not None + ) + if owner.staking_key_hash is not None: + assert_certificate_cond(len(owner.staking_key_hash) == PUBLIC_KEY_HASH_SIZE) + if owner.staking_key_path: + assert_certificate_cond(SCHEMA_STAKING.match(owner.staking_key_path)) + + if owner.staking_key_path: + owners_as_path_count += 1 + + assert_certificate_cond(owners_as_path_count == 1) + + +def _validate_pool_relay(pool_relay: CardanoPoolRelayParametersType) -> None: + if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP: + assert_certificate_cond( + pool_relay.ipv4_address is not None or pool_relay.ipv6_address is not None + ) + if pool_relay.ipv4_address is not None: + assert_certificate_cond(len(pool_relay.ipv4_address) == IPV4_ADDRESS_SIZE) + if pool_relay.ipv6_address is not None: + assert_certificate_cond(len(pool_relay.ipv6_address) == IPV6_ADDRESS_SIZE) + assert_certificate_cond( + pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER + ) + elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME: + assert_certificate_cond( + pool_relay.host_name is not None + and len(pool_relay.host_name) <= MAX_URL_LENGTH + ) + assert_certificate_cond( + pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER + ) + elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME: + assert_certificate_cond( + pool_relay.host_name is not None + and len(pool_relay.host_name) <= MAX_URL_LENGTH + ) + else: + raise INVALID_CERTIFICATE + + +def _validate_pool_metadata(pool_metadata: CardanoPoolMetadataType) -> None: + assert_certificate_cond(len(pool_metadata.url) <= MAX_URL_LENGTH) + assert_certificate_cond(len(pool_metadata.hash) == POOL_METADATA_HASH_SIZE) + assert_certificate_cond(all((32 <= ord(c) < 127) for c in pool_metadata.url)) + + +def _cborize_pool_owners( + keychain: seed.Keychain, pool_owners: List[CardanoPoolOwnerType] +) -> List[bytes]: + result = [] + + for pool_owner in pool_owners: + if pool_owner.staking_key_path: + result.append(get_public_key_hash(keychain, pool_owner.staking_key_path)) + elif pool_owner.staking_key_hash: + result.append(pool_owner.staking_key_hash) + else: + raise ValueError + + return result + + +def _cborize_ipv6_address(ipv6_address: Optional[bytes]) -> Optional[bytes]: + if ipv6_address is None: + return None + + # ipv6 addresses are serialized to CBOR as uint_32[4] little endian + assert len(ipv6_address) == IPV6_ADDRESS_SIZE + + result = b"" + for i in range(0, 4): + result += bytes(reversed(ipv6_address[i * 4 : i * 4 + 4])) + + return result + + +def _cborize_pool_relays( + pool_relays: List[CardanoPoolRelayParametersType], +) -> List[CborSequence]: + result: List[CborSequence] = [] + + for pool_relay in pool_relays: + if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP: + result.append( + ( + pool_relay.type, + pool_relay.port, + pool_relay.ipv4_address, + _cborize_ipv6_address(pool_relay.ipv6_address), + ) + ) + elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME: + result.append( + ( + pool_relay.type, + pool_relay.port, + pool_relay.host_name, + ) + ) + elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME: + result.append( + ( + pool_relay.type, + pool_relay.host_name, + ) + ) + + return result + + +def _cborize_pool_metadata( + pool_metadata: Optional[CardanoPoolMetadataType], +) -> Optional[CborSequence]: + if not pool_metadata: + return None + + return (pool_metadata.url, pool_metadata.hash) diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py index d748f1686..2a4d33f36 100644 --- a/core/src/apps/cardano/helpers/__init__.py +++ b/core/src/apps/cardano/helpers/__init__.py @@ -5,3 +5,11 @@ NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!") INVALID_CERTIFICATE = wire.ProcessError("Invalid certificate") INVALID_WITHDRAWAL = wire.ProcessError("Invalid withdrawal") INVALID_METADATA = wire.ProcessError("Invalid metadata") +INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE = wire.ProcessError( + "Stakepool registration transaction cannot contain other certificates nor withdrawals" +) +INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS = wire.ProcessError( + "Stakepool registration transaction can contain only external inputs" +) + +LOVELACE_MAX_SUPPLY = 45_000_000_000 * 1_000_000 diff --git a/core/src/apps/cardano/helpers/staking_use_cases.py b/core/src/apps/cardano/helpers/staking_use_cases.py index 2c1d2ad67..defece9d7 100644 --- a/core/src/apps/cardano/helpers/staking_use_cases.py +++ b/core/src/apps/cardano/helpers/staking_use_cases.py @@ -8,7 +8,7 @@ from .utils import to_account_path if False: from typing import List from trezor.messages import CardanoAddressParametersType - from . import seed + from .. import seed """ diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index eb117fbb0..ee56138c3 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -6,6 +6,8 @@ from trezor.messages import ( ButtonRequestType, CardanoAddressType, CardanoCertificateType, + CardanoPoolMetadataType, + CardanoPoolOwnerType, ) from trezor.strings import format_amount from trezor.ui.button import ButtonDefault @@ -16,16 +18,23 @@ from trezor.utils import chunks from apps.common.confirm import confirm, require_confirm, require_hold_to_confirm from apps.common.layout import address_n_to_str, show_warning +from . import seed +from .address import ( + encode_human_readable_address, + get_public_key_hash, + pack_reward_address_bytes, +) from .helpers import protocol_magics from .helpers.utils import to_account_path if False: - from typing import List + from typing import List, Optional from trezor import wire from trezor.messages import ( CardanoBlockchainPointerType, CardanoTxCertificateType, CardanoTxWithdrawalType, + CardanoPoolParametersType, ) from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType @@ -42,6 +51,7 @@ CERTIFICATE_TYPE_NAMES = { CardanoCertificateType.STAKE_REGISTRATION: "Stake key registration", CardanoCertificateType.STAKE_DEREGISTRATION: "Stake key deregistration", CardanoCertificateType.STAKE_DELEGATION: "Stake delegation", + CardanoCertificateType.STAKE_POOL_REGISTRATION: "Stakepool registration", } # Maximum number of characters per line in monospace font. @@ -52,7 +62,7 @@ def format_coin_amount(amount: int) -> str: return "%s %s" % (format_amount(amount, 6), "ADA") -async def confirm_sending(ctx: wire.Context, amount: int, to: str): +async def confirm_sending(ctx: wire.Context, amount: int, to: str) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Confirm sending:") page1.bold(format_coin_amount(amount)) @@ -68,7 +78,7 @@ async def confirm_sending(ctx: wire.Context, amount: int, to: str): async def show_warning_tx_no_staking_info( ctx: wire.Context, address_type: EnumTypeCardanoAddressType, amount: int -): +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Change " + ADDRESS_TYPE_NAMES[address_type].lower()) page1.normal("address has no stake") @@ -83,7 +93,7 @@ async def show_warning_tx_pointer_address( ctx: wire.Context, pointer: CardanoBlockchainPointerType, amount: int, -): +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Change address has a") page1.normal("pointer with staking") @@ -105,7 +115,7 @@ async def show_warning_tx_different_staking_account( ctx: wire.Context, staking_account_path: List[int], amount: int, -): +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Change address staking") page1.normal("rights do not match") @@ -124,7 +134,7 @@ async def show_warning_tx_staking_key_hash( ctx: wire.Context, staking_key_hash: bytes, amount: int, -): +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Change address staking") page1.normal("rights do not match") @@ -162,7 +172,11 @@ async def confirm_transaction( async def confirm_certificate( ctx: wire.Context, certificate: CardanoTxCertificateType -) -> bool: +) -> None: + # stake pool registration requires custom confirmation logic not covered + # in this call + assert certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION + pages = [] page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) @@ -181,9 +195,108 @@ async def confirm_certificate( await require_confirm(ctx, Paginated(pages)) +async def confirm_stake_pool_parameters( + ctx: wire.Context, + pool_parameters: CardanoPoolParametersType, + network_id: int, + protocol_magic: int, +) -> None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("Confirm:") + page1.bold("Stake pool registration") + page1.normal("Network:") + page1.bold(protocol_magics.to_ui_string(protocol_magic)) + + page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page2.normal("Pool id:") + page2.bold(hexlify(pool_parameters.pool_id).decode()) + + page3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page3.normal("Pool reward account:") + page3.bold(pool_parameters.reward_account) + + page4 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page4.normal("Pledge: " + format_coin_amount(pool_parameters.pledge)) + page4.normal("Cost: " + format_coin_amount(pool_parameters.cost)) + margin_percentage = ( + 100.0 * pool_parameters.margin_numerator / pool_parameters.margin_denominator + ) + percentage_formatted = ("%f" % margin_percentage).rstrip("0").rstrip(".") + page4.normal("Margin: %s%%" % percentage_formatted) + + await require_confirm(ctx, Paginated([page1, page2, page3, page4])) + + +async def confirm_stake_pool_owners( + ctx: wire.Context, + keychain: seed.keychain, + owners: List[CardanoPoolOwnerType], + network_id: int, +) -> None: + pages = [] + for index, owner in enumerate(owners, 1): + page = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page.normal("Pool owner #%d:" % (index)) + + if owner.staking_key_path: + page.bold(address_n_to_str(owner.staking_key_path)) + page.normal( + encode_human_readable_address( + pack_reward_address_bytes( + get_public_key_hash(keychain, owner.staking_key_path), + network_id, + ) + ) + ) + else: + page.bold( + encode_human_readable_address( + pack_reward_address_bytes(owner.staking_key_hash, network_id) + ) + ) + + pages.append(page) + + await require_confirm(ctx, Paginated(pages)) + + +async def confirm_stake_pool_metadata( + ctx: wire.Context, + metadata: Optional[CardanoPoolMetadataType], +) -> None: + + if metadata is None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("Pool has no metadata") + page1.normal("(anonymous pool)") + + await require_confirm(ctx, page1) + return + + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("Pool metadata url:") + page1.bold(metadata.url) + + page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page2.normal("Pool metadata hash:") + page2.bold(hexlify(metadata.hash).decode()) + + await require_confirm(ctx, Paginated([page1, page2])) + + +async def confirm_stake_pool_registration_final( + ctx: wire.Context, +) -> None: + + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("Confirm signing the stake pool registration as an owner") + + await require_hold_to_confirm(ctx, page1) + + async def confirm_withdrawal( ctx: wire.Context, withdrawal: CardanoTxWithdrawalType -) -> bool: +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Confirm withdrawal") page1.normal("for account:") diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 48afa3d06..1af870983 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -16,14 +16,16 @@ from .address import ( derive_address_bytes, derive_human_readable_address, get_address_bytes_unsafe, - get_public_key_hash, validate_output_address, ) from .byron_address import get_address_attributes +from .certificates import cborize_certificate, validate_certificate from .helpers import ( - INVALID_CERTIFICATE, INVALID_METADATA, + INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE, + INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS, INVALID_WITHDRAWAL, + LOVELACE_MAX_SUPPLY, network_ids, protocol_magics, staking_use_cases, @@ -33,6 +35,10 @@ from .helpers.utils import to_account_path from .layout import ( confirm_certificate, confirm_sending, + confirm_stake_pool_metadata, + confirm_stake_pool_owners, + confirm_stake_pool_parameters, + confirm_stake_pool_registration_final, confirm_transaction, confirm_withdrawal, show_warning_tx_different_staking_account, @@ -43,12 +49,12 @@ from .layout import ( from .seed import is_byron_path, is_shelley_path if False: - from typing import Dict, List, Tuple from trezor.messages.CardanoSignTx import CardanoSignTx + from trezor.messages.CardanoTxCertificateType import CardanoTxCertificateType from trezor.messages.CardanoTxInputType import CardanoTxInputType from trezor.messages.CardanoTxOutputType import CardanoTxOutputType - from trezor.messages.CardanoTxCertificateType import CardanoTxCertificateType from trezor.messages.CardanoTxWithdrawalType import CardanoTxWithdrawalType + from typing import Dict, List, Tuple # the maximum allowed change address. this should be large enough for normal # use and still allow to quickly brute-force the correct bip32 path @@ -56,9 +62,6 @@ MAX_CHANGE_ADDRESS_INDEX = const(1000000) ACCOUNT_PATH_INDEX = const(2) BIP_PATH_LENGTH = const(5) -LOVELACE_MAX_SUPPLY = 45_000_000_000 * 1_000_000 - -POOL_HASH_SIZE = 28 METADATA_HASH_SIZE = 32 MAX_METADATA_LENGTH = 500 @@ -67,24 +70,33 @@ MAX_METADATA_LENGTH = 500 async def sign_tx( ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain ) -> CardanoSignedTx: - try: - if msg.fee > LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Fee is out of range!") + if msg.fee > LOVELACE_MAX_SUPPLY: + raise wire.ProcessError("Fee is out of range!") + + validate_network_info(msg.network_id, msg.protocol_magic) + + if _has_stake_pool_registration(msg): + return await _sign_stake_pool_registration_tx(ctx, msg, keychain) + else: + return await _sign_standard_tx(ctx, msg, keychain) - validate_network_info(msg.network_id, msg.protocol_magic) +async def _sign_standard_tx( + ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain +) -> CardanoSignedTx: + try: for i in msg.inputs: await validate_path( ctx, keychain, i.address_n, SCHEMA_ADDRESS.match(i.address_n) ) _validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id) - _validate_certificates(msg.certificates) + _validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id) _validate_withdrawals(msg.withdrawals) _validate_metadata(msg.metadata) # display the transaction in UI - await _show_tx(ctx, keychain, msg) + await _show_standard_tx(ctx, keychain, msg) # sign the transaction bundle and prepare the result serialized_tx, tx_hash = _serialize_tx(keychain, msg) @@ -98,6 +110,50 @@ async def sign_tx( return tx +async def _sign_stake_pool_registration_tx( + ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain +) -> CardanoSignedTx: + """ + 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. + """ + try: + _validate_stake_pool_registration_tx_structure(msg) + + _ensure_no_signing_inputs(msg.inputs) + _validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id) + _validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id) + _validate_metadata(msg.metadata) + + await _show_stake_pool_registration_tx(ctx, keychain, msg) + + # sign the transaction bundle and prepare the result + serialized_tx, tx_hash = _serialize_tx(keychain, msg) + tx = CardanoSignedTx(serialized_tx=serialized_tx, tx_hash=tx_hash) + + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Signing failed") + + return tx + + +def _has_stake_pool_registration(msg: CardanoSignTx): + return any( + cert.type == CardanoCertificateType.STAKE_POOL_REGISTRATION + for cert in msg.certificates + ) + + def validate_network_info(network_id: int, protocol_magic: int) -> None: """ We are only concerned about checking that both network_id and protocol_magic @@ -111,6 +167,15 @@ def validate_network_info(network_id: int, protocol_magic: int) -> None: raise wire.ProcessError("Invalid network id/protocol magic combination!") +def _validate_stake_pool_registration_tx_structure(msg: CardanoSignTx): + if ( + len(msg.certificates) != 1 + or not _has_stake_pool_registration(msg) + or len(msg.withdrawals) != 0 + ): + raise INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE + + def _validate_outputs( keychain: seed.Keychain, outputs: List[CardanoTxOutputType], @@ -139,14 +204,16 @@ def _validate_outputs( raise wire.ProcessError("Total transaction amount is out of range!") -def _validate_certificates(certificates: List[CardanoTxCertificateType]) -> None: - for certificate in certificates: - if not SCHEMA_STAKING.match(certificate.path): - raise INVALID_CERTIFICATE +def _ensure_no_signing_inputs(inputs: List[CardanoTxInputType]): + if any(i.address_n for i in inputs): + raise INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS - if certificate.type == CardanoCertificateType.STAKE_DELEGATION: - if certificate.pool is None or len(certificate.pool) != POOL_HASH_SIZE: - raise INVALID_CERTIFICATE + +def _validate_certificates( + certificates: List[CardanoTxCertificateType], protocol_magic: int, network_id: int +) -> None: + for certificate in certificates: + validate_certificate(certificate, protocol_magic, network_id) def _validate_withdrawals(withdrawals: List[CardanoTxWithdrawalType]) -> None: @@ -176,10 +243,10 @@ def _validate_metadata(metadata: bytes) -> None: def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, bytes]: - tx_body = _build_tx_body(keychain, msg) + tx_body = _cborize_tx_body(keychain, msg) tx_hash = _hash_tx_body(tx_body) - witnesses = _build_witnesses( + witnesses = _cborize_witnesses( keychain, msg.inputs, msg.certificates, @@ -197,9 +264,9 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b return serialized_tx, tx_hash -def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: - inputs_for_cbor = _build_inputs(msg.inputs) - outputs_for_cbor = _build_outputs( +def _cborize_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: + inputs_for_cbor = _cborize_inputs(msg.inputs) + outputs_for_cbor = _cborize_outputs( keychain, msg.outputs, msg.protocol_magic, msg.network_id ) @@ -211,11 +278,11 @@ def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: } if msg.certificates: - certificates_for_cbor = _build_certificates(keychain, msg.certificates) + certificates_for_cbor = _cborize_certificates(keychain, msg.certificates) tx_body[4] = certificates_for_cbor if msg.withdrawals: - withdrawals_for_cbor = _build_withdrawals( + withdrawals_for_cbor = _cborize_withdrawals( keychain, msg.withdrawals, msg.protocol_magic, msg.network_id ) tx_body[5] = withdrawals_for_cbor @@ -228,11 +295,11 @@ def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: return tx_body -def _build_inputs(inputs: List[CardanoTxInputType]) -> List[Tuple[bytes, int]]: - return [(input.prev_hash, input.prev_index) for input in inputs] +def _cborize_inputs(inputs: List[CardanoTxInputType]) -> List[Tuple[bytes, int]]: + return [(tx_input.prev_hash, tx_input.prev_index) for tx_input in inputs] -def _build_outputs( +def _cborize_outputs( keychain: seed.Keychain, outputs: List[CardanoTxOutputType], protocol_magic: int, @@ -254,29 +321,14 @@ def _build_outputs( return result -def _build_certificates( - keychain: seed.Keychain, certificates: List[CardanoTxCertificateType] +def _cborize_certificates( + keychain: seed.Keychain, + certificates: List[CardanoTxCertificateType], ) -> List[Tuple]: - result = [] - for certificate in certificates: - public_key_hash = get_public_key_hash(keychain, certificate.path) - - stake_credential = [0, public_key_hash] - if certificate.type == CardanoCertificateType.STAKE_DELEGATION: - certificate_for_cbor = ( - certificate.type, - stake_credential, - certificate.pool, - ) - else: - certificate_for_cbor = (certificate.type, stake_credential) - - result.append(certificate_for_cbor) - - return result + return [cborize_certificate(keychain, cert) for cert in certificates] -def _build_withdrawals( +def _cborize_withdrawals( keychain: seed.Keychain, withdrawals: List[CardanoTxWithdrawalType], protocol_magic: int, @@ -308,7 +360,7 @@ def _hash_tx_body(tx_body: Dict) -> bytes: return hashlib.blake2b(data=tx_body_cbor, outlen=32).digest() -def _build_witnesses( +def _cborize_witnesses( keychain: seed.Keychain, inputs: List[CardanoTxInputType], certificates: List[CardanoTxCertificateType], @@ -316,10 +368,10 @@ def _build_witnesses( tx_body_hash: bytes, protocol_magic: int, ) -> Dict: - shelley_witnesses = _build_shelley_witnesses( + shelley_witnesses = _cborize_shelley_witnesses( keychain, inputs, certificates, withdrawals, tx_body_hash ) - byron_witnesses = _build_byron_witnesses( + byron_witnesses = _cborize_byron_witnesses( keychain, inputs, tx_body_hash, protocol_magic ) @@ -334,7 +386,7 @@ def _build_witnesses( return witnesses -def _build_shelley_witnesses( +def _cborize_shelley_witnesses( keychain: seed.Keychain, inputs: List[CardanoTxInputType], certificates: List[CardanoTxCertificateType], @@ -345,25 +397,30 @@ def _build_shelley_witnesses( # include only one witness for each path paths = set() - for input in inputs: - if not is_shelley_path(input.address_n): - continue - paths.add(tuple(input.address_n)) + for tx_input in inputs: + if is_shelley_path(tx_input.address_n): + paths.add(tuple(tx_input.address_n)) for certificate in certificates: - if not _is_certificate_witness_required(certificate.type): - continue - paths.add(tuple(certificate.path)) + if certificate.type in ( + CardanoCertificateType.STAKE_DEREGISTRATION, + CardanoCertificateType.STAKE_DELEGATION, + ): + paths.add(tuple(certificate.path)) + elif certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + for pool_owner in certificate.pool_parameters.owners: + if pool_owner.staking_key_path: + paths.add(tuple(pool_owner.staking_key_path)) for withdrawal in withdrawals: paths.add(tuple(withdrawal.path)) for path in paths: - witness = _build_shelley_witness(keychain, tx_body_hash, list(path)) + witness = _cborize_shelley_witness(keychain, tx_body_hash, list(path)) shelley_witnesses.append(witness) return shelley_witnesses -def _build_shelley_witness( +def _cborize_shelley_witness( keychain: seed.Keychain, tx_body_hash: bytes, path: List[int] ) -> List[Tuple[bytes, bytes]]: node = keychain.derive(path) @@ -376,11 +433,7 @@ def _build_shelley_witness( return public_key, signature -def _is_certificate_witness_required(certificate_type: int) -> bool: - return certificate_type != CardanoCertificateType.STAKE_REGISTRATION - - -def _build_byron_witnesses( +def _cborize_byron_witnesses( keychain: seed.Keychain, inputs: List[CardanoTxInputType], tx_body_hash: bytes, @@ -390,10 +443,9 @@ def _build_byron_witnesses( # include only one witness for each path paths = set() - for input in inputs: - if not is_byron_path(input.address_n): - continue - paths.add(tuple(input.address_n)) + for tx_input in inputs: + if is_byron_path(tx_input.address_n): + paths.add(tuple(tx_input.address_n)) for path in paths: node = keychain.derive(list(path)) @@ -410,7 +462,7 @@ def _build_byron_witnesses( return byron_witnesses -async def _show_tx( +async def _show_standard_tx( ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx ) -> None: total_amount = await _show_outputs(ctx, keychain, msg) @@ -427,6 +479,23 @@ async def _show_tx( ) +async def _show_stake_pool_registration_tx( + ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx +) -> None: + stake_pool_registration_certificate = msg.certificates[0] + pool_parameters = stake_pool_registration_certificate.pool_parameters + + # display the transaction (certificate) in UI + await confirm_stake_pool_parameters( + ctx, pool_parameters, msg.network_id, msg.protocol_magic + ) + await confirm_stake_pool_owners( + ctx, keychain, pool_parameters.owners, msg.network_id + ) + await confirm_stake_pool_metadata(ctx, pool_parameters.metadata) + await confirm_stake_pool_registration_final(ctx) + + async def _show_outputs( ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx ) -> int: @@ -488,8 +557,8 @@ async def _show_change_output_staking_warnings( # addresses from the same account as inputs should be hidden def _should_hide_output(output: List[int], inputs: List[CardanoTxInputType]) -> bool: - for input in inputs: - inp = input.address_n + for tx_input in inputs: + inp = tx_input.address_n if ( len(output) != BIP_PATH_LENGTH or output[: (ACCOUNT_PATH_INDEX + 1)] != inp[: (ACCOUNT_PATH_INDEX + 1)] diff --git a/core/src/trezor/messages/CardanoCertificateType.py b/core/src/trezor/messages/CardanoCertificateType.py index ef56cb070..eb6bc15f4 100644 --- a/core/src/trezor/messages/CardanoCertificateType.py +++ b/core/src/trezor/messages/CardanoCertificateType.py @@ -6,3 +6,4 @@ if False: STAKE_REGISTRATION = 0 # type: Literal[0] STAKE_DEREGISTRATION = 1 # type: Literal[1] STAKE_DELEGATION = 2 # type: Literal[2] +STAKE_POOL_REGISTRATION = 3 # type: Literal[3] diff --git a/core/src/trezor/messages/CardanoPoolMetadataType.py b/core/src/trezor/messages/CardanoPoolMetadataType.py new file mode 100644 index 000000000..6ab0ffade --- /dev/null +++ b/core/src/trezor/messages/CardanoPoolMetadataType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolMetadataType(p.MessageType): + + def __init__( + self, + *, + url: str, + hash: bytes, + ) -> None: + self.url = url + self.hash = hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('url', p.UnicodeType, p.FLAG_REQUIRED), + 2: ('hash', p.BytesType, p.FLAG_REQUIRED), + } diff --git a/core/src/trezor/messages/CardanoPoolOwnerType.py b/core/src/trezor/messages/CardanoPoolOwnerType.py new file mode 100644 index 000000000..cfd391ee8 --- /dev/null +++ b/core/src/trezor/messages/CardanoPoolOwnerType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolOwnerType(p.MessageType): + + def __init__( + self, + *, + staking_key_path: List[int] = None, + staking_key_hash: bytes = None, + ) -> None: + self.staking_key_path = staking_key_path if staking_key_path is not None else [] + self.staking_key_hash = staking_key_hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('staking_key_path', p.UVarintType, p.FLAG_REPEATED), + 2: ('staking_key_hash', p.BytesType, None), + } diff --git a/core/src/trezor/messages/CardanoPoolParametersType.py b/core/src/trezor/messages/CardanoPoolParametersType.py new file mode 100644 index 000000000..f65c44938 --- /dev/null +++ b/core/src/trezor/messages/CardanoPoolParametersType.py @@ -0,0 +1,57 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .CardanoPoolMetadataType import CardanoPoolMetadataType +from .CardanoPoolOwnerType import CardanoPoolOwnerType +from .CardanoPoolRelayParametersType import CardanoPoolRelayParametersType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolParametersType(p.MessageType): + + def __init__( + self, + *, + pool_id: bytes, + vrf_key_hash: bytes, + pledge: int, + cost: int, + margin_numerator: int, + margin_denominator: int, + reward_account: str, + owners: List[CardanoPoolOwnerType] = None, + relays: List[CardanoPoolRelayParametersType] = None, + metadata: CardanoPoolMetadataType = None, + ) -> None: + self.owners = owners if owners is not None else [] + self.relays = relays if relays is not None else [] + self.pool_id = pool_id + self.vrf_key_hash = vrf_key_hash + self.pledge = pledge + self.cost = cost + self.margin_numerator = margin_numerator + self.margin_denominator = margin_denominator + self.reward_account = reward_account + self.metadata = metadata + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('pool_id', p.BytesType, p.FLAG_REQUIRED), + 2: ('vrf_key_hash', p.BytesType, p.FLAG_REQUIRED), + 3: ('pledge', p.UVarintType, p.FLAG_REQUIRED), + 4: ('cost', p.UVarintType, p.FLAG_REQUIRED), + 5: ('margin_numerator', p.UVarintType, p.FLAG_REQUIRED), + 6: ('margin_denominator', p.UVarintType, p.FLAG_REQUIRED), + 7: ('reward_account', p.UnicodeType, p.FLAG_REQUIRED), + 8: ('owners', CardanoPoolOwnerType, p.FLAG_REPEATED), + 9: ('relays', CardanoPoolRelayParametersType, p.FLAG_REPEATED), + 10: ('metadata', CardanoPoolMetadataType, None), + } diff --git a/core/src/trezor/messages/CardanoPoolRelayParametersType.py b/core/src/trezor/messages/CardanoPoolRelayParametersType.py new file mode 100644 index 000000000..67eea36a2 --- /dev/null +++ b/core/src/trezor/messages/CardanoPoolRelayParametersType.py @@ -0,0 +1,39 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeCardanoPoolRelayType = Literal[0, 1, 2] + except ImportError: + pass + + +class CardanoPoolRelayParametersType(p.MessageType): + + def __init__( + self, + *, + type: EnumTypeCardanoPoolRelayType, + ipv4_address: bytes = None, + ipv6_address: bytes = None, + host_name: str = None, + port: int = None, + ) -> None: + self.type = type + self.ipv4_address = ipv4_address + self.ipv6_address = ipv6_address + self.host_name = host_name + self.port = port + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('type', p.EnumType("CardanoPoolRelayType", (0, 1, 2)), p.FLAG_REQUIRED), + 2: ('ipv4_address', p.BytesType, None), + 3: ('ipv6_address', p.BytesType, None), + 4: ('host_name', p.UnicodeType, None), + 5: ('port', p.UVarintType, None), + } diff --git a/core/src/trezor/messages/CardanoPoolRelayType.py b/core/src/trezor/messages/CardanoPoolRelayType.py new file mode 100644 index 000000000..3623702f4 --- /dev/null +++ b/core/src/trezor/messages/CardanoPoolRelayType.py @@ -0,0 +1,8 @@ +# Automatically generated by pb2py +# fmt: off +if False: + from typing_extensions import Literal + +SINGLE_HOST_IP = 0 # type: Literal[0] +SINGLE_HOST_NAME = 1 # type: Literal[1] +MULTIPLE_HOST_NAME = 2 # type: Literal[2] diff --git a/core/src/trezor/messages/CardanoTxCertificateType.py b/core/src/trezor/messages/CardanoTxCertificateType.py index b817f0e52..3af871263 100644 --- a/core/src/trezor/messages/CardanoTxCertificateType.py +++ b/core/src/trezor/messages/CardanoTxCertificateType.py @@ -2,11 +2,13 @@ # fmt: off import protobuf as p +from .CardanoPoolParametersType import CardanoPoolParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeCardanoCertificateType = Literal[0, 1, 2] + EnumTypeCardanoCertificateType = Literal[0, 1, 2, 3] except ImportError: pass @@ -19,15 +21,18 @@ class CardanoTxCertificateType(p.MessageType): path: List[int] = None, type: EnumTypeCardanoCertificateType = None, pool: bytes = None, + pool_parameters: CardanoPoolParametersType = None, ) -> None: self.path = path if path is not None else [] self.type = type self.pool = pool + self.pool_parameters = pool_parameters @classmethod def get_fields(cls) -> Dict: return { - 1: ('type', p.EnumType("CardanoCertificateType", (0, 1, 2)), None), + 1: ('type', p.EnumType("CardanoCertificateType", (0, 1, 2, 3)), None), 2: ('path', p.UVarintType, p.FLAG_REPEATED), 3: ('pool', p.BytesType, None), + 4: ('pool_parameters', CardanoPoolParametersType, None), } diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index e24f4310e..ecfc1ddce 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +from ipaddress import ip_address from typing import List from . import messages, tools @@ -23,8 +24,17 @@ PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 42} NETWORK_IDS = {"mainnet": 1, "testnet": 0} REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs") -REQUIRED_FIELDS_INPUT = ("path", "prev_hash", "prev_index") -REQUIRED_FIELDS_CERTIFICATE = ("path", "type") +REQUIRED_FIELDS_INPUT = ("prev_hash", "prev_index") +REQUIRED_FIELDS_CERTIFICATE = ("type",) +REQUIRED_FIELDS_POOL_PARAMETERS = ( + "pool_id", + "vrf_key_hash", + "pledge", + "cost", + "margin", + "reward_account", + "owners", +) REQUIRED_FIELDS_WITHDRAWAL = ("path", "amount") INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" @@ -77,16 +87,14 @@ def create_certificate_pointer( ) -def create_input(input) -> messages.CardanoTxInputType: - if not all(k in input for k in REQUIRED_FIELDS_INPUT): +def create_input(tx_input) -> messages.CardanoTxInputType: + if not all(k in tx_input for k in REQUIRED_FIELDS_INPUT): raise ValueError("The input is missing some fields") - path = input["path"] - return messages.CardanoTxInputType( - address_n=tools.parse_path(path), - prev_hash=bytes.fromhex(input["prev_hash"]), - prev_index=input["prev_index"], + address_n=tools.parse_path(tx_input.get("path")), + prev_hash=bytes.fromhex(tx_input["prev_hash"]), + prev_index=tx_input["prev_index"], ) @@ -131,34 +139,125 @@ def _create_change_output(output) -> messages.CardanoTxOutputType: def create_certificate(certificate) -> messages.CardanoTxCertificateType: + CERTIFICATE_MISSING_FIELDS_ERROR = ValueError( + "The certificate is missing some fields" + ) + if not all(k in certificate for k in REQUIRED_FIELDS_CERTIFICATE): - raise ValueError("The certificate is missing some fields") + raise CERTIFICATE_MISSING_FIELDS_ERROR - path = certificate["path"] certificate_type = certificate["type"] if certificate_type == messages.CardanoCertificateType.STAKE_DELEGATION: if "pool" not in certificate: - raise ValueError("The certificate is missing some fields") + raise CERTIFICATE_MISSING_FIELDS_ERROR - pool = certificate["pool"] return messages.CardanoTxCertificateType( type=certificate_type, - path=tools.parse_path(path), - pool=bytes.fromhex(pool), + path=tools.parse_path(certificate["path"]), + pool=bytes.fromhex(certificate["pool"]), ) - elif ( - certificate_type == messages.CardanoCertificateType.STAKE_REGISTRATION - or certificate_type == messages.CardanoCertificateType.STAKE_DEREGISTRATION + elif certificate_type in ( + messages.CardanoCertificateType.STAKE_REGISTRATION, + messages.CardanoCertificateType.STAKE_DEREGISTRATION, ): + if "path" not in certificate: + raise CERTIFICATE_MISSING_FIELDS_ERROR + return messages.CardanoTxCertificateType( + type=certificate_type, + path=tools.parse_path(certificate["path"]), + ) + elif certificate_type == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION: + pool_parameters = certificate["pool_parameters"] + + if any( + required_param not in pool_parameters + for required_param in REQUIRED_FIELDS_POOL_PARAMETERS + ): + raise CERTIFICATE_MISSING_FIELDS_ERROR + + if pool_parameters.get("metadata") is not None: + pool_metadata = messages.CardanoPoolMetadataType( + url=pool_parameters["metadata"]["url"], + hash=bytes.fromhex(pool_parameters["metadata"]["hash"]), + ) + else: + pool_metadata = None + return messages.CardanoTxCertificateType( type=certificate_type, - path=tools.parse_path(path), + pool_parameters=messages.CardanoPoolParametersType( + pool_id=bytes.fromhex(pool_parameters["pool_id"]), + vrf_key_hash=bytes.fromhex(pool_parameters["vrf_key_hash"]), + pledge=int(pool_parameters["pledge"]), + cost=int(pool_parameters["cost"]), + margin_numerator=int(pool_parameters["margin"]["numerator"]), + margin_denominator=int(pool_parameters["margin"]["denominator"]), + reward_account=pool_parameters["reward_account"], + metadata=pool_metadata, + owners=[ + _create_pool_owner(pool_owner) + for pool_owner in pool_parameters.get("owners", []) + ], + relays=[ + _create_pool_relay(pool_relay) + for pool_relay in pool_parameters.get("relays", []) + ] + if "relays" in pool_parameters + else [], + ), ) else: raise ValueError("Unknown certificate type") +def _create_pool_owner(pool_owner) -> messages.CardanoPoolOwnerType: + if "staking_key_path" in pool_owner: + return messages.CardanoPoolOwnerType( + staking_key_path=tools.parse_path(pool_owner["staking_key_path"]) + ) + + return messages.CardanoPoolOwnerType( + staking_key_hash=bytes.fromhex(pool_owner["staking_key_hash"]) + ) + + +def _create_pool_relay(pool_relay) -> messages.CardanoPoolRelayParametersType: + pool_relay_type = int(pool_relay["type"]) + + if pool_relay_type == messages.CardanoPoolRelayType.SINGLE_HOST_IP: + ipv4_address_packed = ( + ip_address(pool_relay["ipv4_address"]).packed + if "ipv4_address" in pool_relay + else None + ) + ipv6_address_packed = ( + ip_address(pool_relay["ipv6_address"]).packed + if "ipv6_address" in pool_relay + else None + ) + + return messages.CardanoPoolRelayParametersType( + type=pool_relay_type, + port=int(pool_relay["port"]), + ipv4_address=ipv4_address_packed, + ipv6_address=ipv6_address_packed, + ) + elif pool_relay_type == messages.CardanoPoolRelayType.SINGLE_HOST_NAME: + return messages.CardanoPoolRelayParametersType( + type=pool_relay_type, + port=int(pool_relay["port"]), + host_name=pool_relay["host_name"], + ) + elif pool_relay_type == messages.CardanoPoolRelayType.MULTIPLE_HOST_NAME: + return messages.CardanoPoolRelayParametersType( + type=pool_relay_type, + host_name=pool_relay["host_name"], + ) + + raise ValueError("Unknown pool relay type") + + def create_withdrawal(withdrawal) -> messages.CardanoTxWithdrawalType: if not all(k in withdrawal for k in REQUIRED_FIELDS_WITHDRAWAL): raise ValueError("Withdrawal is missing some fields") diff --git a/python/src/trezorlib/messages/CardanoCertificateType.py b/python/src/trezorlib/messages/CardanoCertificateType.py index ef56cb070..eb6bc15f4 100644 --- a/python/src/trezorlib/messages/CardanoCertificateType.py +++ b/python/src/trezorlib/messages/CardanoCertificateType.py @@ -6,3 +6,4 @@ if False: STAKE_REGISTRATION = 0 # type: Literal[0] STAKE_DEREGISTRATION = 1 # type: Literal[1] STAKE_DELEGATION = 2 # type: Literal[2] +STAKE_POOL_REGISTRATION = 3 # type: Literal[3] diff --git a/python/src/trezorlib/messages/CardanoPoolMetadataType.py b/python/src/trezorlib/messages/CardanoPoolMetadataType.py new file mode 100644 index 000000000..ac265c4b0 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoPoolMetadataType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolMetadataType(p.MessageType): + + def __init__( + self, + *, + url: str, + hash: bytes, + ) -> None: + self.url = url + self.hash = hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('url', p.UnicodeType, p.FLAG_REQUIRED), + 2: ('hash', p.BytesType, p.FLAG_REQUIRED), + } diff --git a/python/src/trezorlib/messages/CardanoPoolOwnerType.py b/python/src/trezorlib/messages/CardanoPoolOwnerType.py new file mode 100644 index 000000000..9579347a1 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoPoolOwnerType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolOwnerType(p.MessageType): + + def __init__( + self, + *, + staking_key_path: List[int] = None, + staking_key_hash: bytes = None, + ) -> None: + self.staking_key_path = staking_key_path if staking_key_path is not None else [] + self.staking_key_hash = staking_key_hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('staking_key_path', p.UVarintType, p.FLAG_REPEATED), + 2: ('staking_key_hash', p.BytesType, None), + } diff --git a/python/src/trezorlib/messages/CardanoPoolParametersType.py b/python/src/trezorlib/messages/CardanoPoolParametersType.py new file mode 100644 index 000000000..d44ab87da --- /dev/null +++ b/python/src/trezorlib/messages/CardanoPoolParametersType.py @@ -0,0 +1,57 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .CardanoPoolMetadataType import CardanoPoolMetadataType +from .CardanoPoolOwnerType import CardanoPoolOwnerType +from .CardanoPoolRelayParametersType import CardanoPoolRelayParametersType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoPoolParametersType(p.MessageType): + + def __init__( + self, + *, + pool_id: bytes, + vrf_key_hash: bytes, + pledge: int, + cost: int, + margin_numerator: int, + margin_denominator: int, + reward_account: str, + owners: List[CardanoPoolOwnerType] = None, + relays: List[CardanoPoolRelayParametersType] = None, + metadata: CardanoPoolMetadataType = None, + ) -> None: + self.owners = owners if owners is not None else [] + self.relays = relays if relays is not None else [] + self.pool_id = pool_id + self.vrf_key_hash = vrf_key_hash + self.pledge = pledge + self.cost = cost + self.margin_numerator = margin_numerator + self.margin_denominator = margin_denominator + self.reward_account = reward_account + self.metadata = metadata + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('pool_id', p.BytesType, p.FLAG_REQUIRED), + 2: ('vrf_key_hash', p.BytesType, p.FLAG_REQUIRED), + 3: ('pledge', p.UVarintType, p.FLAG_REQUIRED), + 4: ('cost', p.UVarintType, p.FLAG_REQUIRED), + 5: ('margin_numerator', p.UVarintType, p.FLAG_REQUIRED), + 6: ('margin_denominator', p.UVarintType, p.FLAG_REQUIRED), + 7: ('reward_account', p.UnicodeType, p.FLAG_REQUIRED), + 8: ('owners', CardanoPoolOwnerType, p.FLAG_REPEATED), + 9: ('relays', CardanoPoolRelayParametersType, p.FLAG_REPEATED), + 10: ('metadata', CardanoPoolMetadataType, None), + } diff --git a/python/src/trezorlib/messages/CardanoPoolRelayParametersType.py b/python/src/trezorlib/messages/CardanoPoolRelayParametersType.py new file mode 100644 index 000000000..31c58d914 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoPoolRelayParametersType.py @@ -0,0 +1,39 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeCardanoPoolRelayType = Literal[0, 1, 2] + except ImportError: + pass + + +class CardanoPoolRelayParametersType(p.MessageType): + + def __init__( + self, + *, + type: EnumTypeCardanoPoolRelayType, + ipv4_address: bytes = None, + ipv6_address: bytes = None, + host_name: str = None, + port: int = None, + ) -> None: + self.type = type + self.ipv4_address = ipv4_address + self.ipv6_address = ipv6_address + self.host_name = host_name + self.port = port + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('type', p.EnumType("CardanoPoolRelayType", (0, 1, 2)), p.FLAG_REQUIRED), + 2: ('ipv4_address', p.BytesType, None), + 3: ('ipv6_address', p.BytesType, None), + 4: ('host_name', p.UnicodeType, None), + 5: ('port', p.UVarintType, None), + } diff --git a/python/src/trezorlib/messages/CardanoPoolRelayType.py b/python/src/trezorlib/messages/CardanoPoolRelayType.py new file mode 100644 index 000000000..3623702f4 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoPoolRelayType.py @@ -0,0 +1,8 @@ +# Automatically generated by pb2py +# fmt: off +if False: + from typing_extensions import Literal + +SINGLE_HOST_IP = 0 # type: Literal[0] +SINGLE_HOST_NAME = 1 # type: Literal[1] +MULTIPLE_HOST_NAME = 2 # type: Literal[2] diff --git a/python/src/trezorlib/messages/CardanoTxCertificateType.py b/python/src/trezorlib/messages/CardanoTxCertificateType.py index 532735621..bc4ef8808 100644 --- a/python/src/trezorlib/messages/CardanoTxCertificateType.py +++ b/python/src/trezorlib/messages/CardanoTxCertificateType.py @@ -2,11 +2,13 @@ # fmt: off from .. import protobuf as p +from .CardanoPoolParametersType import CardanoPoolParametersType + if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeCardanoCertificateType = Literal[0, 1, 2] + EnumTypeCardanoCertificateType = Literal[0, 1, 2, 3] except ImportError: pass @@ -19,15 +21,18 @@ class CardanoTxCertificateType(p.MessageType): path: List[int] = None, type: EnumTypeCardanoCertificateType = None, pool: bytes = None, + pool_parameters: CardanoPoolParametersType = None, ) -> None: self.path = path if path is not None else [] self.type = type self.pool = pool + self.pool_parameters = pool_parameters @classmethod def get_fields(cls) -> Dict: return { - 1: ('type', p.EnumType("CardanoCertificateType", (0, 1, 2)), None), + 1: ('type', p.EnumType("CardanoCertificateType", (0, 1, 2, 3)), None), 2: ('path', p.UVarintType, p.FLAG_REPEATED), 3: ('pool', p.BytesType, None), + 4: ('pool_parameters', CardanoPoolParametersType, None), } diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 3ff184542..14fe398b6 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -27,6 +27,10 @@ from .CardanoAddressParametersType import CardanoAddressParametersType from .CardanoBlockchainPointerType import CardanoBlockchainPointerType from .CardanoGetAddress import CardanoGetAddress from .CardanoGetPublicKey import CardanoGetPublicKey +from .CardanoPoolMetadataType import CardanoPoolMetadataType +from .CardanoPoolOwnerType import CardanoPoolOwnerType +from .CardanoPoolParametersType import CardanoPoolParametersType +from .CardanoPoolRelayParametersType import CardanoPoolRelayParametersType from .CardanoPublicKey import CardanoPublicKey from .CardanoSignTx import CardanoSignTx from .CardanoSignedTx import CardanoSignedTx @@ -303,6 +307,7 @@ from . import ButtonRequestType from . import Capability from . import CardanoAddressType from . import CardanoCertificateType +from . import CardanoPoolRelayType from . import DebugLinkShowTextStyle from . import DebugSwipeDirection from . import FailureType diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index dfa15d6bd..572ddbecd 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -29,7 +29,9 @@ pytestmark = [ @parametrize_using_common_fixtures( - "cardano/sign_tx.json", "cardano/sign_tx.slip39.json" + "cardano/sign_tx_stake_pool_registration.json", + "cardano/sign_tx.json", + "cardano/sign_tx.slip39.json", ) def test_cardano_sign_tx(client, parameters, result): inputs = [cardano.create_input(i) for i in parameters["inputs"]] @@ -74,7 +76,9 @@ def test_cardano_sign_tx(client, parameters, result): assert response.serialized_tx.hex() == result["serialized_tx"] -@parametrize_using_common_fixtures("cardano/sign_tx.failed.json") +@parametrize_using_common_fixtures( + "cardano/sign_tx.failed.json", "cardano/sign_tx_stake_pool_registration.failed.json" +) def test_cardano_sign_tx_failed(client, parameters, result): inputs = [cardano.create_input(i) for i in parameters["inputs"]] outputs = [cardano.create_output(o) for o in parameters["outputs"]] diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index c1d081519..1e3f302b2 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1,21 +1,24 @@ { -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters0-result0]": "6aa71de5007b0faf1eea4b1cfda1da6a739f852c0d875a1e59d83c03178c2f98", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters1-result1]": "7abf2e87a9b1e50afdf3502ba9480b07a59d59ccccf24915b46fb81285ae3fa8", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters10-result10]": "9e09260bd9eb848694f6265008d9faee059d2b3b28d64688807f8bf725acd052", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters11-result11]": "c32706d1092edf9ac2504c88eddfe3e55b8a5b6c0e2d6fcd7fbf84232aabfcb8", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters12-result12]": "39c495f6c8d1a046044b8d49569f51f615b163abeae98c0be8313761de828862", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters13-result13]": "f03f064e8829a27a49296c28755493983d86a235ddeac1c926c7195dd254940f", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters14-result14]": "623341dfed3aaca40284ec5b444fc768edc5af9c706d8c4e4f7a5e1e90343652", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters15-result15]": "0f79d964628581aae91593f7a1e7bf9b4748b900d7973e1b48a78382cc8f6c4e", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters16-result16]": "4597efa8c2d34df7ab70c626a244d14b783fa7be1f88f213c2f9a39d726976e2", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters2-result2]": "539936eee440830f64536228980a78b098a8e421e8b6c819fe49bc8efcac9b18", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters3-result3]": "6aa71de5007b0faf1eea4b1cfda1da6a739f852c0d875a1e59d83c03178c2f98", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters4-result4]": "68505427a1021dd3ef7dea03956d1ea3167c8fa3016e90328151618c45d7695e", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters5-result5]": "223fc826cc19d6dd9d768a7564d19d054aa6c596557a40b9e5448c546444d36b", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters6-result6]": "0830d903c59c2d98782c0a87ba5400b656139b26ae3243ae24a1d8874b37bf7e", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters7-result7]": "18adb579bcfe99ca7f62c4c01f32003d19c32313bcd4cf20d93e9f15e7a67ec8", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters8-result8]": "a5ca1d59dedfe9052a9ccaf7fd9aaa98c80a646936dfeb74501848477a9dcb77", -"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters9-result9]": "d99b762b76e5edae82ec0b3f34db1f6122f83350188c8bb44a7214b4d88d2014", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters0-result0]": "f5592f2f6201cc8ac9c16d3c6b171ce824026a811a88828618cb296a34736c3e", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters1-result1]": "7aee8a0c6563e0efd8bd394c486fd639501f01be0f8f8119051e5b56059a6fab", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters10-result10]": "ca6ccfa1be6a0a6238f9df89c1988260096188bf3679582f71f645bb4748a7dd", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters11-result11]": "8432eda8c3fe635430d7c2624e7b2ad9afdc1bc02fc03116c199557d57cef499", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters12-result12]": "8adfc9bad89bb9b3e7c3e06e2a1b0b4ea4f132ff08d1408a379ab571b75b83b2", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters13-result13]": "3b6a8b864c5e0048df8d6a6d65fca5b57ea4d0aa164102e4f55fa5cb27a4f882", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters14-result14]": "cd7194bab223396175cb6b9f470b10dd04289db445d096974d51d25cb876372a", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters15-result15]": "47e0ed559b895a6e2149c4cb4842fd90382f0f3c85f1bece8e953a194acc7b0b", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters16-result16]": "db6cf62fc7b0288a849e3c5e1429bce68278f2ffddb3c06fae596d04fcfcced9", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters17-result17]": "38cbd5c3b44aa837ce09e70f650057253efc06ba89fa0769c3fde9068bd03c67", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters18-result18]": "e8764a1052f67e7c9e59baf6d684a68e5abd7c71ec94efdf7020c19215029e5d", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters19-result19]": "640f577d6434802d9975413fe49690cf5b1f023a73e2f6289bf5431b89c5c786", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters2-result2]": "88b0104aacf640351044b134f1bc91b62aef9251c936b4cdc112cbd2f623ad74", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters3-result3]": "a83d3cc90d8ef2660471d906524b9d2d044ab78c656f21f2b8d9141e6ee49429", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters4-result4]": "a413ddafc1e5dbef2e21541937529cb0ff1ff2622574e69cc10e3f09e19984cf", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters5-result5]": "195d1303aa0a4bf05b6d5c1131e9250b2249687d48f6a998bf4d03d601ae361a", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters6-result6]": "e68a1bbe79d5c986c2efb1dd629c394cac62421ad44a37407278e139778edf95", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters7-result7]": "0befa45bf43d6c3267bd81f6fd44af0c6e7abe22e4a8e3f5866b279dd1ff338a", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters8-result8]": "c91d4621064bacea6a8acf24ee938c0ed3c91901da17da81d06fe37781ce8ca9", +"cardano-test_sign_tx.py::test_cardano_sign_tx[parameters9-result9]": "6e6fa6af3768075b34ea40b0936991e2a59a7f996e950df4f77a6c9f0c4e23ba", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters0-result0]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters1-result1]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters10-result10]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", @@ -31,29 +34,38 @@ "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters2-result2]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters20-result20]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters21-result21]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters22-result22]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters23-result23]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters24-result24]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters25-result25]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters26-result26]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters27-result27]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters28-result28]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters29-result29]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters3-result3]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters30-result30]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters4-result4]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters5-result5]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters6-result6]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters7-result7]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters8-result8]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters9-result9]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_autolock.py::test_apply_auto_lock_delay": "38c720e0d29b7487060f2a0f8d256a5e5b4f735511e049c43f6ea62e560603ae", -"test_autolock.py::test_apply_auto_lock_delay_valid[10]": "a751228f82166c107a8e8872919e2b010ef3079763adc473066e7a3ada36f864", -"test_autolock.py::test_apply_auto_lock_delay_valid[123]": "caf130bf5fa66fa5ac17432689046c1b6cd8b6a495bac3abef3c414d89b81e3f", -"test_autolock.py::test_apply_auto_lock_delay_valid[3601]": "b2a9a7f3e50afb04fb174627a07b939284aa0acc8b3b53af56f75a35ff1b32c9", -"test_autolock.py::test_apply_auto_lock_delay_valid[536870]": "ca2b4707227cc15089f4d6ba710706d2d5c270f19a4668c09f04f175143b202e", -"test_autolock.py::test_apply_auto_lock_delay_valid[60]": "af8d06c92fc5f9aad5685bf56e59b26ec44418a6174ff73db69803f23785802a", -"test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "437cc6b0780d482a835c23f0935683c40177ae4b0ff31da4fc99eba603545ffe", -"test_autolock.py::test_autolock_cancels_ui": "bb4776bfea145528544554b11bdf13ae99f63a371e8eb0885b0a9bd5b608e027", -"test_autolock.py::test_autolock_default_value": "b9f4cd94638f5f8f4c02026b0ccaee89b42406ab63ce7fcef5c9164467de939b", -"test_basic.py-test_device_id_different": "bc6acd0386b9d009e6550519917d6e08632b3badde0b0cf04c95abe5f773038a", +"test_autolock.py::test_apply_auto_lock_delay": "374c0a05defdff548f7456a328241885dae1798dbfcefe6335fe72a31dddb95c", +"test_autolock.py::test_apply_auto_lock_delay_valid[10]": "814accb30dc6baa977f567418943d69b1b74193e02da9cb4a0ae3199bc38bc9e", +"test_autolock.py::test_apply_auto_lock_delay_valid[123]": "4371667cbade4d9b6689b040044e97ea4704e523175fa666c5afa2766817a752", +"test_autolock.py::test_apply_auto_lock_delay_valid[3601]": "f68a8a86e304b4da46bf37ca6ea621199ad12699e34ca5bb5c686ed438f38f3c", +"test_autolock.py::test_apply_auto_lock_delay_valid[536870]": "94b446b079c49ce1de35a235eb392e2674f84455e704ae70ce34d911cb3283b4", +"test_autolock.py::test_apply_auto_lock_delay_valid[60]": "7277ef42d405c8574f6b46ea676ed54dda27d1472d4c919fe8288f3c09736c56", +"test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "0928987c48f16f0f79a5c6c992aa9282f91c3f8d7b61177ae90318fba8d41dd3", +"test_autolock.py::test_autolock_cancels_ui": "26874b271f9e9fe04ee9154b32fd640a969dc93426b7c77eebdf823750bf436f", +"test_autolock.py::test_autolock_default_value": "bb75c33cf21eace3f1a94e07a628d34b85691451ca5c3b62b1337afdc484d147", +"test_basic.py-test_device_id_different": "ea1ad2312172311b7b1a2e9b23dd12f489c071656b37e82720915f0381f157a4", "test_basic.py-test_device_id_same": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_basic.py-test_features": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_basic.py-test_ping": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_applysettings.py-test_apply_homescreen_toif": "408bdb69368ebdf1d299c6d43c1571f86cb1a0f1f606c5badd2f05ce7731f121", -"test_msg_applysettings.py-test_apply_settings": "8f9f6013bb8a44fda279e9c7d091328fd7ccb39222a02bee701918528355083a", -"test_msg_applysettings.py-test_apply_settings_passphrase": "40de0143b32b5d06ece43d47be27bb91499f0c2417754ddb8e9e03ff41a7f6d4", +"test_msg_applysettings.py-test_apply_homescreen_toif": "d8779189bbf826dfd88ccb85bb55a46cdcb954e4a371efda5272daf12db4969e", +"test_msg_applysettings.py-test_apply_settings": "960589746d0acb5b38295af98a30dace8febe58ae35d062b0a4ea91b73fbfa7e", +"test_msg_applysettings.py-test_apply_settings_passphrase": "b549a407b461cb9ae3b59b0ffe7407e1749df99d8c17d6297a92cdf178a7274d", "test_msg_applysettings.py-test_apply_settings_passphrase_on_device": "3e6527e227bdde54f51bc9c417b176d0d87fdb6c40c4761368f50eb201b4beed", "test_msg_applysettings.py-test_apply_settings_rotation": "6f0fa323dd2c82d01994273c023d3ed5e43d43c9c562664a10266f4a7f7ba4cc", "test_msg_applysettings.py-test_experimental_features": "3127d41bd8615097295b917110ece9dd364986809288c7f958ff71d52106e346", @@ -190,64 +202,64 @@ "test_msg_lisk_signtx.py-test_lisk_sign_tx_send_with_data": "ea969f90b6e4b840bb8728a9a99e5d07f59f478dba6b144218148d9db83f7a49", "test_msg_lisk_verifymessage.py-test_verify": "45df85077b20182803b5c4363386c555845e070f3a8a019add99e34dad510a07", "test_msg_lisk_verifymessage.py-test_verify_long": "d7d0ae3402b9ca6c7b0e61164fa483c4ba9549d306780c98ae15edd2dde51285", -"test_msg_loaddevice.py-test_load_device_1": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605", -"test_msg_loaddevice.py-test_load_device_2": "dc13c8486d8a59c5062e19139d8b3cea4ece1a3bc93592be7dc226f83ba54477", +"test_msg_loaddevice.py-test_load_device_1": "83a92a294ddd6410a897726c67979081602d47e98206bc5f043d45532ad8d899", +"test_msg_loaddevice.py-test_load_device_2": "1f3d44ad9cc1372b430afbf50652c8388c85b5c7ed2f829aa785a00f18ee6100", "test_msg_loaddevice.py-test_load_device_slip39_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605", "test_msg_loaddevice.py-test_load_device_slip39_basic": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605", -"test_msg_loaddevice.py-test_load_device_utf": "ad7c162c76a13a161166aba78c461ad5525a9a5da846e8d99854248d521e6979", -"test_msg_monero_getaddress.py-test_monero_getaddress": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7", -"test_msg_nem_getaddress.py-test_nem_getaddress": "e726f99401a20eb74c33d755cecea2a3f69b7ae5b541302677ee05f80f5aef19", -"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation": "b5f6dd88b31d18d648b5741bb521c9fd1961732e2ed520256657c2e8a9ec3539", -"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation_levy": "8145638044e332510cd356d44910ccc493c8ced38b2086dd0438f56f860742ea", -"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation_properties": "7dd1dd750dbf7b15ad20aa0a2ab99e69d1fc41cc8c4092b1030a3193e9a2186d", -"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_supply_change": "aa1a4b35ee4409b8cfe2ca908eb18251c2152c634e0239d162f52e40b31db4a8", -"test_msg_nem_signtx_multisig.py-test_nem_signtx_aggregate_modification": "b89a43ac3e5095ba09eed4d5cd1687e8aa9c788a8a04625c7957bf407ec2b8a7", -"test_msg_nem_signtx_multisig.py-test_nem_signtx_multisig": "b079079747af3e849b186296300402b9061bb5c935864e5e40c4fbc19d225f79", -"test_msg_nem_signtx_multisig.py-test_nem_signtx_multisig_signer": "2ea597bb8abec191fcaa08b6dc323669c514278be7428c90e1b51331d8f120d7", -"test_msg_nem_signtx_others.py-test_nem_signtx_importance_transfer": "7bc67eccfcfbbf24b21a422855fe14457b7b46d105bbbeafd022b9a08cd2cc51", -"test_msg_nem_signtx_others.py-test_nem_signtx_provision_namespace": "6b9ddabb24d5bd9c33769aa9c6acb7d340f802714251684faab6158369c1fb00", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_encrypted_payload": "8be92fe2b419640a3606b290d1ac7db789314b16a8ca337a65ac21bdf51a8b1e", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_known_mosaic": "495f2eab53517bdc7a6584f42c611c42502492a4d6e80777a349a93c2365a5db", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_known_mosaic_with_levy": "b2ff2a0df957e576bed19e333de05dca8e9359793c5a3e66b56d6e847b0e33fc", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_multiple_mosaics": "28f27d4e80b05c13c3991cbca3d71f2fd060caa5a9bf4e8475b3207e66ebfc40", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_simple": "d36a67610b16f835b174a053fe60104a03ea5d49fbd612d73f5d8cdb31fce421", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_unknown_mosaic": "1fd9bf33c3c481d8b76fbdddfc3e8d91df6a1a97661a8f8c4b57cd3df41e83f0", -"test_msg_nem_signtx_transfers.py-test_nem_signtx_xem_as_mosaic": "842307e1734fea44aca9e53e2d76e0c6206348c4461f9eb1a36021bed1f681c8", +"test_msg_loaddevice.py-test_load_device_utf": "433ecad2d6f347f7426c2b17687d62d2eead2fc8feb382c4f3e6ea37dbefd731", +"test_msg_monero_getaddress.py-test_monero_getaddress": "020e5d945cec751dbf68e255be94e33902c5984c15ccb09628c70c6ebeaaeb35", +"test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "96f96e6d7df1d1fe2473205f5a1ad8b667304243e8633373aa6b01a9bc38eb6d", +"test_msg_nem_getaddress.py-test_nem_getaddress": "4582fe8957b8e5206e48d467d9eb84a39e67ffba5e8a49064003f1c7294137be", +"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation": "5d72d75fc30bd11ef6d610b449f3fad21b9f9698e8e696c7808f9d3750b89a66", +"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation_levy": "5fe2aeef1d25267cef47f35ec597b2f7143f268787de25a0bd6ea1a8380c8ab3", +"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_creation_properties": "5ff19fd208b8ab5b1cea09fa88868fe4319a98ab8e918eab1efa8bc7968cbb55", +"test_msg_nem_signtx_mosaics_t2.py-test_nem_signtx_mosaic_supply_change": "84dfba9831a5bb4ef2fb4fa4ec6433151fb78f8a18b6aa9e3f23027c27e80d7c", +"test_msg_nem_signtx_multisig.py-test_nem_signtx_aggregate_modification": "1b50b5c1b3d9376b89b5aaf30c16fed1eb6b5a7a88659e87a0e0629ca2623e52", +"test_msg_nem_signtx_multisig.py-test_nem_signtx_multisig": "100df58edd0ef8dd72288594a54139aba1af3f55b5e19ee75823407a23c2613d", +"test_msg_nem_signtx_multisig.py-test_nem_signtx_multisig_signer": "c1fe251b11daddecd5075982a35175ba11bb601171e3edef443686fc3c43bcab", +"test_msg_nem_signtx_others.py-test_nem_signtx_importance_transfer": "a4781a279d7bf574fd86a4cfe7b03d52af4de4e7d01bd2b326a1a605bffaceb6", +"test_msg_nem_signtx_others.py-test_nem_signtx_provision_namespace": "71d3e2e097424b11e95064fb93ae06f34113308d0ebd40fd8e2826bc3231b360", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_encrypted_payload": "54d6bf4584d23086dd4311893f42c97229d2e47af0a7f3e9f0aaa30e40700b39", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_known_mosaic": "0aab9460cabbc6d6cf49fbe7f6c6306809c7f11f003da75d65c3d66dd84cb7eb", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_known_mosaic_with_levy": "7b08657db57c245c617fc355a19a15ce05600b0fdde39a67aaee358f92f4bb14", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_multiple_mosaics": "9d233a8d198500dc57fa333396143856ab241a4827dc53aa8130586190fe857c", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_simple": "e31d77e6c850b10a587eb93b2d3ab747cd979c03b591b91a448331f2816f255c", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_unknown_mosaic": "10555d0222e2fb2bdeaa8eec2b6ae425bdcb658ef38fd261c24afc94f58dab97", +"test_msg_nem_signtx_transfers.py-test_nem_signtx_xem_as_mosaic": "5930695897c2300c6e4750248b9961c154cfdeb7e23c7bf76f3cc81e5a26f33e", "test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[label-test]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[language-test]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[passphrase_protection-True]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[pin_protection-True]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[u2f_counter-1]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_recoverydevice_bip39_dryrun.py::test_dry_run": "1462a2534e0bdee573e96396316500046db0a188de7740065070d0dfb1bb0fa5", -"test_msg_recoverydevice_bip39_dryrun.py::test_invalid_seed_core": "a3f0dd0d5d24e6500df0eacd3d5eb3d1670c54a01a036e5bbd546a9aac733e85", -"test_msg_recoverydevice_bip39_dryrun.py::test_seed_mismatch": "85c61f5304a32e8b84a37ef80d035cfdcbf89a8631bde53409b1ec7f1013740c", +"test_msg_recoverydevice_bip39_dryrun.py::test_dry_run": "c2a3a87c27919c1051b65b015d7e8da26c8c3f9460d4385680df6f00b793c802", +"test_msg_recoverydevice_bip39_dryrun.py::test_invalid_seed_core": "907eb2bb211d240e3663ccb686bfcc43326a45919b6eb5922ea1a767a8484aa5", +"test_msg_recoverydevice_bip39_dryrun.py::test_seed_mismatch": "fd86b449afe992bde4ca2df227779317f419fa94d92d0b32b95a906172948147", "test_msg_recoverydevice_bip39_dryrun.py::test_uninitialized": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c", "test_msg_recoverydevice_bip39_t2.py::test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_recoverydevice_bip39_t2.py::test_tt_nopin_nopassphrase": "86e52bb95d0f53193cc83e828f6e6baea59ebcaa26e06784bbb4f6873ee442ac", -"test_msg_recoverydevice_bip39_t2.py::test_tt_pin_passphrase": "7a7b9d20cc5b2d6fcdf0e35d90cfcd46bfe536067becdea5568fd7f3d102306f", -"test_msg_recoverydevice_slip39_advanced.py::test_abort": "a54d4f29cf1fc3ce26831f52d0ae98a30a2f3e108f822cce08a9bfdd3319356e", -"test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "c972403fc15f00527f12b3226bdb918a5c29315ba88e496982f09a4fdac43218", -"test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "137427360db303e288035972866df29ab0b272d30c8b11108bc68252f1aef748", -"test_msg_recoverydevice_slip39_advanced.py::test_noabort": "78a8cc92a79f90b45c3e14f01c1c57ba0fbef63438c9abe9cd1feb35b0e03c0a", -"test_msg_recoverydevice_slip39_advanced.py::test_same_share": "1ed63220ab59dd2feab4a42ffa565a9ff50980a72da022c35f6134869534c0fe", -"test_msg_recoverydevice_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f150abe2dd2b]": "c69e74416015afdeb589d257511c3a8a693c1f584717d948f93a3250d6713ef6", -"test_msg_recoverydevice_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0ae0458ff0c6d62": "9131fad9e499bb4cb3ee18c5535b89647d149746464334e0854052410b6a33d8", -"test_msg_recoverydevice_slip39_advanced_dryrun.py::test_2of3_dryrun": "fdf2733eac6e1cc6f5758cf599dc6a02e3000145cd83150f0727602d98744b8d", -"test_msg_recoverydevice_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "950a00e2a14070cb9c78658dd13064cf860cd125d604df242cf8a22ce9cf7a5e", -"test_msg_recoverydevice_slip39_basic.py::test_1of1": "de184147e0786f76c324019964ffebd0f170474d0e1a72b0aa120daa36c624d7", -"test_msg_recoverydevice_slip39_basic.py::test_abort": "a54d4f29cf1fc3ce26831f52d0ae98a30a2f3e108f822cce08a9bfdd3319356e", -"test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "01b6945fab5f321da8858b58e7ea9f2fb1e7391884545cb563d1a34aab0c3e7a", -"test_msg_recoverydevice_slip39_basic.py::test_noabort": "3db993abfb7e8d35e4a0acf1d8975d42fe51d1bee630639238f642b5c6c5f26d", -"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "45330e1d06ad7b4fc5710c0cd44fdd40afd9bbb7ce1e1c291eadb0306536719a", -"test_msg_recoverydevice_slip39_basic.py::test_same_share": "3a5317f3bcf96931bb9b262f31fd3461d14560dce0a6c068e545283e9bf526a0", -"test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "c3cbc4aa0243f89d421de05ee02a941b44e0794ae1f9ca064d7ecea6b3dd4176", -"test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "c7151e24b74ddb70ce6d10459f5ba318e8a7947cbc8abecc90df97d5abdb7609", -"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[0]": "3164a3744b29cdd345cbae18b8963a008e89c4d4bcebe98d2c320bf714c9c299", -"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[1]": "b85543b48047ebb93b1b8c509d0596205d193bf99b3cd1c6650b24d97f6bd6d4", -"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[2]": "6fff99c5997b08bc18d6f6dbfe67a141eda00a848168af5927b46eff48e46770", -"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_dryrun": "d84427489f691ecc222b62f83af3e97fa09097404dcba07772a43b5eb0c689e8", -"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "55f2dd6b4958659f071c3f57e06286f872ac38af4828f446a0f4e91c657dfccc", +"test_msg_recoverydevice_bip39_t2.py::test_tt_nopin_nopassphrase": "2908caefeacaa731e7247db9a2875f7550cacc62d4d1b62a2334f08168c3f22d", +"test_msg_recoverydevice_bip39_t2.py::test_tt_pin_passphrase": "61ac4aaca83579969a302d4e03fc5ec8b1e7d622eac5eab2aff4e3368ee61490", +"test_msg_recoverydevice_slip39_advanced.py::test_abort": "d994be4628c6b374a8aeff99f2ef7c7be6fd1e05121a10dad123a404ba7e923a", +"test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "d31239d6b8c1945f3bab7a268f3cae36fa3f77183d8b3a97ea6588f663a3ce88", +"test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "ee07a786398226d80cb41464229769171925beb6c4e0025960970eb125d483af", +"test_msg_recoverydevice_slip39_advanced.py::test_noabort": "0b1c1f230998cb8c650812045984c40427f180b285fcabfde30f8f2dd7560d92", +"test_msg_recoverydevice_slip39_advanced.py::test_same_share": "1500840679f5d0541b2202ba3d3375776faa4057aeb53407f41ab42bb8ac2dab", +"test_msg_recoverydevice_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f150abe2dd2b]": "cd8cc5a9a90379a8557e5b43b4218758b125b533e7baa1b92acb2ea9139f5f2e", +"test_msg_recoverydevice_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0ae0458ff0c6d62": "ca228984335961a172bf009af0af73f8ec9edaf65ed9ecd80cceb2fead7a4ca7", +"test_msg_recoverydevice_slip39_advanced_dryrun.py::test_2of3_dryrun": "f06aa2facf36c4b8773c966e607e376067df6113217bfb9e1fa3fa941c2cae37", +"test_msg_recoverydevice_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "0c1567a119ca622a3b02cf8587e0736befce2d9fa4028fa392fba526a4fe9e49", +"test_msg_recoverydevice_slip39_basic.py::test_1of1": "e75ebdfcd4e1558d3227809f7aaf245e6fa4398ac6480a520e7bfe16335d6bd4", +"test_msg_recoverydevice_slip39_basic.py::test_abort": "d994be4628c6b374a8aeff99f2ef7c7be6fd1e05121a10dad123a404ba7e923a", +"test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "509efcccd25f681a0d04c1760f1f213a3746f672772bcdcf5a3df547e669e45e", +"test_msg_recoverydevice_slip39_basic.py::test_noabort": "f455bdead158468dfe880d74557d796fd5af5a87107987ef35866fdfd7f03ad6", +"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "357f9b7f2194621c5cd64e3af3376cc76375aab946bc87d03ac8ea3007265ddb", +"test_msg_recoverydevice_slip39_basic.py::test_same_share": "df831b728982131580adf5e3e536f602f69e3969b0d288840613e4a647e41ff4", +"test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "94b21c4a430b064b75ef10f8bf4f440a4f902f51c2aae6d6f58185d44bd87d1b", +"test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "66af54a3d24114f23e73081a2cedcaacff1b1a9ff423afa40c2017d02f312afe", +"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[0]": "ea2d2dafddc64cfa1e874e6aa46f13a9b7aee185a6dc53f91da9f6a5f1b94697", +"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[1]": "2a4f594f045f8a51071046ae4ea0d2ebdc51b5879ffb9c43d2b5a63910d8d031", +"test_msg_recoverydevice_slip39_basic.py::test_wrong_nth_word[2]": "50cd0c3604d99a1b523500c108036e628543af06ced5b4cb7ea1d09f3f538816", +"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_dryrun": "db689145d77d786034a827dd386096c97fe0d8de8f4fc787db6e3fb5430c9fa1", +"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "5720bb6428bda4af3343bdad115c8dd727c48963520ead9a3987cc0d878bbc7b", "test_msg_resetdevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_resetdevice_bip39_t2.py-test_failed_pin": "ff7fe2e2d69a8e0dda7d9ec811ff0164aa5f85f9c56fe693932749b9be92c868", "test_msg_resetdevice_bip39_t2.py-test_reset_device": "5f1b6cdc46e416430df1afd114bceda57fb644108d594ce1f466460ba4917b41", @@ -324,7 +336,7 @@ "test_msg_signtx_decred.py-test_send_decred_change": "6b44d98d39753a65e4aee69185d7dcecaafd405403f47835d0706ce52083b2ca", "test_msg_signtx_external.py::test_p2pkh_presigned": "075b9a41516faba90ddd8a6ed894ed4b60de1c11dd96400a57d37e64adbc73c4", "test_msg_signtx_external.py::test_p2pkh_with_proof": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_signtx_external.py::test_p2wpkh_in_p2sh_presigned": "f88ace4e725d81fbe79bc243d427f4d2284c478cc605b32c17336226bacb7600", +"test_msg_signtx_external.py::test_p2wpkh_in_p2sh_presigned": "2b37805ae0e06f23e78219a4e9af90091a0b1189f9788fbfd4e1fa507e954e8a", "test_msg_signtx_external.py::test_p2wpkh_in_p2sh_with_proof": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_signtx_external.py::test_p2wpkh_presigned": "af948f06299d23a6a25c8183e9541d6761cdbeafdf5b5f92aca27b832544ddc7", "test_msg_signtx_external.py::test_p2wpkh_with_false_proof": "180fa10c6aab6dafa764dc598ce7cc4ac216ad27051e6f414503fc000f85cae9", @@ -439,6 +451,6 @@ "test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "cd6c1248d9ee4d6416c57026a96190a84ac8608af04fd42c9c8c6b7275226aba", "test_sdcard.py::test_sd_format": "6bb7486932a5d38cdbb9b1368ee92aca3fad384115c744feadfade80c1605dd8", "test_sdcard.py::test_sd_no_format": "f47e897caee95cf98c1b4506732825f853c4b8afcdc2713e38e3b4055973c9ac", -"test_sdcard.py::test_sd_protect_unlock": "52a0a4b847ceab2ef5bc9b22898e14df4e4b703227f4eda9807947702da28af8", +"test_sdcard.py::test_sd_protect_unlock": "49687a221a97a01d822abd0a0be5da8c2c8913004cfa275bfb0c7cbe71bf4a27", "test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe" }