Silent Payments: Sending (take 2) #35302

pull Eunovo wants to merge 19 commits into bitcoin:master from Eunovo:implement-bip352-sending changing 65 files +21730 −143
  1. Eunovo commented at 4:16 AM on May 16, 2026: contributor

    This PR is part of integrating silent payments into Bitcoin Core. It is the second iteration of #28201. Status and tracking for the project is managed in #28536

    Prerequisite PRs:

    Sending

    Silent Payments logic The main focus of this PR is:

    • Applying the Taptweak to a taproot internal private key (this is a copy-paste of the code for applying the taptweak in the signing process)
    • Getting a private key from a given scriptPubKey
    • Creating silent payment outputs
    • Applying the created scriptPubKeys back to the vector of CRecipients
    • The functions are then used together to create silent payment outputs during CreateTransactionInternal.

    Final steps The last commits ensure that:

    • Coin selection is silent payments aware and knows to exclude taproot script path spends and inputs with unknown witness when funding a transaction which pays to a silent payment address
    • The change output type is correctly chosen when paying to a silent payment address
    • Functional tests
  2. DrahtBot commented at 4:16 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/35302.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK 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:

    • #35458 (qa: Avoid extra tracebacks when exception is raised by hodlinator)
    • #35444 (wallet: make descriptor SPKM mutex non-recursive by w0xlt)
    • #35442 (test: remove usages of MAX_BIP125_RBF_SEQUENCE constant from functional tests by rkrux)
    • #35433 (wallet: deprecate replaceable argument from transaction (and psbt) creation (and modification) RPCs by rkrux)
    • #35317 (wallet: fix ignored subtract_fee_from_outputs option by stutxo)
    • #34697 (descriptor: fix musig duplicate checks and origin handling by shuv-amp)
    • #34405 (wallet: skip APS when no partial spend exists by 8144225309)
    • #33034 (wallet: Store transactions in a separate sqlite table by achow101)
    • #32763 (wallet: Replace CWalletTx::mapValue and vOrderForm with explicit class members by achow101)
    • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)
    • #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. rkrux commented at 9:11 AM on May 20, 2026: contributor

    Concept ACK 03c835d

  4. in src/wallet/scriptpubkeyman.cpp:1484 in f9a10e9ea9
    1476 | @@ -1477,6 +1477,30 @@ bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKe
    1477 |      return true;
    1478 |  }
    1479 |  
    1480 | +Key DescriptorScriptPubKeyMan::GetPrivKeyForSilentPayment(const CScript& scriptPubKey) const
    1481 | +{
    1482 | +    std::vector<std::vector<unsigned char>> solutions;
    1483 | +    TxoutType whichType = Solver(scriptPubKey, solutions);
    1484 | +    if (whichType == TxoutType::NONSTANDARD || whichType == TxoutType::MULTISIG || whichType == TxoutType::WITNESS_UNKNOWN ) return {};
    


    theStack commented at 1:21 AM on May 23, 2026:

    in f9a10e9ea9b57f6b21a6dbfd1239c3ba60f721f8: I think it would be more robust to express this condition using the explicit list of relevant output script types as specified in the BIP, i.e. return early if whichType is not any of P2TR, P2WPKH, P2SH-P2WPKH or P2PKH. Otherwise, the condition has to be extended whenever the TxoutType enum class gets extended (e.g. P2A has been added since and is not there in the condition).


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

    Done.

  5. in src/wallet/scriptpubkeyman.cpp:1488 in f9a10e9ea9
    1483 | +    TxoutType whichType = Solver(scriptPubKey, solutions);
    1484 | +    if (whichType == TxoutType::NONSTANDARD || whichType == TxoutType::MULTISIG || whichType == TxoutType::WITNESS_UNKNOWN ) return {};
    1485 | +    std::unique_ptr<FlatSigningProvider> coin_keys = GetSigningProvider(scriptPubKey, true);
    1486 | +    if (!coin_keys || coin_keys->keys.size() != 1) return {};
    1487 | +    const auto& [_, key] = *coin_keys->keys.begin();
    1488 | +    (void) _;
    


    theStack commented at 1:34 AM on May 23, 2026:

    f9a10e9ea9b57f6b21a6dbfd1239c3ba60f721f8: nit: is this line needed? Not sure what the exact rules in C++ are about the single underscore identifier, but I haven't seen this fake-use pattern (probably with the intention to avoid warnings) in other similar places where we use it.


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

    Removed.

  6. in src/wallet/rpc/spend.cpp:436 in e3ef349ad5
     432 | @@ -420,6 +433,11 @@ RPCMethod sendmany()
     433 |              ParseOutputs(sendTo),
     434 |              InterpretSubtractFeeFromOutputInstructions(request.params[4], sendTo.getKeys())
     435 |      );
     436 | +    auto it = std::find_if(recipients.begin(), recipients.end(), [](const auto& r) { return std::holds_alternative<V0SilentPaymentDestination>(r.dest); });
    


    theStack commented at 1:41 AM on May 23, 2026:

    in e3ef349ad5ad2bc283644b5faaa1a6fa234e16bd: could slightly simplify by using std::any_of instead (returns a bool instead of an iterator)


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

    Done.

  7. in src/wallet/rpc/spend.cpp:1280 in e3ef349ad5
    1294 |              outputs = NormalizeOutputs(request.params[0]);
    1295 |              std::vector<CRecipient> recipients = CreateRecipients(
    1296 |                      ParseOutputs(outputs),
    1297 |                      InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys())
    1298 |              );
    1299 | -            CCoinControl coin_control;
    


    theStack commented at 1:42 AM on May 23, 2026:

    in e3ef349ad5ad2bc283644b5faaa1a6fa234e16bd: any reason why the coin_control instance got moved up?


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

    Reverted.

  8. in src/wallet/rpc/spend.cpp:171 in e3ef349ad5
     167 | @@ -168,6 +168,14 @@ static void PreventOutdatedOptions(const UniValue& options)
     168 |      }
     169 |  }
     170 |  
     171 | +static void IsSilentPaymentsEnabled(const CWallet &pwallet)
    


    theStack commented at 1:45 AM on May 23, 2026:

    in e3ef349ad5ad2bc283644b5faaa1a6fa234e16bd: for functions starting with Is, I'd usually expect to return a boolean value. maybe rename with using Ensure as a prefix instead?


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

    Done.

  9. Eunovo commented at 11:45 AM on May 25, 2026: contributor

    @theStack I'll attend to your reviews when I rebase on https://github.com/bitcoin/bitcoin/pull/35301

  10. DrahtBot added the label Needs rebase on May 28, 2026
  11. 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
  12. Merge commit '9bbbb3fc71fba327f12699fd2aa49307e3119d66' into sp-base c8c4b5ae44
  13. 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
  14. Add "sp" HRP cf56b51424
  15. 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
  16. 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
  17. 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
  18. 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
  19. wallet: get serialized size for `V0SilentPayments`
    BIP352 v0 specifies that a silent payment output is a taproot output.
    Taproot scriptPubKeys are a fixed size, so when calculating the
    serialized size for a CRecipient with a V0SilentPayments destination,
    use WitnessV1Taproot for the serialized txout size.
    6f1c28d7b8
  20. wallet: add method for retreiving a private key
    Add a method for retreiving a private key for a given scriptPubKey.
    If the scriptPubKey is a taproot output, tweak the private key with the
    merkle root or hash of the public key, if applicable.
    e190da32cc
  21. wallet: flag silent payments transactions
    Add a flag to the `CoinControl` object if silent payments destinations
    are provided. Before adding the flag, call a function which checks if:
    
    * The wallet has private keys
    * The wallet is unlocked
    
    Without both of the above being true, we cannot send to a silent payment
    address.
    
    During coin selection, if this flag is set, skip taproot inputs when
    script spend data is available. This is based on the assumption that if
    a user provides script spend data, they don't have access to the key
    path spend. As future improvement, we could instead check to see if we
    have access to the key path spend, and only exclude the output when we
    don't regardless of whether or not the user provides script spend data.
    
    Also skip UTXOs of type `WITNESS_UNKNOWN`, although it is very unlikely
    our wallet would ever try to spend a witness unknown output.
    851d4fdd8c
  22. wallet: add `IsInputForSharedSecretDerivation` function 3f3473673c
  23. Eunovo force-pushed on Jun 7, 2026
  24. Eunovo force-pushed on Jun 7, 2026
  25. DrahtBot added the label CI failed on Jun 7, 2026
  26. DrahtBot commented at 11:15 AM on June 7, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/27090770600/job/79953877713</sub> <sub>LLM reason (✨ experimental): (empty)</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. DrahtBot removed the label Needs rebase on Jun 7, 2026
  28. wallet: make coin selection silent payments aware
    The coin selection algorithm wasn't guaranteed
    to return a set of coins that are eligible for silent
    payments transactions even when eligible coins
    were available.
    
    This commit forces coin selection to first select
    eligible coins before attempting to complete
    the result set with ineligible coins.
    
    If the transaction cannot be fulfilled with only
    eligible coins, coin selection will "preset" eligible
    outputs in descending order of `effective_value`
    and attempt to construct a selection from the
    remaining available coins.
    52e97cd683
  29. wallet: add `CreateSilentPaymentsOutputs` function
    `CreateSilentPaymentsOutputs` gets the correct private keys, adds them
    together, groups the silent payments destinations and then generates the
    taproot script pubkeys. These are then passed back to
    CreateTransactionInternal, which uses these scriptPubKeys to update
    vecSend before adding them to the transaction outputs.
    8bb40a41c0
  30. wallet: update wallet change type selection logic for sp dest
    If sending to a silent payments dest, the change type should be taproot
    because silent payments dests appear as taproot outputs on the blockchain.
    1b61cfda15
  31. wallet: enable sending to silent payments address
    Treat silent payments addresses as valid destination. Also disable using
    silent payments addresses with the `addr()` descriptor, as this
    descriptor expects an encoding of a scriptPubKey, whereas a silent
    payment address consists of instructions on how to generate a
    scriptPubKey for the recipient.
    
    Co-authored-by: Novo <eunovo9@gmail.com>
    a432d127ea
  32. wallet/rpc: add sp addr support to sendall 869cf1e073
  33. Eunovo force-pushed on Jun 7, 2026
  34. wallet: support bumpfee for sp transactions
    silent payments transactions outputs  must be
    rederived from the sp destinations  when new
    inputs are added.
    
    This commit save the sp destinations with
    CWalletTx's mapValue to preserve the existing
    serialization format.
    
    feebumper recreates the original send request
    using this saved sp destinations information.
    02b7190976
  35. Eunovo force-pushed on Jun 8, 2026
  36. tests: add sending functional tests dffaa357ae
  37. Eunovo force-pushed on Jun 8, 2026
  38. DrahtBot removed the label CI failed on Jun 8, 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-10 23:51 UTC

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