Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 27 additions & 39 deletions main/core/psbt.c
Original file line number Diff line number Diff line change
Expand Up @@ -718,16 +718,22 @@ output_ownership_t psbt_classify_output(const struct wally_psbt *psbt, size_t i,
return result;
}

// Only a hardened purpose'/coin' prefix names a coin type: a taproot internal
// key derived from an unspendable xpub sits at a bare /0/0, which would read
// as coin 0.
static bool check_keypath_network(const unsigned char *keypath,
size_t keypath_len, bool *is_testnet) {
if (keypath_len < 12) {
return false;
}

uint32_t coin_type;
memcpy(&coin_type, keypath + 8, sizeof(uint32_t));
uint32_t coin_value = coin_type & 0x7FFFFFFF;
uint32_t purpose = bip32_path_u32_le(keypath + 4);
uint32_t coin_type = bip32_path_u32_le(keypath + 8);
if (!bip32_path_is_hardened(purpose) || !bip32_path_is_hardened(coin_type)) {
return false;
}

uint32_t coin_value = bip32_path_unharden(coin_type);
if (coin_value == 0 || coin_value == 1) {
*is_testnet = (coin_value == 1);
return true;
Expand All @@ -736,55 +742,37 @@ static bool check_keypath_network(const unsigned char *keypath,
return false;
}

// Keypaths and taproot_leaf_paths values share the fp(4) | path(4*depth) shape.
static bool map_detect_network(const struct wally_map *keypaths,
bool *is_testnet) {
for (size_t i = 0; i < keypaths->num_items; i++) {
if (check_keypath_network(keypaths->items[i].value,
keypaths->items[i].value_len, is_testnet)) {
return true;
}
}
return false;
}

bool psbt_detect_network(const struct wally_psbt *psbt) {
if (!psbt) {
return false;
}

unsigned char keypath[100];
size_t keypath_len, keypaths_size;
bool is_testnet;

// Check outputs first
size_t num_outputs = 0;
wally_psbt_get_num_outputs(psbt, &num_outputs);

for (size_t i = 0; i < num_outputs; i++) {
if (wally_psbt_get_output_keypaths_size(psbt, i, &keypaths_size) ==
WALLY_OK &&
keypaths_size > 0 &&
wally_psbt_get_output_keypath(psbt, i, 0, keypath, sizeof(keypath),
&keypath_len) == WALLY_OK &&
check_keypath_network(keypath, keypath_len, &is_testnet)) {
return is_testnet;
}
// Taproot outputs store derivation in a separate map (PSBT_OUT_TAP_BIP32),
// whose value has the same fp(4)|path shape as the segwit keypaths.
const struct wally_map *tp = &psbt->outputs[i].taproot_leaf_paths;
if (tp->num_items > 0 &&
check_keypath_network(tp->items[0].value, tp->items[0].value_len,
&is_testnet)) {
for (size_t i = 0; i < psbt->num_outputs; i++) {
if (map_detect_network(&psbt->outputs[i].keypaths, &is_testnet) ||
map_detect_network(&psbt->outputs[i].taproot_leaf_paths, &is_testnet)) {
return is_testnet;
}
}

// Check inputs as fallback
size_t num_inputs = 0;
wally_psbt_get_num_inputs(psbt, &num_inputs);

for (size_t i = 0; i < num_inputs; i++) {
if (wally_psbt_get_input_keypaths_size(psbt, i, &keypaths_size) ==
WALLY_OK &&
keypaths_size > 0 &&
wally_psbt_get_input_keypath(psbt, i, 0, keypath, sizeof(keypath),
&keypath_len) == WALLY_OK &&
check_keypath_network(keypath, keypath_len, &is_testnet)) {
return is_testnet;
}
const struct wally_map *tp = &psbt->inputs[i].taproot_leaf_paths;
if (tp->num_items > 0 &&
check_keypath_network(tp->items[0].value, tp->items[0].value_len,
&is_testnet)) {
for (size_t i = 0; i < psbt->num_inputs; i++) {
if (map_detect_network(&psbt->inputs[i].keypaths, &is_testnet) ||
map_detect_network(&psbt->inputs[i].taproot_leaf_paths, &is_testnet)) {
return is_testnet;
}
}
Expand Down
63 changes: 63 additions & 0 deletions main/core/test/test_psbt_classify.c
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,68 @@ static void test_psbt_classify_adv_taproot_foreign_fp(void) {
PASS();
}

/* Nunchuk's taproot multisig PSBTs carry the unspendable internal key at a
* bare /0/0 ahead of the cosigners' 48'/1'/... leaf keys. */
static void test_detect_network_skips_unhardened_internal_key(void) {
TEST("psbt_detect_network: taproot internal key at /0/0 is not a coin type");

struct ext_key *internal = NULL, *leaf = NULL;
if (!key_get_derived_key("m/0/0", &internal) ||
!key_get_derived_key("m/48'/1'/0'/3'/0/0", &leaf)) {
bip32_key_free(internal);
FAIL("key derivation failed");
return;
}

struct wally_tx *tx = NULL;
struct wally_psbt *psbt = NULL;
uint8_t txid[32] = {0};
uint8_t op_return[] = {0x6a};
if (wally_tx_init_alloc(2, 0, 1, 1, &tx) != WALLY_OK ||
wally_tx_add_raw_input(tx, txid, sizeof(txid), 0, 0xffffffff, NULL, 0,
NULL, 0) != WALLY_OK ||
wally_tx_add_raw_output(tx, 0, op_return, sizeof(op_return), 0) !=
WALLY_OK ||
wally_psbt_from_tx(tx, 0, 0, &psbt) != WALLY_OK) {
wally_tx_free(tx);
bip32_key_free(internal);
bip32_key_free(leaf);
FAIL("psbt alloc");
return;
}
wally_tx_free(tx);

const uint8_t internal_path[] = {0x7c, 0x46, 0x1e, 0x5d, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t leaf_path[] = {
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x01, 0x00,
0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
struct wally_map *tp = &psbt->inputs[0].taproot_leaf_paths;
wally_map_add(tp, internal->pub_key + 1, EC_PUBLIC_KEY_LEN - 1, internal_path,
sizeof(internal_path));
wally_map_add(tp, leaf->pub_key + 1, EC_PUBLIC_KEY_LEN - 1, leaf_path,
sizeof(leaf_path));
bip32_key_free(internal);
bip32_key_free(leaf);

bool ok =
tp->num_items == 2 && tp->items[0].value_len == sizeof(internal_path);
bool is_testnet = psbt_detect_network(psbt);
wally_psbt_free(psbt);

if (!ok) {
FAIL("fixture: internal key entry is not first");
return;
}
if (!is_testnet) {
FAIL("expected testnet from the 48'/1' leaf key");
return;
}
PASS();
}

/* ================================================================
* Input amount provenance tests
*
Expand Down Expand Up @@ -2767,6 +2829,7 @@ int main(void) {
test_psbt_classify_adv_mixed_script();
test_psbt_classify_adv_registry_depth_mismatch();
test_psbt_classify_adv_taproot_foreign_fp();
test_detect_network_skips_unhardened_internal_key();
test_psbt_classify_adv_testnet_flag_mismatch();

printf("\n=== input amount provenance tests ===\n\n");
Expand Down
Loading