Merge bitcoin/bitcoin#35301: Silent Payments: Implement bip352 (take 2)
What changed, and why it matters
This commit adds the first implementation of BIP352 (Silent Payments) to Bitcoin Core. Silent Payments are a new type of privacy-preserving Bitcoin address that lets someone receive payments without publicly revealing a fixed address. The change introduces new code for encoding/decoding silent payment addresses, deriving output public keys, and scanning transactions for outputs belonging to the recipient. It is a feature addition, not a fix for a known security bug. There is no evidence in the commit or supplied references that this introduces a vulnerability or that any security incident occurred.
Review the new BIP352 implementation for correctness against the BIP352 specification, especially edge cases in public key extraction, prevout handling, and secp256k1 silentpayments API usage. Continue monitoring the secp256k1 silentpayments module for upstream security advisories. No immediate security response is indicated by this commit alone.
Security signals we found
New cryptographic feature implementation (BIP352 Silent Payments)
Extensive use of secp256k1 silentpayments module
Input public key extraction from P2PKH, P2WPKH, P2SH-P2WPKH, and P2TR inputs
Validation of compressed/fully-valid public keys and Bech32m checksums
Rejection of unknown witness versions >1 and reserved address versions >=31
No vendor disclosure of security relevance, vulnerability, or incident
Evidence from the diff
The merge commit be5d0b5503fa820da86185537821359648f9f562 integrates PR #35301, implementing BIP352 logic in Bitcoin Core. It adds src/common/bip352.{h,cpp}, extends bech32 with a 1023-character limit for silent payment addresses, adds chain-specific ‘sp’ HRPs, exposes KeyPair::GetSecpKeypair, and includes BIP352 test vectors. The code performs secp256k1 silentpayments operations: address decoding, prevouts summary creation, output generation, and scanning. It validates public keys, rejects unknown segwit versions, skips invalid taproot outputs, and uses BIP352 test vectors. No security-relevant bug, patch, or incident is described in the commit or supplied references.
Changed components
src/common/bip352.cppsrc/common/bip352.hsrc/bech32.hsrc/kernel/chainparams.cppsrc/kernel/chainparams.hsrc/key.hsrc/test/bip352_tests.cppsrc/test/data/bip352_send_and_receive_vectors.jsonInspect captured patch +6951 / −0
### src/CMakeLists.txt
@@ -102,6 +102,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL
coins.cpp
common/args.cpp
common/bloom.cpp
+ common/bip352.cpp
common/config.cpp
common/init.cpp
common/interfaces.cpp
### src/bech32.h
@@ -39,6 +39,7 @@ enum class Encoding {
* and we would never encode an address with such a massive value */
enum CharLimit : size_t {
BECH32 = 90, //!< BIP173/350 imposed character limit for Bech32(m) encoded addresses. This guarantees finding up to 4 errors.
+ SILENT_PAYMENTS = 1023, //!< BIP352 imposed 1023 character limit on Bech32m encoded silent payment addresses. This guarantees finding up to 3 errors.
};
/** Encode a Bech32 or Bech32m string. If hrp contains uppercase characters, this will cause an
### src/common/bip352.cpp
@@ -0,0 +1,502 @@
+// Copyright (c) 2023 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <common/bip352.h>
+
+#include <addresstype.h>
+#include <bech32.h>
+#include <chainparams.h>
+#include <coins.h>
+#include <key.h>
+#include <primitives/transaction.h>
+#include <pubkey.h>
+#include <script/interpreter.h>
+#include <script/script.h>
+#include <script/sign.h>
+#include <script/solver.h>
+#include <script/verify_flags.h>
+#include <secp256k1.h>
+#include <secp256k1_extrakeys.h>
+#include <secp256k1_silentpayments.h>
+#include <span.h>
+#include <streams.h>
+#include <tinyformat.h>
+#include <uint256.h>
+#include <util/strencodings.h>
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <optional>
+#include <span>
+#include <type_traits>
+#include <utility>
+
+namespace bip352 {
+
+PrevoutsSummary::PrevoutsSummary(const secp256k1_silentpayments_prevouts_summary& prevouts_summary)
+ : m_prevouts_summary{std::make_unique<secp256k1_silentpayments_prevouts_summary>(prevouts_summary)} {}
+
+PrevoutsSummary::PrevoutsSummary(PrevoutsSummary&&) noexcept = default;
+PrevoutsSummary& PrevoutsSummary::operator=(PrevoutsSummary&&) noexcept = default;
+
+PrevoutsSummary::~PrevoutsSummary() = default;
+
+const secp256k1_silentpayments_prevouts_summary* PrevoutsSummary::Get() const
+{
+ return m_prevouts_summary.get();
+}
+
+std::optional<SilentPaymentsDestination> SilentPaymentsDestination::From(
+ const CPubKey& scan_pubkey,
+ const CPubKey& spend_pubkey,
+ uint8_t version,
+ std::span<const unsigned char> extension_data
+) {
+ if (version >= 31) return std::nullopt;
+ if (version == 0 && !extension_data.empty()) {
+ // V0 address has no extension data
+ return std::nullopt;
+ }
+ if (!scan_pubkey.IsFullyValid() || !scan_pubkey.IsCompressed()) return std::nullopt;
+ if (!spend_pubkey.IsFullyValid() || !spend_pubkey.IsCompressed()) return std::nullopt;
+ return SilentPaymentsDestination(version, scan_pubkey, spend_pubkey, extension_data);
+}
+
+util::Expected<SilentPaymentsDestination, std::string> DecodeSilentPaymentsAddress(
+ const std::string& str, const CChainParams& params)
+{
+ static constexpr size_t SILENT_PAYMENTS_V0_DATA_SIZE = 66;
+ static constexpr size_t SP_PUBKEYS_SIZE = 2 * CPubKey::COMPRESSED_SIZE;
+
+ const auto dec = bech32::Decode(str, bech32::CharLimit::SILENT_PAYMENTS);
+ if (dec.encoding != bech32::Encoding::BECH32M) {
+ return util::Unexpected{"Silent Payments address must use Bech32m checksum"};
+ }
+ if (dec.hrp != params.SilentPaymentsHRP()) {
+ return util::Unexpected{strprintf("Invalid or unsupported prefix for Silent Payments address (expected %s, got %s).", params.SilentPaymentsHRP(), dec.hrp)};
+ }
+ if (dec.data.empty()) {
+ return util::Unexpected{"Empty Bech32 data section"};
+ }
+ std::vector<unsigned char> data;
+ if (!ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {
+ return util::Unexpected{"Invalid padding in Silent payments address (Bech32m data section)"};
+ }
+ if (data.size() < SILENT_PAYMENTS_V0_DATA_SIZE) {
+ return util::Unexpected{strprintf("Silent payments data payload is too small (expected at least %d, got %d).", SILENT_PAYMENTS_V0_DATA_SIZE, data.size())};
+ }
+ const uint8_t version = dec.data[0];
+ if (version >= 31) {
+ return util::Unexpected{strprintf("This implementation only supports Silent payments addresses v0 through v30 (got %d).", version)};
+ }
+ if (version == 0 && data.size() != SILENT_PAYMENTS_V0_DATA_SIZE) {
+ return util::Unexpected{strprintf("Silent payments version is v0 but data is not the correct size (expected %d, got %d).", SILENT_PAYMENTS_V0_DATA_SIZE, data.size())};
+ }
+ CPubKey scan_pubkey{data.begin(), data.begin() + CPubKey::COMPRESSED_SIZE};
+ CPubKey spend_pubkey{data.begin() + CPubKey::COMPRESSED_SIZE, data.begin() + 2 * CPubKey::COMPRESSED_SIZE};
+ std::span<unsigned char> extension_data{data.data() + SP_PUBKEYS_SIZE, data.size() - SP_PUBKEYS_SIZE};
+ auto sp_dest = SilentPaymentsDestination::From(scan_pubkey, spend_pubkey, version, extension_data);
+ if (!sp_dest) {
+ return util::Unexpected{"Invalid Silent payments address"};
+ }
+ return *sp_dest;
+}
+
+SilentPaymentsLabel::SilentPaymentsLabel(const secp256k1_silentpayments_label& label) {
+ m_label = std::make_unique<secp256k1_silentpayments_label>(label);
+ int ret = secp256k1_silentpayments_recipient_label_serialize(secp256k1_context_static, m_vch, m_label.get());
+ assert(ret);
+}
+
+std::optional<SilentPaymentsLabel> SilentPaymentsLabel::FromBytes(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> vch)
+{
+ secp256k1_silentpayments_label label_obj;
+ if (!secp256k1_silentpayments_recipient_label_parse(secp256k1_context_static, &label_obj, vch.data())) {
+ return std::nullopt;
+ }
+ return SilentPaymentsLabel(label_obj);
+}
+
+SilentPaymentsLabel::SilentPaymentsLabel(SilentPaymentsLabel&&) noexcept = default;
+SilentPaymentsLabel& SilentPaymentsLabel::operator=(SilentPaymentsLabel&&) noexcept = default;
+SilentPaymentsLabel::~SilentPaymentsLabel() = default;
+
+SilentPaymentsLabel::SilentPaymentsLabel(const SilentPaymentsLabel& label)
+ : m_label{std::make_unique<secp256k1_silentpayments_label>(*label.m_label)}
+{
+ memcpy(m_vch, label.m_vch, CPubKey::COMPRESSED_SIZE);
+}
+SilentPaymentsLabel& SilentPaymentsLabel::operator=(const SilentPaymentsLabel& label) {
+ if (this != &label) {
+ m_label = std::make_unique<secp256k1_silentpayments_label>(*label.m_label);
+ memcpy(m_vch, label.m_vch, CPubKey::COMPRESSED_SIZE);
+ }
+ return *this;
+}
+
+const secp256k1_silentpayments_label* SilentPaymentsLabel::Get() const {
+ return m_label.get();
+}
+
+std::optional<PubKey> GetPubKeyFromInput(const CTxIn& txin, const CScript& spk)
+{
+ std::vector<std::vector<unsigned char>> solutions;
+ const TxoutType type = Solver(spk, solutions);
+
+ if (type == TxoutType::WITNESS_V1_TAPROOT) {
+ const auto& stack = txin.scriptWitness.stack;
+ if (stack.empty()) return std::nullopt;
+ const bool has_annex = !stack.back().empty() && stack.back()[0] == ANNEX_TAG;
+ const size_t effective_size = stack.size() - (has_annex ? 1 : 0);
+
+ if (effective_size > 1) {
+ // BIP-352: skip script-path spends using NUMS-H internal key.
+ // Validate control block size before checking internal key.
+ const auto& control = stack[effective_size - 1];
+ if (control.size() < TAPROOT_CONTROL_BASE_SIZE ||
+ control.size() > TAPROOT_CONTROL_MAX_SIZE ||
+ (control.size() - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_NODE_SIZE != 0) {
+ return std::nullopt;
+ }
+ if (std::equal(WitnessV1Taproot::NUMS_H.begin(), WitnessV1Taproot::NUMS_H.end(), control.begin() + 1)) {
+ return std::nullopt;
+ }
+ }
+
+ XOnlyPubKey key{solutions[0]};
+ if (!key.IsFullyValid()) return std::nullopt;
+ return PubKey{key};
+ }
+
+ if (type == TxoutType::WITNESS_V0_KEYHASH) {
+ const auto& stack = txin.scriptWitness.stack;
+ if (stack.empty()) return std::nullopt;
+ CPubKey key{stack.back()};
+ if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;
+ return PubKey{key};
+ }
+
+ if (type == TxoutType::PUBKEYHASH) {
+ std::vector<std::vector<unsigned char>> stack;
+ if (!EvalScript(stack, txin.scriptSig, SCRIPT_VERIFY_NONE, DUMMY_CHECKER, SigVersion::BASE)) {
+ return std::nullopt;
+ }
+ if (stack.empty()) return std::nullopt;
+ CPubKey key{stack.back()};
+ if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;
+ return PubKey{key};
+ }
+
+ if (type == TxoutType::SCRIPTHASH) {
+ // P2SH-P2WPKH only: eval scriptSig, verify redeem script is P2WPKH.
+ std::vector<std::vector<unsigned char>> stack;
+ if (!EvalScript(stack, txin.scriptSig, SCRIPT_VERIFY_NONE, DUMMY_CHECKER, SigVersion::BASE)) {
+ return std::nullopt;
+ }
+ if (stack.empty()) return std::nullopt;
+ CScript redeem{stack.back().begin(), stack.back().end()};
+ if (Solver(redeem, solutions) != TxoutType::WITNESS_V0_KEYHASH) return std::nullopt;
+ if (txin.scriptWitness.stack.empty()) return std::nullopt;
+ CPubKey key{txin.scriptWitness.stack.back()};
+ if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;
+ return PubKey{key};
+ }
+
+ return std::nullopt;
+}
+
+static std::optional<PrevoutsSummary> CreateInputPubkeysTweak(
+ const std::vector<CPubKey>& plain_pubkeys,
+ const std::vector<XOnlyPubKey>& taproot_pubkeys,
+ const COutPoint& smallest_outpoint)
+{
+ secp256k1_silentpayments_prevouts_summary prevouts_summary;
+ std::vector<secp256k1_pubkey> plain_pubkey_objs;
+ std::vector<secp256k1_pubkey*> plain_pubkey_ptrs;
+ plain_pubkey_objs.reserve(plain_pubkeys.size());
+ plain_pubkey_ptrs.reserve(plain_pubkeys.size());
+ for (const CPubKey& pubkey : plain_pubkeys) {
+ bool ret = secp256k1_ec_pubkey_parse(secp256k1_context_static,
+ &plain_pubkey_objs.emplace_back(), pubkey.data(), pubkey.size());
+ // This pubkey is expected to be valid because GetPubKeyFromInput()
+ // already called IsFullyValid() before including it here
+ assert(ret);
+ plain_pubkey_ptrs.push_back(&plain_pubkey_objs.back());
+ }
+
+ std::vector<secp256k1_xonly_pubkey> taproot_pubkey_objs;
+ std::vector<secp256k1_xonly_pubkey*> taproot_pubkey_ptrs;
+ taproot_pubkey_objs.reserve(taproot_pubkeys.size());
+ taproot_pubkey_ptrs.reserve(taproot_pubkeys.size());
+ for (const XOnlyPubKey& pubkey : taproot_pubkeys) {
+ bool ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static,
+ &taproot_pubkey_objs.emplace_back(), pubkey.data());
+ // This xonlypubkey is expected to be valid because
+ // GetPubKeyFromInput() already called IsFullyValid()
+ // before including it here
+ assert(ret);
+ taproot_pubkey_ptrs.push_back(&taproot_pubkey_objs.back());
+ }
+
+ std::array<std::byte, 36> smallest_outpoint_ser;
+ SpanWriter{smallest_outpoint_ser} << smallest_outpoint;
+ bool ret = secp256k1_silentpayments_recipient_prevouts_summary_create(secp256k1_context_static,
+ &prevouts_summary,
+ UCharCast(smallest_outpoint_ser.data()),
+ taproot_pubkey_ptrs.data(), taproot_pubkey_ptrs.size(),
+ plain_pubkey_ptrs.data(), plain_pubkey_ptrs.size()
+ );
+ if (!ret) return std::nullopt;
+ return PrevoutsSummary(prevouts_summary);
+}
+
+util::Expected<PrevoutsSummary, PrevoutsSummaryError> GetSilentPaymentsPrevoutsSummary(const std::vector<CTxIn>& vin, const std::map<COutPoint, Coin>& coins)
+{
+ // Extract the keys from the inputs
+ // or skip if no valid inputs
+ std::vector<CPubKey> pubkeys;
+ std::vector<XOnlyPubKey> xonly_pubkeys;
+ std::vector<COutPoint> tx_outpoints;
+ for (const CTxIn& txin : vin) {
+ const auto coin_it = coins.find(txin.prevout);
+ if (coin_it == coins.end()) return util::Unexpected(PrevoutsSummaryError::MISSING_COIN);
+ const Coin& coin = coin_it->second;
+ int witness_version{0};
+ std::vector<unsigned char> witness_program;
+ // BIP352 v0 skips transactions spending future witness versions.
+ if (coin.out.scriptPubKey.IsWitnessProgram(witness_version, witness_program) && witness_version > 1) {
+ return util::Unexpected(PrevoutsSummaryError::NOT_ELIGIBLE);
+ }
+ tx_outpoints.emplace_back(txin.prevout);
+ auto pubkey = GetPubKeyFromInput(txin, coin.out.scriptPubKey);
+ if (pubkey.has_value()) {
+ std::visit([&pubkeys, &xonly_pubkeys](auto&& pubkey) {
+ using T = std::decay_t<decltype(pubkey)>;
+ if constexpr (std::is_same_v<T, CPubKey>) {
+ pubkeys.push_back(pubkey);
+ } else if constexpr (std::is_same_v<T, XOnlyPubKey>) {
+ xonly_pubkeys.push_back(pubkey);
+ }
+ }, *pubkey);
+ }
+ }
+ if (pubkeys.size() + xonly_pubkeys.size() == 0) return util::Unexpected(PrevoutsSummaryError::NOT_ELIGIBLE);
+ auto smallest_outpoint = std::min_element(tx_outpoints.begin(), tx_outpoints.end(), BIP352Comparator());
+ auto tweak = CreateInputPubkeysTweak(pubkeys, xonly_pubkeys, *smallest_outpoint);
+ if (!tweak.has_value()) return util::Unexpected(PrevoutsSummaryError::NOT_ELIGIBLE);
+ return std::move(*tweak);
+}
+
+static std::optional<std::vector<secp256k1_xonly_pubkey>> CreateOutputs(
+ const std::vector<SilentPaymentsDestination>& recipients,
+ const std::vector<CKey>& plain_keys,
+ const std::vector<KeyPair>& taproot_keypairs,
+ const COutPoint& smallest_outpoint
+) {
+ bool ret;
+ std::vector<const secp256k1_keypair *> taproot_keypair_ptrs;
+ std::vector<const unsigned char *> plain_key_ptrs;
+ taproot_keypair_ptrs.reserve(taproot_keypairs.size());
+ plain_key_ptrs.reserve(plain_keys.size());
+
+ std::vector<secp256k1_silentpayments_recipient> recipient_objs;
+ std::vector<const secp256k1_silentpayments_recipient *> recipient_ptrs;
+ recipient_objs.reserve(recipients.size());
+ recipient_ptrs.reserve(recipients.size());
+
+ std::vector<secp256k1_xonly_pubkey> generated_outputs;
+ std::vector<secp256k1_xonly_pubkey *> generated_output_ptrs;
+ generated_outputs.reserve(recipients.size());
+ generated_output_ptrs.reserve(recipients.size());
+
+ for (size_t i = 0; i < recipients.size(); i++) {
+ secp256k1_silentpayments_recipient recipient_obj;
+ ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.scan_pubkey, recipients[i].GetScanPubKey().data(), recipients[i].GetScanPubKey().size());
+ assert(ret);
+ ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.spend_pubkey, recipients[i].GetSpendPubKey().data(), recipients[i].GetSpendPubKey().size());
+ assert(ret);
+ recipient_obj.index = i;
+ recipient_objs.push_back(recipient_obj);
+ recipient_ptrs.push_back(&recipient_objs[i]);
+
+ secp256k1_xonly_pubkey generated_output{};
+ generated_outputs.push_back(generated_output);
+ generated_output_ptrs.push_back(&generated_outputs[i]);
+ }
+
+ for (const auto& key : plain_keys) {
+ if (!key.IsValid()) return std::nullopt;
+ plain_key_ptrs.push_back(UCharCast(key.begin()));
+ }
+ for (const auto& keypair : taproot_keypairs) {
+ if (!keypair.IsValid()) return std::nullopt;
+ taproot_keypair_ptrs.push_back(keypair.GetSecpKeypair());
+ }
+
+ // Serialize the outpoint
+ std::array<std::byte, 36> smallest_outpoint_ser;
+ SpanWriter{smallest_outpoint_ser} << smallest_outpoint;
+
+ ret = secp256k1_silentpayments_sender_create_outputs(GetSecp256k1SignContext(),
+ generated_output_ptrs.data(),
+ recipient_ptrs.data(), recipient_ptrs.size(),
+ UCharCast(smallest_outpoint_ser.data()),
+ taproot_keypair_ptrs.data(), taproot_keypair_ptrs.size(),
+ plain_key_ptrs.data(), plain_key_ptrs.size()
+ );
+ if (!ret) return std::nullopt;
+ return generated_outputs;
+}
+
+std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<CKey>& plain_keys, const std::vector<KeyPair>& taproot_keys, const COutPoint& smallest_outpoint)
+{
+ if (sp_dests.empty()) return std::map<size_t, WitnessV1Taproot>();
+
+ assert(!smallest_outpoint.IsNull());
+ assert(!plain_keys.empty() || !taproot_keys.empty());
+
+ bool ret;
+ std::map<size_t, WitnessV1Taproot> tr_dests;
+ std::vector<SilentPaymentsDestination> recipients;
+ recipients.reserve(sp_dests.size());
+ for (const auto& [_, addr] : sp_dests) {
+ recipients.push_back(addr);
+ }
+ auto outputs = CreateOutputs(recipients, plain_keys, taproot_keys, smallest_outpoint);
+ // This will fail if any input pubkey is null or
+ // inputs were maliciously crafted to sum to zero
+ if (!outputs) return std::nullopt;
+ assert(sp_dests.size() == outputs->size());
+ size_t output_i{0};
+ for (const auto& [i, _] : sp_dests) {
+ unsigned char xonly_pubkey_bytes[32];
+ ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, xonly_pubkey_bytes, &outputs.value()[output_i]);
+ assert(ret);
+ tr_dests[i] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};
+ output_i++;
+ }
+ return tr_dests;
+}
+
+static const unsigned char* LabelLookupCallback(const unsigned char* key, const void* context) {
+ auto label_context = static_cast<const LabelTweakMap*>(context);
+ auto it = label_context->find(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE>{key, CPubKey::COMPRESSED_SIZE});
+ if (it != label_context->end()) {
+ return it->second.begin();
+ }
+ return nullptr;
+}
+
+static std::pair<SilentPaymentsLabel, uint256> CreateLabel(const CKey& scan_key, const uint32_t m) {
+ secp256k1_silentpayments_label label_obj;
+ unsigned char label_tweak[32];
+ bool ret = secp256k1_silentpayments_recipient_label_create(GetSecp256k1SignContext(), &label_obj, label_tweak, UCharCast(scan_key.data()), m);
+ assert(ret);
+ return {SilentPaymentsLabel(label_obj), uint256{label_tweak}};
+}
+
+static CPubKey CreateLabeledSpendPubKey(const CPubKey& spend_pubkey, const SilentPaymentsLabel& label) {
+ secp256k1_pubkey spend_obj, labeled_spend_obj;
+ bool ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &spend_obj, spend_pubkey.data(), spend_pubkey.size());
+ assert(ret);
+ ret = secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(secp256k1_context_static, &labeled_spend_obj, &spend_obj, label.Get());
+ assert(ret);
+ size_t pubkeylen = CPubKey::COMPRESSED_SIZE;
+ CPubKey labeled_spend_pubkey;
+ ret = secp256k1_ec_pubkey_serialize(secp256k1_context_static, (unsigned char*)labeled_spend_pubkey.begin(), &pubkeylen, &labeled_spend_obj, SECP256K1_EC_COMPRESSED);
+ assert(ret);
+ return labeled_spend_pubkey;
+}
+
+SilentPaymentsReceiver::SilentPaymentsReceiver(const CKey& scan_key, const CPubKey& spend_pubkey,
+ const LabelTweakMap& labels) : m_scan_key(scan_key), m_spend_pubkey(spend_pubkey), m_labels(labels)
+{
+ m_change_it = m_labels.emplace(CreateLabel(scan_key, 0)).first;
+ m_spend_pubkey_obj = std::make_unique<secp256k1_pubkey>();
+ int ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, m_spend_pubkey_obj.get(), m_spend_pubkey.data(), m_spend_pubkey.size());
+ assert(ret);
+}
+
+SilentPaymentsReceiver::~SilentPaymentsReceiver() = default;
+
+const LabelTweakMap& SilentPaymentsReceiver::GetLabels() const {
+ return m_labels;
+}
+
+SilentPaymentsDestination SilentPaymentsReceiver::BuildLabeledDestination(const SilentPaymentsLabel& label) const {
+ CPubKey labeled_spend_pubkey = CreateLabeledSpendPubKey(m_spend_pubkey, label);
+ auto dest{SilentPaymentsDestination::From(m_scan_key.GetPubKey(), labeled_spend_pubkey)};
+ assert(dest);
+ return *dest;
+}
+
+SilentPaymentsDestination SilentPaymentsReceiver::GenerateLabeledAddress(uint32_t m) {
+ assert(m >= 1);
+ auto it = m_labels.emplace(CreateLabel(m_scan_key, m)).first;
+ return BuildLabeledDestination(it->first);
+}
+
+SilentPaymentsDestination SilentPaymentsReceiver::GetChangeDestination() const {
+ return BuildLabeledDestination(m_change_it->first);
+}
+
+std::optional<std::vector<SilentPaymentsOutput>> SilentPaymentsReceiver::Scan(
+ const PrevoutsSummary& prevouts_summary,
+ const std::vector<XOnlyPubKey>& tx_outputs
+) const {
+ bool ret;
+ std::vector<secp256k1_silentpayments_found_output> found_output_objs;
+ std::vector<secp256k1_silentpayments_found_output *> found_output_ptrs;
+ std::vector<secp256k1_xonly_pubkey> tx_output_objs;
+ std::vector<const secp256k1_xonly_pubkey *> tx_output_ptrs;
+ found_output_objs.reserve(tx_outputs.size());
+ found_output_ptrs.reserve(tx_outputs.size());
+ tx_output_objs.reserve(tx_outputs.size());
+ tx_output_ptrs.reserve(tx_outputs.size());
+
+ assert(m_scan_key.IsValid());
+ assert(m_spend_pubkey_obj);
+
+ for (const XOnlyPubKey& tx_output : tx_outputs) {
+ secp256k1_xonly_pubkey tx_output_obj;
+ 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.back());
+ found_output_objs.emplace_back();
+ found_output_ptrs.push_back(&found_output_objs.back());
+ }
+ if (tx_output_ptrs.empty()) return std::vector<SilentPaymentsOutput>{};
+
+ // Scan the outputs!
+ uint32_t n_found_outputs = 0;
+ ret = secp256k1_silentpayments_recipient_scan_outputs(secp256k1_context_static,
+ found_output_ptrs.data(), &n_found_outputs,
+ tx_output_ptrs.data(), tx_output_ptrs.size(),
+ UCharCast(m_scan_key.begin()),
+ prevouts_summary.Get(),
+ m_spend_pubkey_obj.get(),
+ LabelLookupCallback,
+ &m_labels
+ );
+ if (!ret) return std::nullopt;
+
+ std::vector<SilentPaymentsOutput> outputs;
+ for (size_t i = 0; i < n_found_outputs; i++) {
+ SilentPaymentsOutput sp_output;
+ ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, sp_output.output.begin(), &found_output_objs[i].output);
+ assert(ret);
+ sp_output.tweak = uint256{found_output_objs[i].tweak};
+ if (found_output_objs[i].found_with_label) {
+ sp_output.label = SilentPaymentsLabel(found_output_objs[i].label);
+ }
+ outputs.emplace_back(std::move(sp_output));
+ }
+ return outputs;
+}
+}; // namespace bip352
### src/common/bip352.h
@@ -0,0 +1,305 @@
+// Copyright (c) 2023 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_COMMON_BIP352_H
+#define BITCOIN_COMMON_BIP352_H
+
+#include <addresstype.h>
+#include <attributes.h>
+#include <compat/byteswap.h>
+#include <key.h>
+#include <primitives/transaction.h>
+#include <pubkey.h>
+#include <uint256.h>
+#include <util/expected.h>
+
+#include <array>
+#include <compare>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <variant>
+#include <vector>
+
+struct secp256k1_silentpayments_label;
+struct secp256k1_silentpayments_prevouts_summary;
+struct secp256k1_pubkey;
+class CChainParams;
+class CScript;
+class Coin;
+
+namespace bip352 {
+
+using PubKey = std::variant<CPubKey, XOnlyPubKey>;
+
+class PrevoutsSummary
+{
+private:
+ std::unique_ptr<secp256k1_silentpayments_prevouts_summary> m_prevouts_summary;
+
+public:
+ PrevoutsSummary(const secp256k1_silentpayments_prevouts_summary& prevouts_summary);
+ PrevoutsSummary(PrevoutsSummary&&) noexcept;
+ PrevoutsSummary& operator=(PrevoutsSummary&&) noexcept;
+ ~PrevoutsSummary();
+
+ // Delete copy constructors
+ PrevoutsSummary(const PrevoutsSummary&) = delete;
+ PrevoutsSummary& operator=(const PrevoutsSummary&) = delete;
+
+ const secp256k1_silentpayments_prevouts_summary* Get() const LIFETIMEBOUND;
+};
+
+struct BIP352Comparator {
+ bool operator()(const COutPoint& a, const COutPoint& b) const {
+ // BIP352 defines the "smallest outpoint" based on a lexicographic
+ // sort of the outpoints, using the 36-byte serialization:
+ // <txid, 32-bytes little-endian>:<vout, 4-bytes little-endian>
+ if (a.hash != b.hash) return a.hash < b.hash;
+ return internal_bswap_32(a.n) < internal_bswap_32(b.n);
+ }
+};
+
+struct SilentPaymentsDestination
+{
+private:
+ uint8_t m_version;
+ CPubKey m_scan_pubkey;
+ CPubKey m_spend_pubkey;
+ std::vector<unsigned char> m_extension_data;
+
+ SilentPaymentsDestination(
+ uint8_t version,
+ const CPubKey& scan_pubkey,
+ const CPubKey& spend_pubkey,
+ std::span<const unsigned char> extension_data = {}
+ ) : m_version(version), m_scan_pubkey(scan_pubkey),
+ m_spend_pubkey(spend_pubkey),
+ m_extension_data(extension_data.begin(), extension_data.end()) {};
+public:
+ static std::optional<SilentPaymentsDestination> From(
+ const CPubKey& scan_pubkey,
+ const CPubKey& spend_pubkey,
+ uint8_t version = 0,
+ std::span<const unsigned char> extension_data = {}
+ );
+
+ uint8_t GetVersion() const { return m_version; }
+ const CPubKey& GetScanPubKey() const { return m_scan_pubkey; }
+ const CPubKey& GetSpendPubKey() const { return m_spend_pubkey; }
+ std::span<const unsigned char> GetExtensionData() const { return m_extension_data; }
+
+ bool operator==(const SilentPaymentsDestination&) const = default;
+};
+
+//! Decode a BIP352 "sp1..." address. Returns the destination, or an error message on failure.
+util::Expected<SilentPaymentsDestination, std::string> DecodeSilentPaymentsAddress(
+ const std::string& str, const CChainParams& params);
+
+class SilentPaymentsLabel {
+private:
+ std::unique_ptr<secp256k1_silentpayments_label> m_label;
+ unsigned char m_vch[CPubKey::COMPRESSED_SIZE];
+
+ //! Parses raw bytes into a fully valid label
+ //! returns std::nullopt if vch is not a validly-encoded label.
+ static std::optional<SilentPaymentsLabel> FromBytes(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> vch);
+
+public:
+ SilentPaymentsLabel(const secp256k1_silentpayments_label& label);
+
+ SilentPaymentsLabel(SilentPaymentsLabel&&) noexcept;
+ SilentPaymentsLabel& operator=(SilentPaymentsLabel&&) noexcept;
+ SilentPaymentsLabel(const SilentPaymentsLabel&);
+ SilentPaymentsLabel& operator=(const SilentPaymentsLabel&);
+
+ ~SilentPaymentsLabel();
+
+ friend bool operator==(const SilentPaymentsLabel& a, const SilentPaymentsLabel& b) {
+ return memcmp(a.m_vch, b.m_vch, CPubKey::COMPRESSED_SIZE) == 0;
+ }
+ friend bool operator<(const SilentPaymentsLabel& a, const SilentPaymentsLabel& b) {
+ return memcmp(a.m_vch, b.m_vch, CPubKey::COMPRESSED_SIZE) < 0;
+ }
+ friend bool operator>(const SilentPaymentsLabel& a, const SilentPaymentsLabel& b) {
+ return b < a;
+ }
+ //! Transparent comparisons against a raw 33-byte compressed label key, so a
+ //! LabelTweakMap can be looked up by key bytes (e.g. from a secp256k1 callback)
+ //! without constructing a fully-parsed SilentPaymentsLabel.
+ friend bool operator<(const SilentPaymentsLabel& a, std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> b) {
+ return memcmp(a.m_vch, b.data(), CPubKey::COMPRESSED_SIZE) < 0;
+ }
+ friend bool operator<(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> a, const SilentPaymentsLabel& b) {
+ return memcmp(a.data(), b.m_vch, CPubKey::COMPRESSED_SIZE) < 0;
+ }
+
+ template <typename Stream>
+ void Serialize(Stream& s) const
+ {
+ s << std::span{m_vch, CPubKey::COMPRESSED_SIZE};
+ }
+
+ template <typename Stream>
+ static std::optional<SilentPaymentsLabel> Unserialize(Stream& s)
+ {
+ std::array<unsigned char, CPubKey::COMPRESSED_SIZE> vch;
+ s >> std::span{vch};
+ return FromBytes(std::span{vch});
+ }
+
+ const secp256k1_silentpayments_label* Get() const LIFETIMEBOUND;
+};
+
+struct SilentPaymentsOutput {
+ XOnlyPubKey output;
+ uint256 tweak;
+ std::optional<SilentPaymentsLabel> label;
+};
+
+/**
+ * @brief Get the public key from an input.
+ *
+ * Get the public key from a silent payments eligible input. This requires knowledge of the prevout
+ * scriptPubKey to determine the type of input and whether or not it is eligible for silent payments.
+ *
+ * If the input is not eligible for silent payments, the input is skipped (indicated by returning a nullopt).
+ *
+ * @param txin The transaction input.
+ * @param spk The scriptPubKey of the prevout.
+ * @return The public key, or nullopt if not found.
+ */
+std::optional<PubKey> GetPubKeyFromInput(const CTxIn& txin, const CScript& spk);
+
+/**
+ * @brief Generate silent payments taproot destinations.
+ *
+ * Given a set of silent payments destinations, generate the requested number of outputs. If a silent payment
+ * destination is repeated, this indicates multiple outputs are requested for the same recipient. The silent payment
+ * destinations are passed in a map where the key indicates their desired position in the final tx.vout array.
+ *
+ * @param sp_dests The silent payments destinations.
+ * @param plain_keys The private keys for non-taproot inputs.
+ * @param taproot_keys The keypairs for taproot inputs.
+ * @param smallest_outpoint The smallest_outpoint from the transaction inputs.
+ * @pre smallest_outpoint is not null, and at least one of plain_keys or taproot_keys is non-empty.
+ * @return The generated silent payments taproot destinations or std::nullopt if the set of provided inputs is invalid:
+ * - The size of any group (i.e. recipients sharing the same scan public key)
+ * exceeds the protocol limit SECP256K1_SILENTPAYMENTS_RECIPIENT_GROUP_LIMIT.
+ * - There is an invalid key in the set.
+ * - The inputs sum to zero.
+ */
+std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<CKey>& plain_keys, const std::vector<KeyPair>& taproot_keys, const COutPoint& smallest_outpoint);
+
+enum class PrevoutsSummaryError {
+ //! A prevout referenced by an input in `vin` has no corresponding entry in `coins`.
+ MISSING_COIN,
+ //! This transaction is not eligible to be scanned for silent payments outputs: either none
+ //! of its inputs yielded a public key eligible for silent payments (e.g. an input spends an
+ //! unknown segwit version), or the eligible inputs' public keys summed to the point at infinity.
+ NOT_ELIGIBLE,
+};
+
+/**
+ * @brief Get silent payments public data from transaction inputs.
+ *
+ * Get the necessary data from the transaction inputs to be able to scan the transaction outputs for silent payments outputs.
+ * This requires knowledge of the prevout scriptPubKey, which is passed via `coins`.
+ *
+ * This function returns the public key sum and the input hash separately and is intended to be used by the wallet when scanning
+ * a transaction.
+ *
+ * If there are no eligible inputs, or one of the inputs spends an unknown segwit version (i.e > 1), this transaction is not
+ * eligible to be scanned for silent payments outputs; see PrevoutsSummaryError.
+ *
+ * @param vin The transaction inputs.
+ * @param coins The coins (potentially) spent in this transaction.
+ * @return util::Expected<PrevoutsSummary, PrevoutsSummaryError> The silent payments public data, or the reason it could not be computed.
+ */
+util::Expected<PrevoutsSummary, PrevoutsSummaryError> GetSilentPaymentsPrevoutsSummary(const std::vector<CTxIn>& vin, const std::map<COutPoint, Coin>& coins);
+
+using LabelTweakMap = std::map<SilentPaymentsLabel, uint256, std::less<>>;
+
+/**
+ * @brief A silent payments recipient's scanning identity.
+ *
+ * Bundles a recipient's scan key, spend public key, and known labels (always including the change
+ * label) into a single self-contained object capable of scanning transactions and deriving the
+ * recipient's change destination.
+ *
+ * The change label is created automatically on construction if one is not supplied through
+ * the labels map; additional labels are registered on creation with GenerateLabeledAddress().
+ */
+class SilentPaymentsReceiver {
+private:
+ CKey m_scan_key;
+ CPubKey m_spend_pubkey;
+ std::unique_ptr<secp256k1_pubkey> m_spend_pubkey_obj;
+ LabelTweakMap m_labels;
+ LabelTweakMap::const_iterator m_change_it;
+
+ SilentPaymentsDestination BuildLabeledDestination(const SilentPaymentsLabel& label) const;
+
+public:
+ SilentPaymentsReceiver(const CKey& scan_key, const CPubKey& spend_pubkey, const LabelTweakMap& labels = {});
+ ~SilentPaymentsReceiver();
+
+ // Default move would leave m_scan_key and m_labels empty while
+ // m_spend_pubkey remains fully populated, leaving the
+ // SilentPaymentsReceiver object in an inconsistent state.
+ SilentPaymentsReceiver(SilentPaymentsReceiver&&) = delete;
+ SilentPaymentsReceiver& operator=(SilentPaymentsReceiver&&) = delete;
+
+ SilentPaymentsReceiver(const SilentPaymentsReceiver&) = delete;
+ SilentPaymentsReceiver& operator=(const SilentPaymentsReceiver&) = delete;
+
+ /**
+ * @brief Get this recipient's registered labels, including the change label.
+ *
+ * @return const LabelTweakMap& Each label mapped to its scalar tweak.
+ */
+ const LabelTweakMap& GetLabels() const;
+
+ /**
+ * @brief Register label `m` (e.g. for a labeled sub-address to hand out to a payer) and generate
+ * the resulting address.
+ *
+ * Derives the label from this recipient's own scan key and `m`, registers it so Scan() can
+ * recognize outputs sent to it, and returns the address to hand out.
+ *
+ * @param m An integer m, greater than 0 (m = 0 is reserved for the change label).
+ * @return SilentPaymentsDestination The labeled destination, with `B_spend -> B_spend + label`.
+ */
+ SilentPaymentsDestination GenerateLabeledAddress(uint32_t m);
+
+ /**
+ * @brief Get this recipient's silent payments change destination.
+ *
+ * @return SilentPaymentsDestination The destination to use for this recipient's own change outputs,
+ * i.e. `B_spend -> B_spend + change_label`.
+ */
+ SilentPaymentsDestination GetChangeDestination() const;
+
+ /**
+ * @brief Scan a transaction for silent payments outputs.
+ *
+ * Scan the transaction for silent payments outputs intended for this recipient. The output, shared
+ * secret tweak, and (optionally) label public key is returned for each output found. If the output
+ * was sent to a labeled address, the label tweak is added to the shared secret tweak. The shared
+ * secret tweak is needed to spend the output, by adding it to the spend secret key. If no outputs
+ * are found, this transaction does not contain silent payments outputs for this recipient.
+ *
+ * @param prevouts_summary The silent payments public data.
+ * @param tx_outputs The taproot output public keys.
+ * @return The found outputs or std::nullopt in the case of an error.
+ */
+ std::optional<std::vector<SilentPaymentsOutput>> Scan(const PrevoutsSummary& prevouts_summary, const std::vector<XOnlyPubKey>& tx_outputs) const;
+};
+}; // namespace bip352
+#endif // BITCOIN_COMMON_BIP352_H
### src/kernel/chainparams.cpp
@@ -180,6 +180,7 @@ class CMainParams : public CChainParams {
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4};
bech32_hrp = "bc";
+ silent_payments_hrp = "sp";
vFixedSeeds = std::vector<uint8_t>(std::begin(chainparams_seed_main), std::end(chainparams_seed_main));
@@ -300,6 +301,7 @@ class CTestNetParams : public CChainParams {
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
bech32_hrp = "tb";
+ silent_payments_hrp = "tsp";
vFixedSeeds = std::vector<uint8_t>(std::begin(chainparams_seed_test), std::end(chainparams_seed_test));
@@ -413,6 +415,7 @@ class CTestNet4Params : public CChainParams {
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
bech32_hrp = "tb";
+ silent_payments_hrp = "tsp";
vFixedSeeds = std::vector<uint8_t>(std::begin(chainparams_seed_testnet4), std::end(chainparams_seed_testnet4));
@@ -564,6 +567,7 @@ class SigNetParams : public CChainParams {
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
bech32_hrp = "tb";
+ silent_payments_hrp = "tsp";
fDefaultConsistencyChecks = false;
m_is_mockable_chain = false;
@@ -672,6 +676,7 @@ class CRegTestParams : public CChainParams
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
bech32_hrp = "bcrt";
+ silent_payments_hrp = "sprt";
// Copied from Testnet4.
m_headers_sync_params = HeadersSyncParams{
### src/kernel/chainparams.h
@@ -113,6 +113,7 @@ class CChainParams
const std::vector<std::string>& DNSSeeds() const { return vSeeds; }
const std::vector<unsigned char>& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; }
const std::string& Bech32HRP() const { return bech32_hrp; }
+ const std::string& SilentPaymentsHRP() const { return silent_payments_hrp; }
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
const HeadersSyncParams& HeadersSync() const { return m_headers_sync_params; }
@@ -190,6 +191,7 @@ class CChainParams
std::vector<std::string> vSeeds;
std::vector<unsigned char> base58Prefixes[MAX_BASE58_TYPES];
std::string bech32_hrp;
+ std::string silent_payments_hrp;
ChainType m_chain_type;
CBlock genesis;
std::vector<uint8_t> vFixedSeeds;
### src/key.h
@@ -7,6 +7,7 @@
#ifndef BITCOIN_KEY_H
#define BITCOIN_KEY_H
+#include <attributes.h>
#include <pubkey.h>
#include <script/keyorigin.h>
#include <serialize.h>
@@ -24,6 +25,7 @@
struct secp256k1_context_struct;
typedef struct secp256k1_context_struct secp256k1_context;
+struct secp256k1_keypair;
/**
* CPrivKey is a serialized private key, with all parameters included
@@ -319,6 +321,12 @@ class KeyPair
friend KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const;
[[nodiscard]] bool SignSchnorr(const uint256& hash, std::span<unsigned char> sig, const uint256& aux) const;
+ //! Pointer to this KeyPair's internal `secp256k1_keypair` data or nullptr if invalid.
+ const secp256k1_keypair* GetSecpKeypair() const LIFETIMEBOUND
+ {
+ return IsValid() ? reinterpret_cast<const secp256k1_keypair*>(m_keypair->data()) : nullptr;
+ }
+
//! Check whether this keypair is valid.
bool IsValid() const { return !!m_keypair; }
### src/test/CMakeLists.txt
@@ -19,6 +19,7 @@ add_executable(test_bitcoin
bech32_tests.cpp
bip32_tests.cpp
bip324_tests.cpp
+ bip352_tests.cpp
blockchain_tests.cpp
blockencodings_tests.cpp
blockfilter_index_tests.cpp
@@ -146,6 +147,7 @@ include(TargetDataSources)
target_json_data_sources(test_bitcoin
data/base58_encode_decode.json
data/bip341_wallet_vectors.json
+ data/bip352_send_and_receive_vectors.json
data/blockfilters.json
data/key_io_invalid.json
data/key_io_valid.json
### src/test/bip352_tests.cpp
@@ -0,0 +1,396 @@
+// Copyright (c) 2026-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <common/bip352.h>
+#include <chainparams.h>
+#include <coins.h>
+#include <span.h>
+#include <addresstype.h>
+#include <script/solver.h>
+#include <test/data/bip352_send_and_receive_vectors.json.h>
+
+#include <test/util/setup_common.h>
+
+#include <boost/test/unit_test.hpp>
+#include <test/util/json.h>
+#include <vector>
+#include <util/chaintype.h>
+#include <util/strencodings.h>
+#include <streams.h>
+
+namespace bip352 {
+BOOST_FIXTURE_TEST_SUITE(bip352_tests, BasicTestingSetup)
+
+CKey ParseHexToCKey(std::string_view hex) {
+ CKey output;
+ std::vector<unsigned char> hex_data = ParseHex(hex);
+ output.Set(hex_data.begin(), hex_data.end(), true);
+ return output;
+};
+
+BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)
+{
+ UniValue tests;
+ BOOST_REQUIRE(tests.read(json_tests::bip352_send_and_receive_vectors));
+
+ for (const auto& vec : tests.getValues()) {
+ // run sending tests
+ BOOST_TEST_MESSAGE(vec["comment"].get_str());
+
+ for (const auto& sender : vec["sending"].getValues()) {
+ const UniValue& given = sender["given"];
+ const UniValue& expected = sender["expected"];
+
+ std::vector<COutPoint> outpoints;
+ std::vector<CKey> keys;
+ std::vector<KeyPair> taproot_keys;
+ for (const auto& input : given["vin"].getValues()) {
+ COutPoint outpoint{Txid::FromHex(input["txid"].get_str()).value(), input["vout"].getInt<uint32_t>()};
+ outpoints.push_back(outpoint);
+ const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str());
+ CScript spk = CScript(spk_bytes.begin(), spk_bytes.end());
+ const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str());
+ CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end());
+ CTxIn txin{outpoint, script_sig};
+ // read the field txWitness as a stream and write txWitness >> witness.stack;
+ const auto witness_str = ParseHex(input["txinwitness"].get_str());
+ if (!witness_str.empty()) {
+ SpanReader(witness_str) >> txin.scriptWitness.stack;
+ }
+
+ // check if this is a silent payments input by trying to extract the public key
+ const auto& pubkey = GetPubKeyFromInput(txin, spk);
+ if (pubkey.has_value()) {
+ std::vector<std::vector<unsigned char>> solutions;
+ TxoutType type = Solver(spk, solutions);
+ if (type == TxoutType::WITNESS_V1_TAPROOT) {
+ taproot_keys.emplace_back(ParseHexToCKey(input["private_key"].get_str()).ComputeKeyPair(nullptr));
+ } else {
+ keys.emplace_back(ParseHexToCKey(input["private_key"].get_str()));
+ }
+ }
+ }
+ if (taproot_keys.empty() && keys.empty()) {
+ BOOST_CHECK(expected["outputs"].getValues()[0].empty());
+ continue;
+ }
+ // silent payments logic
+ auto smallest_outpoint = std::min_element(outpoints.begin(), outpoints.end(), BIP352Comparator());
+ std::map<size_t, SilentPaymentsDestination> sp_dests;
+ const std::vector<UniValue>& silent_payments_addresses = given["recipients"].getValues();
+ size_t sp_index = 0;
+ for (size_t i = 0; i < silent_payments_addresses.size(); ++i) {
+ auto sp = DecodeSilentPaymentsAddress(silent_payments_addresses[i]["address"].get_str(), Params());
+ BOOST_REQUIRE(sp.has_value());
+ if (!silent_payments_addresses[i]["scan_pub_key"].isNull()) {
+ BOOST_CHECK_EQUAL(HexStr(sp->GetScanPubKey()), silent_payments_addresses[i]["scan_pub_key"].get_str());
+ BOOST_CHECK_EQUAL(HexStr(sp->GetSpendPubKey()), silent_payments_addresses[i]["spend_pub_key"].get_str());
+ }
+ size_t count = silent_payments_addresses[i]["count"].isNull() ? 1 : (size_t)silent_payments_addresses[i]["count"].getInt<int>();
+ for (size_t j = 0; j < count; ++j) {
+ sp_dests.emplace(sp_index++, *sp);
+ }
+ }
+ auto sp_tr_dests = GenerateSilentPaymentsTaprootDestinations(sp_dests, keys, taproot_keys, *smallest_outpoint);
+ // This means the inputs summed to zero, which realistically would only happen maliciously. In this case, just move on
+ if (!sp_tr_dests.has_value()) {
+ // Check that we actually expect zero outputs to be generated for this test
+ BOOST_CHECK(expected["outputs"].getValues()[0].empty());
+ continue;
+ }
+ bool match = false;
+ for (const auto& candidate_set : expected["outputs"].getValues()) {
+ BOOST_CHECK(sp_tr_dests->size() == candidate_set.size());
+ std::vector<WitnessV1Taproot> expected_spks;
+ for (const auto& output : candidate_set.getValues()) {
+ const WitnessV1Taproot tap{XOnlyPubKey(ParseHex(output.get_str()))};
+ expected_spks.push_back(tap);
+ }
+ match = std::all_of(sp_tr_dests->begin(), sp_tr_dests->end(), [&](const auto& entry) {
+ return std::find(expected_spks.begin(), expected_spks.end(), entry.second) != expected_spks.end();
+ });
+ if (match) break;
+ }
+ BOOST_CHECK(match);
+ }
+
+ // Test receiving
+ for (const auto& recipient : vec["receiving"].getValues()) {
+
+ const UniValue& given = recipient["given"];
+ const UniValue& expected = recipient["expected"];
+
+ std::vector<CTxIn> vin;
+ std::map<COutPoint, Coin> coins;
+ for (const auto& input : given["vin"].getValues()) {
+ COutPoint outpoint{Txid::FromHex(input["txid"].get_str()).value(), input["vout"].getInt<uint32_t>()};
+ const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str());
+ CScript spk = CScript(spk_bytes.begin(), spk_bytes.end());
+ const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str());
+ CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end());
+ CTxIn txin{outpoint, script_sig};
+ // read the field txWitness as a stream and write txWitness >> witness.stack;
+ const auto witness_str = ParseHex(input["txinwitness"].get_str());
+ if (!witness_str.empty()) {
+ SpanReader(witness_str) >> txin.scriptWitness.stack;
+ }
+ vin.push_back(txin);
+ coins[outpoint] = Coin{CTxOut{{}, spk}, 0, false};
+ }
+ auto pub_tweak_data = GetSilentPaymentsPrevoutsSummary(vin, coins);
+ // If we don't get any tweak data from the transaction inputs, it is not a silent payment
+ // transaction, so we skip it.
+ if (!pub_tweak_data.has_value()) {
+ // Make sure this is expected and not just a failure of the GetSilentPaymentsPrevoutsSummary func
+ BOOST_CHECK(expected["outputs"].empty());
+ continue;
+ }
+ std::vector<XOnlyPubKey> output_pub_keys;
+ for (const auto& pubkey : given["outputs"].getValues()) {
+ output_pub_keys.emplace_back(ParseHex(pubkey.get_str()));
+ }
+
+ CKey scan_priv_key = ParseHexToCKey(given["key_material"]["scan_priv_key"].get_str());
+ CKey spend_priv_key = ParseHexToCKey(given["key_material"]["spend_priv_key"].get_str());
+ SilentPaymentsDestination sp_address{SilentPaymentsDestination::From(scan_priv_key.GetPubKey(), spend_priv_key.GetPubKey()).value()};
+ auto expected_address = DecodeSilentPaymentsAddress(expected["addresses"][0].get_str(), Params());
+ BOOST_REQUIRE(expected_address.has_value());
+ BOOST_CHECK(sp_address == *expected_address);
+
+ // The change label is registered automatically; only non-change labels need registering.
+ SilentPaymentsReceiver receiver{scan_priv_key, sp_address.GetSpendPubKey()};
+ auto given_labels{given["labels"].getValues()};
+ for (size_t i = 0; i < given_labels.size(); i++) {
+ const uint32_t m = given_labels[i].getInt<uint32_t>();
+ const SilentPaymentsDestination labeled_addr = (m == 0) ? receiver.GetChangeDestination() : receiver.GenerateLabeledAddress(m);
+ // expected["addresses"] contains the base silent payments address (at index 0)
+ // followed by the labeled addresses
+ auto sp = DecodeSilentPaymentsAddress(expected["addresses"][i+1].get_str(), Params());
+ BOOST_REQUIRE(sp.has_value());
+ BOOST_CHECK(labeled_addr == *sp);
+ }
+
+ // Scanning
+ const auto& found_outputs = receiver.Scan(*pub_tweak_data, output_pub_keys);
+ BOOST_REQUIRE(found_outputs.has_value());
+ // The transaction may be a silent payments transaction, but it does not contain any outputs for us,
+ // so we continue to the next transaction.
+ if (found_outputs->empty()) {
+ BOOST_CHECK(expected["outputs"].empty());
+ continue;
+ }
+ if (!expected["n_outputs"].isNull()) {
+ BOOST_CHECK_EQUAL(found_outputs->size(), (size_t)expected["n_outputs"].getInt<int>());
+ } else {
+ std::map<XOnlyPubKey, uint256> expected_outputs;
+ for (const auto& output : expected["outputs"].getValues()) {
+ expected_outputs.emplace(
+ XOnlyPubKey{ParseHex(output["pub_key"].get_str())},
+ uint256{ParseHex(output["priv_key_tweak"].get_str())});
+ }
+ BOOST_TEST_MESSAGE(found_outputs->size());
+ BOOST_REQUIRE_EQUAL(found_outputs->size(), expected_outputs.size());
+ for (const auto& output : *found_outputs) {
+ const auto expected_output = expected_outputs.find(output.output);
+ BOOST_REQUIRE(expected_output != expected_outputs.end());
+ BOOST_CHECK(output.tweak == expected_output->second);
+ }
+ }
+ }
+ }
+}
+
+BOOST_AUTO_TEST_CASE(bip352_preserves_requested_output_indexes)
+{
+ CKey sender_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000001");
+ CKey scan_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000002");
+ CKey spend_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000003");
+ SilentPaymentsDestination sp_dest{SilentPaymentsDestination::From(scan_key.GetPubKey(), spend_key.GetPubKey()).value()};
+ std::map<size_t, SilentPaymentsDestination> sp_dests{{2, sp_dest}, {5, sp_dest}};
+ COutPoint smallest_outpoint{Txid::FromHex("0000000000000000000000000000000000000000000000000000000000000001").value(), 0};
+
+ auto generated = GenerateSilentPaymentsTaprootDestinations(sp_dests, {sender_key}, {}, smallest_outpoint);
+
+ BOOST_REQUIRE(generated.has_value());
+ BOOST_CHECK_EQUAL(generated->size(), sp_dests.size());
+ BOOST_CHECK_EQUAL(generated->count(0), 0);
+ BOOST_CHECK_EQUAL(generated->count(1), 0);
+ BOOST_CHECK_EQUAL(generated->count(2), 1);
+ BOOST_CHECK_EQUAL(generated->count(5), 1);
+}
+
+BOOST_AUTO_TEST_CASE(bip352_skips_transactions_spending_unknown_segwit_versions)
+{
+ CKey key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000001");
+ CPubKey pubkey = key.GetPubKey();
+ COutPoint eligible_outpoint{Txid::FromHex("0000000000000000000000000000000000000000000000000000000000000001").value(), 0};
+ COutPoint unknown_segwit_outpoint{Txid::FromHex("0000000000000000000000000000000000000000000000000000000000000002").value(), 0};
+
+ CTxIn eligible_input{eligible_outpoint};
+ eligible_input.scriptWitness.stack.emplace_back(64, 0);
+ eligible_input.scriptWitness.stack.emplace_back(pubkey.begin(), pubkey.end());
+
+ std::map<COutPoint, Coin> coins;
+ coins[eligible_outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessV0KeyHash{pubkey})}, 0, false};
+ coins[unknown_segwit_outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessUnknown{2, std::vector<unsigned char>(32, 1)})}, 0, false};
+
+ BOOST_REQUIRE(GetSilentPaymentsPrevoutsSummary({eligible_input}, coins).has_value());
+ BOOST_CHECK(!GetSilentPaymentsPrevoutsSummary({eligible_input, CTxIn{unknown_segwit_outpoint}}, coins).has_value());
+}
+
+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, SilentPaymentsDestination> sp_dests;
+ sp_dests.emplace(0, SilentPaymentsDestination::From(scan_key.GetPubKey(), spend_key.GetPubKey()).value());
+ const auto sp_tr_dests = 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 = 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);
+
+ SilentPaymentsReceiver receiver{scan_key, spend_key.GetPubKey()};
+ const auto found_outputs = receiver.Scan(*prevouts_summary, output_pub_keys);
+ BOOST_REQUIRE(found_outputs.has_value());
+ BOOST_REQUIRE_EQUAL(found_outputs->size(), 1);
+ BOOST_CHECK(found_outputs->front().output == expected_output);
+}
+
+BOOST_AUTO_TEST_CASE(bip352_label_serialize_roundtrip)
+{
+ CKey scan_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000002");
+ CPubKey spend_pubkey = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000003").GetPubKey();
+ SilentPaymentsReceiver receiver{scan_key, spend_pubkey};
+
+ BOOST_REQUIRE_EQUAL(receiver.GetLabels().size(), 1);
+ const SilentPaymentsLabel& label = receiver.GetLabels().begin()->first;
+
+ DataStream stream;
+ label.Serialize(stream);
+ auto roundtripped = SilentPaymentsLabel::Unserialize(stream);
+ BOOST_REQUIRE(roundtripped.has_value());
+ BOOST_CHECK(*roundtripped == label);
+ BOOST_CHECK(stream.empty());
+}
+
+BOOST_AUTO_TEST_CASE(bip352_decode_address)
+{
+ struct ValidVector {
+ ChainType chain;
+ std::string address;
+ std::string scan_pubkey;
+ std::string spend_pubkey;
+ std::string extension_data;
+ uint8_t version;
+ };
+ const ValidVector valid_vectors[]{
+ {ChainType::MAIN,
+ "sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68gdjvfn2",
+ "0295fa435f2d74f73b4baaec28115df0b32e0f1b10a340ade948c56a647fe92cdc",
+ "0388ce2529c809f21008959fd3b2697f6f7cf5a116acd7a93f3b1c369aab0c7a3a",
+ "", 0},
+ {ChainType::MAIN,
+ "sp1pq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68t02m0h0s8cwhy",
+ "0295fa435f2d74f73b4baaec28115df0b32e0f1b10a340ade948c56a647fe92cdc",
+ "0388ce2529c809f21008959fd3b2697f6f7cf5a116acd7a93f3b1c369aab0c7a3a",
+ "deadbeef", 1},
+ {ChainType::TESTNET4,
+ "tsp1qqthpye3hdcnydp9temp7yduy6uw5h2nw8u9fz677ccrna280qwj3uq60zeqs3zfpj3age62h4ljq2lyawwdecmk8a545yysk4x3tu3skjqm2thu6",
+ "02ee1266376e264684abcec3e23784d71d4baa6e3f0a916bdec6073ea8ef03a51e",
+ "034f1641088921947a8ce957afe4057c9d739b9c6ec7ed2b421216a9a2be461690",
+ "", 0},
+ {ChainType::SIGNET,
+ "tsp1qqvrl2accqtatgtkllv5fdsfapq6vqnrr0uf2whhmjyas4teg7ljfqqlpec0ze90m97t3stv92p5ekzkcwzmpx0x8p7ljj9xqcjjpvnfurc00tjn9",
+ "0307f5771802fab42edffb2896c13d0834c04c637f12a75efb913b0aaf28f7e490",
+ "03e1ce1e2c95fb2f97182d8550699b0ad870b6133cc70fbf2914c0c4a4164d3c1e",
+ "", 0},
+ {ChainType::REGTEST,
+ "sprt1qq04xgllnqqfdlxjkr355rmsahfn9t8l6sd0k20t4uka4c73nvvpnwqu4kvg0nm62d5jtmqp54lkc7tul0dzt25ejtn4f80505z3u0h5jag59rdg4",
+ "03ea647ff30012df9a561c6941ee1dba66559ffa835f653d75e5bb5c7a33630337",
+ "0395b310f9ef4a6d24bd8034afed8f2f9f7b44b553325cea93be8fa0a3c7de92ea",
+ "", 0},
+ };
+
+ for (const auto& vec : valid_vectors) {
+ SelectParams(vec.chain);
+
+ auto sp = DecodeSilentPaymentsAddress(vec.address, Params());
+ BOOST_REQUIRE_MESSAGE(sp.has_value(), vec.address);
+ BOOST_CHECK_EQUAL(sp->GetVersion(), vec.version);
+ BOOST_CHECK_EQUAL(HexStr(sp->GetScanPubKey()), vec.scan_pubkey);
+ BOOST_CHECK_EQUAL(HexStr(sp->GetSpendPubKey()), vec.spend_pubkey);
+ BOOST_CHECK_EQUAL(HexStr(sp->GetExtensionData()), vec.extension_data);
+
+ // Bech32(m) is case-insensitive as a whole; an all-uppercase address must decode identically.
+ std::string flipped = ToUpper(vec.address);
+ auto sp_flipped = DecodeSilentPaymentsAddress(flipped, Params());
+ BOOST_REQUIRE_MESSAGE(sp_flipped.has_value(), flipped);
+ BOOST_CHECK(*sp_flipped == *sp);
+ }
+
+ // `chain` is the one chain (if any) whose HRP matches the address, where `expected_error`
+ // applies; on every other chain the HRP check itself is expected to fail first.
+ // `chain_independent` addresses fail bech32m checksum verification before the HRP is even
+ // compared, so `expected_error` applies on every chain instead.
+ struct InvalidVector {
+ std::string address;
+ std::string expected_error;
+ std::optional<ChainType> chain = std::nullopt;
+ bool chain_independent = false;
+ };
+ const InvalidVector invalid_vectors[]{
+ {"spx1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g37pn04",
+ "Invalid or unsupported prefix for Silent Payments address (expected sp, got spx)."}, // wrong HRP; never matches any chain
+ {"sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68gcwu9kg",
+ "Silent Payments address must use Bech32m checksum", std::nullopt, /*chain_independent=*/true}, // bad checksum
+ {"sp1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2qugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g25havg",
+ "Invalid Silent payments address", ChainType::MAIN}, // invalid scan pubkey
+ {"sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq57qc9nf",
+ "Invalid Silent payments address", ChainType::MAIN}, // invalid spend pubkey
+ {"sp1lq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68gaafydt",
+ "This implementation only supports Silent payments addresses v0 through v30 (got 31).", ChainType::MAIN}, // reserved version 31
+ {"sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcznsxsn",
+ "Silent payments data payload is too small (expected at least 66, got 33).", ChainType::MAIN}, // payload too small
+ {"tsp1qqthpye3hdcnydp9temp7yduy6uw5h2nw8u9fz677ccrna280qwj3uq60zeqs3zfpj3age62h4ljq2lyawwdecmk8a545yysk4x3tu3skjqm2thum",
+ "Silent Payments address must use Bech32m checksum", std::nullopt, /*chain_independent=*/true}, // bad checksum
+ {"sp1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf26rn7",
+ "Silent Payments address must use Bech32m checksum", std::nullopt, /*chain_independent=*/true}, // bad checksum
+ {"sprt1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2qu4kvg0nm62d5jtmqp54lkc7tul0dzt25ejtn4f80505z3u0h5jagf9zkp5",
+ "Invalid Silent payments address", ChainType::REGTEST}, // invalid scan pubkey
+ };
+
+ for (const auto& vec : invalid_vectors) {
+ for (const auto chain : {ChainType::MAIN, ChainType::TESTNET, ChainType::TESTNET4, ChainType::SIGNET, ChainType::REGTEST}) {
+ SelectParams(chain);
+ auto sp = DecodeSilentPaymentsAddress(vec.address, Params());
+ BOOST_REQUIRE_MESSAGE(!sp.has_value(), vec.address);
+ if (vec.chain_independent || chain == vec.chain) {
+ BOOST_CHECK_EQUAL(sp.error(), vec.expected_error);
+ } else {
+ BOOST_CHECK_MESSAGE(sp.error().starts_with("Invalid or unsupported prefix for Silent Payments address"), sp.error());
+ }
+ }
+ }
+
+ SelectParams(ChainType::MAIN);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+} // namespace bip352
### src/test/data/bip352_send_and_receive_vectors.json
[binary or diff unavailable]Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.