Implementing a Watch Only wallet on top of bitcoind

Tilted-Transparent-Protect-Bitcoin-Wallet
Submit to StumbleUpon

Hello and welcome back to my blog!

This article talks about the technical implementation details of building a watch only wallet on top of bitcoind - it’s aimed at bitcoin / altcoin business owners/developers rather than individuals.

sig

Watch only

Watch only wallets do not contain the private keys for the addresses they’re watching, which makes them extremely secure. An attacker who gained access to your watch-only wallet would not be able to withdraw any coins. You cannot spend coins from a watch only wallet directly, but you can get the balance of an address, or export a transaction to be signed externally. This makes them fall into the category of cold storage.

The reference bitcoin client, bitcoind currently does not support watch only wallets, which is a great shame. However, it does expose a nice JSON RPC API which we can use to help us.

Sacrifices

Before we get started it is necessary to explore the sacrifices we make going down this route:

Assisted manual block-chain parsing

It will be necessary to look at every transaction, block by block which means we will need to deal with block orphans and general bookkeeping for tracking which block we’re up to.

Custom bookkeeping of transaction outputs

The bitcoind wallet doesn’t track transaction outputs for addresses it doesn’t have the private keys for. This means we will need to track them ourselves, checking and updating them whenever a new block arrives.

No wallet accounts

Again, there are no private keys in this wallet, so the accounts feature of bitcoind isn’t useable.

Parsing the block-chain

The general idea here is that we want to be able to move from one block to the next, examining the transactions contained within. If we have the hash of the block we’d like to start from, we can get going – if we just want to start from the current block, we can do:

getblockhash( getblockcount() )

Using the getblock() RPC function we can then pull down a JSON encoded block from bitcoind, which looks like this:

{
    "hash" : "000000004cabf003eeba1c75e6842a5a098dbb98ebf0c4482936806190cdfd46",
 
    "confirmations" : 1,
    "size" : 5164,
    "height" : 205401,
    "version" : 2,
    "merkleroot" : "ebcc75ac9cff8719568ea20e09d39ec0060fdb5cf013b6ce1cd082f779f0de09",
    "tx" : [
        "5405a7a0c04f79770971a42538dfd70387dd202b3f99f7011182e015e28f5c52",
        "f6977b1ace8248745752eb2e4e6a407d61741d75b6a8354c0f5cd486d4037630",
        "f14d21aa726448d0f42abfdb5c8c5fded875b6373fccf6cbb9afff193adf14b3",
        "94656ee578f0ac91d7ff6bef7953603ab067bc13f636d04ed46d8e9a9fdbf195",
        "f6481983bdb8d1646c0011a4c848f0557992dbcf26844f3d492cf1f9e2e2b710",
        "da286b224f577e1c94c0788e8f57c5a7073c8a763299dcf7a2833e3c19ee1591",
        "c5ee69d9453508b1de3f1bec7a5da8880aea0366d28c8240dfe9c851171a6918",
        "ba0829f62f96dbca3a607b147e4798553bb4475416f31ae11cfb7be95a697321",
        "20d64b1dc51bfe9874343f9dcd8f44d2c6996d53864d67f881f2c68e16e1f426",
        "ecb3bba37f21abfafa4ba8def7e90b0d40caa7cf0779454ae68a88663e2c90f1",
        "afd97003a7f9b611971005686c73d786be31e9c08b20a94c2441512e4c0c79d0",
        "9170ebf9e286832f0a674e209e518fb6b66fab1f39259bf537abd331db68f004",
        "2959b3c74d550ca2b415d75e091a9033606a81fca40ede60463c12243292a011",
        "59f7be590589278dace5403338cc702cebb04323cdfaeaf3f6f88ab111d2e33e",
        "c4adbcef0e71ddbdefc98b65064b7b297c0345f749e382a9d5bd095759a78d9b",
        "9d7403c7cfe88ce4fb2c353955188e85ea408feefd3b39ddbf9dd6d4110bd3d8",
        "09510ae3c3a745772228b2c75cfb500a7b1c22a6c9edfa66d594c62f30a62b3f",
        "d4afff493fc312219832a18ec27f79635efd58f168331d6f7d6b67bd663ce5c4",
        "7fee3aba77da9aaf9f3dc486a15e590f48cfd4751094900dd398743fb791b9ca"
    ],
    "time" : 1395151737,
    "nonce" : 80102656,
    "bits" : "1d00ffff",
    "difficulty" : 1.00000000,
    "previousblockhash" : "00000000d67d50da2cc50da6b977543d7ea52364d75eeb4f7cf2ef4048c30090"
}

