Silent Payments: Implement bip352 (take 2) #35301

pull Eunovo wants to merge 8 commits into bitcoin:master from Eunovo:implement-bip352 changing 47 files +20196 −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. Squashed 'src/secp256k1/' changes from 7262adb4b4..14be7f2abe
    14be7f2abe silentpayments: skip slow benchmarks for low iters count (<= 2)
    ea46d6502e docs: update README
    8587aeb029 ci: enable silentpayments module
    1375d38a32 tests: add sha256 tag test
    eb3748dab1 tests: add constant time tests
    e1674f389a tests: add BIP-352 test vectors
    9163035ab7 silentpayments: optimize scanning by using batch inversion
    dc36ff6ec3 silentpayments: add benchmarks for scanning
    b7f09f3eb5 silentpayments: add examples/silentpayments.c
    460565c662 silentpayments: respect per-group recipients protocol limit (K_max=2323)
    72658fc85e silentpayments: receiving
    c7a64d0cd8 silentpayments: recipient label support
    be56a66992 silentpayments: sending
    7bfae739e3 build: add skeleton for new silentpayments (BIP352) module
    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: 14be7f2abe4797c1a58249cb901cc6f02972cb4d
    bb1bce36cf
  3. Merge commit 'bb1bce36cf8b9292a65f0f03ab855e7133357b9f' into new-sp-base ece2374760
  4. 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.
    bccfd81105
  5. 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:

    • #35302 (Silent Payments: Sending (take 2) by Eunovo)
    • #32966 (Silent Payments: Receiving by Eunovo)
    • #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-->

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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;

  11. 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.

  12. 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.

  13. 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.

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

    Concept ACK

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

    Concept ACK aca8a8f

  17. 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.

  18. 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.

  19. 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

  20. 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.

  21. 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.

  22. 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

  23. 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.

  24. Add "sp" HRP ac7ab47ddb
  25. Add V0SilentPaymentsDestination address type 15a2bdd5a1
  26. 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
    d874991c7f
  27. 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.
    e812b6d470
  28. 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
    804d7ae60b
  29. Eunovo force-pushed on May 21, 2026
  30. 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)?

  31. 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 :)

  32. DrahtBot added the label CI failed on May 21, 2026
  33. 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>


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-05-21 20:51 UTC

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