Reading a Bitcoin block, byte by byte

We move from DCoin to a historical Bitcoin block: separate disk framing from the 80-byte header, decode nBits, recompute the block hash, and parse a complete legacy coinbase transaction. Field diagrams, exact hexadecimal data and a small checked decoder connect the concepts to their serialized representation.

Updated 8 min read

A block explorer turns a Bitcoin block into tidy labels: timestamp, transaction count, hash, reward. Those labels are interpretations of a byte sequence. Reading the sequence ourselves reveals which parts are actual data, which are display conventions and which belong to the software storing the block rather than to the block itself.

We now leave DCoin's simplified model and examine a historical Bitcoin example. The block header is from July 2011; the file extract was inspected in a later Bitcoin Core installation. This matters because a legacy example is easier to read, but should not be mistaken for a complete description of every modern transaction or storage format.

We have the block's file-record prefix, its full header, its transaction count and its complete first transaction. We do not have all 154 transactions reproduced here. We can verify the header hash and parse the coinbase, but cannot reconstruct the entire Merkle tree from this extract alone.

4.1 #Separate the file record from the block

The examined blk00003.dat record begins:

f9 be b4 d9  c2 bf 00 00  01 00 00 00 ...

The first four bytes identify the mainnet record boundary. The next four encode the serialized block's length as a little-endian unsigned integer: c2 bf 00 00 is 0x0000bfc2, or 49,090 bytes. The block begins after those eight framing bytes.

MagicSizeHeaderCountTransactions0488889FileframingSerializedblockO®setsarebytes;boxesarenottoscale
Figure 4.1. Offsets from the beginning of this file record. The header begins at byte 8, the transaction count at byte 88, and the first transaction at byte 89. Field widths are schematic.

These magic and size fields are not part of the 80-byte block header and do not enter its proof-of-work hash. If we obtain a serialized block through bitcoin-cli getblock BLOCK_HASH 0, the result starts at the header, without this disk-record prefix.

There is a modern storage caveat too. Bitcoin Core 28.0 introduced XOR obfuscation of block files by default, using a key stored in the blocks directory. A raw hexdump of a newer installation may therefore not resemble this historical record until the storage encoding is accounted for. This is a local file-storage detail, not a change to Bitcoin's consensus block serialization. See the 28.0 release notes.

Terminal listing of Bitcoin Core data and block files

Original terminal session locating the block files in the Bitcoin Core data directory. Open full-size original.

Highlighted record framing bytes (2 extracts)

Hex dump with mainnet record magic highlighted

Original extract highlighting the four magic bytes. Open full-size original.

Hex dump with block length highlighted

Original extract highlighting the 49,090-byte length. Open full-size original.

4.2 #Read the 80-byte header

Relative to the beginning of the serialized block, the header fields are:

Offset Bytes Field Example
0 4 Version 01 00 00 00
4 32 Previous header hash Starts 47 0c 5f a9
36 32 Merkle root Starts 10 f5 06 59
68 4 Timestamp de eb 23 4e
72 4 Compact target, nBits cf bb 0a 1a
76 4 Nonce e2 5b 6f 9a
Table 4.1. The fixed header layout. Multi-byte numeric fields use little-endian encoding. Hash fields are stored in internal byte order, which is reversed for the conventional displayed identifier.
Version4Previoushash32Merkleroot32Time4Bits4Nonce480bytestotal
Figure 4.2. The two 32-byte hash fields dominate the header. The timestamp, target and nonce occupy the final 12 bytes. Numeric labels give lengths, not offsets.

The timestamp decodes to 1310976990, or 2011-07-18 08:16:30 UTC. It is a timestamp claimed in the header and constrained by consensus rules, not a precise measurement of when every peer received the block. The nonce is 2590989282. Version 1 describes this historical header; it is not a claim that every Bitcoin block today uses version 1.

The complete header is short enough to reproduce and hash:

from datetime import datetime, timezone
from hashlib import sha256
from struct import unpack
 
def hash256(data):
    return sha256(sha256(data).digest()).digest()
 
