1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-19 04:58:11 +00:00

feat(legacy): Support native SegWit external inputs with non-ownership proof.

This commit is contained in:
Andrew Kozlik 2022-12-21 17:29:53 +01:00 committed by matejcik
parent fa2d618f7d
commit ec9756cabd
9 changed files with 261 additions and 43 deletions

View File

@ -0,0 +1 @@
Support native SegWit external inputs with non-ownership proof.

View File

@ -150,6 +150,9 @@ bool fsm_checkCoinPath(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n, uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool show_warning); bool has_multisig, bool show_warning);
bool fsm_getOwnershipId(uint8_t *script_pubkey, size_t script_pubkey_size,
uint8_t ownership_id[32]);
void fsm_abortWorkflows(void); void fsm_abortWorkflows(void);
#endif #endif

View File

@ -528,12 +528,14 @@ static bool formatFeeRate(uint64_t fee, uint64_t tx_weight, char *output,
} }
void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit, void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
uint64_t total_in, uint64_t total_out, uint64_t change_out, uint64_t total_in, uint64_t external_in,
uint64_t total_out, uint64_t change_out,
uint64_t tx_weight) { uint64_t tx_weight) {
char str_out[32] = {0}; char str_out[32] = {0};
formatAmountDifference(coin, amount_unit, total_in, change_out, str_out, formatAmountDifference(coin, amount_unit, total_in, change_out, str_out,
sizeof(str_out)); sizeof(str_out));
if (external_in == 0) {
char str_fee[32] = {0}; char str_fee[32] = {0};
formatAmountDifference(coin, amount_unit, total_in, total_out, str_fee, formatAmountDifference(coin, amount_unit, total_in, total_out, str_fee,
sizeof(str_fee)); sizeof(str_fee));
@ -549,6 +551,14 @@ void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("Confirm sending:"), str_out, _("including fee:"), _("Confirm sending:"), str_out, _("including fee:"),
str_fee, show_fee_rate ? str_fee_rate : NULL, NULL); str_fee, show_fee_rate ? str_fee_rate : NULL, NULL);
} else {
char str_spend[32] = {0};
formatAmountDifference(coin, amount_unit, total_in - external_in,
change_out, str_spend, sizeof(str_spend));
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("You are contributing:"), str_spend,
_("to the total amount:"), str_out, NULL, NULL);
}
} }
void layoutConfirmReplacement(const char *description, uint8_t txid[32]) { void layoutConfirmReplacement(const char *description, uint8_t txid[32]) {

View File

@ -57,7 +57,8 @@ void layoutConfirmOutput(const CoinInfo *coin, AmountUnit amount_unit,
void layoutConfirmOmni(const uint8_t *data, uint32_t size); void layoutConfirmOmni(const uint8_t *data, uint32_t size);
void layoutConfirmOpReturn(const uint8_t *data, uint32_t size); void layoutConfirmOpReturn(const uint8_t *data, uint32_t size);
void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit, void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
uint64_t total_in, uint64_t total_out, uint64_t change_out, uint64_t total_in, uint64_t external_in,
uint64_t total_out, uint64_t change_out,
uint64_t tx_weight); uint64_t tx_weight);
void layoutConfirmReplacement(const char *description, uint8_t txid[32]); void layoutConfirmReplacement(const char *description, uint8_t txid[32]);
void layoutConfirmModifyOutput(const CoinInfo *coin, AmountUnit amount_unit, void layoutConfirmModifyOutput(const CoinInfo *coin, AmountUnit amount_unit,

View File

@ -72,6 +72,7 @@ enum {
} signing_stage; } signing_stage;
static bool foreign_address_confirmed; // indicates that user approved warning static bool foreign_address_confirmed; // indicates that user approved warning
static bool taproot_only; // indicates whether all internal inputs are Taproot static bool taproot_only; // indicates whether all internal inputs are Taproot
static bool has_unverified_external_input;
static uint32_t idx1; // The index of the input or output in the current tx static uint32_t idx1; // The index of the input or output in the current tx
// which is being processed, signed or serialized. // which is being processed, signed or serialized.
static uint32_t idx2; // The index of the input or output in the original tx static uint32_t idx2; // The index of the input or output in the original tx
@ -98,8 +99,9 @@ static uint8_t sig[64]; // Used in Phase 1 to store signature of original tx
#if !BITCOIN_ONLY #if !BITCOIN_ONLY
static uint8_t decred_hash_prefix[32]; static uint8_t decred_hash_prefix[32];
#endif #endif
static uint64_t total_in, total_out, change_out; static uint64_t total_in, external_in, total_out, change_out;
static uint64_t orig_total_in, orig_total_out, orig_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 progress, progress_step, progress_meta_step;
static uint32_t tx_weight; static uint32_t tx_weight;
@ -362,14 +364,6 @@ static bool is_external_input(uint32_t i) {
return external_inputs[i / 32] & (1 << (i % 32)); return external_inputs[i / 32] & (1 << (i % 32));
} }
static bool has_external_input(void) {
uint32_t sum = 0;
for (size_t i = 0; i < sizeof(external_inputs) / sizeof(uint32_t); ++i) {
sum |= external_inputs[i];
}
return sum != 0;
}
void send_req_1_input(void) { void send_req_1_input(void) {
signing_stage = STAGE_REQUEST_1_INPUT; signing_stage = STAGE_REQUEST_1_INPUT;
resp.has_request_type = true; resp.has_request_type = true;
@ -1100,13 +1094,16 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin,
foreign_address_confirmed = false; foreign_address_confirmed = false;
taproot_only = true; taproot_only = true;
has_unverified_external_input = false;
signatures = 0; signatures = 0;
idx1 = 0; idx1 = 0;
total_in = 0; total_in = 0;
external_in = 0;
total_out = 0; total_out = 0;
change_out = 0; change_out = 0;
change_count = 0; change_count = 0;
orig_total_in = 0; orig_total_in = 0;
orig_external_in = 0;
orig_total_out = 0; orig_total_out = 0;
orig_change_out = 0; orig_change_out = 0;
memzero(external_inputs, sizeof(external_inputs)); memzero(external_inputs, sizeof(external_inputs));
@ -1175,6 +1172,13 @@ static bool signing_validate_input(const TxInputType *txinput) {
signing_abort(); signing_abort();
return false; return false;
} }
if (txinput->has_ownership_proof) {
fsm_sendFailure(FailureType_Failure_DataError,
_("Ownership proof provided but not expected."));
signing_abort();
return false;
}
} else if (txinput->script_type == InputScriptType_EXTERNAL) { } else if (txinput->script_type == InputScriptType_EXTERNAL) {
if (txinput->address_n_count != 0) { if (txinput->address_n_count != 0) {
fsm_sendFailure(FailureType_Failure_DataError, fsm_sendFailure(FailureType_Failure_DataError,
@ -1214,6 +1218,13 @@ static bool signing_validate_input(const TxInputType *txinput) {
return false; return false;
} }
if (txinput->has_commitment_data && !txinput->has_ownership_proof) {
fsm_sendFailure(FailureType_Failure_DataError,
_("commitment_data field provided but not expected."));
signing_abort();
return false;
}
if (txinput->has_orig_hash) { if (txinput->has_orig_hash) {
if (!txinput->has_orig_index) { if (!txinput->has_orig_index) {
fsm_sendFailure(FailureType_Failure_DataError, fsm_sendFailure(FailureType_Failure_DataError,
@ -1892,7 +1903,7 @@ static bool signing_add_orig_output(TxOutputType *orig_output) {
} }
static bool signing_confirm_tx(void) { static bool signing_confirm_tx(void) {
if (has_external_input()) { if (has_unverified_external_input) {
layoutConfirmUnverifiedExternalInputs(); layoutConfirmUnverifiedExternalInputs();
if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
@ -1952,12 +1963,23 @@ static bool signing_confirm_tx(void) {
} }
uint64_t orig_fee = orig_total_in - orig_total_out; uint64_t orig_fee = orig_total_in - orig_total_out;
// Reject adding external inputs to the original transaction, so that we
// don't have to deal with the UI implications. This could be used for
// BIP-78 Payjoins when we support presigned external inputs.
if (external_in != orig_external_in) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Adding external inputs is not supported."));
signing_abort();
return false;
}
// Sanity check. Replacement transactions are only allowed to make // Sanity check. Replacement transactions are only allowed to make
// amendments which do not increase the amount that we are spending on // amendments which do not increase the amount that we are spending on
// external outputs. Additional funds can only go towards the fee, which is // external outputs. Additional funds can only go towards the fee, which is
// confirmed by the user. The check may fail if the replacement transaction // confirmed by the user. The check may fail if the replacement transaction
// starts mixing accounts and breaks change-output identification. // starts mixing accounts and breaks change-output identification.
if (total_out - change_out > orig_total_out - orig_change_out) { if (total_out - change_out - external_in >
orig_total_out - orig_change_out - orig_external_in) {
fsm_sendFailure(FailureType_Failure_ProcessError, fsm_sendFailure(FailureType_Failure_ProcessError,
_("Invalid replacement transaction.")); _("Invalid replacement transaction."));
signing_abort(); signing_abort();
@ -2000,8 +2022,8 @@ static bool signing_confirm_tx(void) {
} }
// last confirmation // last confirmation
layoutConfirmTx(coin, amount_unit, total_in, total_out, change_out, layoutConfirmTx(coin, amount_unit, total_in, external_in, total_out,
tx_weight); change_out, tx_weight);
if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
signing_abort(); signing_abort();
@ -2824,12 +2846,40 @@ void signing_txack(TransactionType *tx) {
to.is_segwit = true; to.is_segwit = true;
#endif #endif
} else if (tx->inputs[0].script_type == InputScriptType_EXTERNAL) { } else if (tx->inputs[0].script_type == InputScriptType_EXTERNAL) {
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict) { if (tx->inputs[0].has_ownership_proof) {
fsm_sendFailure(FailureType_Failure_ProcessError, uint8_t ownership_id[OWNERSHIP_ID_SIZE] = {0};
_("External inputs not allowed.")); if (!fsm_getOwnershipId(tx->inputs[0].script_pubkey.bytes,
tx->inputs[0].script_pubkey.size,
ownership_id)) {
signing_abort(); signing_abort();
return; return;
} }
if (!tx_input_verify_nonownership(coin, tx->inputs, ownership_id)) {
fsm_sendFailure(FailureType_Failure_DataError,
_("Invalid external input."));
signing_abort();
return;
}
if (!add_amount(&external_in, tx->inputs[0].amount)) {
return;
}
if (tx->inputs[0].has_orig_hash) {
if (!add_amount(&orig_external_in, tx->inputs[0].amount)) {
return;
}
}
} else {
has_unverified_external_input = true;
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Unverifiable external input."));
signing_abort();
return;
}
}
set_external_input(idx1); set_external_input(idx1);
} else { } else {
fsm_sendFailure(FailureType_Failure_DataError, fsm_sendFailure(FailureType_Failure_DataError,

View File

@ -1310,3 +1310,147 @@ bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type,
out->ownership_proof.size = r; out->ownership_proof.size = r;
return true; return true;
} }
bool tx_input_verify_nonownership(
const CoinInfo *coin, const TxInputType *txinput,
const uint8_t ownership_id[OWNERSHIP_ID_SIZE]) {
size_t r = 0;
// Check versionMagic.
if (txinput->ownership_proof.size < r + sizeof(SLIP19_VERSION_MAGIC) ||
memcmp(txinput->ownership_proof.bytes + r, SLIP19_VERSION_MAGIC,
sizeof(SLIP19_VERSION_MAGIC)) != 0) {
return false;
}
r += sizeof(SLIP19_VERSION_MAGIC);
// Skip flags.
r += 1;
// Ensure that there is only one ownership ID.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 1) {
return false;
}
r += 1;
// Ensure that the ownership ID is not ours.
if (txinput->ownership_proof.size < r + OWNERSHIP_ID_SIZE ||
memcmp(txinput->ownership_proof.bytes + r, ownership_id,
OWNERSHIP_ID_SIZE) == 0) {
return false;
}
r += OWNERSHIP_ID_SIZE;
// Compute the ownership proof digest.
Hasher hasher = {0};
hasher_InitParam(&hasher, HASHER_SHA2, NULL, 0);
hasher_Update(&hasher, txinput->ownership_proof.bytes, r);
tx_script_hash(&hasher, txinput->script_pubkey.size,
txinput->script_pubkey.bytes);
tx_script_hash(&hasher, txinput->commitment_data.size,
txinput->commitment_data.bytes);
uint8_t digest[SHA256_DIGEST_LENGTH] = {0};
hasher_Final(&hasher, digest);
// Ensure that there is no scriptSig, since we only support native SegWit
// ownership proofs.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 0) {
return false;
}
r += 1;
if (txinput->script_pubkey.size == 22 &&
memcmp(txinput->script_pubkey.bytes, "\x00\x14", 2) == 0) {
// SegWit v0 (probably P2WPKH)
const uint8_t *pubkey_hash = txinput->script_pubkey.bytes + 2;
// Ensure that there are two stack items.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 2) {
return false;
}
r += 1;
// Read the signature.
if (txinput->ownership_proof.size < r + 1) {
return false;
}
size_t signature_size = txinput->ownership_proof.bytes[r];
r += 1;
uint8_t signature[64] = {0};
if (txinput->ownership_proof.size < r + signature_size ||
ecdsa_sig_from_der(txinput->ownership_proof.bytes + r,
signature_size - 1, signature) != 0) {
return false;
}
r += signature_size;
// Read the public key.
if (txinput->ownership_proof.size < r + 34 ||
txinput->ownership_proof.bytes[r] != 33) {
return false;
}
const uint8_t *public_key = txinput->ownership_proof.bytes + r + 1;
r += 34;
// Check the public key matches the scriptPubKey.
uint8_t expected_pubkey_hash[20] = {0};
ecdsa_get_pubkeyhash(public_key, coin->curve->hasher_pubkey,
expected_pubkey_hash);
if (memcmp(pubkey_hash, expected_pubkey_hash,
sizeof(expected_pubkey_hash)) != 0) {
return false;
}
// Ensure that we have read the entire ownership proof.
if (r != txinput->ownership_proof.size) {
return false;
}
#ifdef USE_SECP256K1_ZKP_ECDSA
if (coin->curve->params == &secp256k1) {
if (zkp_ecdsa_verify_digest(coin->curve->params, public_key, signature,
digest) != 0) {
return false;
}
} else
#endif
{
if (ecdsa_verify_digest(coin->curve->params, public_key, signature,
digest) != 0) {
return false;
}
}
} else if (txinput->script_pubkey.size == 34 &&
memcmp(txinput->script_pubkey.bytes, "\x51\x20", 2) == 0) {
// SegWit v1 (P2TR)
const uint8_t *output_public_key = txinput->script_pubkey.bytes + 2;
// Ensure that there is one stack item consisting of 64 bytes.
if (txinput->ownership_proof.size < r + 2 ||
memcmp(txinput->ownership_proof.bytes + r, "\x01\x40", 2) != 0) {
return false;
}
r += 2;
// Read the signature.
const uint8_t *signature = txinput->ownership_proof.bytes + r;
r += 64;
// Ensure that we have read the entire ownership proof.
if (r != txinput->ownership_proof.size) {
return false;
}
if (zkp_bip340_verify_digest(output_public_key, signature, digest) != 0) {
return false;
}
} else {
// Unsupported script type.
return false;
}
return true;
}

View File

@ -152,5 +152,8 @@ bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type,
size_t script_pubkey_size, size_t script_pubkey_size,
const uint8_t *commitment_data, const uint8_t *commitment_data,
size_t commitment_data_size, OwnershipProof *out); size_t commitment_data_size, OwnershipProof *out);
bool tx_input_verify_nonownership(
const CoinInfo *coin, const TxInputType *txinput,
const uint8_t ownership_id[OWNERSHIP_ID_SIZE]);
#endif #endif

View File

@ -577,7 +577,6 @@ def test_p2wpkh_in_p2sh_with_proof(client: Client):
pass pass
@pytest.mark.skip_t1
def test_p2wpkh_with_proof(client: Client): def test_p2wpkh_with_proof(client: Client):
inp1 = messages.TxInputType( inp1 = messages.TxInputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
@ -611,16 +610,18 @@ def test_p2wpkh_with_proof(client: Client):
) )
with client: with client:
t1 = client.features.model == "1"
tt = client.features.model == "T"
client.set_expected_responses( client.set_expected_responses(
[ [
request_input(0), request_input(0),
request_input(1), request_input(1),
request_output(0), request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput), (tt, messages.ButtonRequest(code=B.ConfirmOutput)),
request_output(1), request_output(1),
messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput), (tt, messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx), messages.ButtonRequest(code=B.SignTx),
request_input(0), request_input(0),
request_meta(TXHASH_e5b7e2), request_meta(TXHASH_e5b7e2),
@ -636,6 +637,7 @@ def test_p2wpkh_with_proof(client: Client):
request_input(1), request_input(1),
request_output(0), request_output(0),
request_output(1), request_output(1),
(t1, request_input(0)),
request_input(1), request_input(1),
request_finished(), request_finished(),
] ]
@ -656,7 +658,7 @@ def test_p2wpkh_with_proof(client: Client):
# Test corrupted ownership proof. # Test corrupted ownership proof.
inp1.ownership_proof[10] ^= 1 inp1.ownership_proof[10] ^= 1
with pytest.raises(TrezorFailure, match="Invalid signature"): with pytest.raises(TrezorFailure, match="Invalid signature|Invalid external input"):
btc.sign_tx( btc.sign_tx(
client, client,
"Testnet", "Testnet",
@ -666,7 +668,6 @@ def test_p2wpkh_with_proof(client: Client):
) )
@pytest.mark.skip_t1
@pytest.mark.setup_client( @pytest.mark.setup_client(
mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
) )
@ -703,17 +704,20 @@ def test_p2tr_with_proof(client: Client):
) )
with client: with client:
t1 = client.features.model == "1"
tt = client.features.model == "T"
client.set_expected_responses( client.set_expected_responses(
[ [
request_input(0), request_input(0),
request_input(1), request_input(1),
request_output(0), request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput), (tt, messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx), messages.ButtonRequest(code=B.SignTx),
request_input(0), request_input(0),
request_input(1), request_input(1),
request_output(0), request_output(0),
(t1, request_input(0)),
request_input(1), request_input(1),
request_finished(), request_finished(),
] ]
@ -732,11 +736,10 @@ def test_p2tr_with_proof(client: Client):
# Test corrupted ownership proof. # Test corrupted ownership proof.
inp1.ownership_proof[10] ^= 1 inp1.ownership_proof[10] ^= 1
with pytest.raises(TrezorFailure, match="Invalid signature"): with pytest.raises(TrezorFailure, match="Invalid signature|Invalid external input"):
btc.sign_tx(client, "Testnet", [inp1, inp2], [out1], prev_txes=TX_CACHE_TESTNET) btc.sign_tx(client, "Testnet", [inp1, inp2], [out1], prev_txes=TX_CACHE_TESTNET)
@pytest.mark.skip_t1
def test_p2wpkh_with_false_proof(client: Client): def test_p2wpkh_with_false_proof(client: Client):
inp1 = messages.TxInputType( inp1 = messages.TxInputType(
# tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9 # tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9

View File

@ -269,7 +269,10 @@
"T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.SATOSHI]": "177fd9048c7661977db228678f9be74b1e769deb4cec87aa146ef51d20c604a1", "T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.SATOSHI]": "177fd9048c7661977db228678f9be74b1e769deb4cec87aa146ef51d20c604a1",
"T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[None]": "f2be7c23251127b50596f1a772a9eb933e0b1cef4c30afbc912930d1413f8694", "T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[None]": "f2be7c23251127b50596f1a772a9eb933e0b1cef4c30afbc912930d1413f8694",
"T1_bitcoin-test_signtx_external.py::test_p2tr_external_unverified": "19e56e826e17f0b5cb5ab26d684dd4d1ef73da2711edc8bef2a086962c1382b0", "T1_bitcoin-test_signtx_external.py::test_p2tr_external_unverified": "19e56e826e17f0b5cb5ab26d684dd4d1ef73da2711edc8bef2a086962c1382b0",
"T1_bitcoin-test_signtx_external.py::test_p2tr_with_proof": "5c2f96acb6e23f1e5698276e697c7afa299454fa12f1b2f6eae8b35490d69f6c",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_external_unverified": "6695ba71c171c82c4335db3f70dbb5f861c44141ea0eeea98e295ab6b51da3bb", "T1_bitcoin-test_signtx_external.py::test_p2wpkh_external_unverified": "6695ba71c171c82c4335db3f70dbb5f861c44141ea0eeea98e295ab6b51da3bb",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_with_false_proof": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_with_proof": "8f55fbd32b618e6373ed0c68dee2b3e275e0fce6ff7502bbb34ab4702238dae7",
"T1_bitcoin-test_signtx_invalid_path.py::test_attack_path_segwit": "ee152e7534c4dd60f939b7403a8169eb7d1703e3bdab819a575bc80f261212a9", "T1_bitcoin-test_signtx_invalid_path.py::test_attack_path_segwit": "ee152e7534c4dd60f939b7403a8169eb7d1703e3bdab819a575bc80f261212a9",
"T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail_asap": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail_asap": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",