A DNS with Bitcoin Blockchain resolution for full SPV over UDP DNS
Complete unstable, use weird conventions and checksums to route data over CNAME, TXT e AAAA records.
- Python 3.5
- Bitcoind full node
- A domain name
- Get blockhash by height
- Get blockheaders by hash
- Get merkleproof
- Push tx (todo)
- Verify unspent (todo)
Clone and build local environment
$ git clone https://github.com/chaindns/chaindnsd.git
$ cd chaindnsd
$ ./localenv.sh
Edit chaindnsd/chaindns/settings.py with your favorite editor, and change bitcoinrpc credentials to connect to your bitcoind. No SSL support available. If you're going on the internet with rpc, use an SSH tunnel.
Change the following:
BITCOIND_USER = b'rpcuser'
BITCOIND_PASS = b'passw0rd'
BITCOIND_HOSTNAME = b'http://bitcoin_node:8332'
At this point, to test chaindnsd in a local environment is not mandatory to have a real domain name.
$ venv/bin/python chaindnsd.py
And no more! Run chaindnsd, as you have seen in settings.py, chaindnsd will bind to localhost:8053
Try with the genesis block:
There are two ways to fetch the blockhash for a given height,the IPv6 way:
$ dig 0.blockhash.btc.domain.co AAAA @localhost -p 8053
And pay attention to the answer section:
;; ANSWER SECTION:
0.blockhash.btc.domain.co. 31449600 IN AAAA 1000:19:d668:9c08:5ae1:6583:1e93:4ff7
0.blockhash.btc.domain.co. 31449600 IN AAAA 1001:63ae:46a2:a6c1:72b3:f1b6:a8c:e26f
The block hash is stripped of the 8 leading zeros required since the genesis block, and encoded in two ipv6 addresses, hopefully this query will be cached for 31449600 seconds, if the block is old enough. To avoid orphans caching, TTL is set to 0 for blocks with <6 confirmations.
The data over ipv6 encoding works as follow. 14 bytes are encoded on each IP
Read the first ip:
- 10 is the loopback interface prefix
- 00 is the data chunk index
All after the network prefix is data, ipv6 addresses are 0-padded, so, the blockhash is:
00000000 + 00 19d6689c085ae165831e934ff7 + 63ae46a2a6c172b3f1b6 + 0 + a8ce26f
Added leading zeros, padding, data from the first ip, data from the second, padding, more data from the second ip, an you can bet this is the genesis block:
000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
The IPv4 way, instead, is a little more readable, but pay attention to the notes down below:
$ dig 0.blockhash.btc.domain.co AAAA @localhost -p 8053
the inequivocable leading zeros stripped answer:
;; ANSWER SECTION:
111230.blockhash.btc.domain.co. 31449600 IN CNAME 000115459751a80e2c453ed13b5e5aaf16a5ed0edd31dd74281df7c9.block.btc.domain.co.
and the recursion:
$ dig 000115459751a80e2c453ed13b5e5aaf16a5ed0edd31dd74281df7c9.block.btc.domain.co @localhost -p 8053
drives to:
000115459751a80e2c453ed13b5e5aaf16a5ed0edd31dd74281df7c9.block.btc.domain.co. 31449600 IN A 127.0.0.1
This redundance of API is because I don't know which policy is the best to encourage the caching of this information. The AAAA query size is 108bytes, with a single query.
The CNAME query size is 129bytes, plus 52bytes of the A record.
I would say the IPv6 way is the best one, though is a little tricky to implement and less human readable.
Block headers are served as TXT records.
You can also check, with the block header API, at which height of the blockchain this block is, with the int over ipv6 encoding.
Again, remove the 8 leading zeros from the blockhash (63 chars hosts limit).
$ dig 0019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.blockheader.btc.domain.com AAAA @localhost -p 8053
The answer is the header, with 2 checksum bytes appended, base64 encoded.
0019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.blockheader.btc. 31449600 IN TXT
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO6Pt/Xp7ErJ6xyw+Z3aPYX/IG8OIilEyOp+4qkse
Xkopq19J//8AHR2sK3yvQg=="
You can use Python to decode and check the block header:
Python 3.5.2 (default, Jun 6 2017, 19:18:05)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import base64
>>> import binascii
>>> import hashlib
>>> header = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO6Pt/Xp7ErJ6xyw+Z3aPYX/IG8OIilEyOp+4qkseXkopq19J//8AHR2sK3yvQg=="
>>> base64.b64decode(header)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\xa3\xed\xfdz{\x12\xb2z\xc7,>gv\x8fa\x7f\xc8\x1b\xc3\x88\x8aQ2:\x9f\xb8\xaaK\x1e^J)\xab_I\xff\xff\x00\x1d\x1d\xac+|\xafB'
>>> header_bytes = base64.b64decode(header)
checksum:
>>> assert hashlib.sha256(header_bytes[:-2]).digest()[:2] == header_bytes[-2:])
back from block headers to blockhash
>>> binascii.hexlify(hashlib.sha256(hashlib.sha256(header_bytes[:-2]).digest()).digest()[::-1])
b'000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
The meaning of the block header is out of scope of the readme, if you are in doubt you may want to read more about how the bitcoin blockchain works.
In addition with the headers, the blockheight (if available) is returned with AAAA records:
$ dig 0019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.blockheader.btc.domain.co AAAA @localhost -p 8053
In the int over AAAA encoding, 1000 is the data prefix (without indexes), and the remaining part of the IP is the integer.
This is 0, the genesis block:
0019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.blockheader.btc.domain.co.
31449600 IN AAAA 1000::
This is, for example, the record for the block 400002
1000::40:2
apply padding and you have :0040:0002
The merkle proof API make you able to verify independently if a transaction is the blockchain.
Actually this requires a little work to mantain updated a blocks checkpoints list, is not mandatory, but helps, as in any SPV protocol: the more checkpoints you have, the less bandwidth you need.
The merkle proof API gives you the branches of the merkle tree you need to verify the provided transaction, if it's actually in the blockchain.
$ dig ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.merkleproof.btc.domain.co AAAA @localhost -p 8053
The merkle proof response for this transaction is splitted in two chunks, cause the block is pretty full, and we need a lot of data to rebuild the merkle root.
;; ANSWER SECTION:
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.merkleproof.btc.domain.co. 86400 IN TXT "ABQukbDWj5RV9jSQL7MOSFNM0ClWneBuBQG9/f7d9Tn/AHM7TcHq3FwY/R/UHTbN3OYzpQZoCYQFjA3VjiZkbjd8AbPUa2e1eyMvg3JZh/128AZvLzj910tlaT+/64mx2w=="
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.merkleproof.btc.domain.co. 86400 IN TXT "AYH9Afx1EO7TX+nQk3t7LnE1LdYImVdO1JyGyLH3Txgo6Y1dAuWhKcn/alzRjFAiI8PnQFD2roXE16VfHCzQn1p7f4+TAY6dWKH+Qf77istXA4pldbiEPZ09zfY0DfVLqw=="
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.merkleproof.btc.domain.co. 86400 IN TXT "As+d55/cAU64SlSTCVSJeodT4UfE795qnA+HPbXC44VD9XcKsSB7AfAoTCnUqJmNe8NPkHsfFUTv4qhfsotCRFhcWmjM3UbYARLpLxsHcIqK4YdgPwBQc3DWkvYDPZ+nbA=="
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.merkleproof.btc.domain.co. 86400 IN AAAA 1000::1
We can notice there's another chunk (1) thanks to the int over AAAA convention (1000::1).
Using pagination we can request the second proof chunk:
$ dig ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.1.merkleproof.btc.domain.co AAAA @localhost -p 8053
And the response is the second part of data:
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.1.merkleproof.btc.domain.co. 86400 IN TXT "ABWoeMM73Sy1AdU7dyRYsoC84A0K+don8J+pnM/FjOQwxBmbhK6A/mDYARfekuK3D8FXKR8Chao6hqpjDR2sOw2uYP4AnpHXDrwZAcoU9m1sHXuas0TET5zPODfcWGAsHg=="
ff39f5ddfefdbd01056ee09d5629d04c.53480eb32f9034f655948fd6b0912e14.1.merkleproof.btc.domain.co. 86400 IN TXT "AT+ZTXYCGMPzZoh0AZ1V8P5xM0sD7appW3hethNY/dW6/8y4tHFrZlY25snvAQ=="
Let's see how to verify in python the merkle tree, assuming we have a function to rebuild the merkle proof for the merkle module (client w.i.p.!), let's the code explain itself:
import base64
def build_merkle_proof(data, root, proof=None):
proof = proof and [x for x in proof] or []
d = {0: 'SELF', 1: 'R', 2: 'L'}
if data:
proof.append((data[:32], d[data[32]]))
return check_merkle_proof(data[33:], root, proof=proof)
return proof + [(binascii.unhexlify(root)[::-1], 'ROOT')]
a = base64.b64decode('ABQukbDWj5RV9jSQL7MOSFNM0ClWneBuBQG9/f7d9Tn/AHM7TcHq3FwY/R/UHTbN3OYzpQZoCYQFjA3VjiZkbjd8AbPUa2e1eyMvg3JZh/128AZvLzj910tlaT+/64mx2w==')
b = base64.b64decode('AYH9Afx1EO7TX+nQk3t7LnE1LdYImVdO1JyGyLH3Txgo6Y1dAuWhKcn/alzRjFAiI8PnQFD2roXE16VfHCzQn1p7f4+TAY6dWKH+Qf77istXA4pldbiEPZ09zfY0DfVLqw==')
c = base64.b64decode('As+d55/cAU64SlSTCVSJeodT4UfE795qnA+HPbXC44VD9XcKsSB7AfAoTCnUqJmNe8NPkHsfFUTv4qhfsotCRFhcWmjM3UbYARLpLxsHcIqK4YdgPwBQc3DWkvYDPZ+nbA==')
d = base64.b64decode('ABWoeMM73Sy1AdU7dyRYsoC84A0K+don8J+pnM/FjOQwxBmbhK6A/mDYARfekuK3D8FXKR8Chao6hqpjDR2sOw2uYP4AnpHXDrwZAcoU9m1sHXuas0TET5zPODfcWGAsHg==')
e = base64.b64decode('AT+ZTXYCGMPzZoh0AZ1V8P5xM0sD7appW3hethNY/dW6/8y4tHFrZlY25snvAQ==')
assert a[0] == 0
assert b[0] == 1
assert c[0] == 2
assert d[0] == 0
assert e[0] == 1
fromtxt = a[1:] + b[1:] + c[1:] + d[1:] + e[1:]
build_merkle_proof(fromtxt, hexroot)
check_chain(merkle_proof)
print('Proof verified')
Every chunk is splitted in more TXT records. Since some public DNS shuffle the answers, please consider the first byte as data index to rebuild the chunk (up to int 127).
When the whole data is rebuilt, we use the check_chain function from the merkle module to check the proof.
We have verified a bitcoin transaction over a DNS! Next step is to fetch the whole headers to a known checkpoint to ensure the proof is legit.