Bots on Bitcoin: My adventures hunting for bots in the Bitcoin mempool.

Address reuse is admittedly a dumb thing to do on Bitcoin. We know not to do it — it ties your transactions together which makes it easier to see what your economic activity on chain is.

A subtler reason not to reuse addresses is that – in some very very rare cases – it can lead to leakage of the private key that’s attached to that address.

Well, rare unless you’ve engineered them specifically…

The address reuse that I’m doing here is particularly dumb. I’m reusing a key that – due to some particularly bad decision making on my part – is compromised. This means someone else out there has figured out what the private key for this address is.

As soon as I send money to this address (again) they’re going to steal it. In fact, the reason I’m reusing this address is that I’m setting a ‘trap’, of sorts, for the person who has the private key — I want to see what I can learn about them, what do they know?

I’m losing bitcoin for science.

Creating a bot trap with ECDSA nonce reuse

Bitcoin transactions are often secured with cryptographic public keys. These keys require a signature from the person holding the corresponding private key in order to be spent.

Anyone who knows the private key can spend any bitcoin that is locked to the matching public key.

Here’s how you can leak your private key just by spending two outputs both locked to the same pubkey: by reusing the nonce.

Signatures in ECDSA

You need three things to produce an ECDSA signature:

  • A message that you’re signing
  • A nonce value
  • A private key

The signature algorithm is a math equation; you plug these three things into the equation and on the other side you get s. I’ve looked pretty hard for the proper name of s. My best guess is that it’s called the result. Once you have this result, you pair it with the nonce you used. Together the nonce and result constitute an ECDSA signature.

These two things – the nonce r and the result s – are what you put in a transaction to prove you can spend some bitcoin. Together r and s are the signature.

To recap:r and s = an ECDSA signature.

Here’s the catch though: If you reuse the nonce r and make a second signature with the same private key, you will leak your private key.

Once the private key is leaked, any funds sent to that same public key are unsecured — anyone can spend them.

In fact, there are bots on bitcoin that are waiting and watching to see if anyone reuses a nonce so they can figure out the private key and steal any bitcoin that get locked back to that address.

We can set up a honeypot for bots on Bitcoin with some work.[1] Will anyone be watching?

Launching the trap

To build a nonce-reuse honeypot on Bitcoin we have to create three UTXOs. All of these UTXOs must be locked to the same public key.

Once we have three outputs that are locked to the same pubkey, we need to spend two of those outputs. When you spend an output, you create a signature for it. By spending these outputs, we’ll publish signatures, which provides an observer enough information to figure out our private key.

Once our private key is discoverable, anyone should be able to spend the bitcoin left in the third and final UTXO that we’ve made to the same address.

In order for our private key to leak, we need to make two signatures that contain the same nonce value. To do this, we had to use a modified sign transaction method. Lucky for us, the method in libsecp256k1 that calculates signatures allows you to pass in a custom function to decide the nonce for the signature.

We can do this using the sign function in Coincurve, which is a Python wrapper of the libsecp256k1 library.

Here’s the method that we use to fix the nonce r to the same number for every signature round.

@ffi.callback("int(unsigned char *, unsigned char *, unsigned char *, unsigned char *, void *, unsigned int)")
def nonce_fn(nonce, msg, key, algo, data, counter):
    for i in range(31):
        nonce[i] = 0
    nonce[31] = 5
    return 1

Then we can produce a signature using the transaction hash tx_hash, the private key, and this nonce function.

sig = privkey.sign(bytes(tx_hash), hasher=None, custom_nonce=(nonce_fn, ffi.NULL))

I used this code to produce two signatures for two different transactions. You can see these two transactions on the blockchain, along with their signatures. Transaction 5602c95a619a178684d77ffcd8e6e4a15f7dd6c91d4fd16456d1230cb7c3b0f3 (mempool) and transaction 5e107e699c81b7438d767e8230c257e7f3a604b07e282ad3c91d8abd80513d1e (mempool).

Here’s the relevant signature data from the two signatures.

Transaction 5602c95a619a178684d77ffcd8e6e4a15f7dd6c91d4fd16456d1230cb7c3b0f3
Transaction 5e107e699c81b7438d767e8230c257e7f3a604b07e282ad3c91d8abd80513d1e


Let’s pull out just the r and s values for these signatures. Notice that the r values are exacly the same for both signatures. The s values are different because we signed different messages.

r: 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4
s: 1b5f0ebedc66610f58137e41a41e1e71ac93770834fd5eb32f391206bb20bb5a

r: 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4
s: 074ba35e79dbce3f5d69465c80ac2f82052614677615ab1311fd00bd41bcc0d8

Once these signatures were published, it took someone seven blocks to steal the third UTXO that I had made, which was also locked to the address 1CksAcSBH3KLxXsXTd7esnDNZXqqWBMNoz.

You can see the stealing transaction here. They got 1500 sats.


But can they SegWit?

Once we’d established that a bot was looking for nonce reuse and was capable of stealing funds, we tried to trick it by using the same, compromised pubkey except with a SegWit style P2WPKH locking script.

Can the bot identify a SegWit address, figure out that it is the same pubkey as the non-Segwit address and steal our funds a second time?

The answer is yes. It can steal SegWit funds. In fact, they got stolen even faster while our transaction that locked coins to the vulnerable address was still in the mempool. Less than nine minutes passed between our publication of a stealable UTXO and the UTXO being stolen.

The transaction where our ‘SegWit locked funds’ were stolen is f656efdb018617bfac07e0476686783641bae3b4d17761d72b02a71bc52f5f17. You can see more details on mempool.

An astute observer will be able to confirm that yes; we really did lock the same funds up to the same pubkey, like an idiot.

The compromised pubkey is 03eba60bef7013d8349e75fa3e7b6f80d14eea1e4739f1cddf3b5b6fe954196acc.

Note that we could have tried to steal the funds back before the UTXO got mined, but we weren’t fast enough to send a transaction before the transactions were included in a block. RIP

Protecting your funds

Wow! Bots lurking in the Bitcoin mempool. What a nightmare. Are your funds safe?

The good news is that yes, your funds are safe. There’s an easy way to avoid the chance of nonce reuse — don’t reuse addresses. If you do end up reusing an address, use software that uses the libsecp256k1 library without modifications. This library randomly chooses a nonce value, with a very, very low chance of an accidental collision.

Further Reading

How does someone get a private key from nonce reuse?

  • This blogpost by cryptographer Bill Buchanan is a great demonstration of exactly how to do it (with code examples!).

Is nonce reuse problematic on Schnorr? Great question.

tldr: reusing nonces is very bad no matter what signature algorithm you’re using (Schnorr or ECDSA).

A similar nonce-reuse, private key leaking experiment on Ethereum.

  • Robert Miller’s original post on finding nonce reuse bots on Ethereum, which this post was largely inspired by.

If you thought this post was interesting and want to learn more about Bitcoin signatures and transaction formats, Base58⛓🔓 runs a class on transactions. We just filled our first class, but you can sign up to hear about new class offerings here:

1] Robert Miller did this on Ethereum a few weeks ago; we’ve replicated the same general thing here except on Bitcoin. Bots who take money are a well known problem on the eth network; we wanted to see if there were equally devious devs waiting in the wings on Bitcoin.