diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index d7177c207..f7dfd8b16 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -923,48 +923,6 @@ "error_message": "Invalid withdrawal" } }, - { - "description": "Duplicate withdrawal", - "parameters": { - "protocol_magic": 764824073, - "network_id": 1, - "fee": 42, - "ttl": 10, - "certificates": [], - "withdrawals": [ - { - "path": "m/1852'/1815'/0'/2/0", - "amount": "1000" - }, - { - "path": "m/1852'/1815'/0'/2/0", - "amount": "2000" - } - ], - "auxiliary_data": null, - "inputs": [ - { - "path": "m/1852'/1815'/0'/0/0", - "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", - "prev_index": 0 - } - ], - "outputs": [ - { - "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", - "amount": "3003112" - } - ], - "mint": [], - "script_data_hash": null, - "signing_mode": "ORDINARY_TRANSACTION", - "additional_witness_requests": [], - "include_network_id": false - }, - "result": { - "error_message": "Duplicate withdrawals" - } - }, { "description": "Auxiliary data hash has incorrect length", "parameters": { @@ -1514,6 +1472,331 @@ "error_message": "Invalid token bundle in output" } }, + { + "description": "Repeated asset name in mint token group", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "1234" + } + ] + } + ], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid mint token bundle" + } + }, + { + "description": "Repeated policyId in mint", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + } + ] + }, + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696f", + "mint_amount": "7878754" + } + ] + } + ], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid mint token bundle" + } + }, + { + "description": "Asset names in multiasset token group in wrong order", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "76652474436f696e", + "amount": "1234" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + } + ] + } + ], + "mint": [], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid token bundle in output" + } + }, + { + "description": "PolicyIds in multiasset output in wrong order", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696d", + "amount": "7878754" + } + ] + }, + { + "policy_id": "9aa292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696f", + "amount": "7878754" + } + ] + } + ] + } + ], + "mint": [], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid token bundle in output" + } + }, + { + "description": "Asset names in mint token group in wrong order", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "76652474436f696e", + "mint_amount": "1234" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "1234" + } + ] + } + ], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid mint token bundle" + } + }, + { + "description": "PolicyIds in mint in wrong order", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112" + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "14652474436f696d", + "mint_amount": "7878754" + } + ] + }, + { + "policy_id": "9aa292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "24652474436f696e", + "mint_amount": "7878754" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "34652474436f696f", + "mint_amount": "7878754" + } + ] + } + ], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid mint token bundle" + } + }, { "description": "Additional witness requests in ORDINARY_TRANSACTION", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx.json b/common/tests/fixtures/cardano/sign_tx.json index 0e894265c..2291dcf57 100644 --- a/common/tests/fixtures/cardano/sign_tx.json +++ b/common/tests/fixtures/cardano/sign_tx.json @@ -1707,6 +1707,144 @@ } ] } + }, + { + "description": "Ordinary transaction with multiple correctly ordered tokens", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": 47, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "f565", + "amount": "9878754" + }, + { + "asset_name_bytes": "f5652474436f69", + "amount": "7878754" + }, + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "amount": "1234" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + }, + { + "policy_id": "d6a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + } + ] + }, + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "2000000", + "token_bundle": [ + { + "policy_id": "a6a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "amount": "7878754" + } + ] + } + ] + } + ], + "mint": [ + { + "policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "f565", + "mint_amount": "9878754" + }, + { + "asset_name_bytes": "f5652474436f69", + "mint_amount": "7878754" + }, + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "-7878754" + }, + { + "asset_name_bytes": "75652474436f696e", + "mint_amount": "-1234" + } + ] + }, + { + "policy_id": "96a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + } + ] + }, + { + "policy_id": "d6a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39", + "tokens": [ + { + "asset_name_bytes": "74652474436f696e", + "mint_amount": "7878754" + } + ] + } + ], + "script_data_hash": null, + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "f1bda77315626ce61c784f3e60a74128f29b7d00a0c8baca030464293da2d7c4", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "4254a6cae8156771ac8e1e75ceb8003f8370a7f6b08013fda32174b56025fcb01bc9d7f4e1eed10bc2c0ff1020eda47475f728cc824ef98a109fd9fe6a1c4105", + "chain_code": null + } + ] + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json index 6f4caa07e..2ef944e56 100644 --- a/common/tests/fixtures/cardano/sign_tx.multisig.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.multisig.failed.json @@ -131,6 +131,92 @@ "error_message": "Invalid certificate" } }, + { + "description": "Multisig transaction with repeated withdrawal", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "script_hash": "19fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "1000" + }, + { + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "3000" + }, + { + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, + { + "description": "Multisig transaction with wthdrawal addresses in wrong order", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "certificates": [], + "withdrawals": [ + { + "script_hash": "39fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "3000" + }, + { + "script_hash": "29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "signing_mode": "MULTISIG_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid withdrawal" + } + }, { "description": "Multisig transaction with 1852 multisig witness request", "parameters": { diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index c1eeda341..6f340c385 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -421,15 +421,13 @@ async def _process_asset_groups( should_show_tokens: bool, ) -> None: """Read, validate and serialize the asset groups of an output.""" - # until the CIP with canonical CBOR is finalized storing the seen_policy_ids is the only way we can check for - # duplicate policy_ids - seen_policy_ids: set[bytes] = set() + previous_policy_id: bytes = b"" for _ in range(asset_groups_count): asset_group: CardanoAssetGroup = await ctx.call( CardanoTxItemAck(), CardanoAssetGroup ) - _validate_asset_group(asset_group, seen_policy_ids) - seen_policy_ids.add(asset_group.policy_id) + _validate_asset_group(asset_group, previous_policy_id) + previous_policy_id = asset_group.policy_id tokens: HashBuilderDict[bytes, int] = HashBuilderDict(asset_group.tokens_count) with asset_groups_dict.add(asset_group.policy_id, tokens): @@ -450,13 +448,11 @@ async def _process_tokens( should_show_tokens: bool, ) -> None: """Read, validate, confirm and serialize the tokens of an asset group.""" - # until the CIP with canonical CBOR is finalized storing the seen_asset_name_bytes is the only way we can check for - # duplicate tokens - seen_asset_name_bytes: set[bytes] = set() + previous_asset_name_bytes: bytes = b"" for _ in range(tokens_count): token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) - _validate_token(token, seen_asset_name_bytes) - seen_asset_name_bytes.add(token.asset_name_bytes) + _validate_token(token, previous_asset_name_bytes) + previous_asset_name_bytes = token.asset_name_bytes if should_show_tokens: await confirm_sending_token(ctx, policy_id, token) @@ -577,32 +573,26 @@ async def _process_withdrawals( if withdrawals_count == 0: return - # until the CIP with canonical CBOR is finalized storing the seen_withdrawals is the only way we can check for - # duplicate withdrawals - seen_withdrawals: set[tuple[int, ...] | bytes] = set() + previous_reward_address: bytes = b"" for _ in range(withdrawals_count): withdrawal: CardanoTxWithdrawal = await ctx.call( CardanoTxItemAck(), CardanoTxWithdrawal ) _validate_withdrawal( - withdrawal, seen_withdrawals, signing_mode, account_path_checker - ) - await confirm_withdrawal(ctx, withdrawal) - reward_address_type = ( - CardanoAddressType.REWARD - if withdrawal.path - else CardanoAddressType.REWARD_SCRIPT - ) - reward_address = derive_address_bytes( keychain, - CardanoAddressParametersType( - address_type=reward_address_type, - address_n_staking=withdrawal.path, - script_staking_hash=withdrawal.script_hash, - ), + withdrawal, + signing_mode, protocol_magic, network_id, + account_path_checker, + previous_reward_address, + ) + reward_address = _derive_withdrawal_reward_address_bytes( + keychain, withdrawal, protocol_magic, network_id ) + previous_reward_address = reward_address + + await confirm_withdrawal(ctx, withdrawal) withdrawals_dict.add(reward_address, withdrawal.amount) @@ -649,15 +639,13 @@ async def _process_minting( await show_warning_tx_contains_mint(ctx) - # until the CIP with canonical CBOR is finalized storing the seen_policy_ids is the only way we can check for - # duplicate policy_ids - seen_policy_ids: set[bytes] = set() + previous_policy_id: bytes = b"" for _ in range(token_minting.asset_groups_count): asset_group: CardanoAssetGroup = await ctx.call( CardanoTxItemAck(), CardanoAssetGroup ) - _validate_asset_group(asset_group, seen_policy_ids, is_mint=True) - seen_policy_ids.add(asset_group.policy_id) + _validate_asset_group(asset_group, previous_policy_id, is_mint=True) + previous_policy_id = asset_group.policy_id tokens: HashBuilderDict[bytes, int] = HashBuilderDict(asset_group.tokens_count) with minting_dict.add(asset_group.policy_id, tokens): @@ -676,13 +664,11 @@ async def _process_minting_tokens( tokens_count: int, ) -> None: """Read, validate, confirm and serialize the tokens of an asset group.""" - # until the CIP with canonical CBOR is finalized storing the seen_asset_name_bytes is the only way we can check for - # duplicate tokens - seen_asset_name_bytes: set[bytes] = set() + previous_asset_name_bytes: bytes = b"" for _ in range(tokens_count): token: CardanoToken = await ctx.call(CardanoTxItemAck(), CardanoToken) - _validate_token(token, seen_asset_name_bytes, is_mint=True) - seen_asset_name_bytes.add(token.asset_name_bytes) + _validate_token(token, previous_asset_name_bytes, is_mint=True) + previous_asset_name_bytes = token.asset_name_bytes await confirm_token_minting(ctx, policy_id, token) assert token.mint_amount is not None # _validate_token @@ -890,7 +876,7 @@ async def _show_output( def _validate_asset_group( - asset_group: CardanoAssetGroup, seen_policy_ids: set[bytes], is_mint: bool = False + asset_group: CardanoAssetGroup, previous_policy_id: bytes, is_mint: bool = False ) -> None: INVALID_TOKEN_BUNDLE = ( INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT @@ -900,12 +886,12 @@ def _validate_asset_group( raise INVALID_TOKEN_BUNDLE if asset_group.tokens_count == 0: raise INVALID_TOKEN_BUNDLE - if asset_group.policy_id in seen_policy_ids: + if not cbor.are_canonically_ordered(previous_policy_id, asset_group.policy_id): raise INVALID_TOKEN_BUNDLE def _validate_token( - token: CardanoToken, seen_asset_name_bytes: set[bytes], is_mint: bool = False + token: CardanoToken, previous_asset_name_bytes: bytes, is_mint: bool = False ) -> None: INVALID_TOKEN_BUNDLE = ( INVALID_TOKEN_BUNDLE_MINT if is_mint else INVALID_TOKEN_BUNDLE_OUTPUT @@ -920,7 +906,9 @@ def _validate_token( if len(token.asset_name_bytes) > MAX_ASSET_NAME_LENGTH: raise INVALID_TOKEN_BUNDLE - if token.asset_name_bytes in seen_asset_name_bytes: + if not cbor.are_canonically_ordered( + previous_asset_name_bytes, token.asset_name_bytes + ): raise INVALID_TOKEN_BUNDLE @@ -943,10 +931,13 @@ async def _show_certificate( def _validate_withdrawal( + keychain: seed.Keychain, withdrawal: CardanoTxWithdrawal, - seen_withdrawals: set[tuple[int, ...] | bytes], signing_mode: CardanoTxSigningMode, + protocol_magic: int, + network_id: int, account_path_checker: AccountPathChecker, + previous_reward_address: bytes, ) -> None: validate_stake_credential( withdrawal.path, withdrawal.script_hash, signing_mode, INVALID_WITHDRAWAL @@ -958,10 +949,11 @@ def _validate_withdrawal( credential = tuple(withdrawal.path) if withdrawal.path else withdrawal.script_hash assert credential # validate_stake_credential - if credential in seen_withdrawals: - raise wire.ProcessError("Duplicate withdrawals") - else: - seen_withdrawals.add(credential) + reward_address = _derive_withdrawal_reward_address_bytes( + keychain, withdrawal, protocol_magic, network_id + ) + if not cbor.are_canonically_ordered(previous_reward_address, reward_address): + raise INVALID_WITHDRAWAL account_path_checker.add_withdrawal(withdrawal) @@ -971,6 +963,29 @@ def _validate_script_data_hash(script_data_hash: bytes) -> None: raise INVALID_SCRIPT_DATA_HASH +def _derive_withdrawal_reward_address_bytes( + keychain: seed.Keychain, + withdrawal: CardanoTxWithdrawal, + protocol_magic: int, + network_id: int, +) -> bytes: + reward_address_type = ( + CardanoAddressType.REWARD + if withdrawal.path + else CardanoAddressType.REWARD_SCRIPT + ) + return derive_address_bytes( + keychain, + CardanoAddressParametersType( + address_type=reward_address_type, + address_n_staking=withdrawal.path, + script_staking_hash=withdrawal.script_hash, + ), + protocol_magic, + network_id, + ) + + def _get_output_address( keychain: seed.Keychain, protocol_magic: int, diff --git a/core/src/apps/common/cbor.py b/core/src/apps/common/cbor.py index 81614f7f6..9ea69f6f3 100644 --- a/core/src/apps/common/cbor.py +++ b/core/src/apps/common/cbor.py @@ -318,3 +318,13 @@ def create_array_header(size: int) -> bytes: def create_map_header(size: int) -> bytes: return _header(_CBOR_MAP, size) + + +def are_canonically_ordered(previous: Value, current: Value) -> bool: + """ + Returns True if `previous` is smaller than `current` with regards to + the cbor map key ordering as defined in + https://datatracker.ietf.org/doc/html/rfc7049#section-3.9 + """ + u, v = encode(previous), encode(current) + return len(u) < len(v) or (len(u) == len(v) and u < v)