Tuesday, October 14, 2014

Zero-confirmation bitcoin transactions

Bitcoin transactions and confirmations


Bitcoin transactions are incorporated into the bitcoin blockchain, that acts as a shared ledger. Once the transaction has been part of the blockchain for a long enough period of time, it is accepted as truth by all the nodes in the network and becomes an unalterable part of the bitcoin permanent record. This makes it irreversible and resistant to both network events and deliberate tampering in the form of double-spending attacks.

A block that is added to the blockchain that contains the transaction is called a confirmation. So a newly broadcast transaction is said to have zero confirmations. When it's included in a block, it has one confirmation. Another block is mined on top of the block containing the transaction, we have two confirmations. Generally, a transaction is not considered definitive unless it has six confirmations. On average this takes about an hour.

In this post we will develop a crude system for handling zero-confirmation transactions without incurring into any appreciable risk.



BTC-mirror, a zero-confirmation application


Let's assume, for the sake of argument, that you really hate bitcoins, and that you want to get rid of them as soon as possible. Unimaginable, I know, but bear with me. So, what we want to do is to send those bitcoins back to the sender as quickly as humanly possible.

We cannot immediately create a new bitcoin transaction to send an equal amount of coins back to the sending address. If the original transaction gets double-spent or whatever, the new transaction will still be incorporated into the blockchain and we will lose money. Of course, we could just wait for six confirmations, but that would mean holding onto those coins for over an hour.

Instead we will craft a bitcoin transaction by hand that includes the original transaction as one of the inputs. If the original transaction ends up being rejected, our new transaction will be invalid as well, and will not be incorporated into the blockchain. Because of this mechanism we can broadcast our transaction immediately into the network, before the original transaction is even incorporated in a block. Zero confirmations.


First things first


In order to interact with the bitcoin network we are going to run a full node. Yes, I know, the blockchain is really large and it takes a long time to sync. Suck it up. We also need a full transaction index. Set "txindex=1" in your bitcoin.conf before starting your node as re-indexing everything is a huge pain.

If you feel uncomfortable sending around real money, or if you want to cut down on the server load in terms of network traffic and disk space, you can also set your node up on the test network (testnet=1). It's up to you.
For this project I used bitcoind version 0.9.3.0, your mileage may vary using different versions.

Set up an RPC endpoint, username and password (rpcport, rpcuser, rpcpassword options). Pick a proper password.

We will also be using python 2.7 and python-bitcoinrpc. Go install them now.


Bitcoin shield activated


Now we're ready to implement our application. Using the walletnotify option in bitcoin.conf we can run a script every time a transaction is received that affects our wallet:

walletnotify=/home/drx/btcmirror.py %s

The script will run with a single parameter, the transaction identifier (txid) of the transaction that affects our wallet. It may fire multiple time for a single transaction, so you should take care to process every txid just once. In our case, we don't really have to since the transaction input trick will protect us, as we would effectively be double-spending the original transaction. In other applications this can be a disastrous oversight that will cost you a lot of money.

After all the required preliminary python incantations (imports, main function definitions, etc), it's time to get to work. First we use bitcoinrpc to obtain a proxy to our bitcoin node's RPC service. Then we ask for the transaction details of the transaction we're being called to process:

access = AuthServiceProxy("http://:@127.0.0.1:")
txid = sys.argv[1]
txinfo = access.gettransaction(txid)

We can now use the transaction information txinfo to find out which addresses in our wallets were affected and how. We are only going to mirror transactions that affect our wallet only as a receiver. This prevents us from mirroring send transactions and change consolidations that would create infinite loops:

myaddresses = set()
for details in txinfo["details"]:
    if details["category"] != "receive":
        return
    myaddresses.add(details["address"])

Now we need to find the actual transaction outputs that credited these addresses, and figure out the total amount we need to send back. For that we need to dive into the transaction itself:

tx = access.decoderawtransaction(txinfo["hex"])
newtx_inputs = []
total_amount = Decimal(0)
for vout in tx["vout"]:
    for address in vout["scriptPubKey"]["addresses"]:
        if address in myaddresses:
            newtx_inputs.append({"txid":txid,"vout":vout["n"]})
            total_amount += vout["value"]

We also need to find the sender's address, so we know where to send the money. This is done by looking at the inputs of the original transaction. The transaction can have multiple inputs that are all under the control of the sending party. We just send everything back to one of the input addresses at random. This is not just laziness on our part, it also results in a smaller transaction, which in turn results in a smaller blockchain on every single node.

The sender address does not appear in this transaction, it appears in the transactions that created the inputs. Therefore we need to look back in the ledger. This is the part that needs the transaction index to be enabled on your node.

addresses = []
for vin in tx["vin"]:
    # For every input, find the txid and output number used
    intxid = vin["txid"]
    invout = vin["vout"]
    # Get the outputs of the input transaction
    intx = access.getrawtransaction(intxid, verbose=1)
    vout = intx["vout"][invout]
    addresses.extend(vout["scriptPubKey"]["addresses"])
to_address = random.choice(addresses)


Now we are finally ready to put everything back together into a new bitcoin transaction, sign it and send it to the network:

newtx_hex = access.createrawtransaction(newtx_inputs,{to_address:float(total_amount)})
newtx_hex = access.signrawtransaction(newtx_hex)["hex"]
access.sendrawtransaction(newtx_hex)

Congratulations, you are now immune to bitcoin.


1 comment: