Skip to content

Instantly share code, notes, and snippets.

@w0xlt
Last active October 21, 2022 20:35
Show Gist options
  • Save w0xlt/c81277ae8677b6c0d3dd073893210875 to your computer and use it in GitHub Desktop.
Save w0xlt/c81277ae8677b6c0d3dd073893210875 to your computer and use it in GitHub Desktop.
Running Silent Payments on Signet (v4)

Running Silent Payments on Signet (v4)

Definition

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

Caveats

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.

Starting the node

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.

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,
  "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."
}

Creating a silent transaction

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.

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

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.

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

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.0008}"
./src/bitcoin-cli  -signet -rpcwallet="receiver" sendall "[\"$sender_address_2\"]"

Conclusion

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.

@josibake
Copy link

this is awesome, thanks for writing it up! just ran through it without any issues.

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