Anatomy of a Block: The Role of the Header, Body, Nonce, and Timestamp
Learning Objectives
- ✓블록 헤더의 주요 필드(prev_hash, timestamp, nonce, difficulty, merkle_root)의 역할을 각각 설명할 수 있다
- ✓이전 블록 해시가 체인의 불변성을 보장하는 메커니즘을 논리적으로 설명할 수 있다
- ✓Python으로 제네시스 블록을 생성하고 블록 해시를 계산할 수 있다
Anatomy of a Block: The Role of Header, Body, Nonce, and Timestamp
From Transactions to Blocks — Why Bundle Them?
Thousands of transactions pour through the Bitcoin network every second. The "digital checks" we completed in Lesson 3 — structs composed of sender, recipient, amount, signature, and txid, protected by the dual shield of hashing and signatures. What would happen if we processed each one individually?
# ❌ Processing transactions one by one?
for tx in all_transactions: # thousands of them
validate(tx)
broadcast_to_all_nodes(tx) # propagate across the entire network
reach_consensus(tx) # reach consensus
# ⏱️ per-transaction consensus = network explosion
I felt this problem firsthand while building DApps in the early days of Ethereum. When every node in the world reaches consensus on each individual transaction, network traffic explodes exponentially. Satoshi Nakamoto's solution was surprisingly simple: bundle transactions into a single chunk (a block) and reach consensus at the block level.
# ✅ Bundle and process in blocks
block = bundle(pending_transactions[:2000]) # bundle up to ~2000 transactions
validate_block(block)
broadcast_block(block) # broadcast only once
reach_consensus(block) # consensus only once
# ⏱️ thousands of times more efficient
This is exactly why blocks exist. Think of a notary office. Instead of stamping each contract individually, they organize multiple contracts into a bundle and seal the whole thing at once. A block is precisely "one page of a notarized ledger of transactions."
🤔 Think about it: Bitcoin generates one block approximately every 10 minutes. If each block holds a maximum of ~2,000 transactions, what is the approximate throughput (TPS)?
View answer
2,000 ÷ 600 seconds ≈ about 3.3 TPS. This is the core reason Bitcoin is criticized as "slow." Visa processes approximately 1,700 TPS. You can now see why Ethereum Layer 2 and the Lightning Network are necessary. However, Bitcoin's slow speed is a trade-off with security. The long 10-minute interval guarantees all nodes worldwide enough time to participate in consensus.
Block Anatomy: Header and Body
Let's open up a block. The structure is surprisingly simple: Header and Body — just two parts.
| Section | Role | Analogy |
|---|---|---|
| Block Header | Block metadata — defines "what" this block is | Shipping label on a package |
| Block Body | Actual transaction data | The items inside the package |
The 6 Fields of a Block Header
The header defines a block's identity. An actual Bitcoin block header is exactly 80 bytes — smaller than a single tweet. Let's examine each key field.
1. index (Block Number)
Indicates the position of the block in the chain. The genesis block is #0, the next is #1. Simple, but it's the label that lets humans read the block order.
2. timestamp
The time the block was created. Uses Unix timestamps (seconds since January 1, 1970). Why does it matter? It provides the basis for difficulty adjustment. Bitcoin compares timestamps every 2,016 blocks and adjusts difficulty to maintain the "1 block per 10 minutes" pace.
3. prev_hash (Previous Block Hash)
The hash value of the immediately preceding block. This is the core of what creates the "chain." Do you remember the avalanche effect of SHA-256 from Lesson 1? If even 1 bit of a previous block's data changes, the prev_hash changes completely. Then the current block's hash changes, and the next block's, and the one after that... dominoes fall all the way to the end.
4. nonce
Short for "Number used ONCE." It's the only value a miner can freely change. We'll cover this in depth in Lesson 5 when we implement Proof of Work, but for now think of it as "the number of attempts in a guessing game."
5. difficulty
The condition the block hash must satisfy. It can be simplified as "how many leading zeros must the hash have." If difficulty is 4, the hash must start with 0000....
6. merkle_root
A value that compresses all transactions in the block into a single hash. We'll dive deep into Merkle trees in Lesson 8, but for now think of it as "the fingerprint of all transactions." If even one transaction changes, the Merkle root changes completely — the avalanche effect from Lesson 1 is at work here too.
🤔 Think about it: Among the 6 block header fields, which can a miner freely change, and why must it be that one?
View answer
nonce. All the other fields are determined by the block's content or the chain's state. prev_hash is determined by the previous block, timestamp by the current time, merkle_root by the transaction list, difficulty by network consensus rules, and index by the chain length. Miners increment the nonce from 0, calculating the hash each time, and when they find a hash that satisfies the difficulty condition, they "successfully mine" a block. This is the core principle of Proof of Work.
prev_hash: The Magic Link That Creates the Chain
If you ask me what the most brilliant design in blockchain is, I'd say prev_hash without hesitation. This single field binds individual blocks from a loose sequence into a "blockchain."
Let's verify this directly in code.
import hashlib
import json
# Simple block hash simulation
def simple_hash(data):
"""Hash data with SHA-256"""
return hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
# Let's create 3 blocks
block_0 = {"index": 0, "data": "Genesis", "prev_hash": "0" * 64}
block_0["hash"] = simple_hash(block_0)
block_1 = {"index": 1, "data": "Alice→Bob 5BTC", "prev_hash": block_0["hash"]}
block_1["hash"] = simple_hash(block_1)
block_2 = {"index": 2, "data": "Bob→Charlie 3BTC", "prev_hash": block_1["hash"]}
block_2["hash"] = simple_hash(block_2)
# Print the chain
for b in [block_0, block_1, block_2]:
print(f"Block #{b['index']} | hash: {b['hash'][:16]}... | prev: {b['prev_hash'][:16]}...")
# Output:
Block #0 | hash: 7cb0f5e5a3a18c57... | prev: 0000000000000000...
Block #1 | hash: 3da96e3a039be7ec... | prev: 7cb0f5e5a3a18c57...
Block #2 | hash: 9a6fd52d2c6d5e35... | prev: 3da96e3a039be7ec...
Block #1's prev_hash exactly matches Block #0's hash. That's the "chain."
Tampering Attempt: Why prev_hash Is a Barrier
Here's where the really interesting question emerges. What happens if someone tampers with Block #1's data?
import hashlib
import json
def simple_hash(data):
return hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
# Build original chain
block_0 = {"index": 0, "data": "Genesis", "prev_hash": "0" * 64}
block_0["hash"] = simple_hash(block_0)
block_1 = {"index": 1, "data": "Alice→Bob 5BTC", "prev_hash": block_0["hash"]}
block_1["hash"] = simple_hash(block_1)
block_2 = {"index": 2, "data": "Bob→Charlie 3BTC", "prev_hash": block_1["hash"]}
block_2["hash"] = simple_hash(block_2)
# ⚠️ Attacker tampers with Block #1's data!
print("=== Before tampering ===")
print(f"Block #1 hash: {block_1['hash'][:20]}...")
print(f"Block #2 prev: {block_2['prev_hash'][:20]}...")
print(f"Match: {block_1['hash'] == block_2['prev_hash']}")
# Attack: tamper 5BTC → 500BTC
block_1["data"] = "Alice→Bob 500BTC"
block_1["hash"] = simple_hash(block_1) # recalculate hash
print("\n=== After tampering ===")
print(f"Block #1 hash: {block_1['hash'][:20]}...")
print(f"Block #2 prev: {block_2['prev_hash'][:20]}...")
print(f"Match: {block_1['hash'] == block_2['prev_hash']}")
# Output:
=== Before tampering ===
Block #1 hash: 3da96e3a039be7ec4a6a...
Block #2 prev: 3da96e3a039be7ec4a6a...
Match: True
=== After tampering ===
Block #1 hash: f827a193cc78d9b102b1...
Block #2 prev: 3da96e3a039be7ec4a6a...
Match: False
Match is False! The moment Block #1 is tampered with, its hash changes completely and diverges from Block #2's prev_hash. Anyone validating this chain will immediately detect that "Block #1 has been tampered with."
What if the attacker also modifies Block #2's prev_hash? Then Block #2's hash changes, it mismatches with Block #3... ultimately, everything from the tampered point to the last block must be recalculated. Add Proof of Work (Lesson 5) on top of that, and this recalculation requires astronomical computing power. This is exactly why blockchain is said to be "practically tamper-proof."
🔍 Deep dive: Bitcoin's actual 80-byte block header
An actual Bitcoin block header is composed of exactly 80 bytes:
| Field | Size | Description |
|---|---|---|
| version | 4 bytes | Block version |
| prev_block_hash | 32 bytes | SHA-256d of the previous block header |
| merkle_root | 32 bytes | Root of the transaction Merkle tree |
| timestamp | 4 bytes | Unix timestamp |
| bits | 4 bytes | Compact difficulty target |
| nonce | 4 bytes | Miner's nonce |
SHA-256d means applying SHA-256 twice: SHA-256(SHA-256(header)). Our project applies it once, but real Bitcoin uses double hashing for enhanced security.
The Genesis Block: Where Everything Begins
Every blockchain has a Genesis Block — block #0. Since no previous block exists, there is no prev_hash. By convention, it is filled with "0" * 64 (64 zeros).
Bitcoin's actual genesis block contains a famous message:
"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
A headline from the British Times dated January 3, 2009 — reporting on an imminent bank bailout. With this single line, Satoshi stamped the reason Bitcoin was born. Every time I deploy a smart contract, I think of this message. It's the origin of what we're building.
import hashlib
import json
import time
def create_genesis_block():
"""Create the genesis block — the starting point of the chain"""
genesis = {
"index": 0,
"timestamp": 1231006505, # Same timestamp as Bitcoin's genesis block
"transactions": [], # Genesis block typically has no transactions
"prev_hash": "0" * 64, # No previous block
"nonce": 0,
"difficulty": 1
}
# Calculate block hash
block_string = json.dumps(genesis, sort_keys=True)
genesis["hash"] = hashlib.sha256(block_string.encode()).hexdigest()
return genesis
genesis = create_genesis_block()
print("=== Genesis Block ===")
for key, value in genesis.items():
if key == "hash":
print(f" {key}: {value[:32]}...")
elif key == "prev_hash":
print(f" {key}: {'0' * 32}...")
else:
print(f" {key}: {value}")
# Output:
=== Genesis Block ===
index: 0
timestamp: 1231006505
transactions: []
prev_hash: 00000000000000000000000000000000...
nonce: 0
difficulty: 1
hash: 7dac2aa6131669792251e00d4c1a6e30...
🤔 Think about it: Why does the genesis block typically have no transactions? Bitcoin's genesis block actually contains a 50 BTC coinbase transaction — can that 50 BTC be spent?
View answer
An interesting fact — the 50 BTC included in Bitcoin's genesis block can never be spent. Due to an intentional design choice (or bug) in the code, the coinbase transaction of the genesis block is never registered in the UTXO database. Whether Satoshi intended this, no one knows. In any case, those 50 BTC (worth billions in today's value) are locked away forever.
Implementing the Block Class: Building It in Real Code
Enough theory. It's time to build the Block class for our project. If Lesson 2's Wallet signs with a private key, and Lesson 3's Transaction creates a signed record of a transfer, then Block is the container that bundles and seals those transactions.
import hashlib
import json
import time
class Block:
"""The basic unit of a blockchain — a container that bundles and seals transactions"""
def __init__(self, index, transactions, prev_hash, difficulty=1, nonce=0):
# --- Header fields ---
self.index = index # Block number
self.timestamp = time.time() # Creation time (Unix timestamp)
self.prev_hash = prev_hash # Previous block hash
self.nonce = nonce # Mining nonce
self.difficulty = difficulty # Difficulty
# --- Body ---
self.transactions = transactions # Transaction list
# --- Computed field ---
self.hash = self.calculate_hash() # This block's hash
def calculate_hash(self):
"""Hash block header data with SHA-256"""
block_header = {
"index": self.index,
"timestamp": self.timestamp,
"prev_hash": self.prev_hash,
"nonce": self.nonce,
"difficulty": self.difficulty,
"transactions": self.transactions
}
block_string = json.dumps(block_header, sort_keys=True)
return hashlib.sha256(block_string.encode()).hexdigest()
# Test
block = Block(
index=1,
transactions=[{"sender": "Alice", "recipient": "Bob", "amount": 5}],
prev_hash="0" * 64
)
print(f"Block #{block.index}")
print(f"Timestamp: {block.timestamp}")
print(f"Hash: {block.hash[:32]}...")
print(f"Nonce: {block.nonce}")
# Output:
Block #1
Timestamp: 1711540800.123456
Hash: a8b3f9c1d2e4567890abcdef12345678...
Nonce: 0
Why calculate_hash() Is the Core
calculate_hash() serializes all block header data into a single string and runs SHA-256 on it. All 5 properties of hash functions from Lesson 1 are at work here:
- Deterministic: Same block data → always the same hash
- Fast computation: Verification is instant
- Avalanche effect: Changing nonce by just 1 completely changes the hash
- Preimage resistance: Cannot reverse-engineer original data from the hash
- Collision resistance: Two different blocks having the same hash is practically impossible
❌ → 🤔 → ✅ How calculate_hash() Implementation Evolves
calculate_hash() looks simple, but it's also the method where beginners most often introduce bugs. Let's compare the three stages of mistakes that commonly appear in code.
❌ WRONG WAY: Timestamp changes on every call, key ordering not guaranteed
class BadBlock:
def __init__(self, index, transactions, prev_hash):
self.index = index
self.transactions = transactions
self.prev_hash = prev_hash
self.hash = self.calculate_hash()
def calculate_hash(self):
block_data = {
"index": self.index,
"timestamp": time.time(), # 🚨 Current time on every call!
"prev_hash": self.prev_hash,
"transactions": self.transactions
}
# 🚨 No sort_keys — relies on Python dict ordering
block_string = json.dumps(block_data)
return hashlib.sha256(block_string.encode()).hexdigest()
bad = BadBlock(1, [{"sender": "Alice", "recipient": "Bob", "amount": 5}], "0" * 64)
print(bad.hash == bad.calculate_hash())
# False — the hash changes every time you verify! Chain validation becomes impossible
The problem: Calling time.time() inside calculate_hash() produces a different hash every time, even for the same block. Without sort_keys, JSON serialization order can vary across environments, causing hash mismatches between nodes.
🤔 BETTER: Timestamp is fixed, but transaction serialization is unstable
class OkayBlock:
def __init__(self, index, transactions, prev_hash):
self.index = index
self.transactions = transactions
self.prev_hash = prev_hash
self.timestamp = time.time() # ✅ Stored once in __init__
self.hash = self.calculate_hash()
def calculate_hash(self):
block_data = {
"index": self.index,
"timestamp": self.timestamp, # ✅ References stored value
"prev_hash": self.prev_hash,
"transactions": self.transactions # 🚨 What if transactions are objects?
}
block_string = json.dumps(block_data, sort_keys=True)
return hashlib.sha256(block_string.encode()).hexdigest()
# Dict transactions are fine, but...
ok_block = OkayBlock(1, [{"sender": "Alice", "recipient": "Bob", "amount": 5}], "0" * 64)
print(ok_block.hash == ok_block.calculate_hash()) # True ✅
# What if you pass a Transaction object?
# ok_block2 = OkayBlock(1, [Transaction(...)], "0" * 64)
# 💥 TypeError: Object of type Transaction is not JSON serializable
The problem: Works fine with dictionary transactions, but when you pass a Transaction object from Lesson 3, json.dumps() fails to serialize it. This is a problem you will inevitably encounter when integrating classes in a real project.
✅ BEST: Deterministic + object-safe + defensive design
class Block:
def __init__(self, index, transactions, prev_hash, difficulty=1, nonce=0, timestamp=None):
self.index = index
self.timestamp = timestamp or time.time() # ✅ Can be injected externally (easier to test)
self.prev_hash = prev_hash
self.nonce = nonce
self.difficulty = difficulty
# ✅ Convert transactions to a serializable form at storage time
self.transactions = [
tx.to_dict() if hasattr(tx, 'to_dict') else tx
for tx in transactions
]
self.hash = self.calculate_hash()
def calculate_hash(self):
block_data = {
"index": self.index,
"timestamp": self.timestamp, # ✅ References stored value
"prev_hash": self.prev_hash,
"nonce": self.nonce,
"difficulty": self.difficulty,
"transactions": self.transactions # ✅ Already converted to dicts
}
block_string = json.dumps(block_data, sort_keys=True) # ✅ Sorted serialization
return hashlib.sha256(block_string.encode()).hexdigest()
# Handles both Transaction objects and dicts
block = Block(1, [{"sender": "Alice", "recipient": "Bob", "amount": 5}], "0" * 64)
print(block.hash == block.calculate_hash()) # True ✅ — always deterministic
print(block.hash == block.calculate_hash()) # True ✅ — same result no matter how many times called
Key differences summarized:
| ❌ WRONG | 🤔 BETTER | ✅ BEST | |
|---|---|---|---|
| timestamp | Calls time.time() each time | Fixed in __init__ | Fixed + external injection possible |
| sort_keys | Missing | Present | Present |
| Transaction serialization | Not considered | Dict only | Automatic object conversion |
| Deterministic hash | ❌ | Partially ✅ | Fully ✅ |
💡 Remember:
calculate_hash()must be a pure function. It must always produce the same output for the same input — no matter when, where, or how many times it is called. The moment this principle breaks, the entire blockchain validation falls apart.
Let's see just how dramatic the avalanche effect really is. The code below measures how much the hash changes when only the nonce changes by 1.
import hashlib
import json
import time
class Block:
def __init__(self, index, transactions, prev_hash, difficulty=1, nonce=0):
self.index = index
self.timestamp = 1711540800.0 # Fixed timestamp (for reproducibility)
self.prev_hash = prev_hash
self.nonce = nonce
self.difficulty = difficulty
self.transactions = transactions
self.hash = self.calculate_hash()
def calculate_hash(self):
block_header = {
"index": self.index,
"timestamp": self.timestamp,
"prev_hash": self.prev_hash,
"nonce": self.nonce,
"difficulty": self.difficulty,
"transactions": self.transactions
}
block_string = json.dumps(block_header, sort_keys=True)
return hashlib.sha256(block_string.encode()).hexdigest()
# Avalanche effect demo: change nonce by just 1
block_a = Block(1, [{"tx": "test"}], "0" * 64, nonce=0)
block_b = Block(1, [{"tx": "test"}], "0" * 64, nonce=1) # only nonce differs
print(f"nonce=0 → {block_a.hash[:40]}...")
print(f"nonce=1 → {block_b.hash[:40]}...")
# Count how many characters differ
diff_count = sum(1 for a, b in zip(block_a.hash, block_b.hash) if a != b)
print(f"\n{diff_count} out of 64 characters differ — avalanche effect!")
# Output:
nonce=0 → c0e4a5f7b823d19e4f6a8c2b1d3e5f7a90b2...
nonce=1 → 7f2d8e1b6a4c9503d8f1e2a7b5c6d4e8f3a1...
59 out of 64 characters differ — avalanche effect!
The nonce changed from 0 to 1 — a change of just 1 — yet approximately 59 out of 64 characters flipped. This is why miners need to cycle through the nonce billions of times to find the desired hash pattern.
The Block Lifecycle: From Creation to Chain Attachment
Let's map out the entire flow of how a single block is born and connected to the chain.
This diagram contains Lesson 1 (hash calculation), Lesson 2 (signature verification), and Lesson 3 (transaction validation) — all of them. The block is the convergence point of everything we've learned so far.
🤔 Think about it: If a block has a timestamp, can a miner set it to a false value? How does Bitcoin prevent this?
View answer
Great question. Miners can actually manipulate the timestamp to some extent! But Bitcoin has two rules:
- The timestamp must be greater than the median of the previous 11 blocks
- The timestamp cannot be more than 2 hours in the future compared to network time (the average time of connected nodes)
It's not perfect, but it prevents large-scale manipulation. Whenever I write smart contracts on Ethereum that depend on timestamps, I always flag this — never use block.timestamp for precise random number generation or timing-based logic. Miners can manipulate it within the allowed range.
Security Role of Each Block Field: A Complete Summary
| Field | Security Role | What Happens Without It |
|---|---|---|
prev_hash | Guarantees chain immutability | Any block can be freely tampered with |
timestamp | Basis for difficulty adjustment, proof of ordering | Cannot regulate mining speed, weakens double-spend verification |
nonce | The "answer" to Proof of Work | No mining competition, anyone can instantly produce blocks |
difficulty | Determines network security level | Cost of a 51% attack drops drastically |
merkle_root | Summarizes transaction integrity | Verifying individual transactions requires the entire block |
index | Human-readable block ordering | Little functional impact, but debugging becomes difficult |
In my experience, the most underappreciated field is difficulty. When auditing the security of DeFi protocols, people focus only on code vulnerabilities. But network-level security is governed by difficulty. If difficulty is low, the cost of a 51% attack drops, and no matter how perfect the code is, the underlying chain itself becomes vulnerable.
🔨 Project Update
Let's add this lesson's block.py on top of the code we've built in Lessons 1–3.
Project structure:
pychain/
├── hash_utils.py # Lesson 1: SHA-256 hash utilities
├── wallet.py # Lesson 2: Public/private key wallet
├── transaction.py # Lesson 3: Transaction creation and validation
├── block.py # Lesson 4: Block structure ← 🆕 Added today!
└── main.py # Integration entry point
Existing code (Lessons 1–3):
# === hash_utils.py (Lesson 1) ===
import hashlib
import json
def sha256_hash(data):
"""Convert a string or dictionary to a SHA-256 hash"""
if isinstance(data, dict):
data = json.dumps(data, sort_keys=True)
return hashlib.sha256(data.encode()).hexdigest()
# === wallet.py (Lesson 2) ===
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes, serialization
class Wallet:
"""Asymmetric key wallet — sign with private key, verify with public key"""
def __init__(self):
self.private_key = ec.generate_private_key(ec.SECP256K1())
self.public_key = self.private_key.public_key()
def sign(self, message):
"""Sign a message with the private key"""
return self.private_key.sign(
message.encode(),
ec.ECDSA(hashes.SHA256())
)
def get_public_key_bytes(self):
"""Serialize the public key to bytes"""
return self.public_key.public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
)
def verify_signature(public_key_bytes, signature, message):
"""Verify a signature with the public key"""
public_key = serialization.load_der_public_key(public_key_bytes)
try:
public_key.verify(signature, message.encode(), ec.ECDSA(hashes.SHA256()))
return True
except Exception:
return False
# === transaction.py (Lesson 3) ===
import json
from hash_utils import sha256_hash
class Transaction:
"""Signed transaction — a digital check"""
def __init__(self, sender, recipient, amount, signature=None):
self.sender = sender # Sender (public key hex)
self.recipient = recipient # Recipient (public key hex)
self.amount = amount # Amount
self.signature = signature # Digital signature
self.txid = self._calculate_txid()
def _calculate_txid(self):
"""Unique transaction ID — SHA-256 hash"""
tx_data = {
"sender": self.sender,
"recipient": self.recipient,
"amount": self.amount
}
return sha256_hash(tx_data)
def to_dict(self):
"""Convert to dictionary for serialization"""
return {
"sender": self.sender,
"recipient": self.recipient,
"amount": self.amount,
"txid": self.txid
}
🆕 Code added today:
# === block.py (Lesson 4) ===
import hashlib
import json
import time
class Block:
"""The basic unit of a blockchain — a container that seals transactions"""
def __init__(self, index, transactions, prev_hash, difficulty=1, nonce=0, timestamp=None):
# --- Header fields ---
self.index = index # Block number
self.timestamp = timestamp or time.time() # Creation time
self.prev_hash = prev_hash # Previous block hash
self.nonce = nonce # Mining nonce
self.difficulty = difficulty # Difficulty
# --- Body ---
self.transactions = transactions # Transaction list
# --- Computed field ---
self.hash = self.calculate_hash() # Block hash
def calculate_hash(self):
"""Hash block header with SHA-256 to generate the block hash"""
block_data = {
"index": self.index,
"timestamp": self.timestamp,
"prev_hash": self.prev_hash,
"nonce": self.nonce,
"difficulty": self.difficulty,
"transactions": self.transactions
}
block_string = json.dumps(block_data, sort_keys=True)
return hashlib.sha256(block_string.encode()).hexdigest()
def __repr__(self):
"""Print block info in a human-readable format"""
return (
f"Block(#{self.index}, "
f"tx_count={len(self.transactions)}, "
f"hash={self.hash[:16]}...)"
)
def create_genesis_block():
"""Create the genesis block — the starting point of every chain"""
return Block(
index=0,
transactions=[],
prev_hash="0" * 64,
difficulty=1,
nonce=0,
timestamp=1231006505 # Same timestamp as Bitcoin's genesis
)
def create_next_block(prev_block, transactions):
"""Create a new block based on the previous block"""
return Block(
index=prev_block.index + 1,
transactions=transactions,
prev_hash=prev_block.hash,
difficulty=prev_block.difficulty
)
Integration entry point:
# === main.py (integration test) ===
from block import Block, create_genesis_block, create_next_block
print("=" * 50)
print("🏗️ PyChain — Block Structure Test")
print("=" * 50)
# 1. Create genesis block
genesis = create_genesis_block()
print(f"\n📦 Genesis Block: {genesis}")
print(f" prev_hash: {'0' * 16}...")
print(f" hash: {genesis.hash[:16]}...")
# 2. Transaction data (simplified as dicts instead of Lesson 3 Transaction objects)
tx1 = {"sender": "Alice", "recipient": "Bob", "amount": 5.0, "txid": "tx001"}
tx2 = {"sender": "Bob", "recipient": "Charlie", "amount": 3.0, "txid": "tx002"}
# 3. Create Block #1
block_1 = create_next_block(genesis, [tx1, tx2])
print(f"\n📦 Block #1: {block_1}")
print(f" prev_hash: {block_1.prev_hash[:16]}...")
print(f" hash: {block_1.hash[:16]}...")
print(f" Transaction count: {len(block_1.transactions)}")
# 4. Create Block #2
tx3 = {"sender": "Charlie", "recipient": "Dave", "amount": 1.5, "txid": "tx003"}
block_2 = create_next_block(block_1, [tx3])
print(f"\n📦 Block #2: {block_2}")
print(f" prev_hash: {block_2.prev_hash[:16]}...")
print(f" hash: {block_2.hash[:16]}...")
# 5. Validate chain linkage
print("\n" + "=" * 50)
print("🔗 Chain Linkage Validation")
print("=" * 50)
chain = [genesis, block_1, block_2]
for i in range(1, len(chain)):
prev = chain[i - 1]
curr = chain[i]
linked = curr.prev_hash == prev.hash
print(f" Block #{curr.index}.prev_hash == Block #{prev.index}.hash → {linked} ✅" if linked else f" ❌ Chain broken!")
# 6. Hash recalculation validation
print(f"\n🔒 Block #1 hash validation: {block_1.hash == block_1.calculate_hash()} ✅")
print("\nRun the project you've built so far!")
# Expected Output:
==================================================
🏗️ PyChain — Block Structure Test
==================================================
📦 Genesis Block: Block(#0, tx_count=0, hash=7dac2aa613166979...)
prev_hash: 0000000000000000...
hash: 7dac2aa613166979...
📦 Block #1: Block(#1, tx_count=2, hash=3a9f8b2c1d4e7f60...)
prev_hash: 7dac2aa613166979...
hash: 3a9f8b2c1d4e7f60...
Transaction count: 2
📦 Block #2: Block(#2, tx_count=1, hash=e5c8d1a4b7f23690...)
prev_hash: 3a9f8b2c1d4e7f60...
hash: e5c8d1a4b7f23690...
==================================================
🔗 Chain Linkage Validation
==================================================
Block #1.prev_hash == Block #0.hash → True ✅
Block #2.prev_hash == Block #1.hash → True ✅
🔒 Block #1 hash validation: True ✅
Run the project you've built so far!
The actual hash values may vary between runs depending on the timestamp, but the chain linkage validation should always return True. Try running it yourself.
Self-Review Checklist
After writing the code, verify the following:
- Does the
Blockclass have all 6 header fields (index, timestamp, prev_hash, nonce, difficulty, transactions)? - Does
calculate_hash()usejson.dumps(..., sort_keys=True)for deterministic serialization? - Is the genesis block's
prev_hashset to"0" * 64? - Does
create_next_block()correctly passprev_block.hashas the new block'sprev_hash? - Does
calculate_hash()always return the same result for the same input?
Two common mistakes:
❌ Omitting sort_keys=True — If key ordering differs in JSON serialization, the hash differs. We used sort_keys=True for the same reason when calculating txid in Lesson 3.
❌ Calling time.time() inside calculate_hash() — If the timestamp changes every time the hash is computed, the deterministic hash breaks. Set the timestamp once in __init__, and have calculate_hash() reference that stored value.
Next Level: How Senior Engineers Think Differently
1. Intuition About Block Size Limits
Bitcoin's block size is limited to approximately 1MB (4MB weight after SegWit). Without this limit? Massive blocks would overwhelm the network, making it impossible for individuals to run full nodes. The block size debate split the Bitcoin community in two (the direct cause of the Bitcoin Cash fork), while Ethereum took a completely different approach with gas limits. When designing block structures, size limits are a variable you must always consider.
2. The Practical Value of Merkle Root
In our code we put transactions directly in the block, but in reality only the Merkle root goes in the header. The reason is clear: SPV (Simple Payment Verification) nodes — lightweight clients like smartphone wallets — need to be able to verify whether a specific transaction is included without downloading the entire block data. We'll implement this in Lesson 8.
3. Block Propagation Optimization
One thing I learned from running Ethereum node infrastructure: in real networks, techniques like Compact Block Relay (BIP 152) avoid re-transmitting transactions already in the mempool. Only the block header and short transaction IDs are sent, and the receiving node reassembles the rest from its own mempool. A technique that saves over 90% of network bandwidth.
Summary Diagram
Next lesson preview: Right now our blocks have nonce=0 and difficulty=1. That means blocks can be created with zero "cost." In Lesson 5, we implement Proof of Work. "Cycle the nonce until the hash has n leading zeros" — we'll experience firsthand how this one simple rule underpins Bitcoin's security by simulating mining ourselves.
Difficulty Fork
🟢 If it was easy
Key takeaways:
- Block = Header (metadata) + Body (transactions)
- prev_hash creates the chain and detects tampering like a domino effect
- The genesis block is block #0, with prev_hash set to
"0" * 64 calculate_hash()= deterministic serialization → SHA-256
In the next lesson, nonce and difficulty take center stage. If you have a solid understanding of the block.py code, you'll be able to follow the mining loop in Lesson 5 quickly.
🟡 If it was difficult
Let's revisit the block using the shipping package analogy:
- Shipping label (header): Origin (prev_hash), destination (referenced by next block), ship date (timestamp), weight (difficulty), tracking number (index), security code (nonce)
- Contents (body): The actual items (transactions)
- Sealing tape (hash): All shipping label information hashed with SHA-256. If the seal is broken (hash mismatch), it's proof someone opened the package
If calculate_hash() is hard, just remember the essentials: "Turn all block information into a single string, then feed it into SHA-256 from Lesson 1." That's all it is.
Extra practice: In main.py, change Block #1's transaction amount from 5.0 to 500.0, recalculate the hash, and observe the chain validation break firsthand.
🔴 Challenge
Interview question: "In Bitcoin, a block header's nonce is 4 bytes (32 bits). A miner can try approximately 4.3 billion (2^32) values. If 4.3 billion attempts aren't enough at the current difficulty, what does a miner do?"
View answer
Method 1: Change the coinbase transaction's extraNonce — arbitrary data can be placed in the coinbase tx's scriptSig, and since changing it changes the Merkle root, the nonce space effectively becomes infinite.
Method 2: Fine-tune the timestamp — changing the timestamp by 1 second within the allowed range changes the hash input, opening a new nonce space.
Modern ASIC miners compute hashes at the rate of trillions per second (TH/s), so the 4-byte nonce space is exhausted in under a second. Without extraNonce, Bitcoin mining would be practically impossible.