Guide

Verify a Bitcoin block header with Python

Rebuild the genesis header, reproduce its hash and check its encoded target with an offline Python fixture, including selected error paths.

8 min readTransactions
Verify a Bitcoin block header with Python

Verify one header without trusting a block explorer

A Bitcoin block hash is something a short program can reproduce. This guide rebuilds the mainnet genesis block header, hashes it twice with SHA-256, and checks its encoded proof-of-work target. The result is a small, offline exercise for developers who want to understand the bytes behind a block identifier before connecting software to a node.

The reference is Bitcoin Core 31.1's chain parameters, checked on 21 September 2026. That source fixes the genesis timestamp, nonce, difficulty encoding, expected hash and transaction Merkle root. The exercise uses those public constants. It creates no wallet, contacts no peer and handles no money or secrets.

The important limit comes first: reproducing a header hash does not validate a complete block or the current chain. A header commits to transactions through a Merkle tree, but this program does not receive those transactions. It also does not decide whether a miner used the target required by the preceding chain. Keep that boundary visible when adapting the example.

Prerequisites and expected result

Use a local terminal and Python 3 with the standard hashlib and struct modules. No package installation is needed. Save the code below as header_check.py in a disposable folder, then run python3 header_check.py. On systems where Python is called python, use that executable instead. The implementation was executed for this article; its recorded Python version and output are in the education manifest.

The expected block identifier begins with 000000000019d668 and ends with b60a8ce26f. Do not accept only those fragments: the script checks the entire value. Successful execution prints an 80-byte header, the full expected hash, a true target comparison and a false comparison after changing the nonce. It also rejects malformed lengths and invalid target encodings.

An assertion failure means the exercise did not reproduce its expected result. It is useful evidence to investigate, not something to remove so the script can finish. Keep the source and output together if comparing environments. There is no dependency download whose failure can be ignored and no remote response that the program needs to trust.

Understand the serialization before running it

The block-header reference specifies an 80-byte layout. This example writes the version, previous-block hash, Merkle root, timestamp, compact target and nonce in that order. The genesis previous-block reference is zero because there is no earlier block in that chain.

Byte order is the common trap. The numeric fields are serialized little-endian. The Merkle root displayed by tools must be reversed before it is inserted into the header. After hashing, the digest is reversed for the familiar displayed block identifier. Reversing a string character by character would be wrong: the program reverses bytes after decoding hexadecimal.

struct.pack makes the integer widths explicit. Its < prefix selects little-endian encoding. The i code writes the signed version field; each I writes an unsigned field. The length check catches accidental inclusion of transaction bytes, a leading length prefix or an omitted header field. A complete block file is not interchangeable with its header.

Run the complete offline exercise

import hashlib
import struct

EXPECTED = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
MERKLE = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
POW_LIMIT = int("00000000" + "f" * 56, 16)

def target_from_bits(bits):
    size = bits >> 24
    word = bits & 0x007fffff
    if size <= 3:
        word >>= 8 * (3 - size)
        target = word
    else:
        target = word << (8 * (size - 3))
    negative = word != 0 and bool(bits & 0x00800000)
    overflow = word != 0 and (size > 34 or (word > 0xff and size > 33)
                             or (word > 0xffff and size > 32))
    if negative or overflow or target == 0 or target > POW_LIMIT:
        raise ValueError("invalid mainnet proof-of-work target")
    return target

def inspect(header):
    if len(header) != 80:
        raise ValueError("expected exactly 80 header bytes")
    digest = hashlib.sha256(hashlib.sha256(header).digest()).digest()
    bits = struct.unpack_from("<I", header, 72)[0]
    return digest[::-1].hex(), int.from_bytes(digest, "little") <= target_from_bits(bits)

header = (struct.pack("<i", 1) + bytes(32) + bytes.fromhex(MERKLE)[::-1]
          + struct.pack("<III", 1231006505, 0x1d00ffff, 2083236893))
block_hash, satisfies = inspect(header)
assert block_hash == EXPECTED and satisfies
print("header_bytes:", len(header))
print("block_hash:", block_hash)
print("meets_encoded_target:", satisfies)

changed = bytearray(header)
changed[76] ^= 1
changed_hash, changed_satisfies = inspect(bytes(changed))
assert changed_hash != EXPECTED and not changed_satisfies
print("changed_nonce_meets_target:", changed_satisfies)

for bad_header in (header[:-1], header + b"\x00"):
    try:
        inspect(bad_header)
    except ValueError:
        pass
    else:
        raise AssertionError("length control did not reject")
for bad_bits in (0, 0x1d80ffff, 0x23000001, 0x1e00ffff):
    try:
        target_from_bits(bad_bits)
    except ValueError:
        pass
    else:
        raise AssertionError("target control did not reject")
print("negative_controls:", 7)

Run it from the folder containing the file:

python3 header_check.py

The two SHA-256 calls operate on binary data. The inner digest is passed directly into the outer hash; hashing its hexadecimal text instead produces a different result. This distinction also applies when translating the example into another language. A library function that returns a hex string needs decoding before a second hash.

The script does not mine. It checks a known nonce against a known historical header. The changed-nonce case is a selected negative control that was executed, not a claim that every different nonce must fail. Another nonce could in principle produce a hash below the target. That is why the actual comparison is tested rather than inferred from the fact that one field changed.

Decode the target without losing its validity checks

The nBits field stores a compact representation of the target. The top byte determines the scale; the lower portion carries the magnitude and a sign bit. target_from_bits follows Core's compact-number decoding, then rejects negative, zero, overflowing and above-limit targets.

Those rejection paths matter. Python integers grow beyond the fixed width used by Core, so simply shifting an integer would allow values that the consensus implementation rejects. The explicit overflow check prevents a convenient language feature from silently changing the rule being demonstrated. The mainnet proof-of-work limit is also checked separately from whether the hash meets the encoded target.

Core's proof-of-work implementation compares the hash value with a validated target. This program makes that comparison using the digest interpreted as a little-endian integer. It does not convert the displayed hash back through an uncertain parsing convention. Equality is allowed: the condition is less than or equal to the target.

The target is still supplied by the header. A network validator must additionally establish the required target from chain context. Accepting a header's own chosen difficulty without that context would let an attacker ask for an easier test. This exercise deliberately stops before that larger validation problem.

Troubleshoot the result

If the hash differs, first check the Merkle-root reversal, then verify the timestamp and nonce against the pinned source. Do not reverse the entire header. Check that the inner hash uses .digest() rather than .hexdigest(). Print the serialized length before changing any consensus-related logic.

If Python reports an indentation or syntax error, check that only the code block was copied into the file. Markdown fences do not belong in a Python script. If the executable is unavailable, install Python through the normal trusted channel for the operating system; this guide does not require a shell command downloaded from a third-party page.

If a negative control unexpectedly succeeds, keep the output and inspect the exact input. The malformed-length tests should fail before hashing. The target tests should fail in decoding. Distinguishing those paths tells you whether the program rejected the intended condition or merely failed for some unrelated reason.

What this proves, and what remains outside it

The successful fixture shows that these public genesis constants serialize to the expected header, reproduce its block hash and satisfy its encoded target. It also demonstrates selected error paths. It does not test all possible compact encodings, establish a performance bound, validate transactions or reproduce Core's entire consensus engine.

A full node checks much more: previous-block context, transaction rules and the chain it accepts. A block explorer is useful for reading a result, but neither an explorer screenshot nor this script replaces that validation. Use the exercise to understand and test a serialization boundary in disposable development work. Keep production acceptance decisions in maintained consensus software.

Newsletter

Bitcoin, without the noise

What happened in Bitcoin, what it actually changes, and the sources so you can check us. One issue at a time, straight to your inbox.

  • One email per issue, never a drip campaign
  • No tracking pixels and no shared addresses
  • Unsubscribe from any issue in one click

Get the next issue

One email per issue, no tracking pixels, and unsubscribe from any of them. We do not share your address. Privacy policy