header = bytes.fromhex("""
01000000
470c5fa9f8d729d0ecc704d0b7d12331c5f415bd47e490a70003000000000000
10f5065927d8ec42cfd80454e8a77685fb44d6a8a0d05aaa62381b0a37786c33
deeb234e cfbb0a1a e25b6f9a
""")
assert len(header) == 80
version, previous, merkle, timestamp, bits, nonce = unpack("<I32s32sIII", header)
print(datetime.fromtimestamp(timestamp, timezone.utc).isoformat())
print("previous:", previous[::-1].hex())
print("merkle:  ", merkle[::-1].hex())
print("block:   ", hash256(header)[::-1].hex())

The displayed block hash is

00000000000007b9c57dce62d80b9e4d4d2c11af90b000feb5450daa53328378

The [::-1] reversal is a display convention for these identifiers. Hashing the printed hexadecimal text, hashing the header once instead of twice, or hashing the eight disk-framing bytes as well will produce a different result.

Hex dump showing all fields of the historical block header

Original header extract. The highlighted fields correspond to the 80-byte structure decoded above. Open full-size original.

4.3 #Turn nBits into a target

The four bytes cf bb 0a 1a decode as the integer 0x1a0abbcf. In the compact target representation, the top byte is an exponent and the lower bits contain a coefficient and a sign flag.

For this positive example, the exponent is e=26e=26 and the coefficient is c=0x0abbcfc=\mathtt{0x0abbcf}. Because e≥3e\geq3, the target is

T=c 256e−3.T=c\,256^{e-3}.

It is a threshold, not an exact hash the miner was asked to find. The resulting header hash, interpreted in Bitcoin's numeric order, must be at most TT.

Extend the previous Python snippet:

exponent = bits >> 24
coefficient = bits & 0x007fffff
assert not bits & 0x00800000  # This sample is not negative.
target = (coefficient << (8 * (exponent - 3)) if exponent >= 3
          else coefficient >> (8 * (3 - exponent)))
assert int.from_bytes(hash256(header), "little") <= target
assert hash256(header)[::-1].hex() == (
    "00000000000007b9c57dce62d80b9e4d4d2c11af90b000feb5450daa53328378"
)
print(hex(target))

A full validator additionally checks that the target is positive, within the network's proof-of-work limit and exactly the target required for this block's position. Passing a hash comparison against a self-declared easy target would not be sufficient. The header reference documents the compact encoding's edge cases.

4.4 #Locate the transactions

At file offset 88, immediately after the header, the byte 9a encodes a transaction count of 154. This is a CompactSize integer. Small values use one byte; larger values begin with a marker followed by a little-endian integer.

First byte Following bytes Decoded range
00–fc 0 0–252
fd 2 253–65,535
fe 4 65,536–4,294,967,295
ff 8 4,294,967,296–18,446,744,073,709,551,615
Table 4.2. Canonical CompactSize encodings. The marker itself is not part of the decoded number.

This convention also appears in transaction input counts, output counts and script lengths. A reader must distinguish these variable-length integers from fixed four-byte fields. Reading every count as a 32-bit number would shift the cursor and corrupt everything after it.

For the first transaction, 01 means one input. Later, 08 means an eight-byte input script and 43 means a 67-byte output script. These bytes have the same CompactSize role despite occurring in different structures.

Hex dump highlighting the transaction count 9a

Original transaction-count extract: 9a encodes 154 transactions. Open full-size original.

4.5 #Decode the coinbase transaction

The first transaction begins at file offset 89 and occupies 135 bytes. Its single input does not spend an earlier output. A 32-byte zero hash followed by the index ffffffff is the special coinbase outpoint. This is how the block claims its permitted reward.

VersionIncountInputOutcountOutputLocktime4+1+49+1+76+4=135bytesInput:nulloutpoint(36)+length(1)+script(8)+sequence(4)Output:value(8)+length(1)+lockingscript(67)
Figure 4.3. The 135-byte legacy transaction, with its input and output expanded below. Length-prefix bytes are included in the input and output totals.

The output value bytes are 60 a1 5f 2a 01 00 00 00. Decoded as a little-endian integer, they give 5,005,877,600 satoshis, or 50.05877600 BTC. The subsidy in this period was 50 BTC, so the amount above it is 0.05877600 BTC. If the miner claimed all available reward, that is the included fees; from the coinbase amount alone we cannot rule out additional unclaimed fees.

