ScanForSilentPaymentsOutputs skips invalid x-only taproot outputs, but still stores pointers using the original tx_outputs index into the compacted tx_output_objs vector.
If an invalid output appears before a valid one, this can take &tx_output_objs[i] out of bounds or point at the wrong object during scanning.
Diff:
diff --git a/src/common/bip352.cpp b/src/common/bip352.cpp
index 1580e1f8f8..8d019f3469 100644
--- a/src/common/bip352.cpp
+++ b/src/common/bip352.cpp
@@ -316,19 +316,19 @@ std::optional<std::vector<SilentPaymentsOutput>> ScanForSilentPaymentsOutputs(
tx_output_objs.reserve(tx_outputs.size());
tx_output_ptrs.reserve(tx_outputs.size());
- for (size_t i = 0; i < tx_outputs.size(); i++) {
- secp256k1_silentpayments_found_output found_output{};
+ for (const XOnlyPubKey& tx_output : tx_outputs) {
secp256k1_xonly_pubkey tx_output_obj;
- found_output_objs.push_back(found_output);
- found_output_ptrs.push_back(&found_output_objs[i]);
- ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static, &tx_output_obj, tx_outputs[i].data());
+ ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static, &tx_output_obj, tx_output.data());
if (!ret) {
// It is possible that a P2TR output encodes an invalid x-only pubkey.
continue;
}
tx_output_objs.push_back(tx_output_obj);
- tx_output_ptrs.push_back(&tx_output_objs[i]);
+ tx_output_ptrs.push_back(&tx_output_objs.back());
+ found_output_objs.emplace_back();
+ found_output_ptrs.push_back(&found_output_objs.back());
}
+ if (tx_output_ptrs.empty()) return {};
// Parse the pubkeys into secp pubkey and xonly_pubkey objects
ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &spend_pubkey_obj, recipient_spend_pubkey.data(), recipient_spend_pubkey.size());
diff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp
index cd5fda38da..ca9424507d 100644
--- a/src/test/bip352_tests.cpp
+++ b/src/test/bip352_tests.cpp
@@ -197,5 +197,39 @@ BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)
}
}
}
+
+BOOST_AUTO_TEST_CASE(bip352_scan_skips_invalid_taproot_outputs)
+{
+ CKey sender_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000001");
+ CKey scan_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000002");
+ CKey spend_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000003");
+ const COutPoint outpoint{Txid::FromHex("0000000000000000000000000000000000000000000000000000000000000001").value(), 0};
+
+ std::map<size_t, V0SilentPaymentsDestination> sp_dests;
+ sp_dests.emplace(0, V0SilentPaymentsDestination{scan_key.GetPubKey(), spend_key.GetPubKey()});
+ const auto sp_tr_dests = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {sender_key}, {}, outpoint);
+ BOOST_REQUIRE(sp_tr_dests.has_value());
+ const XOnlyPubKey expected_output{sp_tr_dests->begin()->second};
+
+ CTxIn txin{outpoint};
+ const CPubKey sender_pubkey{sender_key.GetPubKey()};
+ txin.scriptWitness.stack.emplace_back();
+ txin.scriptWitness.stack.emplace_back(sender_pubkey.begin(), sender_pubkey.end());
+
+ std::map<COutPoint, Coin> coins;
+ coins[outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessV0KeyHash{sender_pubkey})}, 0, false};
+ const auto prevouts_summary = bip352::GetSilentPaymentsPrevoutsSummary({txin}, coins);
+ BOOST_REQUIRE(prevouts_summary.has_value());
+
+ std::vector<XOnlyPubKey> output_pub_keys;
+ output_pub_keys.emplace_back(ParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"));
+ output_pub_keys.push_back(expected_output);
+
+ std::unordered_map<CKeyID, uint256, SaltedSipHasher> labels;
+ const auto found_outputs = bip352::ScanForSilentPaymentsOutputs(scan_key, *prevouts_summary, spend_key.GetPubKey(), output_pub_keys, labels);
+ BOOST_REQUIRE(found_outputs.has_value());
+ BOOST_REQUIRE_EQUAL(found_outputs->size(), 1);
+ BOOST_CHECK(found_outputs->front().output == expected_output);
+}
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet