🌉❄️📝/Writing/A minimal Bitcoin-inspired implementation/🧊Blocks
Get Notion free

Blocks

Bitcoin is commonly thought about as a ledger of transactions. Let's use that as a starting point.

Headers

We group the transactions into 'blocks'. Each block starts with a header, with information on the block's contents:

Hash = bytes

VERSION: int = 0

def init_header(previous_hash: Hash, timestamp: int, nonce: int) -> bytes:
    """ """
    return (
        VERSION.to_bytes(1, byteorder="big")
        + previous_hash
        + timestamp.to_bytes(4, byteorder="big")
        + nonce.to_bytes(32, byteorder="big")
    )
blocks.py

We use 'hash' as both a verb (to hash something) and a noun (the output of a hash function). It's a little confusing. To help clear things up, let's do something concrete - we solve for the nonce and create our first block.

Since there is no previous block, the previous hash is set to zero. We choose an arbitrary timestamp and initialize the nonce to zero. The header now looks like this.

previous_hash = (0).to_bytes(32, byteorder="big")
timestamp = 1634700000
nonce = 0

header = init_header(previous_hash, timestamp, nonce)
print(f"header:\n{header}")
header:
b'\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\x00ao\x8a\xe0\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'

We can make the header more readable by limiting each line to 16 bytes and separating each byte with a dash delimiter.

import binascii

def prettify(b: bytes) -> str:
    """ """
    n = len(b)
    m = (n // 16) + (n % 16 > 0)

    result = [
        binascii.hexlify(b[i * 16 : (i + 1) * 16], sep="-").decode()
        for i in range(m)
    ]
    return "\n".join(result)

print(f"header:\n{prettify(header)}")
header:
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-61-6f-8a-e0-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00

OK let's hash. We use SHA-256 and hash the header twice. Even though only the header is hashed, we call it the block hash as it's used to identify the whole block.

import hashlib

guess = hashlib.sha256(hashlib.sha256(header).digest())
print(f"block_hash:\n{prettify(guess.digest())}")
block_hash:
48-9d-bd-f5-fd-58-0f-dc-0f-c3-72-54-6a-83-46-7e
10-01-71-84-09-f0-0c-96-25-90-72-7f-ba-21-cf-2a

Let's require the first two bytes be zero. Since each byte is represented by two hex digits, we need the first four characters in the no-delimiter string be zero.

print(f"block_hash - no delimiter:\n{guess.hexdigest()}\n")

is_new_block = guess.hexdigest()[:4] == "0000"
print(f"is_new_block: {is_new_block}")
block_hash - no delimiter:
489dbdf5fd580fdc0fc372546a83467e1001718409f00c962590727fba21cf2a

is_new_block: False

Now we keep incrementing the nonce until the first two bytes are zero.

previous_hash = (0).to_bytes(32, byteorder="big")
timestamp = 1634700000
nonce = 0

while True:
    header = init_header(previous_hash, timestamp, nonce)
    guess = hashlib.sha256(hashlib.sha256(header).digest())

    if guess.hexdigest()[:4] == "0000":
        break

    nonce += 1

print(f"nonce: {nonce}\n")
print(f"block_hash:\n{prettify(guess.digest())}\n")
print(f"header:\n{prettify(header)}")
nonce: 70822

block_hash:
00-00-1f-f5-84-95-c3-dc-2a-1a-aa-69-eb-ca-4e-9b
3e-05-e6-3e-5a-31-9f-b7-3b-cd-cc-bc-db-ba-1e-72

header:
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-61-6f-8a-e0-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-01-14-a6

We've 'mined' our first block! Let's highlight each header attribute for clarity.

Let's wrap the while loop in a function, and allow for a maximum number of iterations - this will be useful in later sections.

from typing import Optional, Tuple
import hashlib

def run_proof_of_work(
    previous_hash: Hash,
    timestamp: int,
    nonce: int = 0,
    iterations: Optional[int] = None,
) -> Tuple[bool, int, Optional[Hash], Optional[bytes]]:
    """Find nonce that makes the first 4 bytes of twice-hashed header all
    zeroes. Maximum number of iterations can be specified."""
    iteration_counter = 0

    while True:
        if iterations is not None and iteration_counter == iterations:
            return False, nonce, None, None

        header = init_header(previous_hash, timestamp, nonce)
        guess = hashlib.sha256(hashlib.sha256(header).digest())

        if guess.hexdigest()[:4] == "0000":
            break

        nonce += 1
        iteration_counter += 1

    return True, nonce, guess.digest(), header
blocks.py

Transactions

For the time being, we restrict the transaction to the reward for mining the block. We use port numbers to signify each 'account'. Since there is no sender in a reward transaction, we set the sender to be zero. In other words, (i) the first transaction in each block is the reward for the miner, and (ii) the reward transaction is signified by having sender zero.

def init_transactions(node_port: int):
    """ """
    sender = (0).to_bytes(2, byteorder="big")
    receiver = node_port.to_bytes(2, byteorder="big")

    transaction_counter = 1
    transactions = sender + receiver

    return transaction_counter, transactions
blocks.py

We start with the first reward going to port 7000.

transaction_counter, transactions = init_transactions(7000)

print(f"transaction_counter: {transaction_counter}\n")
print(f"transactions:\n{prettify(transactions)}")
transaction_counter: 1

transactions:
00-00-1b-58

Blocks

The block consists of the header and the transactions - we similarly wrap that in a function. The first byte of the block is reserved for the block size in bytes. The header size is fixed at 69 bytes i.e. 1 + 32 + 4 + 32.

HEADER_SIZE: int = 69

def init_block(
    header: bytes, transaction_counter: int, transactions: bytes
) -> bytes:
    """ """
    block_size = 1 + HEADER_SIZE + 1 + len(transactions)

    return (
        block_size.to_bytes(1, byteorder="big")
        + header
        + transaction_counter.to_bytes(1, byteorder="big")
        + transactions
    )
blocks.py

Let's collect what what we have so far with a function that creates the very first block, commonly referred to as the genesis block.

def init_genesis_block() -> Tuple[Hash, bytes]:
    """ """
    previous_hash = (0).to_bytes(32, byteorder="big")
    timestamp = 1634700000
    nonce = 70822

    header = init_header(previous_hash, timestamp, nonce)
    guess = hashlib.sha256(hashlib.sha256(header).digest())
    assert guess.hexdigest()[:4] == "0000"

    transaction_counter, transactions = init_transactions(7000)
    block = init_block(header, transaction_counter, transactions)

    return guess.digest(), block
blocks.py

Finally we have in bytes (i) the hash of the the genesis block, and (ii) the genesis block.

genesis_hash, genesis_block = init_genesis_block()

print(f"genesis_hash:\n{prettify(genesis_hash)}\n")
print(f"genesis_block:\n{prettify(genesis_block)}")
genesis_hash:
00-00-1f-f5-84-95-c3-dc-2a-1a-aa-69-eb-ca-4e-9b
3e-05-e6-3e-5a-31-9f-b7-3b-cd-cc-bc-db-ba-1e-72

genesis_block:
4b-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-61-6f-8a-e0-00-00-00-00-00-00-00-00-00-00
00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
00-00-00-01-14-a6-01-00-00-1b-58

As before, let's make clear each section with highlights.

We've simplified a lot! Let's look at a number of things that's different compared to the Bitcoin implementation.

In the next section, we'll be mining more blocks and 'chain' them together.