Bitcoin beacon in Python

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:

In [1]:
import requests
import hashlib
from binascii import hexlify, unhexlify
import hmac
from math import ceil
hash_len = 32
In [2]:
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.

In [3]:
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))
For block number 475533, the retrieved hash and the computed hash...
match! Both hashes are 00000000000000000006ce6b1db8286c229a90a5ebf2adee41cc21403fcd7013

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.

In [4]:
# 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))
extractorInput (640 bits):
0000001000000000000000000010000001111000111010000010110111110101100101111110101000100110010010001010110011110100110100001001011110100101010001110111110100101100001111111000111000000111101100001100010101011000001001100000000000000000000000000000000000000000000000000000000000000000000000001110011110011111001011011101111111111011101101010110010111011101000000100011100110110101101000110010010001010001010010010111101111010101111101010110110000011100010010110011011111001111011110111001111111111000011011110111110001101010100110101011000110100111111111111111111011101111111111110111000111001101001000010101100000111111110111110111100000010111
In [5]:
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))
extractorOutput aka beacon output (32 bits):
10100111100100000011101001011100