1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-05 08:29:13 +00:00
trezor-firmware/tests/device_tests/bitcoin/test_authorize_coinjoin.py

914 lines
32 KiB
Python

# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
import time
import pytest
from trezorlib import btc, device, messages
from trezorlib.debuglink import ProtocolVersion
from trezorlib.debuglink import SessionDebugWrapper as Session
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import TrezorFailure
from trezorlib.tools import parse_path
from ...common import is_core
from ...tx_cache import TxCache
from .payment_req import make_coinjoin_request
from .signtx import (
assert_tx_matches,
request_finished,
request_input,
request_meta,
request_output,
)
B = messages.ButtonRequestType
TX_CACHE_TESTNET = TxCache("Testnet")
TX_CACHE_MAINNET = TxCache("Bitcoin")
FAKE_TXHASH_e5b7e2 = bytes.fromhex(
"e5b7e21b5ba720e81efd6bfa9f854ababdcddc75a43bfa60bf0fe069cfd1bb8a"
)
FAKE_TXHASH_f982c0 = bytes.fromhex(
"f982c0a283bd65a59aa89eded9e48f2a3319cb80361dfab4cf6192a03badb60a"
)
TXHASH_2cc3c1 = bytes.fromhex(
"2cc3c1e33fb1cb7b4fccf4e0fead3fc077a1eb6c22e61561b343b704a5a8da6d"
)
TXHASH_7f3a34 = bytes.fromhex(
"7f3a348106f9f3688069f389c00842b18d26770ec9a96ea94bf21623433a0f72"
)
PIN = "1234"
ROUND_ID_LEN = 32
SLIP25_PATH = parse_path("m/10025h")
@pytest.mark.parametrize("chunkify", (True, False))
@pytest.mark.setup_client(pin=PIN)
def test_sign_tx(session: Session, chunkify: bool):
# NOTE: FAKE input tx
assert session.features.unlocked is False
commitment_data = b"\x0fwww.example.com" + (1).to_bytes(ROUND_ID_LEN, "big")
with session.client as client:
session.client.use_pin_sequence([PIN])
btc.authorize_coinjoin(
session,
coordinator="www.example.com",
max_rounds=2,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
session.call(messages.LockDevice())
with session.client as client:
client.set_expected_responses(
[messages.PreauthorizedRequest, messages.OwnershipProof]
)
btc.get_ownership_proof(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=commitment_data,
preauthorized=True,
)
with session.client as client:
client.set_expected_responses(
[messages.PreauthorizedRequest, messages.OwnershipProof]
)
btc.get_ownership_proof(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/5"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=commitment_data,
preauthorized=True,
)
inputs = [
messages.TxInputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# m/10025h/1h/0h/1h/0/0
# tb1pkw382r3plt8vx6e22mtkejnqrxl4z7jugh3w4rjmfmgezzg0xqpsdaww8z
amount=100_000,
prev_hash=FAKE_TXHASH_e5b7e2,
prev_index=0,
script_type=messages.InputScriptType.EXTERNAL,
script_pubkey=bytes.fromhex(
"5120b3a2750e21facec36b2a56d76cca6019bf517a5c45e2ea8e5b4ed191090f3003"
),
ownership_proof=bytearray.fromhex(
"534c001901019cf1b0ad730100bd7a69e987d55348bb798e2b2096a6a5713e9517655bd2021300014052d479f48d34f1ca6872d4571413660040c3e98841ab23a2c5c1f37399b71bfa6f56364b79717ee90552076a872da68129694e1b4fb0e0651373dcf56db123c5"
),
commitment_data=commitment_data,
),
messages.TxInputType(
address_n=parse_path("m/10025h/1h/0h/1h/1/0"),
amount=7_289_000,
prev_hash=FAKE_TXHASH_f982c0,
prev_index=1,
script_type=messages.InputScriptType.SPENDTAPROOT,
),
]
input_script_pubkeys = [
bytes.fromhex(
"5120b3a2750e21facec36b2a56d76cca6019bf517a5c45e2ea8e5b4ed191090f3003"
),
bytes.fromhex(
"51202f436892d90fb2665519efa3d9f0f5182859124f179486862c2cd7a78ea9ac19"
),
]
outputs = [
# Other's coinjoined output.
messages.TxOutputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# m/10025h/1h/0h/1h/1/0
address="tb1pupzczx9cpgyqgtvycncr2mvxscl790luqd8g88qkdt2w3kn7ymhsrdueu2",
amount=50_000,
script_type=messages.OutputScriptType.PAYTOADDRESS,
),
# Our coinjoined output.
messages.TxOutputType(
# tb1phkcspf88hge86djxgtwx2wu7ddghsw77d6sd7txtcxncu0xpx22shcydyf
address_n=parse_path("m/10025h/1h/0h/1h/1/1"),
amount=50_000,
script_type=messages.OutputScriptType.PAYTOTAPROOT,
),
# Our change output.
messages.TxOutputType(
# tb1pchruvduckkwuzm5hmytqz85emften5dnmkqu9uhfxwfywaqhuu0qjggqyp
address_n=parse_path("m/10025h/1h/0h/1h/1/2"),
amount=7_289_000 - 50_000 - 36_445 - 490,
script_type=messages.OutputScriptType.PAYTOTAPROOT,
),
# Other's change output.
messages.TxOutputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# m/10025h/1h/0h/1h/1/1
address="tb1pvt7lzserh8xd5m6mq0zu9s5wxkpe5wgf5ts56v44jhrr6578hz8saxup5m",
amount=100_000 - 50_000 - 500 - 490,
script_type=messages.OutputScriptType.PAYTOADDRESS,
),
# Coordinator's output.
messages.TxOutputType(
address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q",
amount=36_945,
script_type=messages.OutputScriptType.PAYTOADDRESS,
),
]
output_script_pubkeys = [
bytes.fromhex(
"5120e0458118b80a08042d84c4f0356d86863fe2bffc034e839c166ad4e8da7e26ef"
),
bytes.fromhex(
"5120bdb100a4e7ba327d364642dc653b9e6b51783bde6ea0df2ccbc1a78e3cc13295"
),
bytes.fromhex(
"5120c5c7c63798b59dc16e97d916011e99da5799d1b3dd81c2f2e93392477417e71e"
),
bytes.fromhex(
"512062fdf14323b9ccda6f5b03c5c2c28e35839a3909a2e14d32b595c63d53c7b88f"
),
bytes.fromhex("76a914a579388225827d9f2fe9014add644487808c695d88ac"),
]
coinjoin_req = make_coinjoin_request(
"www.example.com",
inputs,
input_script_pubkeys,
outputs,
output_script_pubkeys,
no_fee_indices=[],
)
with session.client as client:
client.set_expected_responses(
[
messages.PreauthorizedRequest(),
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(),
]
)
signatures, serialized_tx = btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
coinjoin_request=coinjoin_req,
preauthorized=True,
serialize=False,
chunkify=chunkify,
)
assert serialized_tx == b""
assert len(signatures) == 2
assert signatures[0] is None
assert (
signatures[1].hex()
== "c017fce789fa8db54a2ae032012d2dd6d7c76cc1c1a6f00e29b86acbf93022da8aa559009a574792c7b09b2535d288d6e03c6ed169902ed8c4c97626a83fbc11"
)
# Test for a second time.
btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
coinjoin_request=coinjoin_req,
preauthorized=True,
chunkify=chunkify,
)
# Test for a third time, number of rounds should be exceeded.
with pytest.raises(TrezorFailure, match="No preauthorized operation"):
btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
coinjoin_request=coinjoin_req,
preauthorized=True,
chunkify=chunkify,
)
def test_sign_tx_large(session: Session):
# NOTE: FAKE input tx
commitment_data = b"\x0fwww.example.com" + (1).to_bytes(ROUND_ID_LEN, "big")
own_input_count = 10
total_input_count = 400
own_output_count = 30
total_output_count = 1200
output_denom = 10_000 # sats
max_expected_delay = 80 # seconds
btc.authorize_coinjoin(
session,
coordinator="www.example.com",
max_rounds=2,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
# INPUTS.
external_input = messages.TxInputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# m/10025h/1h/0h/1h/0/0
# tb1pkw382r3plt8vx6e22mtkejnqrxl4z7jugh3w4rjmfmgezzg0xqpsdaww8z
amount=output_denom * total_output_count // total_input_count,
prev_hash=FAKE_TXHASH_e5b7e2,
prev_index=0,
script_type=messages.InputScriptType.EXTERNAL,
script_pubkey=bytes.fromhex(
"5120b3a2750e21facec36b2a56d76cca6019bf517a5c45e2ea8e5b4ed191090f3003"
),
ownership_proof=bytearray.fromhex(
"534c001901019cf1b0ad730100bd7a69e987d55348bb798e2b2096a6a5713e9517655bd2021300014052d479f48d34f1ca6872d4571413660040c3e98841ab23a2c5c1f37399b71bfa6f56364b79717ee90552076a872da68129694e1b4fb0e0651373dcf56db123c5"
),
commitment_data=commitment_data,
)
internal_inputs = [
messages.TxInputType(
address_n=parse_path(f"m/10025h/1h/0h/1h/1/{i}"),
amount=output_denom * own_output_count // own_input_count,
prev_hash=FAKE_TXHASH_f982c0,
prev_index=1,
script_type=messages.InputScriptType.SPENDTAPROOT,
)
for i in range(own_input_count)
]
internal_input_script_pubkeys = [
bytes.fromhex(
"51202f436892d90fb2665519efa3d9f0f5182859124f179486862c2cd7a78ea9ac19"
),
bytes.fromhex(
"5120bdb100a4e7ba327d364642dc653b9e6b51783bde6ea0df2ccbc1a78e3cc13295"
),
bytes.fromhex(
"5120c5c7c63798b59dc16e97d916011e99da5799d1b3dd81c2f2e93392477417e71e"
),
bytes.fromhex(
"5120148db939506345b047d945fff64691508c90da036ea3313b38b386ba3ec64ec5"
),
bytes.fromhex(
"51202cf0ba67bc759b413c0a36e33f5223aee574a979cfc1bc6e59b136cc43a8da8d"
),
bytes.fromhex(
"51202ad44db2df5b2a4d46e3655b1ab2402229676e35a3a43c4f7cae73e862c10775"
),
bytes.fromhex(
"51209e101215e14de4bece6cabd552f11e5931cb53119f43e52c10f9c1de0fd03390"
),
bytes.fromhex(
"5120f799c40379196e8507b8adf72c78b6cc12bb9fbae38f3ad744dfcd19a5777253"
),
bytes.fromhex(
"5120db0563942a92fb8c89ced9325c2660607605cd645027d64a9f641e6bc1694020"
),
bytes.fromhex(
"51208f1bbec30c355ec71f7a87c5ea06547c9b9b8a51c7834cd726e13cbb83226d16"
),
]
inputs = internal_inputs + [external_input] * (total_input_count - own_input_count)
input_script_pubkeys = internal_input_script_pubkeys + [
external_input.script_pubkey
] * (total_input_count - own_input_count)
# OUTPUTS.
external_output = messages.TxOutputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# m/10025h/1h/0h/1h/1/0
address="tb1pupzczx9cpgyqgtvycncr2mvxscl790luqd8g88qkdt2w3kn7ymhsrdueu2",
amount=output_denom,
script_type=messages.OutputScriptType.PAYTOADDRESS,
)
external_output_script_pubkey = bytes.fromhex(
"5120e0458118b80a08042d84c4f0356d86863fe2bffc034e839c166ad4e8da7e26ef"
)
internal_output = messages.TxOutputType(
# tb1phkcspf88hge86djxgtwx2wu7ddghsw77d6sd7txtcxncu0xpx22shcydyf
address_n=parse_path("m/10025h/1h/0h/1h/1/1"),
amount=output_denom,
script_type=messages.OutputScriptType.PAYTOTAPROOT,
)
internal_output_script_pubkey = bytes.fromhex(
"5120bdb100a4e7ba327d364642dc653b9e6b51783bde6ea0df2ccbc1a78e3cc13295"
)
outputs = [internal_output] * own_output_count + [external_output] * (
total_output_count - own_output_count
)
output_script_pubkeys = [internal_output_script_pubkey] * own_output_count + [
external_output_script_pubkey
] * (total_output_count - own_output_count)
coinjoin_req = make_coinjoin_request(
"www.example.com",
inputs,
input_script_pubkeys,
outputs,
output_script_pubkeys,
no_fee_indices=[],
)
start = time.time()
btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
coinjoin_request=coinjoin_req,
preauthorized=True,
serialize=False,
)
delay = time.time() - start
assert delay <= max_expected_delay
def test_sign_tx_spend(session: Session):
# NOTE: FAKE input tx
inputs = [
messages.TxInputType(
address_n=parse_path("m/10025h/1h/0h/1h/1/0"),
amount=7_289_000,
prev_hash=FAKE_TXHASH_f982c0,
prev_index=1,
script_type=messages.InputScriptType.SPENDTAPROOT,
),
]
outputs = [
# Our change output.
messages.TxOutputType(
# tb1pchruvduckkwuzm5hmytqz85emften5dnmkqu9uhfxwfywaqhuu0qjggqyp
address_n=parse_path("m/10025h/1h/0h/1h/1/2"),
amount=7_289_000 - 50_000 - 400,
script_type=messages.OutputScriptType.PAYTOTAPROOT,
),
# Payment output.
messages.TxOutputType(
address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q",
amount=50_000,
script_type=messages.OutputScriptType.PAYTOADDRESS,
),
]
# Ensure that Trezor refuses to spend from CoinJoin without user authorization.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
_, serialized_tx = btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
)
with session.client as client:
client.set_expected_responses(
[
messages.ButtonRequest(code=B.Other),
messages.UnlockedPathRequest,
request_input(0),
request_output(0),
request_output(1),
messages.ButtonRequest(code=B.ConfirmOutput),
(is_core(session), messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
request_input(0),
request_output(0),
request_output(1),
request_input(0),
request_finished(),
]
)
_, serialized_tx = btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
unlock_path=SLIP25_PATH,
)
# Transaction does not exist on the blockchain, not using assert_tx_matches()
assert (
serialized_tx.hex()
== "010000000001010ab6ad3ba09261cfb4fa1d3680cb19332a8fe4d9de9ea89aa565bd83a2c082f90100000000ffffffff02c8736e0000000000225120c5c7c63798b59dc16e97d916011e99da5799d1b3dd81c2f2e93392477417e71e50c30000000000001976a914a579388225827d9f2fe9014add644487808c695d88ac014006bc29900d39570fca291c038551817430965ac6aa26f286483559e692a14a82cfaf8e57610eae12a5af05ee1e9600acb31de4757349c0e3066701aa78f65d2a00000000"
)
def test_sign_tx_migration(session: Session):
inputs = [
messages.TxInputType(
address_n=parse_path("m/84h/1h/3h/0/12"),
amount=1_393,
prev_hash=TXHASH_2cc3c1,
prev_index=0,
script_type=messages.InputScriptType.SPENDWITNESS,
sequence=0xFFFFFFFD,
),
messages.TxInputType(
address_n=parse_path("m/84h/1h/3h/0/13"),
amount=8_159,
prev_hash=TXHASH_7f3a34,
prev_index=1,
script_type=messages.InputScriptType.SPENDWITNESS,
sequence=0xFFFFFFFD,
),
]
outputs = [
# CoinJoin account.
messages.TxOutputType(
# tb1pl3y9gf7xk2ryvmav5ar66ra0d2hk7lhh9mmusx3qvn0n09kmaghqh32ru7
address_n=parse_path("m/10025h/1h/0h/1h/0/0"),
amount=1_393 + 8_159 - 190,
script_type=messages.OutputScriptType.PAYTOTAPROOT,
),
]
# Ensure that Trezor refuses to receive to CoinJoin path without the user first authorizing access to CoinJoin paths.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
_, serialized_tx = btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
)
with session.client as client:
client.set_expected_responses(
[
messages.ButtonRequest(code=B.Other),
messages.UnlockedPathRequest,
request_input(0),
request_input(1),
request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput),
(is_core(session), messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
request_input(0),
request_meta(TXHASH_2cc3c1),
request_input(0, TXHASH_2cc3c1),
request_output(0, TXHASH_2cc3c1),
request_input(1),
request_meta(TXHASH_7f3a34),
request_input(0, TXHASH_7f3a34),
request_input(1, TXHASH_7f3a34),
request_input(2, TXHASH_7f3a34),
request_output(0, TXHASH_7f3a34),
request_output(1, TXHASH_7f3a34),
request_input(0),
request_input(1),
request_output(0),
request_input(0),
request_input(1),
request_finished(),
]
)
_, serialized_tx = btc.sign_tx(
session,
"Testnet",
inputs,
outputs,
prev_txes=TX_CACHE_TESTNET,
unlock_path=SLIP25_PATH,
)
assert_tx_matches(
serialized_tx,
hash_link="https://tbtc1.trezor.io/api/tx/3452d339045f8a35f2a083992b8f73d907f8da9653e89ee175022ca8a649b822",
tx_hex="010000000001026ddaa8a504b743b36115e6226ceba177c03fadfee0f4cc4f7bcbb13fe3c1c32c0000000000fdffffff720f3a432316f24ba96ea9c90e77268db14208c089f3698068f3f90681343a7f0100000000fdffffff019224000000000000225120fc485427c6b286466faca747ad0faf6aaf6f7ef72ef7c81a2064df3796dbea2e0247304402202f325d6e3ac764bb9d38003bb11022c5317a59ad8a2513dcabe7af9b23ff7c9f022011ff8161d9ed8cf82667b2b44dbe2f4538d41d8b353d64a01338881bce8de3690121030968050bc0647e28c09616d642cc88ab075b01e40616b53e446e7f122218a9da02483045022100f462c32fd90bf92a1aa4ca9fdb2dd9b5ef9adad6990b9bc7f9ca583e8b72d72a02202a6d9c2a8749d65bdb62a0ec4de27bad5fb13e2ae40be86afb95a477b60a1609012103e4dbaaee8486b328dba46adeb9afc3a56237aa5ca43df24eb61b04e6ca00099300000000",
)
def test_wrong_coordinator(session: Session):
# Ensure that a preauthorized GetOwnershipProof fails if the commitment_data doesn't match the coordinator.
btc.authorize_coinjoin(
session,
coordinator="www.example.com",
max_rounds=10,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
with pytest.raises(TrezorFailure, match="Unauthorized operation"):
btc.get_ownership_proof(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x0fwww.example.org" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
def test_wrong_account_type(session: Session):
params = {
"session": session,
"coordinator": "www.example.com",
"max_rounds": 10,
"max_coordinator_fee_rate": 500_000, # 0.5 %
"max_fee_per_kvbyte": 3500,
"coin_name": "Testnet",
}
# Ensure that Trezor accepts CoinJoin authorizations only for SLIP-0025 paths.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
btc.authorize_coinjoin(
**params,
n=parse_path("m/86h/1h/0h"),
script_type=messages.InputScriptType.SPENDTAPROOT,
)
# Ensure that correct parameters succeed.
btc.authorize_coinjoin(
**params,
n=parse_path("m/10025h/1h/0h/1h"),
script_type=messages.InputScriptType.SPENDTAPROOT,
)
def test_cancel_authorization(session: Session):
# Ensure that a preauthorized GetOwnershipProof fails if the commitment_data doesn't match the coordinator.
btc.authorize_coinjoin(
session,
coordinator="www.example.com",
max_rounds=10,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
device.cancel_authorization(session)
with pytest.raises(TrezorFailure, match="No preauthorized operation"):
btc.get_ownership_proof(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x0fwww.example.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
def test_get_public_key(session: Session):
ACCOUNT_PATH = parse_path("m/10025h/1h/0h/1h")
EXPECTED_XPUB = "tpubDEMKm4M3S2Grx5DHTfbX9et5HQb9KhdjDCkUYdH9gvVofvPTE6yb2MH52P9uc4mx6eFohUmfN1f4hhHNK28GaZnWRXr3b8KkfFcySo1SmXU"
# Ensure that user cannot access SLIP-25 path without UnlockPath.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
resp = btc.get_public_node(
session,
ACCOUNT_PATH,
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
# Get unlock path MAC.
with session.client as client:
client.set_expected_responses(
[
messages.ButtonRequest(code=B.Other),
messages.UnlockedPathRequest,
messages.Failure(code=messages.FailureType.ActionCancelled),
]
)
unlock_path_mac = device.unlock_path(session, n=SLIP25_PATH)
# Ensure that UnlockPath fails with invalid MAC.
invalid_unlock_path_mac = bytes([unlock_path_mac[0] ^ 1]) + unlock_path_mac[1:]
with pytest.raises(TrezorFailure, match="Invalid MAC"):
resp = btc.get_public_node(
session,
ACCOUNT_PATH,
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
unlock_path=SLIP25_PATH,
unlock_path_mac=invalid_unlock_path_mac,
)
# Ensure that user does not need to confirm access when path unlock is requested with MAC.
with session.client as client:
client.set_expected_responses(
[
messages.UnlockedPathRequest,
messages.PublicKey,
]
)
resp = btc.get_public_node(
session,
ACCOUNT_PATH,
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
)
assert resp.xpub == EXPECTED_XPUB
def test_get_address(session: Session):
# Ensure that the SLIP-0025 external chain is inaccessible without user confirmation.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/0/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=True,
)
# Unlock CoinJoin path.
with session.client as client:
client.set_expected_responses(
[
messages.ButtonRequest(code=B.Other),
messages.UnlockedPathRequest,
messages.Failure(code=messages.FailureType.ActionCancelled),
]
)
unlock_path_mac = device.unlock_path(session, SLIP25_PATH)
# Ensure that the SLIP-0025 external chain is accessible after user confirmation.
for chunkify in (True, False):
resp = btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/0/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=True,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
chunkify=chunkify,
)
assert resp == "tb1pl3y9gf7xk2ryvmav5ar66ra0d2hk7lhh9mmusx3qvn0n09kmaghqh32ru7"
resp = btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/0/1"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=False,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
)
assert resp == "tb1p64rqq64rtt7eq6p0htegalcjl2nkjz64ur8xsclc59s5845jty7skp2843"
# Ensure that the SLIP-0025 internal chain is inaccessible even with user authorization.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=True,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
)
with pytest.raises(TrezorFailure, match="Forbidden key path"):
btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/1"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=False,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
)
# Ensure that another SLIP-0025 account is inaccessible with the same MAC.
with pytest.raises(TrezorFailure, match="Forbidden key path"):
btc.get_address(
session,
"Testnet",
parse_path("m/10025h/1h/1h/1h/0/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
show_display=True,
unlock_path=SLIP25_PATH,
unlock_path_mac=unlock_path_mac,
)
def test_multisession_authorization(client: Client):
# Authorize CoinJoin with www.example1.com in session 1.
session1 = client.get_session()
btc.authorize_coinjoin(
session1,
coordinator="www.example1.com",
max_rounds=10,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
# Open a second session.
if client.protocol_version is ProtocolVersion.PROTOCOL_V2:
session_id = b"\x02"
else:
session_id = None
session2 = client.get_session(session_id=session_id)
# Authorize CoinJoin with www.example2.com in session 2.
btc.authorize_coinjoin(
session2,
coordinator="www.example2.com",
max_rounds=10,
max_coordinator_fee_rate=500_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/10025h/1h/0h/1h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDTAPROOT,
)
# Requesting a preauthorized ownership proof for www.example1.com should fail in session 2.
with pytest.raises(TrezorFailure, match="Unauthorized operation"):
ownership_proof, _ = btc.get_ownership_proof(
session2,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example1.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
# Requesting a preauthorized ownership proof for www.example2.com should succeed in session 2.
ownership_proof, _ = btc.get_ownership_proof(
session2,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example2.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
assert (
ownership_proof.hex()
== "534c0019010169d0c751442f4c9adacbd42987121d75b36e3932db217e5bb3784f368f5a4c5d00014097bb2f1f87aea1e809756a6f2ef84109613ccf1bf9b96ffb9305b6193b3942510a8650693ca8af74f0f63401baa384d0c0f7188f1d2df56b91362646c82223a8"
)
# Switch back to the first session.
session1.resume()
# Requesting a preauthorized ownership proof for www.example1.com should succeed in session 1.
ownership_proof, _ = btc.get_ownership_proof(
session1,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example1.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
assert (
ownership_proof.hex()
== "534c0019010169d0c751442f4c9adacbd42987121d75b36e3932db217e5bb3784f368f5a4c5d00014078fefa8243283cd575c885f97fd2e3405c934ab4d3e415ff5fe27d49f347bbb592e03ff6195f46c94a592799748c8dd7daea8b3fc4b2011b7e58a74ee296853b"
)
# Requesting a preauthorized ownership proof for www.example2.com should fail in session 1.
with pytest.raises(TrezorFailure, match="Unauthorized operation"):
ownership_proof, _ = btc.get_ownership_proof(
session1,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example2.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
# Cancel the authorization in session 1.
device.cancel_authorization(session1)
# Requesting a preauthorized ownership proof should fail now.
with pytest.raises(TrezorFailure, match="No preauthorized operation"):
ownership_proof, _ = btc.get_ownership_proof(
session1,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example1.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)
# Switch to the second session.
session2.resume()
# Requesting a preauthorized ownership proof for www.example2.com should still succeed in session 2.
ownership_proof, _ = btc.get_ownership_proof(
session2,
"Testnet",
parse_path("m/10025h/1h/0h/1h/1/0"),
script_type=messages.InputScriptType.SPENDTAPROOT,
user_confirmation=True,
commitment_data=b"\x10www.example2.com" + (1).to_bytes(ROUND_ID_LEN, "big"),
preauthorized=True,
)