This notebook implements the Bitcoin beacon described in the paper On Bitcoin as a public randomness source by Joseph Bonneau, Jeremy Clark, and Steven Goldfeder. A copy of the paper can be found here https://eprint.iacr.org/2015/1015. This beacon uses the >68 (at the time of writing) bits of min-entropy in the block header to generate 32 near-uniform bits. This notebook can be accessed directly here.
Block header manipulation code from https://en.bitcoin.it/wiki/Block_hashing_algorithm
HDKF extractor (an HMAC-based function) code from https://en.wikipedia.org/wiki/HKDF
We begin by importing some useful libraries and defining some functions:
import requests
import hashlib
from binascii import hexlify, unhexlify
import hmac
from math import ceil
hash_len = 32
def getCurrentBlockCount():
"""Return the currrent block count from blockexplorer.com."""
url = 'https://blockexplorer.com/api/status?q=getBlockCount'
r = requests.get(url)
data = r.json()
blockCount = data['blockcount']
return blockCount
def lookupBlockHash(blockCount):
"""Return the block hash, given the block index."""
url = 'https://blockexplorer.com/api/block-index/' + str(blockCount)
r = requests.get(url)
data = r.json()
blockHash = data['blockHash']
return blockHash
def extractBlockHeaderHex(blockCount):
"""Extract the block header in hex, given the block index."""
blockHash = lookupBlockHash(blockCount)
url = 'https://blockexplorer.com/api/rawblock/' + str(blockHash)
r = requests.get(url)
data = r.json()
rawblock = data['rawblock']
headerHex = rawblock[0:160]
return headerHex
def hmac_sha256(key, data):
"""Generate pseudo-random-keyed (PRK-keyed) hash-block"""
return hmac.new(key, data, hashlib.sha256).digest()
def hkdf(length, ikm, salt=b"", info=b""):
"""Generate cryptographically strong output key material (OKM) of any desired length.
Repeatedly generate pseudo-random-keyed (PRK-keyed) hash-blocks, append them into
the output key material, and finally truncate to the desired length.
"""
prk = hmac_sha256(salt, ikm)
t = b""
okm = b""
for i in range(ceil(length / hash_len)):
t = hmac_sha256(prk, t + info + bytes([1+i]))
okm += t
return okm[:length]
Let's first manually compute the latest block hash using extractBlockHeaderHex() to confirm that it is working correctly.
blockCount = getCurrentBlockCount()
lookupHash = lookupBlockHash(blockCount)
# compute the block hash
headerHex = extractBlockHeaderHex(blockCount)
headerUnhex = unhexlify(headerHex) # convert to binary
headerHash = hashlib.sha256(hashlib.sha256(headerUnhex).digest()).digest() # hash twice using SHA256
computedHash = str(hexlify(headerHash[::-1]), 'utf-8') # flip to big-endian
# compare hashes
print("For block number {}, the retrieved hash and the computed hash...".format(blockCount))
if lookupHash == computedHash:
print("match! Both hashes are {}".format(lookupHash))
else:
print("don't match! lookupHash is {} and computedHash is {}".format(lookupHash, computedHash))
Next we create the extractor input and pass it to the HKDF extractor to get our 32 near-uniform bits. The extractor input is just (block header) OR (hashed block header, aka the block hash). We add the hashed block header to prevent malicious miners from exclusively trying hash solutions that produce a certain beacon output. Since the hash of the header is unpredictable, malicious miners must mine normally: finding valid hash solutions, computing the beacon output, then deciding whether to withold the block.
# convert inputs to binary
# (pre-pend and strip a '1' to preserve leading zeros)
header_bin = bin(int('1'+headerHex, 16))[3:] # 640 bits
blockHash_bin = bin(int('1'+lookupHash, 16))[3:] # 256 bits
# build input and feed to hkdf()
extractorInput = int(header_bin,2) | int(blockHash_bin,2)
extractorInput = bin(extractorInput)[2:].zfill(640)
print("extractorInput ({} bits):\n{}".format(len(extractorInput), extractorInput))
extractorInputBytes = extractorInput.encode('utf-8') # convert to bytes
extractorOutputBytes = hkdf(4, extractorInputBytes)
extractorOutput = bin(int.from_bytes(extractorOutputBytes, 'big'))[2:].zfill(32)
print("extractorOutput aka beacon output ({} bits):\n{}".format(len(extractorOutput), extractorOutput))