Skip to content

Instantly share code, notes, and snippets.

Last active Jul 19, 2022
What would you like to do?
Silent Payment Tutorial (initial version)

Running Silent Payments on Signet


Silent Payment is a cryptographic scheme that allows the recipient to publish a public address and the sender to tweak the address, sending coins to a completely unrelated address. The recipient detects payments by verifying mempool, block or UTXO Set transactions.

Current Status

PR #24897 implements an early version of this schema. Currently, it is possible to make silent payments on signet, both on the recipient's and sender's side. The steps to build these payments will be described in the next section.

The purpose of the current implementation is:

  • validate the schema
  • check if the performance is viable

The main downside of the silent payment approach is that it is necessary to verify all P2TR transactions, retrieve the public key of the first input (or all inputs, depending on the implementation) and apply the calculation. The cost of this process can be potentially prohibitive.

Therefore, the current version deliberately does not implement other parts of the proposal, such as a new address format. This requires a separate discussion and changes in wallet behavior. And it doesn't make sense to start a discussion about it before the schema proves itself valid in a feasible time.

Also, this initial version does not change any behavior unless the SILENT_PAYMENT flag is set.

Starting the node

Before starting this tutorial, start the bitcoin node on the signet network and silent_payment index enabled.

The node must be built from PR #24897.

This tutorial also uses jq to make it reproducible. On Ubuntu / Debian, this can be installed with sudo apt-get install jq.

Do not run this tutorial on mainnet. The specification is not yet well defined, nor the implementation well reviewed.

./src/bitcoind -signet -silentpaymentindex -keypool=1

The log will display messages like Syncing silentpaymentindex with block chain from height 9346 during synchronization.

When the sync finishes, the silentpaymentindex is enabled at height ... message is displayed and then the next step can be executed.

Note that the node was started with -keypool=1. It is a workaround to avoid multi-key verification. With this option, the node will generate the keys as requested.

Creating wallets

Create two wallets, one to send funds and one to receive funds.

./src/bitcoin-cli -signet -named createwallet wallet_name="receiver" silent_payment=true

./src/bitcoin-cli -signet -named createwallet wallet_name="sender"

Note that the sender's wallet does not need the silent_payment option, only the recipient's.

This option sets the SILENT_PAYMENT flag on the wallet. This can be verified with the getwalletinfo RPC.

./src/bitcoin-cli -signet -rpcwallet="receiver" getwalletinfo
  "walletname": "receiver",
  "walletversion": 169900,
  "silent_payment": true

Another interesting verification is that the descriptors were created with range: [0,0] and next: 0. This happened because the node was started with -keypool=1.

./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors
  "wallet_name": "receiver",
  "descriptors": [
        "desc": "tr([7e01e60f/86'/1'/0']tpubDC...LdxR3u/0/*)#cn74ecl8",
        "timestamp": 1652394273,
        "active": true,
        "internal": false,
        "range": [0,0],
        "next": 0

Creating a silent transaction

The first step is to fund the sender's wallet. contrib/signet/ can be used or can be accessed directly. requires ImageMagick (sudo apt install imagemagick can be used on Ubuntu / Debian).

sender_address=$(./src/bitcoin-cli -signet -rpcwallet="sender" getnewaddress)

./contrib/signet/ -c ./src/bitcoin-cli -a $sender_address

Wait until newly received coins can be spent. This can be verified with getbalance RPC.

./src/bitcoin-cli -signet -rpcwallet="sender" getbalance

Generate the receiver address.

receiver_address=$(./src/bitcoin-cli -signet -rpcwallet="receiver" getnewaddress '' 'bech32m')

To create a silent transaction, just use the silent_payment option. This instructs send RPC to:

  • validate that all addresses are Taproot
  • tweak each output public key with the private of the first input of the transaction
hex_sil_tx=$(./src/bitcoin-cli  -signet -rpcwallet="sender" -named send outputs="{\"$receiver_address\": 0.0008}"  options="{\"silent_payment\": true, \"add_to_wallet\": false}" | jq -r '.hex')

decoderawtransaction can be used to check if vout addresses are different from receiver_address. This means that the original destination was successfully tweaked to a completely unrelated one.

./src/bitcoin-cli -signet decoderawtransaction $hex_sil_tx

The transaction can be sent with sendrawtransaction or with send RPC without the add_to_wallet: false parameter.

./src/bitcoin-cli -signet sendrawtransaction $hex_sil_tx

Checking the silent transaction

The next step is to verify that the recipient's wallet has successfully received the transaction.

./src/bitcoin-cli -signet -rpcwallet="receiver" listunspent 0

Note that the txid is the same as the one returned by sendrawtransaction RPC and the address is also different from the one previously generated by this wallet.

Another interesting point is that a new descriptor has also been added to the wallet, as shown by listdescriptors.

./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors
  "wallet_name": "receiver",
  "descriptors": [
      "desc": "rawtr(533a8...4d6ae7bca)#pffj3fq6",
      "timestamp": 1652411233372354,
      "active": false

This is because when a wallet with the silent_payment flag identifies a relevant payment, it adds a new rawtr(KEY) descriptor.

Scanning the UTXO Set

scantxoutset RPC is important for testing silent payment because it does not require a wallet and can retrieve the transaction to be spent without scanning blocks.

In this example, the receiver's wallet descriptor tr will be used. scantxoutset must return the same silent transaction detected in the previous step.

receiver_tr_desc=$(./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors true  | jq '.descriptors | [.[] | select(.desc | startswith("tr") and contains("/0/*"))][0] | .desc')

./src/bitcoin-cli -signet scantxoutset "start" "[{\"desc\":$receiver_tr_desc, \"range\": [0, 0]}]" true

The last scantxoutset parameter is the silent_payment option. When enabled, RPC will look for the silent payment.

Note that the range: [0, 0] is set. With this option, only the first key will be used to look up transactions.

The above command must show the same results as the previously run -rpcwallet="receiver" listunspent 0.

Spending From Silent Payment

After successfully receiving the silent payment, the next step is to check if the wallet can spend it.

The commands below send some coins back to the sender.

sender_address_2=$(./src/bitcoin-cli -signet -rpcwallet="sender" getnewaddress)

./src/bitcoin-cli  -signet -rpcwallet="receiver" -named send outputs="{\"$sender_address_2\": 0.0006}"


The user was able to generate an address ($receiver_address), publish it, receive and spend coins from it and still 0 transaction is shown from this address in a blockchain explorer (i.e.,

Other tests

In the previous sections, two tests were done: when transactions are coming from the mempool and when scanning the UTXO Set (which is not related to a wallet, but to custom descriptors).

There is another test that can be done: check if the wallet is able to detect transactions by scanning blocks. This happens when the user loads a wallet after it has been closed for a while and several blocks have passed.

This test can be done as follows:

  • Generate an address from the receiver wallet (which has the silent_payment option).
  • Unload the wallet
  • Make a silent payment to this address
  • Wait for several new blocks to be confirmed
  • Load the receiver wallet
  • Check if the transaction is listed in listunspent RPC

Open Questions

  • Is the performance of the UTXO Set and the blocks scan using the silent payment index satisfactory?
  • Instead of using a flag, a new address/descriptor format could be used. What would be an ideal format?
Copy link

okeygo commented May 26, 2022

Encouraging and facilitating privacy is a must. The scanning process is tedious so far but I guess that proper tweaking will lead to an excellent implementation and adoption will follow by those who care for privacy. Time to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment