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.
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.
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.

Original terminal session locating the block files in the Bitcoin Core data directory. Open full-size original.
Highlighted record framing bytes (2 extracts)

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

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 |
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
00000000000007b9c57dce62d80b9e4d4d2c11af90b000feb5450daa53328378The [::-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.

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 and the coefficient is . Because , the target is
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 .
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 |
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.

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.
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.

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

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

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.