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

feat(legacy): Implement CoinJoin signing.

This commit is contained in:
Andrew Kozlik 2023-01-02 20:10:34 +01:00 committed by matejcik
parent d74c9ba0a8
commit 68bbcbdc4d
8 changed files with 333 additions and 46 deletions

View File

@ -0,0 +1 @@
Implement coinjoin signing.

View File

@ -103,10 +103,11 @@ static uint32_t unlock_path = 0;
return; \
}
#define CHECK_UNLOCKED \
if (!session_isUnlocked()) { \
layoutHome(); \
return; \
#define CHECK_UNLOCKED \
if (!session_isUnlocked()) { \
fsm_sendFailure(FailureType_Failure_ProcessError, _("Locked")); \
layoutHome(); \
return; \
}
#define CHECK_PARAM(cond, errormsg) \

View File

@ -168,7 +168,15 @@ void fsm_msgSignTx(const SignTx *msg) {
CHECK_PARAM(msg->inputs_count + msg->outputs_count >= msg->inputs_count,
_("Value overflow"));
CHECK_PIN
const AuthorizeCoinJoin *authorization = NULL;
if (authorization_type == MessageType_MessageType_AuthorizeCoinJoin) {
authorization = config_getCoinJoinAuthorization();
if (authorization == NULL) {
return;
}
} else {
CHECK_PIN
}
PathSchema unlock = fsm_getUnlockedSchema(MessageType_MessageType_SignTx);
@ -184,11 +192,13 @@ void fsm_msgSignTx(const SignTx *msg) {
const HDNode *node = fsm_getDerivedNode(coin->curve_name, NULL, 0, NULL);
if (!node) return;
signing_init(msg, coin, node, unlock);
signing_init(msg, coin, node, authorization, unlock);
}
void fsm_msgTxAck(TxAck *msg) {
CHECK_UNLOCKED
if (!signing_is_preauthorized()) {
CHECK_UNLOCKED
}
CHECK_PARAM(msg->has_tx, _("No transaction provided"));

View File

@ -11,7 +11,6 @@ Address.address max_size:130
Address.mac type:FT_IGNORE
SignTx.coin_name max_size:21
SignTx.coinjoin_request type:FT_IGNORE
SignMessage.address_n max_count:8
SignMessage.message max_size:1024
@ -96,8 +95,9 @@ AuthorizeCoinJoin.coordinator max_size:37
AuthorizeCoinJoin.address_n max_count:8
AuthorizeCoinJoin.coin_name max_size:21
CoinJoinRequest.mask_public_key max_size:33
CoinJoinRequest.signature max_size:64
# Unused messages.
TxAckPaymentRequest skip_message:true
PaymentRequestMemo skip_message:true
CoinJoinRequest skip_message:true

View File

@ -103,7 +103,7 @@ static uint64_t total_in, external_in, total_out, change_out;
static uint64_t orig_total_in, orig_external_in, orig_total_out,
orig_change_out;
static uint32_t progress, progress_step, progress_meta_step;
static uint32_t tx_weight;
static uint32_t tx_weight, tx_base_weight, our_weight, our_inputs_len;
PathSchema unlocked_schema;
typedef struct {
@ -149,6 +149,13 @@ static bool is_replacement; // Is this a replacement transaction?
static TxInfo orig_info;
static uint8_t orig_hash[32]; // TXID of the original transaction.
/* Variables specific to CoinJoin transactions. */
static secbool is_coinjoin; // Is this a CoinJoin transaction?
static uint64_t coinjoin_coordination_fee_base;
static AuthorizeCoinJoin coinjoin_authorization;
static CoinJoinRequest coinjoin_request;
static Hasher coinjoin_request_hasher;
/* A marker for in_address_n_count to indicate a mismatch in bip32 paths in
input */
#define BIP32_NOCHANGEALLOWED 1
@ -649,6 +656,12 @@ void phase1_request_next_input(void) {
idx2 = 0;
}
if (to.is_segwit) {
tx_base_weight += TXSIZE_SEGWIT_OVERHEAD;
tx_weight += TXSIZE_SEGWIT_OVERHEAD + to.inputs_len;
our_weight += TXSIZE_SEGWIT_OVERHEAD + our_inputs_len;
}
send_req_2_output();
}
}
@ -924,32 +937,46 @@ static bool fill_input_script_pubkey(TxInputType *in) {
static bool validate_path(InputScriptType script_type,
pb_size_t address_n_count, const uint32_t *address_n,
bool has_multisig) {
// Sanity check not critical for security. The main reason for this is that we
// are not comfortable with using the same private key in multiple signature
// schemes (ECDSA and Schnorr) and we want to be sure that the user went
// through a warning screen before we sign the input.
if (!coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, unlocked_schema, true)) {
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict &&
!coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, unlocked_schema, false)) {
fsm_sendFailure(FailureType_Failure_DataError, _("Forbidden key path"));
if (is_coinjoin == sectrue) {
// Check whether the authorization matches the parameters of the input.
if (address_n_count !=
coinjoin_authorization.address_n_count + BIP32_WALLET_DEPTH ||
memcmp(address_n, coinjoin_authorization.address_n,
sizeof(uint32_t) * coinjoin_authorization.address_n_count) !=
0 ||
script_type != coinjoin_authorization.script_type) {
fsm_sendFailure(FailureType_Failure_ProcessError, _("Unauthorized path"));
signing_abort();
return false;
}
} else {
// Sanity check not critical for security. The main reason for this is that
// we are not comfortable with using the same private key in multiple
// signature schemes (ECDSA and Schnorr) and we want to be sure that the
// user went through a warning screen before we sign the input.
if (!coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, unlocked_schema, true)) {
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict &&
!coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, unlocked_schema, false)) {
fsm_sendFailure(FailureType_Failure_DataError, _("Forbidden key path"));
signing_abort();
return false;
}
if (!foreign_address_confirmed) {
if (signing_stage < STAGE_REQUEST_3_INPUT) {
if (!fsm_layoutPathWarning()) {
if (!foreign_address_confirmed) {
if (signing_stage < STAGE_REQUEST_3_INPUT) {
if (!fsm_layoutPathWarning()) {
signing_abort();
return false;
}
foreign_address_confirmed = true;
} else {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Transaction has changed during signing"));
signing_abort();
return false;
}
foreign_address_confirmed = true;
} else {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Transaction has changed during signing"));
signing_abort();
return false;
}
}
}
@ -1131,8 +1158,47 @@ static bool tx_info_init(TxInfo *tx_info, uint32_t inputs_count,
return true;
}
static bool init_coinjoin(const SignTx *msg,
const AuthorizeCoinJoin *authorization) {
if (!msg->has_coinjoin_request) {
fsm_sendFailure(FailureType_Failure_DataError,
_("Missing coinjoin request."));
signing_abort();
return false;
}
if (strcmp(coin->coin_name, authorization->coin_name) != 0) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Unauthorized operation."));
}
memcpy(&coinjoin_authorization, authorization,
sizeof(coinjoin_authorization));
memcpy(&coinjoin_request, &msg->coinjoin_request, sizeof(coinjoin_request));
// Begin hashing the CoinJoin request.
hasher_Init(&coinjoin_request_hasher, HASHER_SHA2);
hasher_Update(&coinjoin_request_hasher, (const uint8_t *)"CJR1", 4);
size_t coordinator_len =
strnlen(authorization->coordinator, sizeof(authorization->coordinator));
tx_script_hash(&coinjoin_request_hasher, coordinator_len,
(const uint8_t *)authorization->coordinator);
uint32_t slip44 = coin->coin_type & PATH_UNHARDEN_MASK;
hasher_Update(&coinjoin_request_hasher, (uint8_t *)&slip44, sizeof(slip44));
hasher_Update(&coinjoin_request_hasher,
(const uint8_t *)&coinjoin_request.fee_rate, 4);
hasher_Update(&coinjoin_request_hasher,
(const uint8_t *)&coinjoin_request.no_fee_threshold, 8);
hasher_Update(&coinjoin_request_hasher,
(const uint8_t *)&coinjoin_request.min_registrable_amount, 8);
hasher_Update(&coinjoin_request_hasher,
coinjoin_request.mask_public_key.bytes, 33);
ser_length_hash(&coinjoin_request_hasher, msg->inputs_count);
return true;
}
void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
PathSchema unlock) {
const AuthorizeCoinJoin *authorization, PathSchema unlock) {
coin = _coin;
amount_unit = msg->has_amount_unit ? msg->amount_unit : AmountUnit_BITCOIN;
serialize = msg->has_serialize ? msg->serialize : true;
@ -1162,7 +1228,10 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
}
#endif
tx_weight = 4 * size;
tx_base_weight = 4 * size;
tx_weight = tx_base_weight;
our_weight = tx_base_weight;
our_inputs_len = 0;
foreign_address_confirmed = false;
taproot_only = true;
@ -1178,10 +1247,14 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
orig_external_in = 0;
orig_total_out = 0;
orig_change_out = 0;
coinjoin_coordination_fee_base = 0;
memzero(external_inputs, sizeof(external_inputs));
memzero(&input, sizeof(TxInputType));
memzero(&output, sizeof(TxOutputType));
memzero(&resp, sizeof(TxRequest));
memzero(&coinjoin_authorization, sizeof(coinjoin_authorization));
memzero(&coinjoin_request, sizeof(coinjoin_request));
memzero(&coinjoin_request_hasher, sizeof(coinjoin_request_hasher));
is_replacement = false;
unlocked_schema = unlock;
signing = true;
@ -1190,6 +1263,13 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
// this means 50 % per phase.
progress_step = (500 << PROGRESS_PRECISION) / info.inputs_count;
is_coinjoin = (authorization != NULL) ? sectrue : secfalse;
if (is_coinjoin == sectrue) {
if (!init_coinjoin(msg, authorization)) {
return;
}
}
uint32_t branch_id = 0;
#if !BITCOIN_ONLY
branch_id = info.branch_id;
@ -1561,6 +1641,59 @@ static bool tx_info_check_outputs_hash(TxInfo *tx_info) {
return true;
}
static bool coinjoin_add_input(TxInputType *txi) {
// Masks for the signable and no_fee bits in coinjoin_flags.
const uint8_t COINJOIN_FLAGS_SIGNABLE = 0x01;
const uint8_t COINJOIN_FLAGS_NO_FEE = 0x02;
hasher_Update(&coinjoin_request_hasher, (uint8_t *)&txi->coinjoin_flags, 1);
if (txi->script_type == InputScriptType_EXTERNAL) {
return true;
}
// Compute the masking bit for the signable bit in coinjoin flags.
static CONFIDENTIAL uint8_t output_private_key[32] = {0};
uint8_t shared_secret[65] = {0};
bool res = (zkp_bip340_tweak_private_key(node.private_key, NULL,
output_private_key) == 0);
res = res && (ecdh_multiply(&secp256k1, output_private_key,
coinjoin_request.mask_public_key.bytes,
shared_secret) == 0);
memzero(&output_private_key, sizeof(output_private_key));
if (!res) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Failed to derive shared secret."));
signing_abort();
return false;
}
Hasher mask_hasher = {0};
uint8_t mask[SHA256_DIGEST_LENGTH] = {0};
hasher_Init(&mask_hasher, HASHER_SHA2);
hasher_Update(&mask_hasher, shared_secret + 1, 32);
tx_prevout_hash(&mask_hasher, txi);
hasher_Final(&mask_hasher, mask);
// Ensure that the input can be signed.
bool signable = (txi->coinjoin_flags ^ mask[0]) & COINJOIN_FLAGS_SIGNABLE;
if (!signable) {
fsm_sendFailure(FailureType_Failure_ProcessError, _("Unauthorized input"));
signing_abort();
return false;
}
// Add to coordination_fee_base, except for remixes and small inputs which are
// not charged a coordination fee.
bool no_fee = txi->coinjoin_flags & COINJOIN_FLAGS_NO_FEE;
if (txi->amount > coinjoin_request.no_fee_threshold && !no_fee) {
if (!add_amount(&coinjoin_coordination_fee_base, txi->amount)) {
return false;
}
}
return true;
}
static bool signing_add_input(TxInputType *txinput) {
// hash all input data to check it later (relevant for fee computation)
if (!tx_input_check_hash(&info.hasher_check, txinput)) {
@ -1583,6 +1716,12 @@ static bool signing_add_input(TxInputType *txinput) {
return false;
}
if (is_coinjoin == sectrue) {
if (!coinjoin_add_input(txinput)) {
return false;
}
}
#if !BITCOIN_ONLY
if (coin->decred) {
if (serialize) {
@ -1671,6 +1810,12 @@ static bool signing_add_output(TxOutputType *txoutput) {
bool is_change = is_change_output(&info, txoutput);
uint32_t output_weight = tx_output_weight(coin, txoutput);
tx_weight += output_weight;
if (is_change) {
our_weight += output_weight;
}
// Don't allow adding new external outputs in replacement transactions. There
// is actually nothing wrong with adding new external outputs, but the only
// way to pay for them would be by supplying a new (verified) external input,
@ -1704,7 +1849,7 @@ static bool signing_add_output(TxOutputType *txoutput) {
// Skip confirmation of change-outputs and skip output confirmation altogether
// in replacement transactions.
bool skip_confirm = is_change || is_replacement;
bool skip_confirm = is_change || is_replacement || (is_coinjoin == sectrue);
int co = compile_output(coin, amount_unit, &root, txoutput, &bin_output,
!skip_confirm);
if (!skip_confirm) {
@ -1957,7 +2102,7 @@ static bool signing_add_orig_output(TxOutputType *orig_output) {
return true;
}
static bool signing_confirm_tx(void) {
static bool payment_confirm_tx(void) {
if (has_unverified_external_input) {
layoutConfirmUnverifiedExternalInputs();
if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
@ -2089,6 +2234,130 @@ static bool signing_confirm_tx(void) {
return true;
}
static bool coinjoin_confirm_tx(void) {
// Minimum registrable output amount accepted by the CoinJoin coordinator. The
// CoinJoin request may specify an even lower amount.
const uint64_t MIN_REGISTRABLE_OUTPUT_AMOUNT = 5000;
// Largest possible weight of an output supported by Trezor (P2TR or P2WSH).
const uint64_t MAX_OUTPUT_WEIGHT = 4 * (8 + 1 + 1 + 1 + 32);
// Public keys for CoinJoin request signatures.
const uint8_t COINJOIN_REQ_PUBKEY[] = {
0x02, 0x57, 0x03, 0xbb, 0xe1, 0x5b, 0xb0, 0x8e, 0x98, 0x21, 0xfe,
0x64, 0xaf, 0xf6, 0xb2, 0xef, 0x1a, 0x31, 0x60, 0xe3, 0x79, 0x9d,
0xd8, 0xf0, 0xce, 0xbf, 0x2c, 0x79, 0xe8, 0x67, 0xdd, 0x12, 0x5d};
#if DEBUG_LINK
// secp256k1 public key of m/0h for "all all ... all" seed.
const uint8_t COINJOIN_REQ_PUBKEY_DEBUG[] = {
0x03, 0x0f, 0xdf, 0x5e, 0x28, 0x9b, 0x5a, 0xef, 0x53, 0x62, 0x90,
0x95, 0x3a, 0xe8, 0x1c, 0xe6, 0x0e, 0x84, 0x1f, 0xf9, 0x56, 0xf3,
0x66, 0xac, 0x12, 0x3f, 0xa6, 0x9d, 0xb3, 0xc7, 0x9f, 0x21, 0xb0};
#endif
// Finish hashing the CoinJoin request.
hasher_Update(&coinjoin_request_hasher, info.hash_prevouts,
sizeof(info.hash_prevouts));
hasher_Update(&coinjoin_request_hasher, info.hash_outputs,
sizeof(info.hash_outputs));
// Verify the CoinJoin request signature.
uint8_t coinjoin_request_digest[SHA256_DIGEST_LENGTH] = {0};
hasher_Final(&coinjoin_request_hasher, coinjoin_request_digest);
#if DEBUG_LINK
if (ecdsa_verify_digest(&secp256k1, COINJOIN_REQ_PUBKEY_DEBUG,
coinjoin_request.signature.bytes,
coinjoin_request_digest) == 0) {
// success
} else
#endif
if (ecdsa_verify_digest(&secp256k1, COINJOIN_REQ_PUBKEY,
coinjoin_request.signature.bytes,
coinjoin_request_digest) == 0) {
// success
} else {
fsm_sendFailure(FailureType_Failure_DataError,
_("Invalid signature in coinjoin request."));
signing_abort();
return false;
}
if (has_unverified_external_input) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Unverifiable external input."));
signing_abort();
return false;
}
uint64_t mining_fee = 0;
if (total_out <= total_in) {
mining_fee = total_in - total_out;
}
// The maximum mining fee that the user should be paying.
uint64_t our_max_mining_fee =
coinjoin_authorization.max_fee_per_kvbyte * ((our_weight + 3) / 4) / 1000;
// The coordination fee for the user's inputs.
uint64_t our_coordination_fee =
MIN(coinjoin_request.fee_rate,
coinjoin_authorization.max_coordinator_fee_rate) *
coinjoin_coordination_fee_base / FEE_RATE_DECIMALS / 100;
// Total fees that the user is paying.
uint64_t our_fees = 0;
if (change_out <= total_in - external_in) {
our_fees = total_in - external_in - change_out;
}
// For the next step we need to estimate an upper bound on the mining fee used
// by the coordinator. The coordinator does not include the base weight of the
// transaction when computing the mining fee, so we take this into account.
uint64_t max_fee_per_output =
MAX_OUTPUT_WEIGHT * mining_fee / (tx_weight - tx_base_weight);
// Calculate the minimum registrable output amount in a CoinJoin plus the
// mining fee that it would cost to register. Amounts below this value are
// left to the coordinator or miners and effectively constitute an extra fee
// for the user.
uint64_t min_allowed_output_amount_plus_fee =
MIN(coinjoin_request.min_registrable_amount,
MIN_REGISTRABLE_OUTPUT_AMOUNT) +
max_fee_per_output;
if (our_fees > our_coordination_fee + our_max_mining_fee +
min_allowed_output_amount_plus_fee) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Total fee over threshold."));
signing_abort();
return false;
}
if (coinjoin_authorization.max_rounds < 1) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Exceeded number of coinjoin rounds."));
signing_abort();
return false;
}
coinjoin_authorization.max_rounds -= 1;
if (coinjoin_authorization.max_rounds >= 1) {
config_setCoinJoinAuthorization(&coinjoin_authorization);
} else {
config_setCoinJoinAuthorization(NULL);
}
return true;
}
static bool signing_confirm_tx(void) {
if (is_coinjoin == sectrue) {
return coinjoin_confirm_tx();
} else {
return payment_confirm_tx();
}
}
static uint32_t signing_hash_type(const TxInputType *txinput) {
uint32_t hash_type = SIGHASH_ALL;
if (txinput->script_type == InputScriptType_SPENDTAPROOT) {
@ -2851,12 +3120,17 @@ void signing_txack(TransactionType *tx) {
return;
}
tx_weight += tx_input_weight(coin, &tx->inputs[0]);
uint32_t input_weight = tx_input_weight(coin, &tx->inputs[0]);
#if !BITCOIN_ONLY
if (coin->decred) {
tx_weight += tx_decred_witness_weight(&tx->inputs[0]);
input_weight += tx_decred_witness_weight(&tx->inputs[0]);
}
#endif
tx_weight += input_weight;
if (is_internal_input_script_type(tx->inputs[0].script_type)) {
our_weight += input_weight;
our_inputs_len += 1;
}
if (tx->inputs[0].script_type != InputScriptType_SPENDTAPROOT &&
tx->inputs[0].script_type != InputScriptType_EXTERNAL) {
@ -2884,9 +3158,6 @@ void signing_txack(TransactionType *tx) {
}
}
} else if (is_segwit_input_script_type(tx->inputs[0].script_type)) {
if (!to.is_segwit) {
tx_weight += TXSIZE_SEGWIT_OVERHEAD + to.inputs_len;
}
#if !ENABLE_SEGWIT_NONSEGWIT_MIXING
// don't mix segwit and non-segwit inputs
if (idx1 == 0) {
@ -3006,7 +3277,6 @@ void signing_txack(TransactionType *tx) {
!signing_add_output(&tx->outputs[0])) {
return;
}
tx_weight += tx_output_weight(coin, &tx->outputs[0]);
if (tx->outputs[0].has_orig_hash) {
memcpy(&output, &tx->outputs[0], sizeof(output));
@ -3663,3 +3933,7 @@ void signing_abort(void) {
memzero(&root, sizeof(root));
memzero(&node, sizeof(node));
}
bool signing_is_preauthorized(void) {
return signing && (is_coinjoin == sectrue);
}

View File

@ -29,8 +29,9 @@
#include "messages-bitcoin.pb.h"
void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
PathSchema unlock);
const AuthorizeCoinJoin *authorization, PathSchema unlock);
void signing_abort(void);
void signing_txack(TransactionType *tx);
bool signing_is_preauthorized(void);
#endif

View File

@ -44,7 +44,6 @@ ROUND_ID_LEN = 32
SLIP25_PATH = parse_path("m/10025h")
@pytest.mark.skip_t1
@pytest.mark.setup_client(pin=PIN)
def test_sign_tx(client: Client):
# NOTE: FAKE input tx
@ -251,7 +250,6 @@ def test_sign_tx(client: Client):
)
@pytest.mark.skip_t1
def test_sign_tx_large(client: Client):
# NOTE: FAKE input tx
@ -474,7 +472,6 @@ def test_sign_tx_spend(client: Client):
)
@pytest.mark.skip_t1
def test_wrong_coordinator(client: Client):
# Ensure that a preauthorized GetOwnershipProof fails if the commitment_data doesn't match the coordinator.
@ -501,7 +498,6 @@ def test_wrong_coordinator(client: Client):
)
@pytest.mark.skip_t1
def test_wrong_account_type(client: Client):
params = {
"client": client,
@ -528,7 +524,6 @@ def test_wrong_account_type(client: Client):
)
@pytest.mark.skip_t1
def test_cancel_authorization(client: Client):
# Ensure that a preauthorized GetOwnershipProof fails if the commitment_data doesn't match the coordinator.
@ -693,7 +688,6 @@ def test_get_address(client: Client):
)
@pytest.mark.skip_t1
def test_multisession_authorization(client: Client):
# Authorize CoinJoin with www.example1.com in session 1.
btc.authorize_coinjoin(

View File

@ -1,9 +1,15 @@
{
"T1": {
"device_tests": {
"T1_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "60e18060db1756fd0199dd381b41c4b20da89ed1acfe3e1378e2f511d97732bf",
"T1_bitcoin-test_authorize_coinjoin.py::test_get_address": "402c3f89f6ad5fd3bc78f804b376c36c918fc685cc2c77b38c6ae030af738d22",
"T1_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "9b3c916759b79048a4ab3e3fe8ce0ea0cf8d4ae6cfb66a5d712f21edfdb01782",
"T1_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "d2ac218c6bc361f732a65daee2ae7183ca935a7b246545ebe6346dbd33d0a702",
"T1_bitcoin-test_authorize_coinjoin.py::test_sign_tx": "f630f414daeeb8e0aa3d01ed38a512cb9bfb83abd0c022e01dd02d6824ade61d",
"T1_bitcoin-test_authorize_coinjoin.py::test_sign_tx_large": "4365228d89327c72b2d0fa4d487fb28c19ed832e94d8c0c71db83b060c06074a",
"T1_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "edeb75022cc6bff15d1274ba9bac4cf41dd8ea5771436010ae07fd441dc73b69",
"T1_bitcoin-test_authorize_coinjoin.py::test_wrong_account_type": "60e18060db1756fd0199dd381b41c4b20da89ed1acfe3e1378e2f511d97732bf",
"T1_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "60e18060db1756fd0199dd381b41c4b20da89ed1acfe3e1378e2f511d97732bf",
"T1_bitcoin-test_bcash.py::test_attack_change_input": "6111e313995d38c3970c92e48047fe4088c83666c64c6c859f69a232ad62829b",
"T1_bitcoin-test_bcash.py::test_send_bch_change": "6111e313995d38c3970c92e48047fe4088c83666c64c6c859f69a232ad62829b",
"T1_bitcoin-test_bcash.py::test_send_bch_multisig_change": "0962a2e630e06b6d20282cc241be40f41bc1648d0a26247c7c008f32a197d0cb",