This is a block from testnet, hence the low difficulty

This gives us a list of transaction hashes, which is perfect to feed into the RPC call, getrawtransaction(). We can also get a similar list of TXIDs from the mempool, using getrawmempool() which enables us to see 0 confirmation transactions, which is very helpful for instant feedback purposes when displaying a list of deposits a customer has made, for example.

Both getblock() and getrawmempool() return all transactions, even those which the wallet doesn’t have private keys for. This is the key to this process.

So, now we have a way to get all TXIDs we must build a function which takes a list of transaction hashes and processes them, filtering out the ones we are interested in watching.

Processing and filtering transactions

We need to call getrawtransaction() with the list of TXIDs we’re interested in taking a look at. Calling it in batch mode is a good idea, since that will save on RPC overhead. In addition, we’d like the results in JSON, so we set verbose to 1 in the optional parameter.

Here is an example of the result of getrawtransaction()

{
    "hex" : "0100000001a0ddd030fcec3491a66faeb933618a3d3f35bdb564b439baf67e676f0e861344000000006b48304502205c8b1a8ed30cf6f9ed634879b6f98b257fde2764bb1274316524cf5223bf8456022100dd23eb57b5f1cdbeb77921543e23c30962f475162ff330fdc91a44175d871fc801210280d610df7855e1b9645193643c478268d189307372861d0d9c4560d1f63e22bfffffffff02848ed923000000001976a91416ac385ac5dca1580bce6cfc68d08771c23cd64f88ace041d88e030000001976a914febfbe6f1e323ba68e79205555814f7b6f6fda5888ac00000000",
    "txid" : "f6977b1ace8248745752eb2e4e6a407d61741d75b6a8354c0f5cd486d4037630",
 
    "version" : 1,
    "locktime" : 0,
    "vin" : [
        {
            "txid" : "4413860e6f677ef6ba39b464b5bd353f3d8a6133b9ae6fa69134ecfc30d0dda0",
            "vout" : 0,
            "scriptSig" : {
                "asm" : "304502205c8b1a8ed30cf6f9ed634879b6f98b257fde2764bb1274316524cf5223bf8456022100dd23eb57b5f1cdbeb77921543e23c30962f475162ff330fdc91a44175d871fc801 0280d610df7855e1b9645193643c478268d189307372861d0d9c4560d1f63e22bf",
                "hex" : "48304502205c8b1a8ed30cf6f9ed634879b6f98b257fde2764bb1274316524cf5223bf8456022100dd23eb57b5f1cdbeb77921543e23c30962f475162ff330fdc91a44175d871fc801210280d610df7855e1b9645193643c478268d189307372861d0d9c4560d1f63e22bf"
            },
            "sequence" : 4294967295
        }
    ],
    "vout" : [
        {
            "value" : 6.01460356,
            "n" : 0,
            "scriptPubKey" : {
                "asm" : "OP_DUP OP_HASH160 16ac385ac5dca1580bce6cfc68d08771c23cd64f OP_EQUALVERIFY OP_CHECKSIG",
                "hex" : "76a91416ac385ac5dca1580bce6cfc68d08771c23cd64f88ac",
                "reqSigs" : 1,
                "type" : "pubkeyhash",
                "addresses" : [
                    "mhaqULcqPPgehdf9CqWDLNVTULXKksrvPv"
                ]
            }
        },
        {
            "value" : 152.81439200,
            "n" : 1,
            "scriptPubKey" : {
                "asm" : "OP_DUP OP_HASH160 febfbe6f1e323ba68e79205555814f7b6f6fda58 OP_EQUALVERIFY OP_CHECKSIG",
                "hex" : "76a914febfbe6f1e323ba68e79205555814f7b6f6fda5888ac",
                "reqSigs" : 1,
                "type" : "pubkeyhash",
                "addresses" : [
                    "n4jwdNrAeZ5kEQH41CbziAzbeLvJYFoqrH"
                ]
            }
        }
    ],
    "blockhash" : "000000004cabf003eeba1c75e6842a5a098dbb98ebf0c4482936806190cdfd46",
    "confirmations" : 2,
    "time" : 1395151737,
    "blocktime" : 1395151737
}

