Skip to content

Instantly share code, notes, and snippets.

@RubenSomsen
Last active December 17, 2024 21:13
Show Gist options
  • Save RubenSomsen/c43b79517e7cb701ebf77eec6dbb46b8 to your computer and use it in GitHub Desktop.
Save RubenSomsen/c43b79517e7cb701ebf77eec6dbb46b8 to your computer and use it in GitHub Desktop.
Silent Payments – Receive private payments from anyone on a single static address without requiring any interaction or extra on-chain overhead

Silent Payments

Receive private payments from anyone on a single static address without requiring any interaction or extra on-chain overhead.

Update: This now has a BIP and WIP implementation

Overview

The recipient generates a so-called silent payment address and makes it publicly known. The sender then takes a public key from one of their chosen inputs for the payment, and uses it to derive a shared secret that is then used to tweak the silent payment address. The recipient detects the payment by scanning every transaction in the blockchain.

Compared to previous schemes1, this scheme avoids using the Bitcoin blockchain as a messaging layer2 and requires no interaction between sender and recipient3 (other than needing to know the silent payment address). The main downsides are the scanning requirement, the lack of light client support, and the requirement to control your own input(s). An example use case would be private one-time donations.

While most of the individual parts of this idea aren't novel, the resulting protocol has never been seriously considered and may be reasonably viable, particularly if we limit ourselves to detecting only unspent payments by scanning the UTXO set. We'll start by describing a basic scheme, and then introduce a few improvements.

Basic scheme

The recipient publishes their silent payment address, a single 32 byte public key: X = x*G

The sender chooses an input containing a public key: I = i*G

The sender tweaks the silent payment address with the private key that corresponds to their chosen input: X' = hash(i*X)*G + X

Sincei*X == x*I (Diffie-Hellman Key Exchange), the recipient can detect the payment by calculating hash(x*I)*G + X for each input key I in the blockchain and seeing if it matches an output in the corresponding transaction.

Improvements

UTXO set scanning

If we forgo detection of historic transactions and only focus on the current balance, we can limit the protocol to only scanning the transactions that are part of the UTXO set when restoring from backup, which may be faster.

Jonas Nick was kind enough to go through the numbers and run a benchmark of hash(x*I)*G + X on his 3.9GHz Intel(R) Core(TM) i7-7820HQ CPU, which took roughly 72 microseconds per calculation on a single core. The UTXO set currently has 80 million entries, the average transaction has 2.3 inputs, which puts us at 2.3*80000000*72/1000/1000/60 = 221 minutes for a single core (under 2 hours for two cores).

What these numbers do not take into account is database lookups. We need to fetch the transaction of every UTXO, as well as every transaction for every subsequent input in order to extract the relevant public key, resulting in (1+2.3)*80000000 = 264 million lookups. How slow this is and what can be done to improve it is an open question.

Once we're at the tip, every new unspent output will have to be scanned. It's theoretically possible to scan e.g. once a day and skip transactions with fully spent outputs, but that would probably not be worth the added complexity. If we only scan transactions with taproot outputs, we can further limit our efforts, but this advantage is expected to dissipate once taproot use becomes more common.

Variant using all inputs

Instead of tweaking the silent payment address with one input, we could instead tweak it with the combination of all input keys of a transaction. The benefit is that this further lowers the scanning cost, since now we only need to calculate one tweak per transaction, instead of one tweak per input, which is roughly half the work, though database lookups remain unaffected.

The downside is that if you want to combine your inputs with those of others (i.e. coinjoin), every participant has to be willing to assist you in following the Silent Payment protocol in order to let you make your payment. There are also privacy considerations which are discussed in the "Preventing input linkage" section.

Concretely, if there are three inputs (I1, I2, I3), the scheme becomes: hash(i1*X + i2*X + i3*X)*G + X == hash(x*(I1+I2+I3))*G + X.

Scanning key

