Add support for stakepool registration to Cardano

pull/1361/head
Rafael Korbas 4 years ago committed by Tomas Susanka
parent bf524854cd
commit b261f789f3

@ -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

@ -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"
}
}
]
}

@ -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"
}
}
]
}

@ -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

@ -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)

@ -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)

@ -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

@ -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
"""

@ -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:")

@ -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)]

@ -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]

@ -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),
}

@ -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),
}

@ -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),
}

@ -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),
}

@ -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]

@ -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),
}

@ -14,6 +14,7 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
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")

@ -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]

@ -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),
}

@ -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),
}

@ -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),
}

@ -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),
}

@ -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]

@ -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),
}

@ -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

@ -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"]]

@ -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"
}

Loading…
Cancel
Save