From e4c406822c00695aaf7cd420634643236de17849 Mon Sep 17 00:00:00 2001 From: Rafael Korbas Date: Mon, 25 Jan 2021 17:12:32 +0100 Subject: [PATCH] Add multiasset sending and min validity to Cardano transactions --- common/protob/messages-cardano.proto | 13 ++ .../fixtures/cardano/sign_tx.failed.json | 199 ++++++++++++++++++ common/tests/fixtures/cardano/sign_tx.json | 140 ++++++++++++ core/src/apps/cardano/README.md | 91 +++++--- core/src/apps/cardano/helpers/__init__.py | 1 + core/src/apps/cardano/helpers/utils.py | 9 +- core/src/apps/cardano/layout.py | 84 +++++++- core/src/apps/cardano/sign_tx.py | 133 ++++++++++-- .../trezor/messages/CardanoAssetGroupType.py | 31 +++ core/src/trezor/messages/CardanoSignTx.py | 3 + core/src/trezor/messages/CardanoTokenType.py | 29 +++ .../trezor/messages/CardanoTxOutputType.py | 4 + python/src/trezorlib/cardano.py | 72 +++++-- python/src/trezorlib/cli/cardano.py | 4 +- .../messages/CardanoAssetGroupType.py | 31 +++ .../src/trezorlib/messages/CardanoSignTx.py | 3 + .../trezorlib/messages/CardanoTokenType.py | 29 +++ .../trezorlib/messages/CardanoTxOutputType.py | 4 + python/src/trezorlib/messages/__init__.py | 2 + tests/device_tests/cardano/test_sign_tx.py | 6 +- tests/ui_tests/fixtures.json | 48 +++-- 21 files changed, 848 insertions(+), 88 deletions(-) create mode 100644 core/src/trezor/messages/CardanoAssetGroupType.py create mode 100644 core/src/trezor/messages/CardanoTokenType.py create mode 100644 python/src/trezorlib/messages/CardanoAssetGroupType.py create mode 100644 python/src/trezorlib/messages/CardanoTokenType.py diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index 4651a404c..39eb13025 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -126,6 +126,8 @@ message CardanoSignTx { repeated CardanoTxCertificateType certificates = 9; // transaction certificates - added in shelley repeated CardanoTxWithdrawalType withdrawals = 10; // transaction withdrawals - added in shelley optional bytes metadata = 11; // transaction metadata - added in shelley + optional uint64 validity_interval_start = 12; // transaction validity start - added in allegra + /** * Structure representing cardano transaction input */ @@ -144,6 +146,17 @@ message CardanoSignTx { // repeated uint32 address_n = 2; // moved to address_parameters optional uint64 amount = 3; // amount to spend optional CardanoAddressParametersType address_parameters = 4; // parameters used to derive the address + repeated CardanoAssetGroupType token_bundle = 5; // custom assets - added in mary + } + + message CardanoAssetGroupType { + required bytes policy_id = 1; // asset group policy id + repeated CardanoTokenType tokens = 2; // asset name-amount pair + } + + message CardanoTokenType { + required bytes asset_name_bytes = 1; // asset name as bytestring (may be either ascii string or hash) + required uint64 amount = 2; // asset amount } /** diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index b452aa7c2..943d46b0f 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -635,6 +635,205 @@ "result": { "error_message": "Invalid metadata" } + }, + { + "description": "Too many tokens in output", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "01aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "7878754" + }, + { + "asset_name_bytes": "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "04aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "05aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "06aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "07aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "08aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + }, + { + "asset_name_bytes": "09aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "1234" + } + ] + }, + { + "policy_id": "75a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "10aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + }, + { + "asset_name_bytes": "17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": "47" + } + ] + } + ] + } + ] + }, + "result": { + "error_message": "Maximum tx output size" + } + }, + { + "description": "Repeated asset name in multiasset token group", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "74652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ] + }, + "result": { + "error_message": "Invalid token bundle in output" + } + }, + { + "description": "Repeated policyId in multiasset output", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "metadata": "", + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696f", + "amount": "7878754" + } + ] + } + ] + } + ] + }, + "result": { + "error_message": "Invalid token bundle in output" + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx.json b/common/tests/fixtures/cardano/sign_tx.json index 552d290a1..8a937eb93 100644 --- a/common/tests/fixtures/cardano/sign_tx.json +++ b/common/tests/fixtures/cardano/sign_tx.json @@ -541,6 +541,146 @@ "tx_hash": "47cf79f20c6c62edb4162b3b232a57afc1bd0b57c7fd8389555276408a004776", "serialized_tx": "83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018382582f82d818582583581cc817d85b524e3d073795819a25cdbb84cff6aa2bbb3a081980d248cba10242182a001a0fb6fc611a002dd2e882581d60cb03849e268f989b5a843107bad7fa2908246986a8f3d643f8c184800182582f82d818582583581c98c3a558f39d1d993cc8770e8825c70a6d0f5a9eb243501c4526c29da10242182a001aa8566c011a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840cc11adf81cb3c3b75a438325f8577666f5cbb4d5d6b73fa6dbbcf5ab36897df34eecacdb54c3bc3ce7fc594ebb2c7aa4db4700f4290facad9b611a035af8710a582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63545a10242182af6" } + }, + { + "description": "Mary era transaction with multiasset output", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "metadata": "", + "input_flow": [["YES"], ["YES"], ["YES"], ["SWIPE", "YES"], ["SWIPE", "YES"]], + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1234", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + } + ] + }, + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ] + }, + "result": { + "tx_hash": "b7269ddc59e4094a6581c653e0d5dc1e553e3a5fb6ffae47d3d094dff1cfe87b", + "serialized_tx": "83a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff821904d2a1581c95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39a14874652474436f696e1a007838628258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182a030a08182fa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840e9ab9920f24f7fdf10c90c9c1794cd9efea03dd4b3add405e5f9ffb61874d2704d376269649f8f5c57ec69b2df74fa94f73191fbeb21987b4b887743af454c06f6" + } + }, + { + "description": "Mary era transaction with different policies and tokens", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "metadata": "", + "input_flow": [["YES"], ["YES"], ["YES"], ["SWIPE", "YES"], ["YES"], ["SWIPE", "YES"], ["SWIPE", "SWIPE", "YES"], ["YES"]], + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1234", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "456c204e69c3b16f", + "amount": "1234" + } + ] + }, + { + "policy_id": "75a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "7564247542686911", + "amount": "47" + } + ] + } + ] + }, + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ] + }, + "result": { + "tx_hash": "0b929def7bd9f44f5602f809bc0f9be30521f6b93d625525cf33b956993bfb22", + "serialized_tx": "83a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff821904d2a2581c75a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39a1487564247542686911182f581c95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39a248456c204e69c3b16f1904d24874652474436f696e1a007838628258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182a030a08182fa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c158408751e397bd9610735a92e65eab02c04aa61507f425e53c0119ddc06047bfac279439ee2bf6e0d572defa9e5649a1ea1fc2b8144041ab4970f39cd6850d4d670ef6" + } + }, + { + "description": "Mary era transaction with no TTL/validity start", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "certificates": [], + "withdrawals": [], + "metadata": "", + "input_flow": [["YES"]], + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/0", + "amount": "7120787" + } + ] + }, + "result": { + "tx_hash": "b621e22f7cb9aac1a70a3362fde88bdfd31fc100e20f3f3c24a7b853536b4f50", + "serialized_tx": "83a300818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b70001818258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1584088c35c125664935117d9aa1173cae5f01967b02f6b716b1a135570b2fee74728f2f3e39d56b748302c36e2407d7bfefc4054ca1e60dd857e461734ae41d00500f6" + } } ] } diff --git a/core/src/apps/cardano/README.md b/core/src/apps/cardano/README.md index cde412633..02b0faa76 100644 --- a/core/src/apps/cardano/README.md +++ b/core/src/apps/cardano/README.md @@ -1,11 +1,9 @@ # Cardano -MAINTAINER = Gabriel Kerekeš +MAINTAINER = Rafael Korbaš ORIGINAL AUTHOR = Juraj Muravský -SHELLEY UPDATE AUTHOR = Gabriel Kerekeš - REVIEWER = Jan Matejek , Tomas Susanka ----- @@ -13,15 +11,16 @@ REVIEWER = Jan Matejek , Tomas Susanka This feature extends the existing accounting infrastructure defined in the ledger model, which is designed for processing ada-only transactions, to accommodate transactions that simultaneously use a range of assets. These assets include ada and a variety of user-define custom token types. + +Transaction outputs may include custom tokens on top of ADA tokens: +``` +1: [ + [ + address, [ + ADA_amount, { + policy_id: { + asset_name: asset_amount + }}]]] +``` + +Please see the transaction below for more details. + +**The serialized transaction output size is currently limited to 512 bytes. This limitation is a mitigation measure to prevent sending large (especially change) outputs containing many tokens that Trezor would not be able to spend given that currently the full Cardano transaction is held in-memory. Once Cardano-transaction signing is refactored to be streamed, this limit can be lifted** #### Certificates @@ -122,7 +143,7 @@ And these three which are not supported by Trezor at the moment: Stake key de-registration and delegation certificates both need to be witnessed by the corresponding staking key. You can read more on certificates in the [delegation design spec](https://hydra.iohk.io/build/2006688/download/1/delegation_design_spec.pdf#subsection.3.4). -Info about their structure can be found [here](https://github.com/input-output-hk/cardano-ledger-specs/blob/460ee17d22cacb3ac4d90536ebe90500a356a1c9/shelley/chain-and-ledger/shelley-spec-ledger-test/cddl-files/shelley.cddl#L102). +Info about their structure can be found [here](https://github.com/input-output-hk/cardano-ledger-specs/blob/097890495cbb0e8b62106bcd090a5721c3f4b36f/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl#L102). #### Withdrawals @@ -132,7 +153,7 @@ You can read more on withdrawals in the [delegation design spec](https://hydra.i #### Metadata -Each transaction may contain metadata. Metadata format can be found [here](https://github.com/input-output-hk/cardano-ledger-specs/blob/460ee17d22cacb3ac4d90536ebe90500a356a1c9/shelley/chain-and-ledger/shelley-spec-ledger-test/cddl-files/shelley.cddl#L210). It's basically a CBOR serialized map and can contain numbers, bytes, strings or nested maps/lists. +Each transaction may contain metadata. Metadata format can be found [here](https://github.com/input-output-hk/cardano-ledger-specs/blob/097890495cbb0e8b62106bcd090a5721c3f4b36f/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl#L212). It's basically a CBOR serialized map and can contain numbers, bytes, strings or nested maps/lists. Due to memory limitations we currently enforce a maximum size of 500 bytes for metadata. @@ -145,12 +166,11 @@ Due to memory limitations we currently enforce a maximum size of 500 bytes for m You can use a combination of [cardano-node](https://github.com/input-output-hk/cardano-node) and cardano-cli (part of the cardano-node repo) to submit a transaction. ## Serialization format -Cardano uses [CBOR](https://www.rfc-editor.org/info/rfc7049) as a serialization format. [Here](https://github.com/input-output-hk/cardano-ledger-specs/blob/460ee17d22cacb3ac4d90536ebe90500a356a1c9/shelley/chain-and-ledger/shelley-spec-ledger-test/cddl-files/shelley.cddl) is the [CDDL](https://tools.ietf.org/html/rfc8610) specification for Shelley. - +Cardano uses [CBOR](https://www.rfc-editor.org/info/rfc7049) as a serialization format. [Here](https://github.com/input-output-hk/cardano-ledger-specs/blob/097890495cbb0e8b62106bcd090a5721c3f4b36f/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl) is the [CDDL](https://tools.ietf.org/html/rfc8610) specification for after Multi Asset support has been added. #### Raw transaction example ``` -83a600818258200d4a5315236df09f331158cae0f78d3df6cdb952a387bcd160dcb1bd2c708c6b00018182583900667ee84f714720123b92dd159bc306925020c460d464cea40eebc59f6c72a09118a3307789bc6d79e3b2149468f62df586085bcee687ca4d1b00000018fab759cd021a00030d40031a0007a120048182018200581cf228837e81c3baaa1879dbeff94e86fa5eba342aa05cd6d1c3bf23ed05a1581de06c72a09118a3307789bc6d79e3b2149468f62df586085bcee687ca4d1b00000001ad72b9d4a10082825820d198d009e0e482bc940331c3709c7ccdc1decbf0675e7c06380c1da3e129e7265840f62f02511ac77eebdbd87221a3bc9c93cf0971a13107ff41b6ea4ea14d720b361f41ab4994b91022763a10ebe1edf8174ca31ec2c7f56be72759d7e75303b603825820f1cea7b5d7f81e6e7858681634d957117ffe4e78bf9d475dbae9101baddda49858407ac236ad5684d22848a725246e83a043611c8ecebf04864d1b9fae6a33f23684790bc05d44a49b1c0a48df00151acafdcc93c29faf93663c9ed704cefd1a4b0df6 +83a700818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff821904d2a1581c95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39a14874652474436f696e1910e18258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b427719115c02182a030a048182008200581c122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b427705a1581de1122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771903e80814a10082825820bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e5840c6e85c7eec254f765ddc119b1f40ef50944dcb1882822c3d61641785bbc312d1049ed0a92ded74745986f5d464d0d0caafc9f0c66285a056309d3d39cf19b20e8258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c158401feabb9e56bca7d3cb75f0942d1ebaec92f167193c70fb9b416e9ae3d3e0f368f49fde3f4a862eb6a02f9a27834d0b7c1f6dd689616809432c99f3ab7249ad0ef6 ``` #### The same transactions with structure description @@ -163,27 +183,46 @@ Cardano uses [CBOR](https://www.rfc-editor.org/info/rfc7049) as a serialization { # inputs [id, index] # uint(0), array(1), array(2), bytes(32), uint(0) - 0: [[h'0D4...', 0]], - - # outputs [address, amount] - # uint(1), array(1), array(2), bytes(57), uint(107285535181) - 1: [[h'006...', 107285535181]], + 0: [[h'3B4...', 0]], + + # outputs [address, [ada_amount, { policy_id => { asset_name => asset_amount }}]] + # uint(1), array(2) + 1: [ + # multi asset output + # array(2), bytes(57), uint(1234), map(1), bytes(28), map(1), bytes(8), uint(4321) + [ + h'01E...', [ + 1234, { + h'95A...': { + h'74652474436F696E': 4321 + } + } + ] + ], + # output containing only ADA [address, ada_amount] + # array(2), bytes(57), uint(4444) + [h'018...', 4444], + ] # fee - # uint(2), uint(200000) - 2: 200000, + # uint(2), uint(42) + 2: 42, # ttl - # uint(3), uint(500000) - 3: 500000, + # uint(3), uint(10) + 3: 10, # certificates [[type, [keyhash/scripthash, keyhash]]] - # uint(4), array(1), array(2), uint(1), array(2), uint(0), bytes(28) - 4: [[1,[0, h'F22...']]], + # uint(4), array(1), array(2), uint(0), array(2), uint(0), bytes(28) + 4: [[0,[0, h'122...']]], # withdrawal [reward_address: amount] # uint(5), map(1), bytes(29), uint(7204944340) - 5: {h'E06...': 7204944340} + 5: {h'E11...': 1000}, + + # validity_interval_start + # uint(8), uint(20) + 8: 20 }, # witnesses # map(1) @@ -192,9 +231,9 @@ Cardano uses [CBOR](https://www.rfc-editor.org/info/rfc7049) as a serialization # uint(0), array(2) 0: [ # array(2), bytes(32), bytes(64) - [h'D19...', h'F62...'], + [h'BC6...', h'C6E...'], # array(2), bytes(32), bytes(64) - [h'F1C...', h'7AC...'] + [h'5D0...', h'1FE...'] ] }, diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py index 2a4d33f36..3d1e34742 100644 --- a/core/src/apps/cardano/helpers/__init__.py +++ b/core/src/apps/cardano/helpers/__init__.py @@ -4,6 +4,7 @@ INVALID_ADDRESS = wire.ProcessError("Invalid address") NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!") INVALID_CERTIFICATE = wire.ProcessError("Invalid certificate") INVALID_WITHDRAWAL = wire.ProcessError("Invalid withdrawal") +INVALID_TOKEN_BUNDLE_OUTPUT = wire.ProcessError("Invalid token bundle in output") INVALID_METADATA = wire.ProcessError("Invalid metadata") INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE = wire.ProcessError( "Stakepool registration transaction cannot contain other certificates nor withdrawals" diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index 297c6e306..ab0c10a53 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -1,7 +1,7 @@ from apps.cardano.helpers.paths import ACCOUNT_PATH_INDEX, unharden if False: - from typing import List + from typing import List, Optional def variable_length_encode(number: int) -> bytes: @@ -35,3 +35,10 @@ def format_account_number(path: List[int]) -> str: raise ValueError("Path is too short.") return "#%d" % (unharden(path[ACCOUNT_PATH_INDEX]) + 1) + + +def format_optional_int(number: Optional[int]) -> str: + if number is None: + return "n/a" + + return str(number) diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index aecf81a70..9c6217b84 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -25,7 +25,7 @@ from .address import ( pack_reward_address_bytes, ) from .helpers import protocol_magics -from .helpers.utils import format_account_number, to_account_path +from .helpers.utils import format_account_number, format_optional_int, to_account_path if False: from typing import List, Optional @@ -35,6 +35,8 @@ if False: CardanoTxCertificateType, CardanoTxWithdrawalType, CardanoPoolParametersType, + CardanoAssetGroupType, + CardanoTokenType, ) from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType @@ -62,10 +64,22 @@ 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) -> None: +def is_printable_ascii_bytestring(bytestr: bytes) -> bool: + return all((32 < b < 127) for b in bytestr) + + +async def confirm_sending( + ctx: wire.Context, + ada_amount: int, + token_bundle: List[CardanoAssetGroupType], + to: str, +) -> None: + for token_group in token_bundle: + await confirm_sending_token_group(ctx, token_group) + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Confirm sending:") - page1.bold(format_coin_amount(amount)) + page1.bold(format_coin_amount(ada_amount)) page1.normal("to") to_lines = list(chunks(to, 17)) @@ -76,6 +90,55 @@ async def confirm_sending(ctx: wire.Context, amount: int, to: str) -> None: await require_confirm(ctx, Paginated(pages)) +async def confirm_sending_token_group( + ctx: wire.Context, token_group: CardanoAssetGroupType +) -> None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.bold("Policy id: ") + page1.mono(hexlify(token_group.policy_id).decode()) + await require_confirm(ctx, page1) + + for token_number, token in enumerate(token_group.tokens, 1): + if is_printable_ascii_bytestring(token.asset_name_bytes): + await confirm_sending_token_ascii(ctx, token, token_number) + else: + await confirm_sending_token_hex(ctx, token, token_number) + + +async def confirm_sending_token_ascii( + ctx: wire.Context, token: CardanoTokenType, token_number: int +) -> None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("Asset #%s name (ASCII):" % (token_number)) + page1.bold(token.asset_name_bytes.decode("ascii")) + page1.normal("Amount sent:") + page1.bold(format_amount(token.amount, 0)) + await require_confirm(ctx, page1) + + +async def confirm_sending_token_hex( + ctx: wire.Context, token: CardanoTokenType, token_number: int +) -> None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.bold("Asset #%s name (hex):" % (token_number)) + page1.mono(hexlify(token.asset_name_bytes).decode()) + page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page2.normal("Amount sent:") + page2.bold(format_amount(token.amount, 0)) + await require_confirm(ctx, Paginated([page1, page2])) + + +async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: + page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + page1.normal("The following") + page1.normal("transaction output") + page1.normal("contains tokens.") + page1.br_half() + page1.normal("Continue?") + + await require_confirm(ctx, page1) + + async def show_warning_tx_no_staking_info( ctx: wire.Context, address_type: EnumTypeCardanoAddressType, amount: int ) -> None: @@ -156,7 +219,8 @@ async def confirm_transaction( amount: int, fee: int, protocol_magic: int, - ttl: int, + ttl: Optional[int], + validity_interval_start: Optional[int], has_metadata: bool, is_network_id_verifiable: bool, ) -> None: @@ -173,8 +237,8 @@ async def confirm_transaction( if is_network_id_verifiable: page2.normal("Network:") page2.bold(protocol_magics.to_ui_string(protocol_magic)) - page2.normal("Transaction TTL:") - page2.bold(str(ttl)) + page2.normal("Valid since: %s" % format_optional_int(validity_interval_start)) + page2.normal("TTL: %s" % format_optional_int(ttl)) pages.append(page2) if has_metadata: @@ -295,12 +359,14 @@ async def confirm_stake_pool_metadata( await require_confirm(ctx, Paginated([page1, page2])) -async def confirm_transaction_network_ttl(ctx, protocol_magic: int, ttl: int) -> None: +async def confirm_transaction_network_ttl( + ctx, protocol_magic: int, ttl: Optional[int], validity_interval_start: Optional[int] +) -> None: page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) page1.normal("Network:") page1.bold(protocol_magics.to_ui_string(protocol_magic)) - page1.normal("Transaction TTL:") - page1.bold(str(ttl)) + page1.normal("Valid since: %s" % format_optional_int(validity_interval_start)) + page1.normal("TTL: %s" % format_optional_int(ttl)) await require_confirm(ctx, page1) diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 1716653d4..69cebda34 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -22,6 +22,7 @@ from .helpers import ( INVALID_METADATA, INVALID_STAKE_POOL_REGISTRATION_TX_STRUCTURE, INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS, + INVALID_TOKEN_BUNDLE_OUTPUT, INVALID_WITHDRAWAL, LOVELACE_MAX_SUPPLY, network_ids, @@ -49,6 +50,7 @@ from .layout import ( show_warning_tx_different_staking_account, show_warning_tx_network_unverifiable, show_warning_tx_no_staking_info, + show_warning_tx_output_contains_tokens, show_warning_tx_pointer_address, show_warning_tx_staking_key_hash, ) @@ -60,10 +62,17 @@ if False: from trezor.messages.CardanoTxInputType import CardanoTxInputType from trezor.messages.CardanoTxOutputType import CardanoTxOutputType from trezor.messages.CardanoTxWithdrawalType import CardanoTxWithdrawalType - from typing import Dict, List, Tuple + from trezor.messages.CardanoAssetGroupType import CardanoAssetGroupType + from typing import Dict, List, Tuple, Union + + CborizedTokenBundle = Dict[bytes, Dict[bytes, int]] + CborizedTxOutput = Tuple[bytes, Union[int, Tuple[int, CborizedTokenBundle]]] METADATA_HASH_SIZE = 32 +MINTING_POLICY_ID_LENGTH = 28 MAX_METADATA_LENGTH = 500 +MAX_ASSET_NAME_LENGTH = 32 +MAX_TX_OUTPUT_SIZE = 512 @seed.with_keychain @@ -197,10 +206,64 @@ def _validate_outputs( "Each output must have an address field or address_parameters!" ) + _validate_token_bundle(output.token_bundle) + _validate_max_tx_output_size(keychain, output, protocol_magic, network_id) + if total_amount > LOVELACE_MAX_SUPPLY: raise wire.ProcessError("Total transaction amount is out of range!") +def _validate_token_bundle(token_bundle: List[CardanoAssetGroupType]) -> None: + seen_policy_ids = set() + for token_group in token_bundle: + policy_id = bytes(token_group.policy_id) + + if len(policy_id) != MINTING_POLICY_ID_LENGTH: + raise INVALID_TOKEN_BUNDLE_OUTPUT + + if policy_id in seen_policy_ids: + raise INVALID_TOKEN_BUNDLE_OUTPUT + else: + seen_policy_ids.add(policy_id) + + if not token_group.tokens: + raise INVALID_TOKEN_BUNDLE_OUTPUT + + seen_asset_name_bytes = set() + for token in token_group.tokens: + asset_name_bytes = bytes(token.asset_name_bytes) + if len(asset_name_bytes) > MAX_ASSET_NAME_LENGTH: + raise INVALID_TOKEN_BUNDLE_OUTPUT + + if asset_name_bytes in seen_asset_name_bytes: + raise INVALID_TOKEN_BUNDLE_OUTPUT + else: + seen_asset_name_bytes.add(asset_name_bytes) + + +def _validate_max_tx_output_size( + keychain: seed.Keychain, + output: CardanoTxOutputType, + protocol_magic: int, + network_id: int, +) -> None: + """ + This limitation is a mitigation measure to prevent sending + large (especially change) outputs containing many tokens that Trezor + would not be able to spend reliably given that + currently the full Cardano transaction is held in-memory. + Once Cardano-transaction signing is refactored to be streamed, this + limit can be lifted + """ + cborized_output = _cborize_output(keychain, output, protocol_magic, network_id) + serialized_output = cbor.encode(cborized_output) + + if len(serialized_output) > MAX_TX_OUTPUT_SIZE: + raise wire.ProcessError( + "Maximum tx output size (%s bytes) exceeded!" % MAX_TX_OUTPUT_SIZE + ) + + def _ensure_no_signing_inputs(inputs: List[CardanoTxInputType]): if any(i.address_n for i in inputs): raise INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS @@ -271,9 +334,11 @@ def _cborize_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: 0: inputs_for_cbor, 1: outputs_for_cbor, 2: msg.fee, - 3: msg.ttl, } + if msg.ttl: + tx_body[3] = msg.ttl + if msg.certificates: certificates_for_cbor = _cborize_certificates(keychain, msg.certificates) tx_body[4] = certificates_for_cbor @@ -289,6 +354,9 @@ def _cborize_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: if msg.metadata: tx_body[7] = _hash_metadata(bytes(msg.metadata)) + if msg.validity_interval_start: + tx_body[8] = msg.validity_interval_start + return tx_body @@ -301,19 +369,46 @@ def _cborize_outputs( outputs: List[CardanoTxOutputType], protocol_magic: int, network_id: int, -) -> List[Tuple[bytes, int]]: - result = [] - for output in outputs: - amount = output.amount - if output.address_parameters: - address = derive_address_bytes( - keychain, output.address_parameters, protocol_magic, network_id - ) - else: - # output address is validated in _validate_outputs before this happens - address = get_address_bytes_unsafe(output.address) +) -> List[CborizedTxOutput]: + return [ + _cborize_output(keychain, output, protocol_magic, network_id) + for output in outputs + ] - result.append((address, amount)) + +def _cborize_output( + keychain: seed.Keychain, + output: CardanoTxOutputType, + protocol_magic: int, + network_id: int, +) -> CborizedTxOutput: + amount = output.amount + if output.address_parameters: + address = derive_address_bytes( + keychain, output.address_parameters, protocol_magic, network_id + ) + else: + # output address is validated in _validate_outputs before this happens + address = get_address_bytes_unsafe(output.address) + + if not output.token_bundle: + return (address, amount) + else: + return (address, (amount, _cborize_token_bundle(output.token_bundle))) + + +def _cborize_token_bundle( + token_bundle: List[CardanoAssetGroupType], +) -> CborizedTokenBundle: + result = {} + + for token_group in token_bundle: + cborized_policy_id = bytes(token_group.policy_id) + result[cborized_policy_id] = cborized_token_group = {} + + for token in token_group.tokens: + cborized_asset_name = bytes(token.asset_name_bytes) + cborized_token_group[cborized_asset_name] = token.amount return result @@ -481,6 +576,7 @@ async def _show_standard_tx( fee=msg.fee, protocol_magic=msg.protocol_magic, ttl=msg.ttl, + validity_interval_start=msg.validity_interval_start, has_metadata=has_metadata, is_network_id_verifiable=is_network_id_verifiable, ) @@ -500,7 +596,9 @@ async def _show_stake_pool_registration_tx( ctx, keychain, pool_parameters.owners, msg.network_id ) await confirm_stake_pool_metadata(ctx, pool_parameters.metadata) - await confirm_transaction_network_ttl(ctx, msg.protocol_magic, msg.ttl) + await confirm_transaction_network_ttl( + ctx, msg.protocol_magic, msg.ttl, msg.validity_interval_start + ) await confirm_stake_pool_registration_final(ctx) @@ -525,7 +623,10 @@ async def _show_outputs( total_amount += output.amount - await confirm_sending(ctx, output.amount, address) + if len(output.token_bundle) > 0: + await show_warning_tx_output_contains_tokens(ctx) + + await confirm_sending(ctx, output.amount, output.token_bundle, address) return total_amount diff --git a/core/src/trezor/messages/CardanoAssetGroupType.py b/core/src/trezor/messages/CardanoAssetGroupType.py new file mode 100644 index 000000000..b5c028c1f --- /dev/null +++ b/core/src/trezor/messages/CardanoAssetGroupType.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .CardanoTokenType import CardanoTokenType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoAssetGroupType(p.MessageType): + + def __init__( + self, + *, + policy_id: bytes, + tokens: List[CardanoTokenType] = None, + ) -> None: + self.tokens = tokens if tokens is not None else [] + self.policy_id = policy_id + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('policy_id', p.BytesType, p.FLAG_REQUIRED), + 2: ('tokens', CardanoTokenType, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/CardanoSignTx.py b/core/src/trezor/messages/CardanoSignTx.py index 57fc76cce..196e26f13 100644 --- a/core/src/trezor/messages/CardanoSignTx.py +++ b/core/src/trezor/messages/CardanoSignTx.py @@ -30,6 +30,7 @@ class CardanoSignTx(p.MessageType): ttl: int = None, network_id: int = None, metadata: bytes = None, + validity_interval_start: int = None, ) -> None: self.inputs = inputs if inputs is not None else [] self.outputs = outputs if outputs is not None else [] @@ -40,6 +41,7 @@ class CardanoSignTx(p.MessageType): self.ttl = ttl self.network_id = network_id self.metadata = metadata + self.validity_interval_start = validity_interval_start @classmethod def get_fields(cls) -> Dict: @@ -53,4 +55,5 @@ class CardanoSignTx(p.MessageType): 9: ('certificates', CardanoTxCertificateType, p.FLAG_REPEATED), 10: ('withdrawals', CardanoTxWithdrawalType, p.FLAG_REPEATED), 11: ('metadata', p.BytesType, None), + 12: ('validity_interval_start', p.UVarintType, None), } diff --git a/core/src/trezor/messages/CardanoTokenType.py b/core/src/trezor/messages/CardanoTokenType.py new file mode 100644 index 000000000..513a3956f --- /dev/null +++ b/core/src/trezor/messages/CardanoTokenType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoTokenType(p.MessageType): + + def __init__( + self, + *, + asset_name_bytes: bytes, + amount: int, + ) -> None: + self.asset_name_bytes = asset_name_bytes + self.amount = amount + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('asset_name_bytes', p.BytesType, p.FLAG_REQUIRED), + 2: ('amount', p.UVarintType, p.FLAG_REQUIRED), + } diff --git a/core/src/trezor/messages/CardanoTxOutputType.py b/core/src/trezor/messages/CardanoTxOutputType.py index f9c534375..af2818eee 100644 --- a/core/src/trezor/messages/CardanoTxOutputType.py +++ b/core/src/trezor/messages/CardanoTxOutputType.py @@ -3,6 +3,7 @@ import protobuf as p from .CardanoAddressParametersType import CardanoAddressParametersType +from .CardanoAssetGroupType import CardanoAssetGroupType if __debug__: try: @@ -17,10 +18,12 @@ class CardanoTxOutputType(p.MessageType): def __init__( self, *, + token_bundle: List[CardanoAssetGroupType] = None, address: str = None, amount: int = None, address_parameters: CardanoAddressParametersType = None, ) -> None: + self.token_bundle = token_bundle if token_bundle is not None else [] self.address = address self.amount = amount self.address_parameters = address_parameters @@ -31,4 +34,5 @@ class CardanoTxOutputType(p.MessageType): 1: ('address', p.UnicodeType, None), 3: ('amount', p.UVarintType, None), 4: ('address_parameters', CardanoAddressParametersType, None), + 5: ('token_bundle', CardanoAssetGroupType, p.FLAG_REPEATED), } diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index ecfc1ddce..ae10a2595 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -15,7 +15,7 @@ # If not, see . from ipaddress import ip_address -from typing import List +from typing import List, Optional from . import messages, tools from .tools import expect @@ -36,9 +36,13 @@ REQUIRED_FIELDS_POOL_PARAMETERS = ( "owners", ) REQUIRED_FIELDS_WITHDRAWAL = ("path", "amount") +REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") +REQUIRED_FIELDS_TOKEN = ("asset_name_bytes", "amount") INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" +INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY = "The output's token_bundle entry is invalid" + ADDRESS_TYPES = ( messages.CardanoAddressType.BYRON, messages.CardanoAddressType.BASE, @@ -107,15 +111,61 @@ def create_output(output) -> messages.CardanoTxOutputType: if not (contains_address or contains_address_type): raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) + address = None + address_parameters = None + token_bundle = None + if contains_address: - return messages.CardanoTxOutputType( - address=output["address"], amount=int(output["amount"]) - ) + address = output["address"] else: - return _create_change_output(output) + address_parameters = _create_change_output_address_parameters(output) + + if "token_bundle" in output: + token_bundle = _create_token_bundle(output["token_bundle"]) + + return messages.CardanoTxOutputType( + address=address, + address_parameters=address_parameters, + amount=int(output["amount"]), + token_bundle=token_bundle, + ) + + +def _create_token_bundle(token_bundle) -> List[messages.CardanoAssetGroupType]: + result = [] + for token_group in token_bundle: + if not all(k in token_group for k in REQUIRED_FIELDS_TOKEN_GROUP): + raise ValueError(INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY) + + result.append( + messages.CardanoAssetGroupType( + policy_id=bytes.fromhex(token_group["policy_id"]), + tokens=_create_tokens(token_group["tokens"]), + ) + ) + return result -def _create_change_output(output) -> messages.CardanoTxOutputType: + +def _create_tokens(tokens) -> List[messages.CardanoTokenType]: + result = [] + for token in tokens: + if not all(k in token for k in REQUIRED_FIELDS_TOKEN): + raise ValueError(INVALID_OUTPUT_TOKEN_BUNDLE_ENTRY) + + result.append( + messages.CardanoTokenType( + asset_name_bytes=bytes.fromhex(token["asset_name_bytes"]), + amount=int(token["amount"]), + ) + ) + + return result + + +def _create_change_output_address_parameters( + output, +) -> messages.CardanoAddressParametersType: if "path" not in output: raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE) @@ -123,7 +173,7 @@ def _create_change_output(output) -> messages.CardanoTxOutputType: if "stakingKeyHash" in output: staking_key_hash_bytes = bytes.fromhex(output.get("stakingKeyHash")) - address_parameters = create_address_parameters( + return create_address_parameters( int(output["addressType"]), tools.parse_path(output["path"]), tools.parse_path(output.get("stakingPath")), @@ -133,10 +183,6 @@ def _create_change_output(output) -> messages.CardanoTxOutputType: output.get("certificateIndex"), ) - return messages.CardanoTxOutputType( - address_parameters=address_parameters, amount=int(output["amount"]) - ) - def create_certificate(certificate) -> messages.CardanoTxCertificateType: CERTIFICATE_MISSING_FIELDS_ERROR = ValueError( @@ -301,7 +347,8 @@ def sign_tx( inputs: List[messages.CardanoTxInputType], outputs: List[messages.CardanoTxOutputType], fee: int, - ttl: int, + ttl: Optional[int], + validity_interval_start: Optional[int], certificates: List[messages.CardanoTxCertificateType] = (), withdrawals: List[messages.CardanoTxWithdrawalType] = (), metadata: bytes = None, @@ -314,6 +361,7 @@ def sign_tx( outputs=outputs, fee=fee, ttl=ttl, + validity_interval_start=validity_interval_start, certificates=certificates, withdrawals=withdrawals, metadata=metadata, diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 4e7e77dac..362192553 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -57,7 +57,8 @@ def sign_tx(client, file, protocol_magic, network_id, testnet): inputs = [cardano.create_input(input) for input in transaction["inputs"]] outputs = [cardano.create_output(output) for output in transaction["outputs"]] fee = transaction["fee"] - ttl = transaction["ttl"] + ttl = transaction.get("ttl") + validity_interval_start = transaction.get("validity_interval_start") certificates = [ cardano.create_certificate(certificate) for certificate in transaction.get("certificates", ()) @@ -76,6 +77,7 @@ def sign_tx(client, file, protocol_magic, network_id, testnet): outputs, fee, ttl, + validity_interval_start, certificates, withdrawals, metadata, diff --git a/python/src/trezorlib/messages/CardanoAssetGroupType.py b/python/src/trezorlib/messages/CardanoAssetGroupType.py new file mode 100644 index 000000000..097d05af6 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoAssetGroupType.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .CardanoTokenType import CardanoTokenType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoAssetGroupType(p.MessageType): + + def __init__( + self, + *, + policy_id: bytes, + tokens: List[CardanoTokenType] = None, + ) -> None: + self.tokens = tokens if tokens is not None else [] + self.policy_id = policy_id + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('policy_id', p.BytesType, p.FLAG_REQUIRED), + 2: ('tokens', CardanoTokenType, p.FLAG_REPEATED), + } diff --git a/python/src/trezorlib/messages/CardanoSignTx.py b/python/src/trezorlib/messages/CardanoSignTx.py index 2cff681fe..2c6fb5a98 100644 --- a/python/src/trezorlib/messages/CardanoSignTx.py +++ b/python/src/trezorlib/messages/CardanoSignTx.py @@ -30,6 +30,7 @@ class CardanoSignTx(p.MessageType): ttl: int = None, network_id: int = None, metadata: bytes = None, + validity_interval_start: int = None, ) -> None: self.inputs = inputs if inputs is not None else [] self.outputs = outputs if outputs is not None else [] @@ -40,6 +41,7 @@ class CardanoSignTx(p.MessageType): self.ttl = ttl self.network_id = network_id self.metadata = metadata + self.validity_interval_start = validity_interval_start @classmethod def get_fields(cls) -> Dict: @@ -53,4 +55,5 @@ class CardanoSignTx(p.MessageType): 9: ('certificates', CardanoTxCertificateType, p.FLAG_REPEATED), 10: ('withdrawals', CardanoTxWithdrawalType, p.FLAG_REPEATED), 11: ('metadata', p.BytesType, None), + 12: ('validity_interval_start', p.UVarintType, None), } diff --git a/python/src/trezorlib/messages/CardanoTokenType.py b/python/src/trezorlib/messages/CardanoTokenType.py new file mode 100644 index 000000000..0a111fdc9 --- /dev/null +++ b/python/src/trezorlib/messages/CardanoTokenType.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class CardanoTokenType(p.MessageType): + + def __init__( + self, + *, + asset_name_bytes: bytes, + amount: int, + ) -> None: + self.asset_name_bytes = asset_name_bytes + self.amount = amount + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('asset_name_bytes', p.BytesType, p.FLAG_REQUIRED), + 2: ('amount', p.UVarintType, p.FLAG_REQUIRED), + } diff --git a/python/src/trezorlib/messages/CardanoTxOutputType.py b/python/src/trezorlib/messages/CardanoTxOutputType.py index 16da8074a..c7f81fbcf 100644 --- a/python/src/trezorlib/messages/CardanoTxOutputType.py +++ b/python/src/trezorlib/messages/CardanoTxOutputType.py @@ -3,6 +3,7 @@ from .. import protobuf as p from .CardanoAddressParametersType import CardanoAddressParametersType +from .CardanoAssetGroupType import CardanoAssetGroupType if __debug__: try: @@ -17,10 +18,12 @@ class CardanoTxOutputType(p.MessageType): def __init__( self, *, + token_bundle: List[CardanoAssetGroupType] = None, address: str = None, amount: int = None, address_parameters: CardanoAddressParametersType = None, ) -> None: + self.token_bundle = token_bundle if token_bundle is not None else [] self.address = address self.amount = amount self.address_parameters = address_parameters @@ -31,4 +34,5 @@ class CardanoTxOutputType(p.MessageType): 1: ('address', p.UnicodeType, None), 3: ('amount', p.UVarintType, None), 4: ('address_parameters', CardanoAddressParametersType, None), + 5: ('token_bundle', CardanoAssetGroupType, p.FLAG_REPEATED), } diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 9700c953c..9939a07a0 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -24,6 +24,7 @@ from .Cancel import Cancel from .CancelAuthorization import CancelAuthorization from .CardanoAddress import CardanoAddress from .CardanoAddressParametersType import CardanoAddressParametersType +from .CardanoAssetGroupType import CardanoAssetGroupType from .CardanoBlockchainPointerType import CardanoBlockchainPointerType from .CardanoGetAddress import CardanoGetAddress from .CardanoGetPublicKey import CardanoGetPublicKey @@ -34,6 +35,7 @@ from .CardanoPoolRelayParametersType import CardanoPoolRelayParametersType from .CardanoPublicKey import CardanoPublicKey from .CardanoSignTx import CardanoSignTx from .CardanoSignedTx import CardanoSignedTx +from .CardanoTokenType import CardanoTokenType from .CardanoTxCertificateType import CardanoTxCertificateType from .CardanoTxInputType import CardanoTxInputType from .CardanoTxOutputType import CardanoTxOutputType diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index 572ddbecd..2ff9656eb 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -65,7 +65,8 @@ def test_cardano_sign_tx(client, parameters, result): inputs=inputs, outputs=outputs, fee=parameters["fee"], - ttl=parameters["ttl"], + ttl=parameters.get("ttl"), + validity_interval_start=parameters.get("validity_interval_start"), certificates=certificates, withdrawals=withdrawals, metadata=bytes.fromhex(parameters["metadata"]), @@ -96,7 +97,8 @@ def test_cardano_sign_tx_failed(client, parameters, result): inputs=inputs, outputs=outputs, fee=parameters["fee"], - ttl=parameters["ttl"], + ttl=parameters.get("ttl"), + validity_interval_start=parameters.get("validity_interval_start"), certificates=certificates, withdrawals=withdrawals, metadata=bytes.fromhex(parameters["metadata"]), diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 87bab0be4..043582864 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1,25 +1,28 @@ { -"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change0]": "ff1ad82caac745577d51f574da41ad0364b2453524ff2e138a927ea03463103d", -"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change1]": "179ad9e910ba421b51a6492f2e63b5ab8be0c9513d6692cf9bb94dcc210d201c", -"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_multiple_inputs]": "fa870dea0bf144c1142d04b9ef7dad74d357cd7b730e232b19d1c8a28edcb132", -"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change0]": "fa870dea0bf144c1142d04b9ef7dad74d357cd7b730e232b19d1c8a28edcb132", -"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change1]": "699fb7c9c2537a25366a865e512d2c15b6134077bc7c89a6eb58344535fef91d", -"cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_certificate]": "e680f96cbed1ec77a80b49e61e201be7de539d52443b587499bfae05467b58dd", -"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_o-0c37e6dc": "3f92f33e81342622a95308e4ce236368f65d099d7646c9a2b2480f1730fbd458", -"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_output]": "7158886e32f1a1e3cb5876036d0bd627b895de28cc6c6ebdb291aa36f51514b2", -"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_script_address_c-466ef44c": "5e81ca3f97f73ceb1498db47ff41e9fb9565ccca3087ea97159fa00b04926c37", -"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_enterprise_address_ch-15518a4c": "7f0a248889f336eab3042454c5e2a6d99da79beafcdba340538c98010a5ccee0", -"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_pointer_address_change_output]": "bbbdb41e894af11290bc8645d0b7cebc6b4488d6c2bba45e74147ad71e9cc13f", -"cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_certificate_with_no_p-0bbad967": "7e96c91d46c2b1564ce145b7a496fc825e2cd4dc3331839ac83049f1e253379b", -"cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_on_testnet]": "d00e78baff80f218b719ad4b0966a1ac7e5e9a3544cc34365ad0b942bc4a318d", -"cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction0]": "994900bb0b0d52d0872dae1254fc8d5ed337e51be0e2ddd85b9d613bc9b11687", -"cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction1]": "120eb04eae3bbcd492e4e0ab19e25bd2e718e4fa9620d5a0496eaee5704f1951", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_metadata]": "e0707009a202512da0e38ec536d24d0522f12453ca0bcf91ff074e7653b7958f", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration]": "3d2ae35075551db91aa24dedbc388a37f58f0b618e073a73aed6a094a352b306", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_and_withdrawal]": "60f510ca75fc1288c92b50ac38ce2d413b608cc008a5ac519772936a98e60253", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_and_stake-3fdfc583": "604b63443e2d554fd3173e748439c29a968597b4db82ad98f56176cda225128f", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certifica-e7bd462a": "ab4eb02c8769a90c0c5a643826d6affbab9cda1a55b5bfa1cc1066ac97a86368", -"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certificate]": "c804dd78f120d14ef2585f04a148df23ab6da539d35b2b20b5bfc6092a4a0da7", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change0]": "5e2334cff9d0946ec722d69d7065023d707d8208a0ceee928824fd006edb93ad", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change1]": "1ac78675dd64f4acbf7cf8f24bc1492bec5f0bce32a3f860742d5fa1922afb24", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_multiple_inputs]": "8151b1b3cfa5ef79a256409d78b0219eb43b8fb498776b16e2f43bb5c9a38c55", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change0]": "8151b1b3cfa5ef79a256409d78b0219eb43b8fb498776b16e2f43bb5c9a38c55", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change1]": "66a8f9e4a2590c304046c8802673fb373d3fa9f3baa03dbc361818dd56fb1eab", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_different_policies_-1dbb1bfb": "accbbaba30610e45d9cbd74a3a6693cff7b65811dbd7ec0c8db8a6adc014d9d5", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_multiasset_output]": "cf3dd28eb435afc09eace0f2b02e7bae18978d0f47a6a43f27040cb6b6f5460e", +"cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_no_ttl-validity_start]": "3954d5a5374321ba8328a86e5302878d231c9ec8f1bb70de257a5f24ffdc2dd6", +"cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_certificate]": "8e8b165ad23d69e4d5f52c5a6cb1f11b27829d54d5e44dd401b13875667e6287", +"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_o-0c37e6dc": "10cb4b3cbb74b3d44e430f3be1affd489ca6dfde179f1916d0abb82fbfff8764", +"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_output]": "ce02b6982e756401f8dd006e64c86825a4254dc57e6c2077f7c62848c9a3d178", +"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_script_address_c-466ef44c": "4a46dd8ac42295d853646a55ca1b85023b2235af6155be663b1de10a6c98def2", +"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_enterprise_address_ch-15518a4c": "079c395ab5dfa10d9bc6364e4201c128cd02ade9fad730390720784360b17d79", +"cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_pointer_address_change_output]": "d5d251a3a597abc1df5480651dfbf15d59f7b207bf351131906862b0aaa07a51", +"cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_certificate_with_no_p-0bbad967": "28a4cc241374f3125d60d33a87c0456ffe9509817d1849d8bfc0e378d6c48a07", +"cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_on_testnet]": "d8303a92b2d5a67d81543eea2fb822ac2ab994ffa07c7d6d23db24a38f626dab", +"cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction0]": "0f2330f22f60905681622bec047bfd67d1aeb3a30c7523d20dee62b59dd86dec", +"cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction1]": "af40cd3886590c39a36c503c1dddfff60ea3ec8a0948e45c109b45c83056d72a", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_metadata]": "d364e0bc601fc16088bd7717822b554f3d4bd234e1ed779cc5bf9acdb316bf26", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration]": "a22ad00e9e58a64eca1f295191fa36937beb09b41620e64942498788e448e3a3", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_and_withdrawal]": "b89e4b24a93197752607d62fa88f918590ff94a2c40007ead6773a3d1bda7285", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_and_stake-3fdfc583": "a7a3f01b6972aeab505e9df70c83111ef03102b62d5260164ed36a0140d7d9dc", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certifica-e7bd462a": "9912dd5d7fe33c3f4c48e813c39d717947d9c96a15f4349ed153548314c4cf80", +"cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certificate]": "45c35b504a486f709af64c5827231446269b3396335fd014da8c3015a1d799cc", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[all_tx_inputs_must_be_external_(without_path)]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_invalid_pool_size]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_non_staking_path]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", @@ -43,10 +46,13 @@ "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_total_is_too_high]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[pool_reward_address_belongs_to_differe-e79b6855": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[pool_reward_address_is_a_base_address]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_asset_name_in_multiasset_token_group]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_policyid_in_multiasset_output]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[shelley_mainnet_transaction_with_testn-af110e3e": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[shelley_testnet_transaction_with_mainn-ba78ab8f": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_protocol_magic_with_mainnet_network_id]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_transaction_with_mainnet_output]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[too_many_tokens_in_output]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[two_owners_with_path]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[unsupported_address_type]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_amount_is_too_large]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",