diff --git a/tests/device_tests/test_msg_authorize_coinjoin.py b/tests/device_tests/test_msg_authorize_coinjoin.py new file mode 100644 index 0000000000..916373b456 --- /dev/null +++ b/tests/device_tests/test_msg_authorize_coinjoin.py @@ -0,0 +1,342 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2020 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import pytest + +from trezorlib import btc, messages +from trezorlib.exceptions import TrezorFailure +from trezorlib.tools import parse_path + +from ..tx_cache import TxCache +from .signtx import request_finished, request_input, request_meta, request_output + +B = messages.ButtonRequestType + +TX_CACHE_TESTNET = TxCache("Testnet") +TX_CACHE_MAINNET = TxCache("Bitcoin") + +TXHASH_e5b7e2 = bytes.fromhex( + "e5b7e21b5ba720e81efd6bfa9f854ababdcddc75a43bfa60bf0fe069cfd1bb8a" +) +TXHASH_65b811 = bytes.fromhex( + "65b811d3eca0fe6915d9f2d77c86c5a7f19bf66b1b1253c2c51cb4ae5f0c017b" +) + +PIN = "1234" + +pytestmark = pytest.mark.skip_t1 + + +@pytest.mark.setup_client(pin=PIN) +def test_sign_tx(client): + with client: + client.use_pin_sequence([PIN]) + btc.authorize_coinjoin( + client, + coordinator="www.example.com", + max_total_fee=10010, + fee_per_anonymity=5000000, # 0.005 % + n=parse_path("m/84'/1'/0'"), + coin_name="Testnet", + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + client.call(messages.LockDevice()) + + with client: + client.set_expected_responses( + [messages.PreauthorizedRequest(), messages.OwnershipProof()] + ) + btc.get_ownership_proof( + client, + "Testnet", + parse_path("84'/1'/0'/1/0"), + script_type=messages.InputScriptType.SPENDWITNESS, + user_confirmation=True, + commitment_data=b"www.example.com" + (1).to_bytes(8, "big"), + preauthorized=True, + ) + + with client: + client.set_expected_responses( + [messages.PreauthorizedRequest(), messages.OwnershipProof()] + ) + btc.get_ownership_proof( + client, + "Testnet", + parse_path("84'/1'/0'/1/5"), + script_type=messages.InputScriptType.SPENDWITNESS, + user_confirmation=True, + commitment_data=b"www.example.com" + (1).to_bytes(8, "big"), + preauthorized=True, + ) + + inp1 = messages.TxInputType( + # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" + # 84'/1'/0'/0/0 + # tb1qnspxpr2xj9s2jt6qlhuvdnxw6q55jvygcf89r2 + amount=100000, + prev_hash=TXHASH_e5b7e2, + prev_index=0, + script_type=messages.InputScriptType.EXTERNAL, + ownership_proof=bytearray.fromhex( + "534c001900016b2055d8190244b2ed2d46513c40658a574d3bc2deb6969c0535bb818b44d2c40002483045022100d4ad0374c922848c71d913fba59c81b9075e0d33e884d953f0c4b4806b8ffd0c022024740e6717a2b6a5aa03148c3a28b02c713b4e30fc8aeae67fa69eb20e8ddcd9012103505f0d82bbdd251511591b34f36ad5eea37d3220c2b81a1189084431ddb3aa3d" + ), + ) + inp2 = messages.TxInputType( + address_n=parse_path("84'/1'/0'/1/0"), + amount=7289000, + prev_hash=TXHASH_65b811, + prev_index=1, + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + # Other's coinjoined output. + out1 = messages.TxOutputType( + address="tb1qk7j3ahs2v6hrv4v282cf0tvxh0vqq7rpt3zcml", + amount=50000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Our coinjoined output. + out2 = messages.TxOutputType( + address_n=parse_path("84'/1'/0'/1/1"), + amount=50000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Our change output. + out3 = messages.TxOutputType( + address_n=parse_path("84'/1'/0'/1/2"), + amount=7289000 - 50000 - 5 - 5000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Other's change output. + out4 = messages.TxOutputType( + address="tb1q9cqhdr9ydetjzrct6tyeuccws9505hl96azwxk", + amount=100000 - 50000 - 5 - 5000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Coordinator's output. + out5 = messages.TxOutputType( + address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q", + amount=10, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + + with client: + client.set_expected_responses( + [ + messages.PreauthorizedRequest(), + request_input(0), + request_input(1), + request_meta(TXHASH_65b811), + request_input(0, TXHASH_65b811), + request_output(0, TXHASH_65b811), + request_output(1, TXHASH_65b811), + request_output(0), + request_output(1), + request_output(2), + request_output(3), + request_output(4), + request_input(0), + request_meta(TXHASH_e5b7e2), + request_input(0, TXHASH_e5b7e2), + request_output(0, TXHASH_e5b7e2), + request_output(1, TXHASH_e5b7e2), + request_input(0), + request_input(1), + request_output(0), + request_output(1), + request_output(2), + request_output(3), + request_output(4), + request_input(1), + request_finished(), + ] + ) + _, serialized_tx = btc.sign_tx( + client, + "Testnet", + [inp1, inp2], + [out1, out2, out3, out4, out5], + prev_txes=TX_CACHE_TESTNET, + preauthorized=True, + ) + + assert ( + serialized_tx.hex() + == "010000000001028abbd1cf69e00fbf60fa3ba475dccdbdba4a859ffa6bfd1ee820a75b1be2b7e50000000000ffffffff7b010c5faeb41cc5c253121b6bf69bf1a7c5867cd7f2d91569fea0ecd311b8650100000000ffffffff0550c3000000000000160014b7a51ede0a66ae36558a3ab097ad86bbd800786150c3000000000000160014167dae080bca35c9ea49c0c8335dcc4b252a1d70cb616e00000000001600141d03a4d2167961b853d6cadfeab08e4937c5dfe8c3af0000000000001600142e01768ca46e57210f0bd2c99e630e8168fa5fe50a000000000000001976a914a579388225827d9f2fe9014add644487808c695d88ac00024730440220694105071db8c6c8ba3d385d01694b6f7c17546327ab26d4c53a6503fee301e202202dd310c23a195a6cebc904b91ebd15d782e6dacd08670a72ade2795e7d3ff4ec012103505647c017ff2156eb6da20fae72173d3b681a1d0a629f95f49e884db300689f00000000" + ) + + # Test for a second time. + btc.sign_tx( + client, + "Testnet", + [inp1, inp2], + [out1, out2, out3, out4, out5], + prev_txes=TX_CACHE_TESTNET, + preauthorized=True, + ) + + # Test for a third time, fees should exceed max_total_fee. + with pytest.raises(TrezorFailure, match="Fees exceed authorized limit"): + btc.sign_tx( + client, + "Testnet", + [inp1, inp2], + [out1, out2, out3, out4, out5], + prev_txes=TX_CACHE_TESTNET, + preauthorized=True, + ) + + +def test_unfair_fee(client): + # Test unfair mining fee distribution amongst participants. + + with client: + btc.authorize_coinjoin( + client, + coordinator="www.example.com", + max_total_fee=10000, + fee_per_anonymity=5000000, # 0.005 % + n=parse_path("m/84'/1'/0'"), + coin_name="Testnet", + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + inp1 = messages.TxInputType( + # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" + # 84'/1'/0'/0/0 + # tb1qnspxpr2xj9s2jt6qlhuvdnxw6q55jvygcf89r2 + amount=100000, + prev_hash=TXHASH_e5b7e2, + prev_index=0, + script_type=messages.InputScriptType.EXTERNAL, + ownership_proof=bytearray.fromhex( + "534c001900016b2055d8190244b2ed2d46513c40658a574d3bc2deb6969c0535bb818b44d2c40002483045022100d4ad0374c922848c71d913fba59c81b9075e0d33e884d953f0c4b4806b8ffd0c022024740e6717a2b6a5aa03148c3a28b02c713b4e30fc8aeae67fa69eb20e8ddcd9012103505f0d82bbdd251511591b34f36ad5eea37d3220c2b81a1189084431ddb3aa3d" + ), + ) + inp2 = messages.TxInputType( + address_n=parse_path("84'/1'/0'/1/0"), + amount=7289000, + prev_hash=TXHASH_65b811, + prev_index=1, + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + # Other's coinjoined output. + out1 = messages.TxOutputType( + address="tb1qk7j3ahs2v6hrv4v282cf0tvxh0vqq7rpt3zcml", + amount=50000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Our coinjoined output. + out2 = messages.TxOutputType( + address_n=parse_path("84'/1'/0'/1/1"), + amount=50000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Our change output. + out3 = messages.TxOutputType( + address_n=parse_path("84'/1'/0'/1/2"), + amount=7289000 - 50000 - 5 - 6000, # unfair mining fee + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Other's change output. + out4 = messages.TxOutputType( + address="tb1q9cqhdr9ydetjzrct6tyeuccws9505hl96azwxk", + amount=100000 - 50000 - 5 - 4000, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + # Coordinator's output. + out5 = messages.TxOutputType( + address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q", + amount=10, + script_type=messages.OutputScriptType.PAYTOWITNESS, + ) + + with pytest.raises(TrezorFailure, match="fee over threshold"): + btc.sign_tx( + client, + "Testnet", + [inp1, inp2], + [out1, out2, out3, out4, out5], + prev_txes=TX_CACHE_TESTNET, + preauthorized=True, + ) + + +def test_wrong_coordinator(client): + # Ensure that a preauthorized GetOwnershipProof fails if the commitment_data doesn't match the coordinator. + + btc.authorize_coinjoin( + client, + max_total_fee=50000, + coordinator="www.example.com", + n=parse_path("m/84'/1'/0'"), + coin_name="Testnet", + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + with pytest.raises(TrezorFailure, match="Unauthorized operation"): + ownership_proof, _ = btc.get_ownership_proof( + client, + "Testnet", + parse_path("84'/1'/0'/1/0"), + script_type=messages.InputScriptType.SPENDWITNESS, + user_confirmation=True, + commitment_data=b"www.example.org" + (1).to_bytes(8, "big"), + preauthorized=True, + ) + + +def test_change_round_id(client): + # Ensure that if the round ID changes, then GetOwnershipProof fails. + + btc.authorize_coinjoin( + client, + amount=100000000, + max_fee=50000, + coordinator="www.example.com", + n=parse_path("m/84'/1'/0'"), + coin_name="Testnet", + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + with client: + client.set_expected_responses( + [messages.PreauthorizedRequest(), messages.OwnershipProof()] + ) + btc.get_ownership_proof( + client, + "Testnet", + parse_path("84'/1'/0'/1/0"), + script_type=messages.InputScriptType.SPENDWITNESS, + user_confirmation=True, + commitment_data=b"www.example.com" + (1).to_bytes(8, "big"), + preauthorized=True, + ) + + # GetOwnershipProof with changed round ID. + with pytest.raises(TrezorFailure, match="Unauthorized operation"): + ownership_proof, _ = btc.get_ownership_proof( + client, + "Testnet", + parse_path("84'/1'/0'/1/0"), + script_type=messages.InputScriptType.SPENDWITNESS, + user_confirmation=True, + commitment_data=b"www.example.com" + (2).to_bytes(8, "big"), + preauthorized=True, + ) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index a4c6e09a82..7053b79ae6 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -15,6 +15,9 @@ "test_msg_applysettings.py-test_apply_settings_passphrase": "5c1ed9a0be3d14475102d447da0b5d51bbb6dfaaeceff5ea9179064609db7870", "test_msg_applysettings.py-test_apply_settings_passphrase_on_device": "3e6527e227bdde54f51bc9c417b176d0d87fdb6c40c4761368f50eb201b4beed", "test_msg_applysettings.py-test_safety_checks": "19bd500c3b791d51bbd1140085f306a838194593697529263f362acb0b1ab445", +"test_msg_authorize_coinjoin.py::test_sign_tx": "2838d4062333c241b6bbef7e680ec8a5764fe7bcaa41419e4141e146d3586a5d", +"test_msg_authorize_coinjoin.py::test_unfair_fee": "62314e936de46a6caaf02c8eb20f6f471be6e79ca0c5450cad6f67f9cb823f2b", +"test_msg_authorize_coinjoin.py::test_wrong_coordinator": "d8a608beb6165f5667cc44dcff6bdc17ebb4638ddd3bd09e7f0e1e75d1e21135", "test_msg_backup_device.py::test_backup_bip39": "2b63928444b8188eb2241fc03a3b9bc81191cfa9bbf3ef5431894c04ee0ed01f", "test_msg_backup_device.py::test_backup_slip39_advanced": "31900e0e8ad694ce894eee1ce289b425558c1fcd7bcb6128a19c049af436d35f", "test_msg_backup_device.py::test_backup_slip39_basic": "be4d88d882851ce1ddc45165c35952b23121ddca1a811c7fd7c7ef9d31989e8c",