We’re going to go through this step by step, looking at what the relevant parts of this mean and how it can help us identify deposits and withdrawals from the addresses we are interested in watching.

Deposits

In order to watch for deposits, we will need a list of addresses we’re interested in watching.

Deposits to an address always show up in the vout array inside the JSON result. There can be any number of vout structures, so we must loop over each one.

Each vout contains a value field (the amount of bitcoins), an n field (the index inside the transaction) and a scriptPubKey field, which contains the deposit address for this transaction. Together, the TXID of the transaction and the n field of the vout form a unique unspent output, which can be thought of like a deposit.

Inside the scriptPubKey field there is an array of addresses, which is almost always of length 1. It’s an array because of a special and seldom used transaction type, ‘multisig’ but you don’t need to worry about this since you’re likely to control the addresses to be watched and their types. Filter out outputs of this type by checking the type field field of scriptPubKey. The full list of possible types is:

  • nonstandard
  • pubkey
  • pubkeyhash
  • scripthash
  • multisig

Taken from https://github.com/bitcoin/bitcoin/blob/master/src/script.cpp

Most likely, you’ll only be interested in pubkeyhash (a regular bitcoin address) and scripthash (a multi-signature address).

The pseudocode for the loop over all the vouts inside a transaction looks like this:

foreach (Vout vout in t.VOut)
{
	if (vout.ScriptPubKey.Addresses != null)
	{
		if (vout.ScriptPubKey.Addresses.Count == 1)
		{
			string address = vout.ScriptPubKey.Addresses[0];
 
			bool deposit = addressesToWatch.ContainsKey(address);
 
			if (deposit)
			{
				// found a deposit!
				unspent[ BuildKey(t.TxId, vout.n) ] = vout;
			}
		}
	}
}

addressesToWatch is a set of all the bitcoin addresses you’re interested in watching

Once you have found a deposit, you’ll want to store it indexed by TXID and vout index (the n field inside the vout). You’ll also want to take a look at the number of confirmations on the transaction before you consider this output for getting an address balance.

Withdrawals

In order to process withdrawals, we must look at the vin array inside the transaction and cross reference with our stored set of outputs (TXID and index). The vin structure contains a txid and vout field, so this is actually trivial. Whenever we see a TXID and vout that we have a record for being referenced inside the vin of a transaction, we must mark this as spent which excludes it from balance calculations.

foreach (Vin vin in t.VIn)
{
	if (vin.CoinBase == null)
	{
		unspent.RemoveKey( BuildKey(t.TxId, vin.Vout) );
	}
}

Address balance

It is then a trivial matter to calculate the balance of an address, just a loop over all unspent outputs which have the address you are interested in.

decimal addressBalance = unspent.Where( u=>u.Address == balanceAddress && u.Confirmations == requiredConfirms ).Sum( u=>u.Amount );

balanceAddress is the address you’d like to get the balance of and requiredConfirms is the required number of confirmations

Orphaned blocks

When manually parsing the blockchain in real time, it’s quite common to get stuck on an orphaned block. This can happen when two miners submit a solved block at the same time. One block is discarded and becomes an orphan, but if your parser happens to call getblock() at the ‘wrong’ time, you can end up processing the wrong block.

The symptoms of this happening are:

  • The number of confirmations on your current block are 0
  • The nextblockhash field is null
  • getblockcount() returns a higher number than the height field inside your current block

Once you have detected this case, the way to become unstuck from the orphan is to step backwards until you find a block with non 0 confirmations, and then step forward again.

That’s it

You now have all the tools you need to implement a watch only wallet on top of bitcoind!

I hope this helps some developers. Until next time, have fun!

Cheers, Paul.

Submit to StumbleUpon

About Paul Firth

A games industry veteran of ten years, seven of which spent at Sony Computer Entertainment Europe, he has had key technical roles on triple-A titles like the Bafta Award Winning Little Big Planet (PSP), 24: The Game (PS2), special effects work on Heavenly Sword (PS3), some in-show graphics on the BBC’s version of Robot Wars, the TV show, as well as a few more obscure projects.   Now joint CEO of Wildbunny, he is able to give himself hiccups simply by coughing.   1NobNQ88UoYePFi5QbibuRJP3TtLhh65Jp
This entry was posted in Bitcoin, Finance, JSON, Technical and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

WP-SpamFree by Pole Position Marketing