We can extend the silent payment address with a scanning key, which allows for separation of detecting and spending payments. We redefine the silent payment address as the concatenation of X_scan, X_spend, and derivation becomes X' = hash(i*X_scan)*G + X_spend. This allows your internet-connected node to hold the private key of X_scan to detect incoming payments, while your hardware wallet controls X_spend to make payments. If X_scan is compromised, privacy is lost, but your funds are not.

Address reuse prevention

If the sender sends more than one payment, and the chosen input has the same key due to address reuse, then the recipient address will also be the same. To prevent this, we can hash the txid and index of the input, to ensure each address is unique, resulting in X' = hash(i*X,txid,index)*G + X. Note this would make light client support harder (edit: not necessarily, see here).

Noteworthy details

Light clients

Light clients cannot easily be supported due to the need for scanning. The best we could do is give up on address reuse prevention (so we don't require the txid and index), only consider unspent taproot outputs, and download a standardized list of relevant input keys for each block over wifi each night when charging. These input keys can then be tweaked, and the results can be matched against client-side block filters. Possible, but not simple. (edit: some more ideas how to do light client support here)

Effect on BIP32 HD keys

One side-benefit of silent payments is that BIP32 HD keys4 won't be needed for address generation, since every address will automatically be unique. This also means we won't have to deal with a gap limit.

Different inputs

While the simplest thing would be to only support one input type (e.g. taproot key spend), this would also mean only a subset of users can make payments to silent addresses, so this seems undesirable. The protocol should ideally support any input containing at least one public key, and simply pick the first key if more than one is present.

Pay-to-(witness-)public-key-hash inputs actually end up being easiest to scan, since the public key is present in the input script, instead of the output script of the previous transaction (which requires one extra transaction lookup).

Signature nonce instead of input key

Another consideration was to tweak the silent payment address with the signature nonce5, but unfortunately this breaks compatibility with MuSig2 and MuSig-DN, since in those schemes the signature nonce changes depending on the transaction hash. If we let the output address depend on the nonce, then the transaction hash will change, causing a circular reference.

Sending wallet compatibility

Any wallet that wants to support making silent payments needs to support a new address format, pick inputs for the payment, tweak the silent payment address using the private key of one of the chosen inputs, and then proceed to sign the transaction. The scanning requirement is not relevant to the sender, only the recipient.

Preventing input linkage

A potential weakness of Silent Payments is that the input is linked to the output. A coinjoin transaction with multiple inputs from other users can normally obfuscate the sender input from the recipient, but Silent Payments reveal that link. This weakness can be mitigated with the "variant using all inputs", but this variant introduces a different weakness – you now require all other coinjoin users to tweak the silent payment address, which means you're revealing the intended recipient to them.

Luckily, a blinding scheme6 exists that allows us to hide the silent payment address from the other participants. Concretely, let's say there are two inputs, I1 and I2, and the latter one is ours. We add a secret blinding factor to the silent payment address, X + blinding_factor*G = X', then we receive X1' = i1*X' (together with a DLEQ to prove correctness, see full write-up6) from the owner of the first input and remove the blinding factor with X1' - blinding_factor*I1 = X1 (which is equal to i1*X). Finally, we calculate the tweaked address with hash(X1 + i2*X)*G + X. The recipient can simply recognize the payment with hash(x*(I1+I2))*G + X. Note that the owner of the first input cannot reconstruct the resulting address because they don't know i2*X.

The blinding protocol above solves our coinjoin privacy concerns (at the expense of more interaction complexity), but we're left with one more issue – what if you want to make a silent payment, but you control none of the inputs (e.g. sending from an exchange)? In this scenario we can still utilize the blinding protocol, but now the third party sender can try to uncover the intended recipient by brute forcing their inputs on all known silent payment addresses (i.e. calculate hash(i*X)*G + X for every publicly known X). While this is computationally expensive, it's by no means impossible. No solution is known at this time, so as it stands this is a limitation of the protocol – the sender must control one of the inputs in order to be fully private.

Comparison

These are the most important protocols that provide similar functionality with slightly different tradeoffs. All of them provide fresh address generation and are compatible with one-time seed backups. The main benefits of the protocols listed below are that there is no scanning requirement, better light client support, and they don't require control over the inputs of the transaction.

Payment code sharing

This is BIP472. An OP_RETURN message is sent on-chain to the recipient to establish a shared secret prior to making payments. Using the blockchain as a messaging layer like this is generally considered an inefficient use of on-chain resources. This concern can theoretically be alleviated by using other means of communicating, but data availability needs to be guaranteed to ensure the recipient doesn't lose access to the funds. Another concern is that the input(s) used to establish the shared secret may leak privacy if not kept separate.

Xpub sharing

Upon first payment, hand out a fresh xpub instead of an address in order to enable repeat payments. I believe Kixunil's recently published scheme3 is equivalent to this and could be implemented with relative ease. It's unclear how practical this protocol is, as it assumes sender and recipient are able to interact once, yet subsequent interaction is impossible.

Regular address sharing

This is how Bitcoin is commonly used today and may therefore be obvious, but it does satisfy similar privacy requirements. The sender interacts with the recipient each time they want to make a payment, and requests a new address. The main downside is that it requires interaction for every single payment.

Open questions

  • Exactly how slow are the required database lookups? Is there a better approach?
  • Is there any way to make light client support more viable?
  • What is preferred – single input tweaking (revealing an input to the recipient) or using all inputs (increased coinjoin complexity)?
  • Are there any security issues with the proposed cryptography?
  • In general, compared to alternatives, is this scheme worth the added complexity?

Thanks to Kixunil, Calvin Kim, and Jonas Nick, holihawt and Lloyd Fournier for their help/comments, as well as all the authors of previous schemes. Any mistakes are my own.

There is also a discussion of this scheme on the bitcoin-dev mailing list.

Footnotes

  1. Stealth Payments, Peter Todd: https://github.com/genjix/bips/blob/master/bip-stealth.mediawiki

  2. BIP47 payment codes, Justus Ranvier: https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki 2

  3. Reusable taproot addresses, Kixunil: https://gist.github.com/Kixunil/0ddb3a9cdec33342b97431e438252c0a 2

  4. BIP32 HD keys, Pieter Wuille: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

  5. 2020-01-23 ##taproot-bip-review, starting at 18:25: https://gnusha.org/taproot-bip-review/2020-01-23.log

  6. Blind Diffie-Hellman Key Exchange, David Wagner: https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406 2

@alfred-hodler
Copy link

@RubenSomsen

I think it could be valuable if you could elaborate on this, as this is surprising to me. The scanning requirement of SP seems like much more of an implementation barrier than adding support for taproot.

I actually don't think scanning is that big a hurdle if we are talking about large businesses. These systems are fairly large and there would likely be a secondary process/system that receives entire blocks and tries to detect SP payments. Scanning is only an issue for small underpowered computers like phones and older Raspberry Pis. It's spending that's harder as the company now has to be able to spend a new script type that it hasn't integrated into its treasury infrastructure yet. This might sound inane, but it's just the reality of how these things move. There was a big exchange, I think Binance, that took several years to be able to interact with Segwit addresses.

On the withdrawal side I can imagine a situation where a user has a wallet that supports SP but cannot use that due to his counterparty not having taproot support yet, thus being unable to calculate SP addresses and therefore integrate the standard in the first place.

I assume you'd agree with me the specifications shouldn't add support for near-deprecated formats like p2pk even though it'd provide more "freedom".

I disagree. While I don't think anyone should have a valid reason for using p2pk addresses at this point in time, if they want to (for any reason), they should be able to. As long as a transaction is included in a block, it's valid from the consensus perspective and I prefer not to make judgements as to whether people should be allowed to use it. That being said, excluding p2pk, wrapped Segwit and other "legacy" script types is a compromise I can live with. But from the standpoint of usability, excluding p2(w)pkh will stunt the standard too much.

The fact that we're 5 years in and almost 3/4 of all transcations are still non-Segwit (and Segwit has some serious incentives), I wouldn't be too optimistic about the prospect of tying the success of a privacy standard to the adoption of Taproot.

@RubenSomsen
Copy link
Author

@alfred-hodler

I actually don't think scanning is that big a hurdle if we are talking about large businesses

I don't fully know what model you have in mind, but keep in mind that the scanning cost increases linearly with the number of static SP addresses you generate, so if you want to make a separate key for lots of users, this would be a significant amount of effort. I'm also wondering if anything would be gained from this, as it's easy to interact with a custodian while SP is mainly useful for non-interactive scenarios.

Maybe you could clarify what exactly the model of your company is, otherwise I can't evaluate if any of this makes sense.

On the withdrawal side I can imagine a situation where a user has a wallet that supports SP but cannot use that due to his counterparty not having taproot support yet

This confuses me as well, could you clarify? It sounds like you're saying the sender supports SP but the recipient does not. The recipient chooses how they receive the money, so this wouldn't be an issue. It would just be a regular payment.

from the standpoint of usability, excluding p2(w)pkh will stunt the standard too much.

Yes, the above is your argument, and it could be valid if p2(w)pkh + SP is indeed in demand (which is unclear to me at the moment). My point was that this argument is in fact not primarily about "freedom".

But I think right now the most important thing is that you say you have a business use case that could benefit from having SP + p2(w)pkh as opposed to SP + taproot, so I'd like to understand this. If the use case makes sense, it could be a strong argument in favor of your point, and it seems to be what motivates you.

@w0xlt
Copy link

w0xlt commented Aug 17, 2022

PR #24897 has been updated with a new silent payment version, which eliminates some manual steps from the previous version (such as the need to set the keypool to avoid costly multi-key scan).

This is achieved by using a new descriptor type (sp()) that has no range and contains exactly one key.

Example: sp(cQq73sG9....JD51uaRD)#9llg6xjm

This descriptor introduces a new type of output: silent-payment. This output type returns a standard Taproot script (Segwit V1), but with HRP changed from bc to sp on the mainnet (or tsp on testnet and signet).

This output type will always generate the same address (unless another sp descriptor is enabled on the same wallet).

$ ./src/bitcoin-cli -signet getnewaddress '' 'silent-payment'
tsp1pfmjyl7ecpmx8yf8cu6g3ez36jy7s9mzuh5pdnal3k0n588uzgmfs4s4fws

To create a silent transaction, simply use the silent payment address as one of the outputs.
The send RPC will automatically identify and tweak it.

The transaction can contain multiple outputs, combining silent and standard addresses.

I have written a step by step signet tutorial so reviewers can test this new version easily.
https://gist.github.com/w0xlt/a7b498ac1ff14b8c292a22be789bd93f

@Pantamis
Copy link

Pantamis commented Oct 5, 2022

I tested the Silent Payment PR and it works great !

One feature of Silent Payment is that the recipient cannot identify who send him bitcoins just with the Silent Payment transaction. This is great for donors privacy but it makes it hard to use by exchanges or services to receive deposits.

I think it would be nice for the sender to be able to inform the recipient in the transaction who paid him (pseudonymous). I found two ways of doing it inside the silent payment transaction, each solution has its trade-off:

  • The sender can add an OP_RETURN output which contains sender payment code blinded with a value derived from the shared secret x*hash(txid,index)*I
  • The sender uses a Taproot change address with publickey P_change = HMAC(x*hash(txid,index)*I, CHANGE_CONSTANT)*G + Y where Y would be the public key of the sender payment code, the recipient can identify the sender by computing the difference
    P_change - HMAC(x*hash(txid,index)*I, CHANGE_CONSTANT)*G
    CHANGE_CONSTANT is just a protocol key for HMAC to derive a blinding key tweak from shared secret so that no relation can be deduced from X', P_change and Y without knowing x or inputs private keys.

This second solution has the advantage to still look like a standard transaction while providing sender identification. However it makes backup harder to handle for the sender with just the words seed: if he didn't move the funds in the change output, he has to remember that he made a payment to a payment code with public key X to be able to claim funds back. Also, I think that using a scanning key is valuable but the change output can only be used to reveal one public key unless we add another output to the second public key (blinded by shared secret).

The OP_RETURN data don't have to be a spendable public key so we are free to include full sender's payment code blinded with shared secret (scanning key or not) and sender can still claim all his funds with the seed backup. TheOP_RETURN can also include a notification code part to improve scanning. Obviously, the drawback is that it is an OP_RETURN so we may detect it is a silent payment looking at the chain, but it doesn't reveal more than that.

We could imagine that the recipient service indicates in his payment code that he expects his users to identify themself with such protocol (with OP_RETURN or change) so that sender's wallet do it only if required by recipient to make the UX simpler (and it should be possible for both to derive addresses from both payment codes for future payment without any need of identification)

Silent Payment is a great solution to the notification problem, it would be nice to solve the sender's identification too, maybe someone has a better idea ? :)

Personnaly, I find adding an OP_RETURN in the silent payment to be the best tradeoff.

@RubenSomsen
Copy link
Author

@Pantamis, thanks for examining the implementation and sharing your thoughts.

great for donors privacy but it makes it hard to use by exchanges or services to receive deposits

In my view, any service that you can interact with shouldn't be using a non-interactive protocol. The right model for such use cases is for the service to simply hand you an xpub.

Also note that @w0xlt just added support for an identifier which allows you to do distinguish the payment purpose (but it still won't let you identify who paid you). See this comment and the discussion here.

I find adding an OP_RETURN in the silent payment to be the best tradeoff

Once we start adding OP_RETURN data, it seems to me like we'd end up losing some of the defining features of the protocol (no extra overhead, indistinguishable from regular payments). If we did make that tradeoff, a safe one-time method of communicating an xpub as opposed to adding an OP_RETURN to each individual payment seems preferable, such as this variant of BIP47.

Something that does seem reasonable is for the sender to optionally identify themselves out of band to the recipient after the payment was made, though keep in mind the potential failure case where a payment is made but you're unable to contact the recipient.

@pool2win
Copy link

I spent some time capturing the proposed protocol as jupyter notebook using this Bitcoin DSL I have been working on. It helped me concretely grok the proposal and hope it will help others too. Dropping the link here in case it is useful: https://opdup.com/bitcoin-dsl/examples/silent_payments.html

Re scans. I wonder if we should think of a solution outside the bitcoin node? There are some merits in running a process with its own separate database of input pubkeys and their spending taproot internal keys. With such a setup, we can optimise the db scans and don't need to complicate the bitcoin node source code. Given we'll be running sequential scans here most of the time, we can tune the db setup for such a work load. We can even evaluate writing stored procedures to run inside the database - further optimising on data copies etc. Another advantage is we can keep evolving this separate indexing process as we need to, without requiring patches to bitcoin source.

The above separate process and database setup is akin to the electrum server setup.

@RubenSomsen
Copy link
Author

@pool2win nice work. I'm sure it'll help people. Be sure to also check out the BIP, it has a lot of new additions.

While I agree that conceptually it'd be nice to have the scanning take place outside of Bitcoin Core, in practice it has been difficult to do this efficiently. The reason for this is that scanning a block for payments requires knowledge of the scripts that relate to the all the inputs. To my knowledge the only performant options are to fetch these scripts from the UTXO set or the rev*.dat files (this contains spent UTXOs on a per block basis, allowing us to roll back the chain). In theory an application can be built that reads the latter data from disk, but that's a significant undertaking.

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