Silent Payments: Implement bip352 (take 2) #35301

pull Eunovo wants to merge 8 commits into bitcoin:master from Eunovo:implement-bip352 changing 47 files +20438 −60
  1. Eunovo commented at 3:46 AM on May 16, 2026: contributor

    This PR is part of integrating silent payments into Bitcoin Core. It is the second iteration of #28122, now based on https://github.com/bitcoin-core/secp256k1/pull/1765.

    This project is tracked in #28536.

    BIP352 This PR focuses strictly on the BIP logic and attempts to separate it from the wallet and transaction implementation details. This is accomplished by working directly with public and private keys, instead of needing a wallet backend and transactions for testing. Labels for the receiver are optional and thus deferred for a later PR.

    Test vectors from the BIP are included as unit tests.

  2. DrahtBot commented at 3:46 AM on May 16, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35301.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, rkrux

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #27260 (Enhanced error messages for invalid network prefix during address parsing. by portlandhodl)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. in src/common/bip352.cpp:25 in 6355a90494
      20 | +#include <script/solver.h>
      21 | +#include <script/script_error.h>
      22 | +#include <util/check.h>
      23 | +#include <util/strencodings.h>
      24 | +
      25 | +extern secp256k1_context* secp256k1_context_sign; // TODO: this is hacky, is there a better solution?
    


    theStack commented at 1:43 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b: Since #34225 (commit f36d89f4363a25f7948a0f7096201ef8e15045d8), the sign context can be accessed via GetSecp256k1SignContext, i.e. by using this in the API calls below, this line and the symbol visibility change in key.cpp are not needed anymore


    Eunovo commented at 4:03 PM on May 18, 2026:

    Done.

  4. in src/common/bip352.h:103 in 6355a90494
      98 | + * Label public keys can be stored in a cache, mapping the public key to the label tweak. This cache
      99 | + * is used during scanning to determine if a label was used and if so to retrieve the label tweak.
     100 | + *
     101 | + * @param scan_key                 The recipient's scan_key, used to salt the hash
     102 | + * @param m                        An integer m (only use m = 0 for the change label)
     103 | + * @return std::<CPubKey, uint156> The label public key and label tweak.
    


    theStack commented at 1:59 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b: typo: s/uint156/uint256/


    Eunovo commented at 4:03 PM on May 18, 2026:

    Done.

  5. in src/common/bip352.h:156 in 6355a90494 outdated
     151 | + * @param spend_pubkey                                      The recipient's spend public key.
     152 | + * @param output_pub_keys                                   The taproot output public keys.
     153 | + * @param labels                                            The recipient's labels.
     154 | + * @return std::<optional<std::vector<SilentPaymentOutput>> The found outputs, nullopt if none found.
     155 | + */
     156 | +std::optional<std::vector<SilentPaymentOutput>> ScanForSilentPaymentOutputs(const CKey& scan_key, const PrevoutsSummary& prevouts_summary, const CPubKey& spend_pubkey, const std::vector<XOnlyPubKey>& output_pub_keys, const std::map<CPubKey, uint256>& labels);
    


    theStack commented at 2:04 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b: nit: missing doxygen @param entry for prevouts_summary above


    Eunovo commented at 4:04 PM on May 18, 2026:

    Done.

  6. in src/common/bip352.cpp:211 in 6355a90494 outdated
     206 | +    generated_output_ptrs.reserve(recipients.size());
     207 | +
     208 | +    for (size_t i = 0; i < recipients.size(); i++) {
     209 | +        secp256k1_silentpayments_recipient recipient_obj;
     210 | +        ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.scan_pubkey, recipients[i].m_scan_pubkey.data(), recipients[i].m_scan_pubkey.size());
     211 | +        ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.spend_pubkey, recipients[i].m_spend_pubkey.data(), recipients[i].m_spend_pubkey.size());
    


    theStack commented at 2:10 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b:

            ret &= secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.spend_pubkey, recipients[i].m_spend_pubkey.data(), recipients[i].m_spend_pubkey.size());
    

    to ensure both pubkey_parse are successful (or alternatively, could place an extra assert(ret) line after the first call)


    Eunovo commented at 4:06 PM on May 18, 2026:

    I added an assert(ret); after the first call, so that it is easier to determine which of the pubkeys is invalid, in the event of a crash.

  7. in src/common/bip352.cpp:336 in 6355a90494
     331 | +        secp256k1_silentpayments_found_output found_output{};
     332 | +        secp256k1_xonly_pubkey tx_output_obj;
     333 | +        found_output_objs.push_back(found_output);
     334 | +        found_output_ptrs.push_back(&found_output_objs[i]);
     335 | +        ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static, &tx_output_obj, tx_outputs[i].data());
     336 | +        assert(ret);
    


    theStack commented at 2:21 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b: IIUC, this call could fail if a transaction is scanned where one of the P2TR outputs encodes an invalid x-only pubkey (i.e. not on the curve), so I suppose this should be changed to e.g. if (!ret) continue; to avoid a crash (unless we demand from the caller that tx_outputs only contain valid x-only pubkeys already)


    Eunovo commented at 4:04 PM on May 18, 2026:

    Changed to if (!ret) continue;

  8. in src/common/bip352.cpp:32 in 6355a90494
      27 | +namespace bip352 {
      28 | +
      29 | +class PrevoutsSummaryImpl
      30 | +{
      31 | +private:
      32 | +    //! The actual secnonce itself
    


    theStack commented at 2:24 AM on May 18, 2026:

    in 6355a904945d0d5704032e464f20a69a5d82a91b: comment doesn't apply


    Eunovo commented at 4:04 PM on May 18, 2026:

    Done.

  9. in src/addresstype.h:162 in ba4b734ec8
     158 | @@ -140,7 +159,7 @@ struct PayToAnchor : public WitnessUnknown
     159 |   *  * WitnessUnknown: TxoutType::WITNESS_UNKNOWN destination (P2W??? address)
     160 |   *  A CTxDestination is the internal data type encoded in a bitcoin address
     161 |   */
     162 | -using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, PayToAnchor, WitnessUnknown>;
     163 | +using CTxDestination = std::variant<CNoDestination, V0SilentPaymentDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, PayToAnchor, WitnessUnknown>;
    


    theStack commented at 2:30 AM on May 18, 2026:

    in ba4b734ec89d590951574d2714867afab27d1347: nit: could add a corresponding new entry to the comment list a few lines above


    Eunovo commented at 4:11 PM on May 18, 2026:

    Done.

  10. in src/bech32.h:40 in ba4b734ec8
      36 | @@ -37,6 +37,7 @@ enum class Encoding {
      37 |   *  and we would never encode an address with such a massive value */
      38 |  enum CharLimit : size_t {
      39 |      BECH32 = 90,            //!< BIP173/350 imposed character limit for Bech32(m) encoded addresses. This guarantees finding up to 4 errors.
      40 | +    SILENT_PAYMENTS = 1024, //!< BIP352 imposed 1024 character limit on Bech32m encoded silent payment addresses. This guarantees finding up to 3 errors
    


    theStack commented at 2:37 AM on May 18, 2026:

    in ba4b734ec89d590951574d2714867afab27d1347: pedantic nit: according to BIP-352 the limit is 1023 (not sure which of the two values make more sense, as I'm not very familiar with BIP-173; for SPV0 it doesn't matter anyways)


    Eunovo commented at 4:10 PM on May 18, 2026:

    Changed to 1023 to match the BIP specification. AFAICT, there is no reason to use 1024; the most likely reason for it being 1024 is that the BIP might have stated 1024 and was updated to 1023 at some point in the past.

  11. Eunovo force-pushed on May 18, 2026
  12. w0xlt commented at 6:21 PM on May 19, 2026: contributor

    Concept ACK

  13. rkrux commented at 6:47 PM on May 19, 2026: contributor

    Concept ACK aca8a8f

  14. in src/kernel/chainparams.h:116 in 602835e080
     112 | @@ -113,6 +113,7 @@ class CChainParams
     113 |      const std::vector<std::string>& DNSSeeds() const { return vSeeds; }
     114 |      const std::vector<unsigned char>& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; }
     115 |      const std::string& Bech32HRP() const { return bech32_hrp; }
     116 | +    const std::string& SilentPaymentHRP() const { return silent_payment_hrp; }
    


    theStack commented at 2:01 PM on May 20, 2026:

    in 602835e08070cab981842ce4cd5065730b3e8c48 (and 6b71f145ccfdbfe41f7f666d373efb0a68e30375 ff.): nitty nit: personally, I would slightly prefer to use the plural form in the code base since it's the widely used protocol name

        const std::string& SilentPaymentsHRP() const { return silent_payments_hrp; }
    

    Eunovo commented at 3:00 PM on May 21, 2026:

    Done. I also pluralised the name in other function names, variable names and comments.

  15. in src/key_io.cpp:178 in 6b71f145cc
     174 | +                error_str = strprintf("Silent payment version is v0 but data is not the correct size (expected %d, got %d).", SILENT_PAYMENT_V0_DATA_SIZE, data.size());
     175 | +                return CNoDestination();
     176 | +            }
     177 | +            CPubKey scan_pubkey{data.begin(), data.begin() + CPubKey::COMPRESSED_SIZE};
     178 | +            CPubKey spend_pubkey{data.begin() + CPubKey::COMPRESSED_SIZE, data.begin() + 2*CPubKey::COMPRESSED_SIZE};
     179 | +            return V0SilentPaymentDestination{scan_pubkey, spend_pubkey};
    


    theStack commented at 2:35 PM on May 20, 2026:

    in 6b71f145ccfdbfe41f7f666d373efb0a68e30375: Related to a recent off-band discussion we had, I wonder if we should check the validity of the pubkeys (i.e. following the compressed pubkey format and being on the curve) already at this point, e.g. via

                if (!scan_pukey.IsFullyValid() || !spend_pubkey.IsFullyValid()) return CNoDestination();
    

    We don't do the same when decoding taproot addresses (currently the only other address format that directly encodes public keys, without hashing), but the difference with SP here is that an actual output script can't even be derived in this case, so it could make more sense to reject as early as possible.


    Eunovo commented at 2:59 PM on May 21, 2026:

    Done.

  16. in src/test/data/bip352_send_and_receive_vectors.json:1 in aca8a8f3da outdated
       0 | @@ -0,0 +1,2760 @@
       1 | +[
    


    theStack commented at 2:54 PM on May 20, 2026:

    in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: looks like test vectors .json file needs to be updated (to BIP-352 version 1.1.1, see latest change https://github.com/bitcoin/bips/pull/2142).


    Eunovo commented at 2:59 PM on May 21, 2026:

    Updated to the test_vectors in https://github.com/bitcoin/bips/pull/2142

  17. in src/common/bip352.cpp:82 in 0287192f09 outdated
      77 | +    } else if (type == TxoutType::WITNESS_V0_KEYHASH && !txin.scriptWitness.stack.empty()) {
      78 | +        // We ensure the witness stack is not empty before exctracting the public key
      79 | +        // since there are scenarios where it can be, e.g., before the transaction has been signed.
      80 | +        //
      81 | +        // TODO: having this check here feels a bit hacky, will revisit with a more comprehensive solution
      82 | +        pubkey = CPubKey{txin.scriptWitness.stack.back()};
    


    theStack commented at 2:56 PM on May 20, 2026:

    in 0287192f099299fb7a0b7f83f5e3bfb3174ed152: unless I'm missing something, there is nothing wrong in checking that the witness stack is non-empty before accessing it, and the TODO could simply be removed


    Eunovo commented at 2:58 PM on May 21, 2026:

    Removed.

  18. in src/key_io.cpp:80 in 6b71f145cc
      75 | +        data_in.reserve(66);
      76 | +        // Set 0 as the silent payments version
      77 | +        std::vector<unsigned char> data_out = {0};
      78 | +        // ConvertBits will expand each 8-bit byte into 5-bit chunks,
      79 | +        // i.e. (67 * 8 / 5) = 107.2 -> so we reserve 108
      80 | +        data_out.reserve(108);
    


    theStack commented at 4:27 PM on May 20, 2026:

    in 6b71f145ccfdbfe41f7f666d373efb0a68e30375: pedantic nit: the version byte is not part of the ConvertBits input, i.e. this should be

            // ConvertBits will expand each 8-bit byte into 5-bit chunks,
            // i.e. 1 + (66 * 8 / 5) = 106.6 -> so we reserve 107
            data_out.reserve(107);
    

    (verified that data_out has indeed a size of 107 by adding debug outputs)


    Eunovo commented at 2:58 PM on May 21, 2026:

    Updated.

  19. in src/test/bip352_tests.cpp:94 in aca8a8f3da
      89 | +            const std::vector<UniValue>& silent_payment_addresses = given["recipients"].getValues();
      90 | +            for (size_t i = 0; i < silent_payment_addresses.size(); ++i) {
      91 | +                const CTxDestination& tx_dest = DecodeDestination(silent_payment_addresses[i].get_str());
      92 | +                if (const auto* sp = std::get_if<V0SilentPaymentDestination>(&tx_dest)) {
      93 | +                    sp_dests[i] = *sp;
      94 | +                }
    


    theStack commented at 4:31 PM on May 20, 2026:

    in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: could do a round-trip test in the if body, to also add test coverage for encoding SP addresses, e.g.

           auto encoded_sp_addr = EncodeDestination(*sp);
           BOOST_CHECK(encoded_sp_addr == silent_payment_addresses[i].get_str());
    

    Eunovo commented at 2:58 PM on May 21, 2026:

    I added some tests for Encoding and Decoding V0SilentPaymentsDestination to key_io_tests.cpp in https://github.com/bitcoin/bitcoin/pull/35301/commits/15a2bdd5a18ac6d543b669f9230ea5bd7352c497

  20. in src/test/bip352_tests.cpp:37 in aca8a8f3da
      32 | +    key.SetSeed(seed);
      33 | +    for (auto index : path) {
      34 | +        BOOST_CHECK(key.Derive(key, index));
      35 | +    }
      36 | +    return key.key;
      37 | +}
    


    theStack commented at 4:35 PM on May 20, 2026:

    in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: this function is currently unused


    Eunovo commented at 2:57 PM on May 21, 2026:

    Removed.

  21. in src/common/bip352.cpp:264 in 0287192f09 outdated
     259 | +    }
     260 | +    return tr_dests;
     261 | +}
     262 | +
     263 | +const unsigned char* LabelLookupCallback(const unsigned char* key, const void* context) {
     264 | +    auto label_context = static_cast<const std::map<CPubKey, uint256>*>(context);
    


    theStack commented at 12:17 AM on May 21, 2026:

    in 5b0d46e13980f227908186dfdd529d030ff7400a: I suppose using std::unordered_map for the labels cache would be the better choice for performance reasons, at least if we ever support a large number of labels (it doesn't matter until actual SP receiving support is implemented though, and can be re-evaluated and benchmarked then)


    Eunovo commented at 10:02 AM on May 25, 2026:

    Changed to std::unordered_map, but that required that I change the labels cache from map<CPubKey, uint256> to unordered_map<CKeyID, uint256, SaltedSipHasher> because CPubKey doesn't have a hash function suitable for use with unordered_map.

  22. Eunovo force-pushed on May 21, 2026
  23. rkrux commented at 3:01 PM on May 21, 2026: contributor

    Does https://github.com/bitcoin-core/secp256k1/pull/1765 need to be merged first for this to be reviewed (and later merged)?

  24. theStack commented at 3:22 PM on May 21, 2026: contributor

    Does bitcoin-core/secp256k1#1765 need to be merged first for this to be reviewed (and later merged)?

    There are no major API changes expected at this point in bitcoin-core/secp256k1#1765, so I'd say this PR can be already reviewed now. For merging it though, the SP module merge and secp256k1 subtree update have to go in first. Obviously, any review in https://github.com/bitcoin-core/secp256k1/pull/1765 would be much appreciated :)

  25. DrahtBot added the label CI failed on May 21, 2026
  26. DrahtBot commented at 4:40 PM on May 21, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task Windows native, fuzz, VS: https://github.com/bitcoin/bitcoin/actions/runs/26233980188/job/77201857783</sub> <sub>LLM reason (✨ experimental): Fuzz testing failed because script_fuzz_target hit an assertion in src/test/fuzz/script.cpp:161 (tx_destination_1 == DecodeDestination(encoded_dest)).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  27. Eunovo force-pushed on May 22, 2026
  28. in src/common/bip352.cpp:288 in 5b0d46e139 outdated
     283 | +    assert(ret);
     284 | +    ret = secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(secp256k1_context_static, &labeled_spend_obj, &spend_obj, &label_obj);
     285 | +    assert(ret);
     286 | +    size_t pubkeylen = CPubKey::COMPRESSED_SIZE;
     287 | +    CPubKey labeled_spend_pubkey;
     288 | +    ret = secp256k1_ec_pubkey_serialize(secp256k1_context_static, (unsigned char*)labeled_spend_pubkey.begin(), &pubkeylen, &labeled_spend_obj, SECP256K1_EC_COMPRESSED);
    


    theStack commented at 3:03 PM on May 22, 2026:

    in 5b0d46e13980f227908186dfdd529d030ff7400a: could assert(ret) here as well, as this serialization should never fail


    Eunovo commented at 10:00 AM on May 25, 2026:

    Done.

  29. in src/key.cpp:19 in 5b0d46e139
      15 | @@ -16,7 +16,7 @@
      16 |  #include <secp256k1_recovery.h>
      17 |  #include <secp256k1_schnorrsig.h>
      18 |  
      19 | -static secp256k1_context* secp256k1_context_sign = nullptr;
      20 | +secp256k1_context* secp256k1_context_sign = nullptr;
    


    theStack commented at 3:07 PM on May 22, 2026:

    in 5b0d46e13980f227908186dfdd529d030ff7400a: this change isn't needed anymore, since you are using the GetSecp256k1SignContext() access function now


    Eunovo commented at 9:59 AM on May 25, 2026:

    Done.

  30. in src/addresstype.h:153 in 7df966bc37
     148 | +        return false;
     149 | +    }
     150 | +
     151 | +private:
     152 | +    CPubKey m_scan_pubkey;
     153 | +    CPubKey m_spend_pubkey;
    


    theStack commented at 3:11 PM on May 22, 2026:

    in 7df966bc37fe640660ff8a9cd55a9fd5331527dc: consistency micro-nit: in other destination classes, the private: part comes before the public one, so could move this up


    Eunovo commented at 9:59 AM on May 25, 2026:

    Done.

  31. in src/addresstype.h:141 in 7df966bc37
     136 | +    const CPubKey& GetSpendPubKey() const { return m_spend_pubkey; }
     137 | +
     138 | +    friend bool operator==(const V0SilentPaymentsDestination& a, const V0SilentPaymentsDestination& b) {
     139 | +        if (a.m_scan_pubkey != b.m_scan_pubkey) return false;
     140 | +        if (a.m_spend_pubkey != b.m_spend_pubkey) return false;
     141 | +        return true;
    


    theStack commented at 3:13 PM on May 22, 2026:

    in 7df966bc37fe640660ff8a9cd55a9fd5331527dc: nit: I think this could be simplified to a one-liner

            return (a.m_scan_pubkey == b.m_scan_pubkey) && (a.m_spend_pubkey == b.m_spend_pubkey);
    

    without changing the logic or performance (didn't verify though).


    Eunovo commented at 9:59 AM on May 25, 2026:

    Done.

  32. theStack commented at 3:22 PM on May 22, 2026: contributor

    Thanks for the quick follow-up! Left a few more comments, most of them being nits. Mostly reviewed the parts around the SP module API calls so far, will take a closer look at higher-level parts of the protocol (particularly the pubkey extraction logic in GetPubKeyFromInput) within the next days.

  33. Eunovo force-pushed on May 25, 2026
  34. DrahtBot removed the label CI failed on May 25, 2026
  35. in src/key_io.cpp:172 in 061edd8f42 outdated
     168 | +            }
     169 | +            auto version = dec.data[0];  // retrieve the version
     170 | +            if (version >= 31) {
     171 | +                error_str = strprintf("This implementation only supports sending to Silent payments addresses v0 through v30 (got %d).", version);
     172 | +                return CNoDestination();
     173 | +            }
    


    theStack commented at 12:32 AM on May 27, 2026:

    in 061edd8f427674914d98a22e118122fd8dd0a0c8: currently, SP addresses with (not yet specified) versions 1-30 are already accepted and get shoehorned into V0SilentPaymentsDestinations. Is that intentional? I guess it's not and it's fine to only allow V0 destinations for now, but if yes, we should probably add tests for v1-v30 addresses; might be a bit tricky though as the encoding round-trip tests would obviously fail.


    Eunovo commented at 11:07 AM on May 29, 2026:

    I added a new struct, called UnknownSilentPaymentsVersion, to handle versions 1 to 30. I added some valid and invalid addresses with a version greater than zero.

  36. in src/common/bip352.cpp:238 in 80d3a4853a
     233 | +{
     234 | +    bool ret;
     235 | +    std::map<size_t, WitnessV1Taproot> tr_dests;
     236 | +    std::vector<V0SilentPaymentsDestination> recipients;
     237 | +    recipients.reserve(sp_dests.size());
     238 | +    for (const auto& [_, addr] : sp_dests) {
    


    w0xlt commented at 11:27 PM on May 27, 2026:

    GenerateSilentPaymentsTaprootDestinations() documents sp_dests keys as final tx.vout positions, but returns generated outputs under contiguous indexes 0..n-1.

    If SP outputs are mixed with regular outputs, callers would assign them to the wrong positions; the original map keys should be preserved.

    Diff:

    diff --git a/src/common/bip352.cpp b/src/common/bip352.cpp
    index 1580e1f8f8..0ac71d456f 100644
    --- a/src/common/bip352.cpp
    +++ b/src/common/bip352.cpp
    @@ -234,8 +234,11 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD
         bool ret;
         std::map<size_t, WitnessV1Taproot> tr_dests;
         std::vector<V0SilentPaymentsDestination> recipients;
    +    std::vector<size_t> positions;
         recipients.reserve(sp_dests.size());
    -    for (const auto& [_, addr] : sp_dests) {
    +    positions.reserve(sp_dests.size());
    +    for (const auto& [pos, addr] : sp_dests) {
    +        positions.push_back(pos);
             recipients.push_back(addr);
         }
         std::vector<secp256k1_xonly_pubkey> outputs = CreateOutputs(recipients, plain_keys, taproot_keys, smallest_outpoint);
    @@ -245,7 +248,7 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD
             unsigned char xonly_pubkey_bytes[32];
             ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, xonly_pubkey_bytes, &outputs[i]);
             assert(ret);
    -        tr_dests[i] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};
    +        tr_dests[positions[i]] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};
         }
         return tr_dests;
     }
    

    Test:

    diff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp
    index cd5fda38da..bc05fcf107 100644
    --- a/src/test/bip352_tests.cpp
    +++ b/src/test/bip352_tests.cpp
    @@ -27,6 +27,25 @@ CKey ParseHexToCKey(std::string hex) {
         return output;
     };
     
    +BOOST_AUTO_TEST_CASE(bip352_preserves_requested_output_indexes)
    +{
    +    CKey sender_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000001");
    +    CKey scan_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000002");
    +    CKey spend_key = ParseHexToCKey("0000000000000000000000000000000000000000000000000000000000000003");
    +    V0SilentPaymentsDestination sp_dest{scan_key.GetPubKey(), spend_key.GetPubKey()};
    +    std::map<size_t, V0SilentPaymentsDestination> sp_dests{{2, sp_dest}, {5, sp_dest}};
    +    COutPoint smallest_outpoint{Txid::FromHex("0000000000000000000000000000000000000000000000000000000000000001").value(), 0};
    +
    +    auto generated = bip352::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_send_and_receive_test_vectors)
     {
         UniValue tests;
    

    Eunovo commented at 11:07 AM on May 29, 2026:

    Done.

  37. in src/common/bip352.cpp:145 in 80d3a4853a outdated
     140 | +    );
     141 | +    if (!ret) return std::nullopt;
     142 | +    return prevouts_summary;
     143 | +}
     144 | +
     145 | +std::optional<PrevoutsSummary> GetSilentPaymentsPrevoutsSummary(const std::vector<CTxIn>& vin, const std::map<COutPoint, Coin>& coins)
    


    w0xlt commented at 12:08 AM on May 28, 2026:

    GetSilentPaymentsPrevoutsSummary() currently still builds scan data when a transaction has an eligible input plus another input spending an unknown SegWit version (>1) prevout.

    BIP352 v0 says those transactions must be skipped entirely, so this should return no prevouts summary as soon as any spent prevout is witness v2+.

    Diff:

    diff --git a/src/common/bip352.cpp b/src/common/bip352.cpp
    index 1580e1f8f8..2ccae373c8 100644
    --- a/src/common/bip352.cpp
    +++ b/src/common/bip352.cpp
    @@ -152,6 +152,12 @@ std::optional<PrevoutsSummary> GetSilentPaymentsPrevoutsSummary(const std::vecto
         for (const CTxIn& txin : vin) {
             const Coin& coin = coins.at(txin.prevout);
             Assert(!coin.IsSpent());
    +        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 std::nullopt;
    +        }
             tx_outpoints.emplace_back(txin.prevout);
             auto pubkey = GetPubKeyFromInput(txin, coin.out.scriptPubKey);
             if (pubkey.has_value()) {
    

    Test:

    diff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp
    index cd5fda38da..b27823acd4 100644
    --- a/src/test/bip352_tests.cpp
    +++ b/src/test/bip352_tests.cpp
    @@ -27,6 +27,25 @@ CKey ParseHexToCKey(std::string hex) {
         return output;
     };
     
    +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(bip352::GetSilentPaymentsPrevoutsSummary({eligible_input}, coins).has_value());
    +    BOOST_CHECK(!bip352::GetSilentPaymentsPrevoutsSummary({eligible_input, CTxIn{unknown_segwit_outpoint}}, coins).has_value());
    +}
    +
     BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)
     {
         UniValue tests;
    

    Eunovo commented at 11:08 AM on May 29, 2026:

    Done.

  38. in src/key_io.cpp:161 in 80d3a4853a outdated
     157 |          if (dec.data.empty()) {
     158 |              error_str = "Empty Bech32 data section";
     159 |              return CNoDestination();
     160 |          }
     161 | +        if (is_silent_payment) {
     162 | +            if (!ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {
    


    w0xlt commented at 12:31 AM on May 28, 2026:

    Silent Payments decoding looks too permissive: it detects SP addresses by HRP prefix and then accepts both BECH32 and BECH32M without requiring dec.hrp == params.SilentPaymentsHRP().

    That can make spx... or Bech32-checksummed SP payloads decode as valid SP destinations, while BIP352 should require the exact SP HRP and Bech32m.

    Diff:

    diff --git a/src/key_io.cpp b/src/key_io.cpp
    index 335f02a5ba..e77f616cc7 100644
    --- a/src/key_io.cpp
    +++ b/src/key_io.cpp
    @@ -158,6 +158,14 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
                 return CNoDestination();
             }
             if (is_silent_payment) {
    +            if (dec.hrp != params.SilentPaymentsHRP()) {
    +                error_str = strprintf("Invalid or unsupported prefix for Silent Payments address (expected %s, got %s).", params.SilentPaymentsHRP(), dec.hrp);
    +                return CNoDestination();
    +            }
    +            if (dec.encoding != bech32::Encoding::BECH32M) {
    +                error_str = "Silent Payments address must use Bech32m checksum";
    +                return CNoDestination();
    +            }
                 if (!ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {
                     return CNoDestination();
                 }
    

    Test:

    diff --git a/src/test/data/key_io_invalid.json b/src/test/data/key_io_invalid.json
    index 0505dc9e8b..13b3fb0526 100644
    --- a/src/test/data/key_io_invalid.json
    +++ b/src/test/data/key_io_invalid.json
    @@ -209,6 +209,12 @@
         [
             "TB1Q3F9WGNXE9ZMTTMDN5VKVKHYZ8Y0LCV72YV7V5LSXTJXEYHNHEHASLYL0TZ"
         ],
    +    [
    +        "spx1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g37pn04"
    +    ],
    +    [
    +        "sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68gcwu9kg"
    +    ],
         [
             "sp1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2qugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g25havg"
         ],
    

    Eunovo commented at 11:07 AM on May 29, 2026:

    Done.

  39. in src/common/bip352.cpp:319 in 80d3a4853a
     314 | +    found_output_objs.reserve(tx_outputs.size());
     315 | +    found_output_ptrs.reserve(tx_outputs.size());
     316 | +    tx_output_objs.reserve(tx_outputs.size());
     317 | +    tx_output_ptrs.reserve(tx_outputs.size());
     318 | +
     319 | +    for (size_t i = 0; i < tx_outputs.size(); i++) {
    


    w0xlt commented at 12:53 AM on May 28, 2026:

    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
    

    Eunovo commented at 11:08 AM on May 29, 2026:

    Done.

  40. w0xlt commented at 12:58 AM on May 28, 2026: contributor

    A few review comments:

  41. Squashed 'src/secp256k1/' changes from 7262adb4b4..a8f297a642
    a8f297a642 silentpayments: skip slow benchmarks for low iters count (<= 2)
    66aee2af17 docs: update README
    6b33ad2d81 ci: enable silentpayments module
    7d3d103aca tests: add sha256 tag test
    55264b49fc tests: add constant time tests
    b8b81b073c tests: add BIP-352 test vectors
    995babd0d4 silentpayments: optimize scanning by using batch inversion
    0a0bb53264 silentpayments: add benchmarks for scanning
    46b2c577eb silentpayments: add examples/silentpayments.c
    96e3a28a1b silentpayments: respect per-group recipients protocol limit (K_max=2323)
    40e015ae8c silentpayments: receiving
    f4b8da9e66 silentpayments: recipient label support
    a360c392a0 silentpayments: sending
    ff8e6f0938 build: add skeleton for new silentpayments (BIP352) module
    8363a2d8d1 Merge bitcoin-core/secp256k1#1854: tests: compare full MuSig aggregate nonce
    af1fdd1215 tests: compare full MuSig aggregate nonce
    b11340b3ce Merge bitcoin-core/secp256k1#1849: musig: always clear out secret key in `secp256k1_musig_nonce_gen_counter`
    8479eafa57 musig: always clear out secret key in `secp256k1_musig_nonce_gen_counter`
    c1a9e4fe64 Merge bitcoin-core/secp256k1#1848: ci: Bump GCC snapshot major version to 17
    3cca6451a2 ci: Bump GCC snapshot major version to 17
    ea174fe045 Merge bitcoin-core/secp256k1#1846: ci: Replace `ilammy/msvc-dev-cmd` with manual MSVC setup
    285cb788e9 ci: Replace `ilammy/msvc-dev-cmd` with manual MSVC setup
    
    git-subtree-dir: src/secp256k1
    git-subtree-split: a8f297a642651835a0086e85790ab9e6a0b6ca82
    9bbbb3fc71
  42. Merge commit '9bbbb3fc71fba327f12699fd2aa49307e3119d66' into sp-base c8c4b5ae44
  43. crypto: add read-only method to KeyPair
    Add a method for passing a KeyPair object to secp256k1 functions expecting a secp256k1_keypair.
    This allows for passing a KeyPair directly to a secp256k1 function without needing to create a
    temporary secp256k1_keypair object.
    7327c2b2ea
  44. Add "sp" HRP cf56b51424
  45. Add Silent Payments address types
    Add V0SilentPaymentsDestination and UnknownSilentPaymentsDestination
    for v1 to v30; Bip352 reserves v31 for a backwards incompatible change.
    v1 to v30 addresses are handled as specified in Bip352. The extra_data
    will be ignored during sending.
    
    Valid v0 and v1 addresses were added to key_io_valid.json for testing,
    and invalid v0 and a v31 address were added to key_io_invalid.json.
    
    V0SilentPaymentsDestination must not be formed from invalid public keys;
    the operations that must be performed to send to a V0SilentPaymentsDestination,
    require valid public keys. Hence, the V0SilentPaymentsDestination struct takes
    the following precautions to ensure that such invalid states are not possible:
    - The constructor will throw when provided with invalid public keys
    - The public keys are private and const references can be retrieved using member functions.
    16c03f9800
  46. Eunovo force-pushed on May 29, 2026
  47. DrahtBot added the label CI failed on May 29, 2026
  48. DrahtBot removed the label CI failed on May 29, 2026
  49. in src/common/bip352.h:120 in b0a0c54f59 outdated
     115 | + * @param label                       The label tweak
     116 | + * @return V0SilentPaymentsDestination The silent payments destination, with `B_spend -> B_spend + label * G`.
     117 | + *
     118 | + * @see CreateLabelTweak(const CKey& scan_key, const int m);
     119 | + */
     120 | +V0SilentPaymentsDestination GenerateSilentPaymentsLabeledAddress(const V0SilentPaymentsDestination& recipient, const uint256& label);
    


    theStack commented at 8:32 PM on May 30, 2026:

    in b0a0c54f5925c83728d0081f3009eb947b55895c: I think for this function, passing the label (pubkey) rather than the label tweak makes more sense, so the generator point multiplication (already done in CreateLabelTweak via the secp256k1_silentpayments_label_create API function) doesn't have to be repeated manually via .GetPubKey(). The label tweak only becomes relevant once a SP outputs need to be spent, but shouldn't be necessary in a function for address generation.

    **
     * [@brief](/bitcoin-bitcoin/contributor/brief/) Generate a silent payments labeled address.
     *
     * [@param](/bitcoin-bitcoin/contributor/param/) recipient                   The recipient's silent payments destination (i.e. scan and spend public keys).
     * [@param](/bitcoin-bitcoin/contributor/param/) label                       The label
     * [@return](/bitcoin-bitcoin/contributor/return/) V0SilentPaymentsDestination The silent payments destination, with `B_spend -> B_spend + label`.
     *
     * [@see](/bitcoin-bitcoin/contributor/see/) CreateLabelTweak(const CKey& scan_key, const int m);
     */
    V0SilentPaymentsDestination GenerateSilentPaymentsLabeledAddress(const V0SilentPaymentsDestination& recipient, const CPubKey& label);
    

    Eunovo commented at 11:15 PM on June 9, 2026:

    Done.

  50. in src/common/bip352.h:109 in b0a0c54f59
     104 | + * @param m                        An integer m (only use m = 0 for the change label)
     105 | + * @return std::<CPubKey, uint256> The label public key and label tweak.
     106 | + *
     107 | + * @see GenerateSilentPaymentsLabeledAddress
     108 | + */
     109 | +std::pair<CPubKey, uint256> CreateLabelTweak(const CKey& scan_key, int m);
    


    theStack commented at 8:53 PM on May 30, 2026:

    in b0a0c54f5925c83728d0081f3009eb947b55895c: nit: as it's returning more than only the tweak (I suspect in a past iteration it did only that though; that would explain why the tweak is passed to GenerateSilentPaymentsLabeledAddress below currently), could rename the function. Maybe CreateLabelData or simply CreateLabel?


    Eunovo commented at 11:03 AM on June 7, 2026:

    Done.

  51. in src/common/bip352.cpp:266 in b0a0c54f59 outdated
     261 | +
     262 | +const unsigned char* LabelLookupCallback(const unsigned char* key, const void* context) {
     263 | +    auto label_context = static_cast<const std::unordered_map<CKeyID, uint256, SaltedSipHasher>*>(context);
     264 | +    CPubKey label{key, key + CPubKey::COMPRESSED_SIZE};
     265 | +    // Find the pubkey in the map
     266 | +    auto it = label_context->find(label.GetID());
    


    theStack commented at 9:04 PM on May 30, 2026:

    in b0a0c54f5925c83728d0081f3009eb947b55895c: could use directly CPubKey as the label cache map key type, since by using CKeyID additional hashing steps (via .GetID(), performing Hash160, i.e. SHA-256 + RIPEMD-160) are involved for every lookup, which I suspect would be slower. The only remaining gain would be a smaller size of the map in memory (20 bytes vs. 33 bytes keys), but I doubt that this would ever matter in practice.


    Eunovo commented at 10:35 AM on June 7, 2026:

    CPubKey doesn't have a hash function for use with std::unordered_map. We'll have to check if it's better to use a CPubKey + std::map<CPubKey, uint256> or CKeyID + std::unordered_map<CKeyID, uint256, SaltedSipHasher>. The current plan is to eventually support at least 100_000 labels with the Bitcoin Core wallet; we can run a benchmark with this in mind.

  52. theStack commented at 9:10 PM on May 30, 2026: contributor

    Looks good overall, left just some more findings regarding labels and the label cache. It might be worth it to introduce a dedicated "serialized label" type (e.g. std::array<byte, 33>) and avoid using CPubKey, but this could still be done in a later follow-up PR.

  53. common: add bip352.{h,cpp} secp256k1 module
    Wrap the silentpayments module from libsecp256k1. This is placed in
    common as it is intended to be used by:
    
      * RPCs: for parsing addresses
      * Wallet: for sending, receiving, spending silent payments outputs
      * Node: for creating silent payments indexes for light clients
    401abe1f9a
  54. wallet: disable sending to silent payments address
    Have `IsValidDestination` return false for silent payments destinations
    and set an error string when decoding a silent payments address.
    
    This prevents anyone from sending to a silent payments address before
    sending is implemented in the wallet, but also allows the functions to
    be used in the unit testing famework.
    351467a6ce
  55. tests: add BIP352 test vectors as unit tests
    Use the test vectors to test sending and receiving. A few cases are not
    covered here, namely anything that requires testing specific to the
    wallet. For example:
    
    * Taproot script path spending is not tested, as that is better tested in
      a wallets coin selection / signing logic
    * Re-computing outputs during RBF is not tested, as that is better
      tested in a wallets RBF logic
    
    The unit tests are written in such a way that adding new test cases is
    as easy as updating the JSON file
    295ebcaeeb
  56. Eunovo force-pushed on Jun 7, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-11 00:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me