1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-17 21:22:10 +00:00

chore(core): Rework checking of CoinJoin parameters.

[no changelog]
This commit is contained in:
Andrew Kozlik 2022-03-15 19:29:39 +01:00 committed by Andrew Kozlik
parent 8be6689150
commit 94d1a49eea
10 changed files with 121 additions and 158 deletions

View File

@ -19,7 +19,7 @@ if TYPE_CHECKING:
from apps.common.coininfo import CoinInfo
FEE_PER_ANONYMITY_DECIMALS = const(9)
FEE_RATE_DECIMALS = const(8)
class CoinJoinAuthorization:
@ -47,11 +47,11 @@ class CoinJoinAuthorization:
and txi.script_type == self.params.script_type
)
def approve_sign_tx(self, msg: SignTx, fee: int) -> bool:
if self.params.max_total_fee < fee or msg.coin_name != self.params.coin_name:
def approve_sign_tx(self, msg: SignTx) -> bool:
if self.params.max_rounds < 1 or msg.coin_name != self.params.coin_name:
return False
self.params.max_total_fee -= fee
self.params.max_rounds -= 1
authorization.set(self.params)
return True

View File

@ -2,23 +2,25 @@ from micropython import const
from typing import TYPE_CHECKING
from trezor import ui, wire
from trezor.enums import ButtonRequestType
from trezor.messages import AuthorizeCoinJoin, Success
from trezor.strings import format_amount
from trezor.ui.layouts import confirm_action, confirm_coinjoin
from trezor.ui.layouts import confirm_action, confirm_coinjoin, confirm_metadata
from apps.common import authorization
from apps.common import authorization, safety_checks
from apps.common.paths import validate_path
from .authorization import FEE_PER_ANONYMITY_DECIMALS
from .authorization import FEE_RATE_DECIMALS
from .common import BIP32_WALLET_DEPTH
from .keychain import validate_path_against_script_type, with_keychain
from .sign_tx.layout import format_coin_amount
if TYPE_CHECKING:
from apps.common.coininfo import CoinInfo
from apps.common.keychain import Keychain
_MAX_COORDINATOR_LEN = const(36)
_MAX_ROUNDS = const(500)
_MAX_COORDINATOR_FEE_RATE = 5 * pow(10, FEE_RATE_DECIMALS) # 5 %
@with_keychain
@ -30,19 +32,21 @@ async def authorize_coinjoin(
):
raise wire.DataError("Invalid coordinator name.")
if msg.max_rounds > _MAX_ROUNDS and safety_checks.is_strict():
raise wire.DataError("The number of rounds is unexpectedly large.")
if (
msg.max_coordinator_fee_rate > _MAX_COORDINATOR_FEE_RATE
and safety_checks.is_strict()
):
raise wire.DataError("The coordination fee rate is unexpectedly large.")
if msg.max_fee_per_kvbyte > 10 * coin.maxfee_kb and safety_checks.is_strict():
raise wire.DataError("The fee per vbyte is unexpectedly large.")
if not msg.address_n:
raise wire.DataError("Empty path not allowed.")
validation_path = msg.address_n + [0] * BIP32_WALLET_DEPTH
await validate_path(
ctx,
keychain,
validation_path,
validate_path_against_script_type(
coin, address_n=validation_path, script_type=msg.script_type
),
)
await confirm_action(
ctx,
"coinjoin_coordinator",
@ -53,17 +57,27 @@ async def authorize_coinjoin(
icon=ui.ICON_RECOVERY,
)
if msg.fee_per_anonymity:
fee_per_anonymity: str | None = format_amount(
msg.fee_per_anonymity, FEE_PER_ANONYMITY_DECIMALS
)
else:
fee_per_anonymity = None
max_fee_per_vbyte = format_amount(msg.max_fee_per_kvbyte, 3)
await confirm_coinjoin(ctx, coin.coin_name, msg.max_rounds, max_fee_per_vbyte)
await confirm_coinjoin(
validation_path = msg.address_n + [0] * BIP32_WALLET_DEPTH
await validate_path(
ctx,
fee_per_anonymity,
format_coin_amount(msg.max_total_fee, coin, msg.amount_unit),
keychain,
validation_path,
validate_path_against_script_type(
coin, address_n=validation_path, script_type=msg.script_type
),
)
if msg.max_fee_per_kvbyte > coin.maxfee_kb:
await confirm_metadata(
ctx,
"fee_over_threshold",
"High mining fee",
"The mining fee of\n{} sats/vbyte\nis unexpectedly high.",
max_fee_per_vbyte,
ButtonRequestType.FeeOverThreshold,
)
authorization.set(msg)

View File

@ -7,7 +7,7 @@ from trezor.ui.components.common.confirm import INFO
from apps.common import safety_checks
from ..authorization import FEE_PER_ANONYMITY_DECIMALS
from ..authorization import FEE_RATE_DECIMALS
from ..common import input_is_external_unverified
from ..keychain import validate_path_against_script_type
from . import helpers, tx_weight
@ -335,18 +335,6 @@ class CoinJoinApprover(Approver):
# Upper bound on the user's contribution to the weight of the transaction.
self.our_weight = tx_weight.TxWeightCalculator()
# base for coordinator fee to be multiplied by fee_per_anonymity
self.coordinator_fee_base = 0
# size of the current group of outputs
self.group_size = 0
# number of our change outputs in the current group
self.group_our_count = 0
# amount of each output in the current group
self.group_amount = 0
async def add_internal_input(self, txi: TxInput) -> None:
self.our_weight.add_input(txi)
if not self.authorization.check_sign_tx_input(txi, self.coin):
@ -374,7 +362,6 @@ class CoinJoinApprover(Approver):
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
super().add_change_output(txo, script_pubkey)
self.our_weight.add_output(script_pubkey)
self.group_our_count += 1
async def add_payment_request(
self, msg: TxAckPaymentRequest, keychain: Keychain
@ -395,11 +382,16 @@ class CoinJoinApprover(Approver):
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
await super().approve_tx(tx_info, orig_txs)
max_fee_per_vbyte = self.authorization.params.max_fee_per_kvbyte / 1000
max_coordinator_fee_rate = (
self.authorization.params.max_coordinator_fee_rate
/ pow(10, FEE_RATE_DECIMALS + 2)
)
# The mining fee of the transaction as a whole.
mining_fee = self.total_in - self.total_out
# mining_fee > (coin.maxfee per byte * tx size)
if mining_fee > (self.coin.maxfee_kb / 1000) * (self.weight.get_total() / 4):
if mining_fee > max_fee_per_vbyte * self.weight.get_total() / 4:
raise wire.ProcessError("Mining fee over threshold")
# The maximum mining fee that the user should be paying assuming that participants share
@ -410,30 +402,19 @@ class CoinJoinApprover(Approver):
/ (self.weight.get_total() - self.MAX_OUTPUT_WEIGHT)
)
# The coordinator fee for the user's outputs.
our_coordinator_fee = self._get_coordinator_fee()
# The maximum coordination fee for the user's inputs.
our_max_coordinator_fee = max_coordinator_fee_rate * (
self.total_in - self.external_in
)
# Total fees that the user is paying.
our_fees = self.total_in - self.external_in - self.change_out
if our_fees > our_coordinator_fee + our_max_mining_fee:
raise wire.ProcessError("Total fee over threshold")
if our_fees > our_max_coordinator_fee + our_max_mining_fee:
raise wire.ProcessError("Total fee over threshold.")
if not self.authorization.approve_sign_tx(tx_info.tx, our_fees):
raise wire.ProcessError("Fees exceed authorized limit")
# Coordinator fee calculation.
def _get_coordinator_fee(self) -> float:
# Add the coordinator fee for the last group of outputs.
self._new_group(0)
decimal_divisor: float = pow(10, FEE_PER_ANONYMITY_DECIMALS + 2)
return (
self.coordinator_fee_base
* self.authorization.params.fee_per_anonymity
/ decimal_divisor
)
if not self.authorization.approve_sign_tx(tx_info.tx):
raise wire.ProcessError("Exceeded number of CoinJoin rounds.")
def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
super()._add_output(txo, script_pubkey)
@ -441,27 +422,3 @@ class CoinJoinApprover(Approver):
# All CoinJoin outputs must be accompanied by a signed payment request.
if txo.payment_req_index is None:
raise wire.DataError("Missing payment request.")
# Assumption: CoinJoin outputs are grouped by amount. (If this assumption is
# not satisfied, then we will compute a lower coordinator fee, which may lead
# us to wrongfully decline the transaction.)
if self.group_amount != txo.amount:
self._new_group(txo.amount)
self.group_size += 1
def _new_group(self, amount: int) -> None:
# Add the base coordinator fee for the previous group of outputs.
# Skip groups of size 1, because those must be change-outputs.
if self.group_size > 1:
self.coordinator_fee_base += (
self.group_our_count * self.group_size * self.group_amount
)
# Check whether our outputs gained any anonymity.
if self.group_our_count and self.group_size > self.group_our_count:
self.anonymity = True
self.group_size = 0
self.group_our_count = 0
self.group_amount = amount

View File

@ -988,14 +988,17 @@ async def confirm_modify_fee(
async def confirm_coinjoin(
ctx: wire.GenericContext, fee_per_anonymity: str | None, total_fee: str
ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str
) -> None:
text = Text("Authorize CoinJoin", ui.ICON_RECOVERY, new_lines=False)
if fee_per_anonymity is not None:
text.normal("Fee per anonymity set:\n")
text.bold(f"{fee_per_anonymity} %\n")
text.normal("Maximum total fees:\n")
text.bold(total_fee)
text.normal("Coin name: ")
text.bold(f"{coin_name}\n")
text.br_half()
text.normal("Maximum rounds: ")
text.bold(f"{max_rounds}\n")
text.br_half()
text.normal("Maximum mining fee:\n")
text.bold(f"{max_fee_per_vbyte} sats/vbyte")
await raise_if_cancelled(
interact(ctx, HoldToConfirm(text), "coinjoin_final", ButtonRequestType.Other)
)

View File

@ -409,7 +409,7 @@ async def confirm_modify_fee(
async def confirm_coinjoin(
ctx: wire.GenericContext, fee_per_anonymity: str | None, total_fee: str
ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str
) -> None:
raise NotImplementedError

View File

@ -24,13 +24,14 @@ class TestApprover(unittest.TestCase):
def setUp(self):
self.coin = coins.by_name('Bitcoin')
self.fee_per_anonymity_percent = 0.003
self.max_fee_rate_percent = 0.3
self.coordinator_name = "www.example.com"
self.msg_auth = AuthorizeCoinJoin(
coordinator=self.coordinator_name,
max_total_fee=40000,
fee_per_anonymity=int(self.fee_per_anonymity_percent * 10**9),
max_rounds=10,
max_coordinator_fee_rate=int(self.max_fee_rate_percent * 10**8),
max_fee_per_kvbyte=7000,
address_n=[H_(84), H_(0), H_(0)],
coin_name=self.coin.coin_name,
script_type=InputScriptType.SPENDWITNESS,
@ -39,13 +40,15 @@ class TestApprover(unittest.TestCase):
def test_coinjoin_lots_of_inputs(self):
denomination = 10000000
coordinator_fee = int(self.max_fee_rate_percent / 100 * denomination)
fees = coordinator_fee + 500
# Other's inputs.
inputs = [
TxInput(
prev_hash=b"",
prev_index=0,
amount=denomination + 1000000 * (i + 1),
amount=denomination,
script_pubkey=bytes(22),
script_type=InputScriptType.EXTERNAL,
sequence=0xffffffff,
@ -54,13 +57,12 @@ class TestApprover(unittest.TestCase):
]
# Our input.
inputs.insert(
30,
inputs.insert(30,
TxInput(
prev_hash=b"",
prev_index=0,
address_n=[H_(84), H_(0), H_(0), 0, 1],
amount=denomination + 1000000,
amount=denomination,
script_type=InputScriptType.SPENDWITNESS,
sequence=0xffffffff,
)
@ -70,7 +72,7 @@ class TestApprover(unittest.TestCase):
outputs = [
TxOutput(
address="",
amount=denomination,
amount=denomination-fees,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99)
@ -82,32 +84,7 @@ class TestApprover(unittest.TestCase):
TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 0, 2],
amount=denomination,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)
)
coordinator_fee = int(self.fee_per_anonymity_percent / 100 * len(outputs) * denomination)
fees = coordinator_fee + 10000
total_coordinator_fee = coordinator_fee * len(outputs)
# Other's change-outputs.
outputs.extend(
TxOutput(
address="",
amount=1000000 * (i + 1) - fees,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99)
)
# Our change-output.
outputs.append(
TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 1, 1],
amount=1000000 - fees,
amount=denomination-fees,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)
@ -117,7 +94,7 @@ class TestApprover(unittest.TestCase):
outputs.append(
TxOutput(
address="",
amount=total_coordinator_fee,
amount=coordinator_fee * len(outputs),
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)

View File

@ -19,8 +19,9 @@ class TestAuthorization(unittest.TestCase):
def setUp(self):
self.msg_auth = AuthorizeCoinJoin(
coordinator="www.example.com",
max_total_fee=40000,
fee_per_anonymity=int(0.003 * 10**9),
max_rounds=3,
max_coordinator_fee_rate=int(0.3 * 10**8),
max_fee_per_kvbyte=7000,
address_n=[H_(84), H_(0), H_(0)],
coin_name=self.coin.coin_name,
script_type=InputScriptType.SPENDWITNESS,
@ -102,10 +103,10 @@ class TestAuthorization(unittest.TestCase):
msg = SignTx(outputs_count=10, inputs_count=21, coin_name=self.coin.coin_name, lock_time=0)
self.assertTrue(self.authorization.approve_sign_tx(msg, 10000))
self.assertTrue(self.authorization.approve_sign_tx(msg, 20000))
self.assertFalse(self.authorization.approve_sign_tx(msg, 10001))
self.assertTrue(self.authorization.approve_sign_tx(msg, 10000))
self.assertTrue(self.authorization.approve_sign_tx(msg))
self.assertTrue(self.authorization.approve_sign_tx(msg))
self.assertTrue(self.authorization.approve_sign_tx(msg))
self.assertFalse(self.authorization.approve_sign_tx(msg))
if __name__ == '__main__':

View File

@ -60,8 +60,9 @@ def test_sign_tx(client: Client):
btc.authorize_coinjoin(
client,
coordinator="www.example.com",
max_total_fee=10_010,
fee_per_anonymity=5_000_000, # 0.005 %
max_rounds=2,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,
@ -141,21 +142,21 @@ def test_sign_tx(client: Client):
messages.TxOutputType(
# tb1qr5p6f5sk09sms57ket074vywfymuthlgud7xyx
address_n=parse_path("m/84h/1h/0h/1/2"),
amount=7_289_000 - 50_000 - 5 - 5_000,
amount=7_289_000 - 50_000 - 36_445 - 490,
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
# Other's change output.
messages.TxOutputType(
address="tb1q9cqhdr9ydetjzrct6tyeuccws9505hl96azwxk",
amount=100_000 - 50_000 - 5 - 5_000,
amount=100_000 - 50_000 - 500 - 490,
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
# Coordinator's output.
messages.TxOutputType(
address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q",
amount=10,
amount=36_945,
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
@ -217,7 +218,7 @@ def test_sign_tx(client: Client):
assert (
serialized_tx.hex()
== "010000000001028abbd1cf69e00fbf60fa3ba475dccdbdba4a859ffa6bfd1ee820a75b1be2b7e50000000000ffffffff0ab6ad3ba09261cfb4fa1d3680cb19332a8fe4d9de9ea89aa565bd83a2c082f90100000000ffffffff0550c3000000000000160014b7a51ede0a66ae36558a3ab097ad86bbd800786150c3000000000000160014167dae080bca35c9ea49c0c8335dcc4b252a1d70cb616e00000000001600141d03a4d2167961b853d6cadfeab08e4937c5dfe8c3af0000000000001600142e01768ca46e57210f0bd2c99e630e8168fa5fe50a000000000000001976a914a579388225827d9f2fe9014add644487808c695d88ac0002473044022010bcbb2ae63db4bfdfdce298bcf3e302e2b1923d02ff57a2155eaae65fdb2949022026289b6d04d7615bf53b7aa0030b25619c465d639b233297b10d0da9ce1a6ca4012103505647c017ff2156eb6da20fae72173d3b681a1d0a629f95f49e884db300689f00000000"
== "010000000001028abbd1cf69e00fbf60fa3ba475dccdbdba4a859ffa6bfd1ee820a75b1be2b7e50000000000ffffffff0ab6ad3ba09261cfb4fa1d3680cb19332a8fe4d9de9ea89aa565bd83a2c082f90100000000ffffffff0550c3000000000000160014b7a51ede0a66ae36558a3ab097ad86bbd800786150c3000000000000160014167dae080bca35c9ea49c0c8335dcc4b252a1d7011e56d00000000001600141d03a4d2167961b853d6cadfeab08e4937c5dfe872bf0000000000001600142e01768ca46e57210f0bd2c99e630e8168fa5fe551900000000000001976a914a579388225827d9f2fe9014add644487808c695d88ac000247304402204df07c5baacca264696cc4270665cb759be05387dead8942bd41f20309ceb29002203e685b8e9483435d9b70006bb424b5fef7249415a0f212abdf202b5d62859698012103505647c017ff2156eb6da20fae72173d3b681a1d0a629f95f49e884db300689f00000000"
)
# Test for a second time.
@ -231,8 +232,8 @@ def test_sign_tx(client: Client):
preauthorized=True,
)
# Test for a third time, fees should exceed max_total_fee.
with pytest.raises(TrezorFailure, match="Fees exceed authorized limit"):
# Test for a third time, number of rounds should be exceeded.
with pytest.raises(TrezorFailure, match="Exceeded number of CoinJoin rounds"):
btc.sign_tx(
client,
"Testnet",
@ -251,8 +252,9 @@ def test_unfair_fee(client: Client):
btc.authorize_coinjoin(
client,
coordinator="www.example.com",
max_total_fee=10_000,
fee_per_anonymity=5_000_000, # 0.005 %
max_rounds=1,
max_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,
@ -302,21 +304,21 @@ def test_unfair_fee(client: Client):
messages.TxOutputType(
# tb1qr5p6f5sk09sms57ket074vywfymuthlgud7xyx
address_n=parse_path("m/84h/1h/0h/1/2"),
amount=7_289_000 - 50_000 - 5 - 6_000, # unfair mining fee
amount=7_289_000 - 50_000 - 36_445 - 600, # unfair mining fee
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
# Other's change output.
messages.TxOutputType(
address="tb1q9cqhdr9ydetjzrct6tyeuccws9505hl96azwxk",
amount=100_000 - 50_000 - 5 - 4_000,
amount=100_000 - 50_000 - 500 - 400,
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
# Coordinator's output.
messages.TxOutputType(
address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q",
amount=10,
amount=36_945,
script_type=messages.OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
),
@ -350,8 +352,10 @@ def test_wrong_coordinator(client: Client):
btc.authorize_coinjoin(
client,
max_total_fee=50_000,
coordinator="www.example.com",
max_rounds=10,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,
@ -374,8 +378,10 @@ def test_cancel_authorization(client: Client):
btc.authorize_coinjoin(
client,
max_total_fee=50_000,
coordinator="www.example.com",
max_rounds=10,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,
@ -399,8 +405,10 @@ def test_multisession_authorization(client: Client):
# Authorize CoinJoin with www.example1.com in session 1.
btc.authorize_coinjoin(
client,
max_total_fee=50_000,
coordinator="www.example1.com",
max_rounds=10,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,
@ -413,8 +421,10 @@ def test_multisession_authorization(client: Client):
# Authorize CoinJoin with www.example2.com in session 2.
btc.authorize_coinjoin(
client,
max_total_fee=50_000,
coordinator="www.example2.com",
max_rounds=10,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,

View File

@ -229,8 +229,9 @@ def test_experimental_features(client: Client):
btc.authorize_coinjoin(
client,
coordinator="www.example.com",
max_total_fee=10_010,
fee_per_anonymity=5_000_000, # 0.005 %
max_rounds=10,
max_coordinator_fee_rate=50_000_000, # 0.5 %
max_fee_per_kvbyte=3500,
n=parse_path("m/84h/1h/0h"),
coin_name="Testnet",
script_type=messages.InputScriptType.SPENDWITNESS,

View File

@ -600,11 +600,11 @@
"TT_binance-test_sign_tx.py::test_binance_sign_message[message0-expected_response0]": "fab7b62cab76ae2e4370d9ce113569b3aa2d089a5dbc365c8920731f756a4f37",
"TT_binance-test_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "805fc5ef8074c3f5cfee5f7128c2cd068fef42f4f01f9450578f50e791ff811f",
"TT_binance-test_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "323e0a474e71ede187ee1332e42952aeca501b42da95f88b2bad5445a3db858c",
"TT_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "9887e0f4da5c7800f832396e50391beb03229c8edcb5b0e078433703cac6e0d3",
"TT_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "fd412d086cf4ff677f6ae266e88de725549505d9a2abc1c2ba36f8f854461694",
"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx": "c48bbbaf032eacb42567e49f5b4b82ed51fe97bb613e165dca2c51207199f236",
"TT_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "2e5cffe7bd0dc6034852d21612fba8cf1ee3c45a14e76140a4c2786f360f54f0",
"TT_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "65c9435e10e3dede19169a9b02d373bd5acb22eba0ba3b31324271025d65d5a7",
"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx": "87b81a29fe6e27fdfedfdbb1953b3d0178786749eadbb0fe01509c1af8075de5",
"TT_bitcoin-test_authorize_coinjoin.py::test_unfair_fee": "ab1aa516510b627b8ffc65391c1c113922ab08f48baf295861a9b597f27f8ea1",
"TT_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "9887e0f4da5c7800f832396e50391beb03229c8edcb5b0e078433703cac6e0d3",
"TT_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "2e5cffe7bd0dc6034852d21612fba8cf1ee3c45a14e76140a4c2786f360f54f0",
"TT_bitcoin-test_bcash.py::test_attack_change_input": "b824d3eb233f6ba2567dd052fa4b52e9a1f170fe4a39af55c1cc262683f188b9",
"TT_bitcoin-test_bcash.py::test_send_bch_change": "b824d3eb233f6ba2567dd052fa4b52e9a1f170fe4a39af55c1cc262683f188b9",
"TT_bitcoin-test_bcash.py::test_send_bch_external_presigned": "4ea82258a094d49829ab7240465de9cb06198d7b0bb1a56b66bfbda5ccc676c1",
@ -1452,7 +1452,7 @@
"TT_test_msg_applysettings.py::test_apply_settings_passphrase": "b7af5acc4678edc97c6cab669554b20ec63560d8bd36c40f9c292b40992b590c",
"TT_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "ec6ae42f5d061e40d6c4512a743bc2ac1564455a50a838b6f42fd4c7225d8079",
"TT_test_msg_applysettings.py::test_apply_settings_rotation": "1f6da326281b5e4ddff444f96403683badae200f1d4b5cc6b044aeab99141d16",
"TT_test_msg_applysettings.py::test_experimental_features": "d06a7f434a7a76a996ef0a9b1c2dc44b508bb9c361551f65f171fd762c0f4d77",
"TT_test_msg_applysettings.py::test_experimental_features": "0908b33f42fc4a3405dcfa919e2e00171999a589897d2eac04a720227a7a4bb5",
"TT_test_msg_applysettings.py::test_label_too_long": "c09de07fbbf1e047442180e2facb5482d06a1a428891b875b7dd93c9e4704ae1",
"TT_test_msg_applysettings.py::test_safety_checks": "71ac970ca0d87f1ef70a8605dcc4db478a9190333307ff37ee90dd3d3e97e0fa",
"TT_test_msg_backup_device.py::test_backup_bip39": "9b572b12da20b516222ecb0a76fba6d6de5193647405b67625140ed3ed45049c",