The output script begins with 41, a command to push the next 65 bytes, then an uncompressed public key beginning with 04, and ends with ac, OP_CHECKSIG. This is a pay-to-public-key output. The script states a future spending condition; it is not a signature authorizing an ordinary input in the coinbase itself.

Hex dump of the complete first coinbase transaction

Original extract of the first transaction, from its version field through locktime. Open full-size original.

Hex dump highlighting the coinbase input

Original coinbase-input extract: the zero hash and ffffffff index identify the special outpoint. Open full-size original.

Hex dump highlighting the coinbase output value

Original output extract, including the amount of 5,005,877,600 satoshis. Open full-size original.

Here is a complete decoder for this transaction. It checks truncation, canonical length encodings and the final cursor position, so a mistaken field boundary cannot silently produce a plausible-looking answer.

from io import BytesIO
from decimal import Decimal
 
raw = bytes.fromhex("""
01000000 01
0000000000000000000000000000000000000000000000000000000000000000
ffffffff 08 04cfbb0a1a02c002 ffffffff
01 60a15f2a01000000 43
4104c3bb8e003d617f17b9ea0f2d0d626af663342211bd854f61c9818b87ecd6c61c2e4cda448d91147c9d24d10dc7a72e7e7ab0beba4d44d3fa9509807cb99aa32cac
00000000
""")
stream = BytesIO(raw)
 
def take(size):
    result = stream.read(size)
    if len(result) != size:
        raise ValueError("Truncated transaction")
    return result
 
def uint(size):
    return int.from_bytes(take(size), "little")
 
def compact_size():
    first = uint(1)
    if first < 0xfd:
        return first
    size, minimum = {0xfd: (2, 253), 0xfe: (4, 65536),
                     0xff: (8, 4294967296)}[first]
    value = uint(size)
    if value < minimum:
        raise ValueError("Non-canonical CompactSize")
    return value
 
assert len(raw) == 135
assert uint(4) == 1                  # Version
assert compact_size() == 1          # Input count
assert take(32) == bytes(32)         # Null previous transaction
assert uint(4) == 0xffffffff         # Coinbase outpoint index
assert take(compact_size()) == bytes.fromhex("04cfbb0a1a02c002")
assert uint(4) == 0xffffffff         # Sequence
assert compact_size() == 1          # Output count
value = uint(8)
script = take(compact_size())
assert value == 5_005_877_600
assert len(script) == 67 and script[:2] == b"\x41\x04"
assert script[-1] == 0xac            # OP_CHECKSIG
assert uint(4) == 0                  # Locktime
assert stream.tell() == len(raw)
print(f"{Decimal(value) / Decimal(100_000_000):.8f} BTC")

This is a checked reader for one known legacy transaction, not a general transaction parser. An untrusted general parser would also need resource limits and support for the other permitted encodings and structures. Assertions here document the sample; production validation should use explicit error handling rather than assertions that Python can disable.

4.6 #Keep the historical boundary visible

Three details prevent this example from becoming a misleading template.

First, the eight-byte coinbase input script is historical mining data. It predates the rule requiring a block height at the start of the coinbase script. BIP 34 introduced that requirement. We should not decode the old script's first bytes as a modern height or dismiss every coinbase script as meaningless padding.

Second, Segregated Witness adds transaction serialization and commitment rules that this legacy transaction does not exercise. BIP 141 describes the witness format, block weight and the coinbase's witness commitment when required. A modern reader must recognize the marker and flag rather than blindly treat every byte after the version as an ordinary legacy input count.

Third, parsing is not full validation. We have not checked all transactions, recomputed their Merkle root, reconstructed the UTXO set or established the accumulated work of the surrounding chain. What we have done is narrower and reproducible: distinguish framing from consensus bytes, decode an actual header, check its hash against its encoded target, and account for a complete coinbase transaction.

In the final part we will find the same structures inside network messages, where another layer of framing sits around them—and where a packet boundary is not necessarily a message boundary.