From 0fd66c38bcb2b5f18f6f9c8c3fe7974e736f70fe Mon Sep 17 00:00:00 2001 From: rxbryan Date: Tue, 9 Jul 2024 22:07:30 +0100 Subject: [PATCH] feat: add P2SH-P2WPKH support feat: add nested segwit support test nested segwit digest for 1 inputs and 2 output transactions add tests to verify nested segwit transactions test nested segwit digest for 2 inputs and 2 output transactions --- apps/btc_family/btc_pub_key.c | 12 +- apps/btc_family/btc_txn.c | 17 +- apps/btc_family/btc_txn_helpers.c | 27 +- tests/apps/btc_app/btc_txn_helpers_tests.c | 332 +++++++++++++++++++++ tests/unit_test_lists.c | 4 + 5 files changed, 385 insertions(+), 7 deletions(-) diff --git a/apps/btc_family/btc_pub_key.c b/apps/btc_family/btc_pub_key.c index 6c56aac18..17e60f5b2 100644 --- a/apps/btc_family/btc_pub_key.c +++ b/apps/btc_family/btc_pub_key.c @@ -204,7 +204,17 @@ static size_t btc_get_address(const uint8_t *seed, case NON_SEGWIT: hdnode_get_address(&node, g_btc_app->p2pkh_addr_ver, addr, 35); break; - // TODO: add support for taproot and segwit + case PURPOSE_SEGWIT: + ecdsa_get_address_segwit_p2sh( + node.public_key, + g_btc_app->p2sh_addr_ver, + node.curve->hasher_pubkey, + node.curve->hasher_base58, + addr, + 36 + ); + break; + // TODO: add support for taproot default: break; } diff --git a/apps/btc_family/btc_txn.c b/apps/btc_family/btc_txn.c index 38033939f..e01db15d8 100644 --- a/apps/btc_family/btc_txn.c +++ b/apps/btc_family/btc_txn.c @@ -648,11 +648,26 @@ static bool sign_input(scrip_sig_t *signatures) { status = true; for (int idx = 0; idx < btc_txn_context->metadata.input_count; idx++) { // generate the input digest and respective private key - status = btc_digest_input(btc_txn_context, idx, buffer); memcpy(&t_node, &node, sizeof(HDNode)); hdnode_private_ckd(&t_node, btc_txn_context->inputs[idx].change_index); hdnode_private_ckd(&t_node, btc_txn_context->inputs[idx].address_index); hdnode_fill_public_key(&t_node); + + // detect input type + btc_sign_txn_input_script_pub_key_t *script = + &btc_txn_context->inputs[idx].script_pub_key; + btc_script_type_e type = btc_get_script_type(script->bytes, script->size); + if (SCRIPT_TYPE_P2SH == type) { + // replace BIP16 scriptpubkey with redeemscript(P2WPKH) + uint8_t buf[22] = {0}; + buf[0] = 0; // version byte + buf[1] = 20; // push 20 bytes + ecdsa_get_pubkeyhash(t_node.public_key, t_node.curve->hasher_pubkey, buf + 2); + memcpy(btc_txn_context->inputs[idx].script_pub_key.bytes, buf, 22); + btc_txn_context->inputs[idx].script_pub_key.size = 22; + } + + status = btc_digest_input(btc_txn_context, idx, buffer); ecdsa_sign_digest( curve, t_node.private_key, buffer, signatures[idx].bytes, NULL, NULL); signatures[idx].size = btc_sig_to_script_sig( diff --git a/apps/btc_family/btc_txn_helpers.c b/apps/btc_family/btc_txn_helpers.c index bb3546ed8..1fa337eba 100644 --- a/apps/btc_family/btc_txn_helpers.c +++ b/apps/btc_family/btc_txn_helpers.c @@ -390,6 +390,23 @@ static void update_locktime(btc_verify_input_t *verify_input_data, return; } } + +// TODO: Add chunking condition for varint decode +// refer: https://app.clickup.com/t/9002019994/PRF-7288 +static int64_t varint_decode(const uint8_t *raw_txn_chunk, int32_t *offset) { + uint8_t first_byte = raw_txn_chunk[*offset]; + if (first_byte < 0xFD) { + return first_byte; + } else { + // TODO: var-int varies between 1-9 bytes + // current implementation supports decoding + // upto 3 bytes only + uint8_t result[2]; + memcpy(result, raw_txn_chunk + *offset + 1, 2); + *offset += 2; + return U16_READ_LE_ARRAY(result); + } +} /***************************************************************************** * GLOBAL FUNCTIONS *****************************************************************************/ @@ -414,7 +431,7 @@ int btc_verify_input(const uint8_t *raw_txn_chunk, // store the number of inputs in the raw_txn verify_input_data->count = raw_txn_chunk[offset++]; // TODO: Improve varint decode. - // size of variable containing ip-count/op-count + // size of variable containing script size and ip-count/op-count // varies (1-9 Bytes) depending on its value. // refer: // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer @@ -442,9 +459,10 @@ int btc_verify_input(const uint8_t *raw_txn_chunk, } case SCRIPT_LENGTH_CASE: { - if (offset + raw_txn_chunk[offset] + 1 + 4 > CHUNK_SIZE) { + int64_t script_length = varint_decode(raw_txn_chunk, &offset); + if (offset + script_length + 1 + 4 > CHUNK_SIZE) { verify_input_data->prev_offset = - (offset + raw_txn_chunk[offset] + 1 + 4) - CHUNK_SIZE; + (offset + script_length + 1 + 4) - CHUNK_SIZE; update_hash( verify_input_data, raw_txn_chunk, chunk_index, CHUNK_SIZE); verify_input_data->input_parse = @@ -452,7 +470,7 @@ int btc_verify_input(const uint8_t *raw_txn_chunk, verify_input_data->input_index++; return 4; } else { - offset += (raw_txn_chunk[offset] + 1 + 4); + offset += (script_length + 1 + 4); } break; } @@ -652,7 +670,6 @@ bool btc_digest_input(const btc_txn_context_t *context, btc_sign_txn_input_script_pub_key_t *script = &context->inputs[index].script_pub_key; btc_script_type_e type = btc_get_script_type(script->bytes, script->size); - if (SCRIPT_TYPE_P2WPKH == type) { // segwit digest calculation; could fail if segwit_cache not filled status = calculate_p2wpkh_digest(context, index, digest); diff --git a/tests/apps/btc_app/btc_txn_helpers_tests.c b/tests/apps/btc_app/btc_txn_helpers_tests.c index 18d6a6bb9..0d00392ed 100644 --- a/tests/apps/btc_app/btc_txn_helpers_tests.c +++ b/tests/apps/btc_app/btc_txn_helpers_tests.c @@ -400,6 +400,137 @@ TEST(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_fail) { TEST_ASSERT_EQUAL_INT(2, status); } + +TEST(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_in_p2sh) { + /* Test data source: Bip143 + * https://blockstream.info/testnet/tx/b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2?expand + */ + int status = 2; + uint8_t raw_txn[2000] = {0}; + hex_string_to_byte_array( + "0200000000010258afb1ece76f01c24f4935f453d210518163cb1d0383eaec331b202ebeb5e389" + "0000000017160014a76cad25cb569bb47305513ebedd6011dc419deeffffffff2b3682b3592588" + "5001f0e321df28d4ac675a9cbbccef2a69533bea7c5e5ad2c40000000017160014a76cad25cb56" + "9bb47305513ebedd6011dc419deeffffffff02941100000000000017a914bd7aabdeeef211b1bd" + "ad7218e14fea6c032101c087f22f00000000000017a914eaf97514c5ac1e41e413502e97ae42eb" + "f27ace3a870247304402206e038f4712541d699697ed55efc41219df4f244fc72caa5edd653837" + "f6555f6f02201cd8ea15b65fda17992abafaed86e066c3271ac16b9c46c54c2192438843dd0401" + "21029f75e1ef6b04e004a308b1f59215a8a3a5b7958bbcf184cc24ba7ab6574448780248304502" + "2100d15ce61648edc28b8b5a3531b80a1e8fc3b3eebe7d3fc4ca962cb04afc770dda02207c7eaf8" + "82d7fac45d2752f20e48d2f896715cbc5a3b0f5de3e19fea0da99beac0121029f75e1ef6b04e004" + "a308b1f59215a8a3a5b7958bbcf184cc24ba7ab65744487800000000", + 838, + raw_txn); + // only fill necessary values + btc_txn_input_t input[] = { + { + .value = 4500, + .prev_output_index = 0x00000000, + .script_pub_key = {.size = 23}, + }, + { + .value = 12274, + .prev_output_index = 0x00000001, + .script_pub_key = {.size = 23}, + } + }; + + hex_string_to_byte_array( + "b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2", + 64, + input[0].prev_txn_hash); + // revere order of txn-id: + // A2 44 A2 93 47 5A C2 45 DE E8 B3 34 3F 51 EE 22 96 BA B2 4F CC 3F E0 A4 96 28 DF 60 5E 22 95 B0 + cy_reverse_byte_array(input[0].prev_txn_hash, sizeof(input[0].prev_txn_hash)); + + hex_string_to_byte_array( + "b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2", + 64, + input[1].prev_txn_hash); + // revere order of txn-id: + // A2 44 A2 93 47 5A C2 45 DE E8 B3 34 3F 51 EE 22 96 BA B2 4F CC 3F E0 A4 96 28 DF 60 5E 22 95 B0 + cy_reverse_byte_array(input[1].prev_txn_hash, sizeof(input[1].prev_txn_hash)); + + hex_string_to_byte_array("a914bd7aabdeeef211b1bdad7218e14fea6c032101c087", + 46, + input[0].script_pub_key.bytes); + hex_string_to_byte_array("a914eaf97514c5ac1e41e413502e97ae42ebf27ace3a87", + 46, + input[1].script_pub_key.bytes); + + status = btc_verify_input_test(input, raw_txn, 419); + TEST_ASSERT_EQUAL_INT(0, status); + status = btc_verify_input_test(input+1, raw_txn, 419); + TEST_ASSERT_EQUAL_INT(0, status); +} + +TEST(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_in_p2sh_fail) { + /* Test data source: Bip143 + * https://blockstream.info/testnet/tx/b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2?expand + */ + int status = 2; + uint8_t raw_txn[2000] = {0}; + hex_string_to_byte_array( + "0200000000010258afb1ece76f01c24f4935f453d210518163cb1d0383eaec331b202ebeb5e389" + "0000000017160014a76cad25cb569bb47305513ebedd6011dc419deeffffffff2b3682b3592588" + "5001f0e321df28d4ac675a9cbbccef2a69533bea7c5e5ad2c40000000017160014a76cad25cb56" + "9bb47305513ebedd6011dc419deeffffffff02941100000000000017a914bd7aabdeeef211b1bd" + "ad7218e14fea6c032101c087f22f00000000000017a914eaf97514c5ac1e41e413502e97ae42eb" + "f27ace3a870247304402206e038f4712541d699697ed55efc41219df4f244fc72caa5edd653837" + "f6555f6f02201cd8ea15b65fda17992abafaed86e066c3271ac16b9c46c54c2192438843dd0401" + "21029f75e1ef6b04e004a308b1f59215a8a3a5b7958bbcf184cc24ba7ab6574448780248304502" + "2100d15ce61648edc28b8b5a3531b80a1e8fc3b3eebe7d3fc4ca962cb04afc770dda02207c7eaf8" + "82d7fac45d2752f20e48d2f896715cbc5a3b0f5de3e19fea0da99beac0121029f75e1ef6b04e004" + "a308b1f59215a8a3a5b7958bbcf184cc24ba7ab65744487800000000", + 838, + raw_txn); + // only fill necessary values + btc_txn_input_t input[] = { + { + .value = 4500, + .prev_output_index = 0x00000000, + .script_pub_key = {.size = 23}, + }, + { + .value = 12274, + .prev_output_index = 0x00000001, + .script_pub_key = {.size = 23}, + } + }; + + hex_string_to_byte_array( + // invalid txn hash test. valid txn hash/id: + // b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2 + "b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a3", + 64, + input[0].prev_txn_hash); + // revere order of txn-id: + // A3 44 A2 93 47 5A C2 45 DE E8 B3 34 3F 51 EE 22 96 BA B2 4F CC 3F E0 A4 96 28 DF 60 5E 22 95 B0 + cy_reverse_byte_array(input[0].prev_txn_hash, sizeof(input[0].prev_txn_hash)); + + hex_string_to_byte_array( + // invalid txn hash test. valid txn hash/id: + // b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a2 + "b095225e60df2896a4e03fcc4fb2ba9622ee513f34b3e8de45c25a4793a244a3", + 64, + input[1].prev_txn_hash); + // revere order of txn-id: + // A3 44 A2 93 47 5A C2 45 DE E8 B3 34 3F 51 EE 22 96 BA B2 4F CC 3F E0 A4 96 28 DF 60 5E 22 95 B0 + cy_reverse_byte_array(input[1].prev_txn_hash, sizeof(input[1].prev_txn_hash)); + + hex_string_to_byte_array("a914bd7aabdeeef211b1bdad7218e14fea6c032101c087", + 46, + input[0].script_pub_key.bytes); + hex_string_to_byte_array("a914eaf97514c5ac1e41e413502e97ae42ebf27ace3a87", + 46, + input[1].script_pub_key.bytes); + + status = btc_verify_input_test(input, raw_txn, 419); + TEST_ASSERT_EQUAL_INT(2, status); + status = btc_verify_input_test(input+1, raw_txn, 419); + TEST_ASSERT_EQUAL_INT(2, status); +} + /* FIX: Required to fix the hardcoded value of 106 (2 + 33 + 71) since the * signature part of the script can vary (71 bytes | 72 bytes | 73 bytes). * Check the get_transaction_weight function. */ @@ -812,6 +943,207 @@ TEST(btc_txn_helper_test, btc_txn_helper_p2wpkh_digest_1_2) { TEST(btc_txn_helper_test, btc_txn_helper_p2wpkh_digest_2_2) { } + +TEST(btc_txn_helper_test, btc_txn_helper_p2wpkh_in_p2sh_digest_1_2) { + uint8_t calculated_digest[32] = {0}; + uint8_t expected_digest[32] = {0}; + btc_segwit_cache_t expected_cache = {.filled = true}; + btc_txn_input_t input = { + .prev_txn_hash = {0}, + .value = 1000000000, + .prev_output_index = 0x00000001, + .script_pub_key = {.size = 22}, + .change_index = 0, + .address_index = 1, + .sequence = 0xFFFFFFFe, + }; + btc_sign_txn_output_t outputs[] = { + { + .value = 0x000000000bebb4b8, + .script_pub_key = {.size = 25}, + .is_change = false, + .has_changes_index = false, + }, + { + .value = 0x000000002faf0800, + .script_pub_key = {.size = 25}, + .is_change = false, + .has_changes_index = false, + }, + }; + btc_txn_context_t context = { + .init_info = {.derivation_path_count = 3, + .derivation_path = {0x80000000 + 49, + 0x80000000, + 0x80000000}}, + .metadata = {.version = 0x00000001, + .input_count = 0x01, + .output_count = 0x02, + .sighash = 0x00000001, + .locktime = 0x00000492}, + .segwit_cache = {0}, + .change_output_idx = 1, + }; + + context.inputs = &input; + context.outputs = outputs; + + hex_string_to_byte_array( + "db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477", + 64, + input.prev_txn_hash); + hex_string_to_byte_array("001479091972186c449eb1ded22b78e40d009bdf0089", + 44, + input.script_pub_key.bytes); + hex_string_to_byte_array("76a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac", + 50, + outputs[0].script_pub_key.bytes); + hex_string_to_byte_array("76a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac", + 50, + outputs[1].script_pub_key.bytes); + hex_string_to_byte_array( + "b0287b4a252ac05af83d2dcef00ba313af78a3e9c329afa216eb3aa2a7b4613a", + 64, + expected_cache.hash_prevouts); + hex_string_to_byte_array( + "18606b350cd8bf565266bc352f0caddcf01e8fa789dd8a15386327cf8cabe198", + 64, + expected_cache.hash_sequence); + hex_string_to_byte_array( + "de984f44532e2173ca0d64314fcefe6d30da6f8cf27bafa706da61df8a226c83", + 64, + expected_cache.hash_outputs); + hex_string_to_byte_array( + "64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6", + 64, + expected_digest); + + btc_segwit_init_cache(&context); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_cache.hash_prevouts, context.segwit_cache.hash_prevouts, 32); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_cache.hash_sequence, context.segwit_cache.hash_sequence, 32); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_cache.hash_outputs, context.segwit_cache.hash_outputs, 32); + + btc_digest_input(&context, 0, calculated_digest); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_digest, calculated_digest, 32); +} + +TEST(btc_txn_helper_test, btc_txn_helper_p2wpkh_in_p2sh_digest_2_2) { + bool status = false; + uint8_t calculated_digest[32] = {0}; + uint8_t expected_digest[2][32] = {0}; + btc_segwit_cache_t expected_cache = {.filled = true}; + btc_txn_input_t input[] = { + { + .prev_txn_hash = {0}, + .value = 8650, + .prev_output_index = 0x00000000, + .script_pub_key = {.size = 22}, + .address_index = 0, + .sequence = 4294967295, + }, + { + .prev_txn_hash = {0}, + .value = 18123, + .prev_output_index = 0x00000000, + .script_pub_key = {.size = 22}, + .address_index = 0, + .sequence = 4294967295, + } + }; + btc_sign_txn_output_t outputs[] = { + { + .value = 4500, + .script_pub_key = {.size = 23}, + .is_change = false, + .has_changes_index = false, + }, + { + .value = 12274, + .script_pub_key = {.size = 23}, + .is_change = false, + .has_changes_index = false, + }, + }; + btc_txn_context_t context = { + .init_info = {.derivation_path_count = 3, + .derivation_path = {0x80000000 + 49, + 0x80000001, + 0x80000000}}, + .metadata = {.version = 0x00000002, + .input_count = 0x02, + .output_count = 0x02, + .sighash = 0x01, + .locktime = 0}, + .segwit_cache = {0}, + }; + + context.inputs = &input; + context.outputs = outputs; + + hex_string_to_byte_array( + "58afb1ece76f01c24f4935f453d210518163cb1d0383eaec331b202ebeb5e389", + 64, + input[0].prev_txn_hash); + hex_string_to_byte_array("0014a76cad25cb569bb47305513ebedd6011dc419dee", + 44, + input[0].script_pub_key.bytes); + hex_string_to_byte_array( + "2b3682b35925885001f0e321df28d4ac675a9cbbccef2a69533bea7c5e5ad2c4", + 64, + input[1].prev_txn_hash); + hex_string_to_byte_array("0014a76cad25cb569bb47305513ebedd6011dc419dee", + 44, + input[1].script_pub_key.bytes); + /*outputs*/ + hex_string_to_byte_array("a914bd7aabdeeef211b1bdad7218e14fea6c032101c087", + 46, + outputs[0].script_pub_key.bytes); + hex_string_to_byte_array("a914eaf97514c5ac1e41e413502e97ae42ebf27ace3a87", + 46, + outputs[1].script_pub_key.bytes); + hex_string_to_byte_array( + "ce9186e6b6b4ce2ed0d42bdd2ae8a0003188b2b5070139eb53d1bd49ade831b7", + 64, + expected_cache.hash_prevouts); + hex_string_to_byte_array( + "752adad0a7b9ceca853768aebb6965eca126a62965f698a0c1bc43d83db632ad", + 64, + expected_cache.hash_sequence); + hex_string_to_byte_array( + "c42e53e6e72da4a8068e3115134201ebdcd8567fcaedaffc3169ab673ef4f0f9", + 64, + expected_cache.hash_outputs); + + + btc_segwit_init_cache(&context); + TEST_ASSERT_EQUAL_UINT8_ARRAY( + expected_cache.hash_prevouts, context.segwit_cache.hash_prevouts, 32); + TEST_ASSERT_EQUAL_UINT8_ARRAY( + expected_cache.hash_sequence, context.segwit_cache.hash_sequence, 32); + TEST_ASSERT_EQUAL_UINT8_ARRAY( + expected_cache.hash_outputs, context.segwit_cache.hash_outputs, 32); + + /* digest input 0*/ + hex_string_to_byte_array( + "84b6c444101a5d7d41c521b1034bf2c82afb19d45fe2a4fc6bdfae4edd720334", + 64, + expected_digest[0]); + + status = btc_digest_input(&context, 0, calculated_digest); + TEST_ASSERT_TRUE(status); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_digest[0], calculated_digest, 32); + + /* digest input 1*/ + hex_string_to_byte_array( + "7503b14676ff1de1d85819f31e43b96a5f400c273e7f143d8b05aa32e261a2b1", + 64, + expected_digest[1]); + + status = btc_digest_input(&context, 1, calculated_digest); + TEST_ASSERT_TRUE(status); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_digest[1], calculated_digest, 32); +} + TEST(btc_txn_helper_test, btc_txn_helper_get_fee) { /* Test data source: * https://blockchain.info/rawtx/b77a9ff6738877d4eb2d300b1c0ec7ca4d14353e8d501d55139019609ca2e4e5?format=json diff --git a/tests/unit_test_lists.c b/tests/unit_test_lists.c index 276b38da6..e2df08faa 100644 --- a/tests/unit_test_lists.c +++ b/tests/unit_test_lists.c @@ -140,6 +140,8 @@ TEST_GROUP_RUNNER(btc_txn_helper_test) { RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_verify_input_p2pkh_fail); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_fail); + RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_in_p2sh); + RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_verify_input_p2wpkh_in_p2sh_fail); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_transaction_weight_legacy1); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_transaction_weight_legacy2); @@ -157,6 +159,8 @@ TEST_GROUP_RUNNER(btc_txn_helper_test) { RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_p2pkh_digest_1_2); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_p2wpkh_digest_1_2); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_p2wpkh_digest_2_2); + RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_p2wpkh_in_p2sh_digest_1_2); + RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_p2wpkh_in_p2sh_digest_2_2); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_get_fee); RUN_TEST_CASE(btc_txn_helper_test, btc_txn_helper_get_fee_overspend);