Anatomy of a Block: The Role of the Header, Body, Nonce, and Timestamp

Lesson 420min4,105 chars

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.

SectionRoleAnalogy
Block HeaderBlock metadata — defines "what" this block isShipping label on a package
Block BodyActual transaction dataThe 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:

FieldSizeDescription
version4 bytesBlock version
prev_block_hash32 bytesSHA-256d of the previous block header
merkle_root32 bytesRoot of the transaction Merkle tree
timestamp4 bytesUnix timestamp
bits4 bytesCompact difficulty target
nonce4 bytesMiner'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
timestampCalls time.time() each timeFixed in __init__Fixed + external injection possible
sort_keysMissingPresentPresent
Transaction serializationNot consideredDict onlyAutomatic object conversion
Deterministic hashPartially ✅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:

  1. The timestamp must be greater than the median of the previous 11 blocks
  2. 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

FieldSecurity RoleWhat Happens Without It
prev_hashGuarantees chain immutabilityAny block can be freely tampered with
timestampBasis for difficulty adjustment, proof of orderingCannot regulate mining speed, weakens double-spend verification
nonceThe "answer" to Proof of WorkNo mining competition, anyone can instantly produce blocks
difficultyDetermines network security levelCost of a 51% attack drops drastically
merkle_rootSummarizes transaction integrityVerifying individual transactions requires the entire block
indexHuman-readable block orderingLittle 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 Block class have all 6 header fields (index, timestamp, prev_hash, nonce, difficulty, transactions)?
  • Does calculate_hash() use json.dumps(..., sort_keys=True) for deterministic serialization?
  • Is the genesis block's prev_hash set to "0" * 64?
  • Does create_next_block() correctly pass prev_block.hash as the new block's prev_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.

Code Playground

Python비트코인 네트워크에서는 초당 수천 건의 트랜잭션이 쏟아진다. 레슨 3에서 우리가 완성한 "디지털 수표" — sender, recipient, amount, signature, txid로 구성된, 해시와 서명의 이중 보호를 받는 구조체. 이걸 하나하나 따로 처리하면 어떻게 될까?
Python나는 이더리움 초기에 DApp을 만들면서 이 문제를 체감했다. 트랜잭션 하나하나에 대해 전 세계 노드가 합의하면, 네트워크 트래픽이 기하급수적으로 폭증한다. 사토시 나카모토의 해법은 놀라울 만큼 단순했다. **트랜잭션을 묶어서 한 덩어리(블록)로 만들고, 블록 단위로 합의한다.**
Python코드로 직접 확인해보자.
Python여기서 진짜 흥미로운 질문이 나온다. **누군가 블록 #1의 데이터를 변조하면 무슨 일이 벌어질까?**
Python2009년 1월 3일자 영국 타임즈 헤드라인. 은행 구제금융 직전이라는 기사다. 사토시는 이 한 줄로 비트코인이 왜 태어났는지를 각인시켰다. 나는 스마트 컨트랙트를 배포할 때마다 이 메시지를 떠올린다. 우리가 만드는 것의 원점이니까.
Python이론은 충분하다. 우리 프로젝트의 `Block` 클래스를 직접 만들 차례다. 레슨 2의 지갑(Wallet)이 개인키로 서명하고, 레슨 3의 트랜잭션(Transaction)이 서명된 송금 기록을 만들었다면, 블록(Block)은 그 트랜잭션들을 **묶어서 봉인**하는 컨테이너다.
Python**❌ WRONG WAY: 호출할 때마다 시각이 바뀌고, 키 순서도 보장 안 됨**
Python**🤔 BETTER: timestamp는 고정했지만, 트랜잭션 직렬화가 불안정**
Python**✅ BEST: 결정론적 + 객체 안전 + 방어적 설계**
Python눈사태 효과가 실제로 얼마나 극적인지 직접 확인해보자. 아래 코드는 nonce 하나만 바꿨을 때 해시가 얼마나 달라지는지 측정한다.

Q&A