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.
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 purposes of the current implementation are:
- 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.
Also, this initial version does not change any wallet behavior unless the SILENT_PAYMENT
flag is set or an address with sp
HRP is identified.
This is still a work and research in progress. Current status is intended to gather feedback. The silent payment feature is not implemented in any RPC other than send()
, and while there is extensive coverage of functional testing, it may not be enough to guarantee that it is fully working on Bitcoin Core.
Do not run this tutorial on mainnet. The specification is not yet well defined, nor the implementation well reviewed.
Before starting this tutorial, start the bitcoin node on the signet network with the 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
.
If the node has a silent payment index that was run before this version, it is necessary to remove it, as this version implements a new scheme, incompatible with the previous one. This can be done by removing the bitcoin/signet/indexes/silentpaymentindex
folder.
./src/bitcoind -signet -silentpaymentindex
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 ...
and silentpaymentindex thread exit
messages are displayed and then the next step can be executed.
In the previous version, the node was started with -keypool=1
, which was a workaround to avoid costly multi-key verification. This is no longer necessary, as this new version uses a new sp
descriptor that handles one and only one key.
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,
"format": "sqlite",
...
"silent_payment": true
}
Another interesting check is that RPC listdescriptors
will show a new type of descriptor called sp()
. Note that this descriptor has no range and contains exactly one key.
$ ./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors true
{
"wallet_name": "receiver",
"descriptors": [
...,
{
"desc": "sp(cNZzaL5DbP...X6giJFSdvXrUG9By4x)#hmaqx8df",
"timestamp": 1664465259,
"active": true,
"internal": false,
"next": 0
},
...
]
}
This descriptor introduces a new output type: the silent-payment
. This output type returns a standard Taproot script, but with the HRP changed from bc
to sp
on mainnet (or tsp
on testnet and signet). And it adds a new field called identifier after the HRP.
The identifier field tells the recipient and sender how to properly tweaks the address. In the receiver's wallet, each identifier is related to a label. The next
field shown in the descriptor above represents the next identifier to be used. It is also used to control wallet scanning.
If the receiver, for whatever reason, doesn't know which identifiers have been used, there is no problem. The wallet can scan all identifiers from 0 to 99. Currently, only 100 different identifiers per wallet are allowed. This limit, however, can be increased at any time in the future.
In this new version, sp
addresses are obtained via getsilentaddress()
RPC. This command will return a new address if the label is new. Otherwise, it will return the same address that is already assigned to that label.
./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress
{
"address": "tsp001pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kqxn48tq",
"label": "",
"identifier": 0
}
# This will return the same address as above (both have no label)
./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress
{
"address": "tsp001pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kqxn48tq",
"label": "",
"identifier": 0
}
./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress 'donation'
{
"address": "tsp011pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kq80t7lt",
"label": "donation",
"identifier": 1,
"warnings": "This address is not a new identity. It is a re-use of an existing identity with a different label."
}
# This will return the same address as above (both have the same label)
./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress 'donation'
{
"address": "tsp011pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kq80t7lt",
"label": "donation",
"identifier": 1
}
# New label, new address
./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress 'e-commerce'
{
"address": "tsp021pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kqyzqu2k",
"label": "e-commerce",
"identifier": 2,
"warnings": "This address is not a new identity. It is a re-use of an existing identity with a different label."
}
The first step is to fund the sender's wallet. contrib/signet/getcoins.py
can be used or https://signetfaucet.com
can be accessed directly.
getcoins.py
requires ImageMagick (sudo apt install imagemagick
can be used on Ubuntu / Debian).
sender_address=$(./src/bitcoin-cli -signet -rpcwallet="sender" getnewaddress)
$ ./contrib/signet/getcoins.py -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
Let's generate 3 different silent addresses from the receiver's wallet. This makes it possible to track which address the UTXO came from.
$ receiver_address_no_label=$(./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress | jq -r '.address')
$ receiver_address_donation=$(./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress 'donation' | jq -r '.address')
$ receiver_address_ecommerce=$(./src/bitcoin-cli -signet -rpcwallet="receiver" getsilentaddress 'e-commerce' | jq -r '.address')
# Confirm three different addresses were generated
$ echo $receiver_address_no_label
tsp001pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kqxn48tq
$ echo $receiver_address_donation
tsp011pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kq80t7lt
$ echo $receiver_address_ecommerce
tsp021pjgcwd9p6f2rcgf35dlgvj77h2afylg6lp5cdn0cztrk4k54w99kqyzqu2k
To create a silent transaction, simply use the silent payment addresses as outputs. The send
RPC will automatically identify and tweak them.
$ hex_sil_tx=$(./src/bitcoin-cli -signet -rpcwallet="sender" -named send outputs="{\"$receiver_address_no_label\": 0.000301, \"$receiver_address_donation\": 0.000302, \"$receiver_address_ecommerce\": 0.000303}" options="{\"add_to_wallet\": false}" | jq -r '.hex')
decoderawtransaction
can be used to check if vout
addresses are different from the receiver addresses. This means that the original destinations have been successfully tweaked to a completely unrelated one.
Since the addresses are in different HRP format, the scriptPubKey.hex
field can be used to compare them.
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
In the previous version, the silent-payment
parameter was added to send()
RPC. This is also no longer necessary as the command identifies the silent address by the different HRP.
Note that a transaction can contain multiple outputs, combining silent and standard addresses.
The next step is to verify that the recipient's wallet has successfully received the transaction.
$ ./src/bitcoin-cli -signet -rpcwallet="receiver" listunspent 0
This command must return 3 UTXOs. Note that the txid
of all them is the same as the one returned by sendrawtransaction
RPC and the address
is also different from the one previously generated by this wallet.
Also note that each UTXO has a label
field. One should have an empty label, another donation
and one e-commerce
. This way, the user knows which silent address the UTXO came from.
Another interesting point is that new three descriptors have also been added to the wallet, as shown by listdescriptors
.
$ ./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors true
{
"wallet_name": "receiver",
"descriptors": [
...
{
"desc": "rawtr(cTMGezg...SJEtuYEXvWn)#j49mn2lu",
"timestamp": 1664468741153162,
"active": false
},
{
"desc": "rawtr(cTPDQAuy...L9iP5kBodc)#dldzt4ct",
"timestamp": 1664468741247332,
"active": false
},
{
"desc": "rawtr(cTRA9MeQ9Yx...s5LtGDZ)#hggm92s2",
"timestamp": 1664468741304699,
"active": false
},
...
]
}
This is because when a wallet with the silent_payment
flag identifies a relevant payment, it adds a new rawtr(KEY)
descriptor.
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 sp
will be used. scantxoutset
must return the same silent transaction detected in the previous step.
In the commands below, do not forget to add the true
option to listdescriptors
, which lists the descriptor private keys. Scanning the UTXO Set for silent payments requires the private key because it is needed to tweak the address.
$ receiver_sp_desc=$(./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors true | jq '.descriptors | [.[] | select(.desc | startswith("sp"))][0] | .desc')
$ next_index=$(./src/bitcoin-cli -signet -rpcwallet="receiver" listdescriptors true | jq '.descriptors | [.[] | select(.desc | startswith("sp"))][0]| .next')
$ ./src/bitcoin-cli -signet scantxoutset "start" "[{\"desc\": $receiver_sp_desc, \"range\": $next_index}]"
The above command must show the same results as the previously run -rpcwallet="receiver" listunspent
. Note that transactions need at least one confirmation to appear in the UTXO Set.
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.0008}"
./src/bitcoin-cli -signet -rpcwallet="receiver" sendall "[\"$sender_address_2\"]"
The user was able to generate addresses from different labels and with the sp\tsp
HRP which indicates that it is used for silent payments, publish it, receive and spend coins from it.
The coins were labeled according the silent address label. This allows the users to track the origin of the coins.
The sp
descriptor only need one key to derive any silent address.
The next
field represents the next identifier to be used. If it is lost, there is no problem as the user can scan every identifier from 0 to 99.
this is awesome, thanks for writing it up! just ran through it without any issues.