Connecting the Blocks: Chain Structure Implementation and Integrity Verification
Learning Objectives
- ✓블록을 체인으로 연결하는 add_block() 로직을 구현할 수 있다
- ✓체인 전체의 유효성을 검증하는 is_chain_valid() 함수를 작성하고 테스트할 수 있다
- ✓블록 데이터 위·변조 시 검증이 실패하는 과정을 실험으로 입증하고 그 원리를 설명할 수 있다
Connecting the Blocks: Implementing Chain Structure and Integrity Verification
Not One Page of a Ledger, But the Entire Bound Ledger
In Lesson 5, we implemented Proof of Work (PoW) by cycling through nonces to hit leading zero bits. At difficulty 4, an average of over 60,000 hash calculations. Time exploding roughly 16x with each increase in difficulty. "Hard to solve, easy to verify" — remember the Sudoku analogy?
But something was missing. No matter how well you mine a single block, it's nothing more than "one page of a ledger." In Lesson 4 we compared a block to "one notarized page of a transaction ledger" — for that ledger to have meaning, the pages need to be bound together in order. So they can't be pulled out, swapped in, or replaced.
Today we finally build that "binding." We'll connect blocks one by one into a chain, and complete a system that immediately detects if anyone secretly tampers with a middle block.
Honestly, when I did my first Ethereum smart contract security audit, I underestimated the power of "hash chains." I thought, "Isn't it just a hash-linked list?" — and yes, structurally that's all it is. But this simple structure protects trillions of dollars in value. Simplicity is strength.
Today's Mission
📦 Final Deliverables:
├── blockchain.py ← Blockchain class (add_block, is_chain_valid)
└── tamper_demo.py ← Tampering detection demo script
Specifically:
- Design and implement the
Blockchainclass - Auto-generate the genesis block
- Add blocks to the chain with
add_block()(integrated with PoW) - Verify the integrity of the entire chain with
is_chain_valid() - Tamper with a middle block and confirm through experiment that validation fails
The Core Principle of a Chain: Hash Links
The secret of the chain structure is surprisingly simple. Each block contains the hash of the immediately preceding block.
Someone seeing this structure for the first time might say "So what?" The real power reveals itself here:
Change Block #1's data → Block #1's hash changes → Mismatch with Block #2's prev_hash → Block #2 must be re-mined → Block #3 too... → Everything to the end.
Recall the SHA-256 "avalanche effect" from Lesson 1. Even a 1-bit change in input completely changes the hash. When this property meets chain structure, tampering with one block invalidates every block that follows. Like a row of dominoes — tip one over and they all fall.
🤔 Think about it: If there are 1,000 blocks in the chain and you want to tamper with the transaction in the 500th block, how many blocks do you need to re-mine?
View Answer
From the 500th block to the 1,000th block — a total of 501 blocks must be re-mined. When the 500th block's hash changes, the 501st block's prev_hash mismatches, fixing the 501st breaks the 502nd... it's a chain reaction. At difficulty 4 alone it took several seconds to mine one block, and while you're re-mining 501 blocks the honest network keeps moving ahead. This is the reality of blockchain immutability.
Step 1: The Genesis Block — Where Every Chain Begins
Every blockchain has a Genesis Block. Since it's the first block, there's no previous hash (prev_hash). By convention, "0" * 64 (64 zeros) is used.
Bitcoin's genesis block contains a message hidden by Satoshi Nakamoto: "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks". He inscribed distrust of the banking system on the first page of the blockchain. Pretty cool, isn't it?
Let's create a genesis block. We'll use the Block class from Lesson 4.
# genesis_demo.py — Creating the genesis block
import hashlib
import time
class Block:
"""Simplified Block class from Lessons 4-5"""
def __init__(self, index, transactions, prev_hash):
self.index = index
self.timestamp = time.time()
self.transactions = transactions
self.prev_hash = prev_hash
self.nonce = 0
self.hash = self.calculate_hash()
def calculate_hash(self):
data = f"{self.index}{self.timestamp}{self.transactions}{self.prev_hash}{self.nonce}"
return hashlib.sha256(data.encode()).hexdigest()
def mine_block(self, difficulty):
target = "0" * difficulty
while self.hash[:difficulty] != target:
self.nonce += 1
self.hash = self.calculate_hash()
return self.hash
# Genesis block: no previous hash, so fill with zeros
genesis = Block(0, ["Genesis Block"], "0" * 64)
genesis.mine_block(2)
print(f"Index: {genesis.index}")
print(f"Hash: {genesis.hash}")
print(f"Prev Hash: {genesis.prev_hash}")
print(f"Nonce: {genesis.nonce}")
# Expected output (hash value differs each run):
Index: 0
Hash: 00a1b2c3d4e5f6... (starts with 00)
Prev Hash: 0000000000000000000000000000000000000000000000000000000000000000
Nonce: 127
See how the genesis block's prev_hash is 64 zeros? This is the "anchor" of the chain. Everything starts from here.
Step 2: Designing the Blockchain Class
With the anchor of the genesis block set, it's time to stack blocks on top of it. Let's design a Blockchain class to manage the blocks.
Attributes the Blockchain class needs:
chain: Block list — the body of the chaindifficulty: Mining difficulty — linked to PoWpending_transactions: Transactions waiting to be included in a block
Methods:
create_genesis_block(): Called automatically during chain initializationadd_block(): Create new block → link previous hash → mine → add to chainget_latest_block(): Return the last block (to find theprev_hashfor a new block)is_chain_valid(): Traverse the entire chain and verify integrity
🤔 Think about it: Why is
pending_transactionsneeded? Can't we just create a block every time a transaction occurs?
View Answer
No. There are two reasons. First, block mining takes time and computational cost, so creating one block per transaction is inefficient. Bitcoin creates one block every 10 minutes on average and bundles thousands of transactions into it. Second, remember the analogy of transactions as "digital checks" from Lesson 3? It's like collecting checks and taking them all to the bank at once. This "holding area" is exactly what pending_transactions is. In real Bitcoin, this is called the mempool.
Step 3: Implementing the Blockchain Class
Design is done — let's put it into code. Taking in the meaning of each line.
# blockchain.py — Core Blockchain class implementation
import hashlib
import time
class Block:
def __init__(self, index, transactions, prev_hash):
self.index = index
self.timestamp = time.time()
self.transactions = transactions
self.prev_hash = prev_hash
self.nonce = 0
self.hash = self.calculate_hash()
def calculate_hash(self):
data = f"{self.index}{self.timestamp}{self.transactions}{self.prev_hash}{self.nonce}"
return hashlib.sha256(data.encode()).hexdigest()
def mine_block(self, difficulty):
target = "0" * difficulty
while self.hash[:difficulty] != target:
self.nonce += 1
self.hash = self.calculate_hash()
return self.hash
class Blockchain:
def __init__(self, difficulty=2):
self.difficulty = difficulty
self.pending_transactions = []
self.chain = [self._create_genesis_block()]
def _create_genesis_block(self):
"""The chain's first block — fill previous hash with zeros"""
genesis = Block(0, ["Genesis Block"], "0" * 64)
genesis.mine_block(self.difficulty)
return genesis
def get_latest_block(self):
"""Return the last block in the chain"""
return self.chain[-1]
def add_block(self, transactions):
"""Create new block → link previous hash → mine → add to chain"""
prev_hash = self.get_latest_block().hash
new_block = Block(len(self.chain), transactions, prev_hash)
new_block.mine_block(self.difficulty)
self.chain.append(new_block)
return new_block
def is_chain_valid(self):
"""Traverse the entire chain and verify integrity"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# Check 1: Is the current block's hash accurate?
if current.hash != current.calculate_hash():
return False, f"Block #{current.index}: Hash mismatch"
# Check 2: Is the previous block hash link correct?
if current.prev_hash != previous.hash:
return False, f"Block #{current.index}: Chain link broken"
# Check 3: Is the PoW valid?
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"Block #{current.index}: Invalid PoW"
return True, "Chain is valid"
Notice how is_chain_valid() verifies three things:
| Verification Step | What It Checks | Failure Meaning |
|---|---|---|
| Hash recalculation | hash == calculate_hash() | Block data has been tampered |
| Chain link | prev_hash == previous.hash | Block order has been tampered |
| PoW check | Hash starts with 00... | Block inserted without mining |
Just as digital signatures in Lesson 2 proved "I sent this" mathematically, is_chain_valid() proves "this chain has not been tampered with" mathematically. Signatures protect individual transactions, and the hash chain protects the entire transaction history. The two layers overlap to complete blockchain security.
Common Mistake: What Happens If You Implement is_chain_valid() Carelessly
When implementing chain validation for the first time, it's easy to think "Can't we just check the hash?" or "Isn't checking only PoW enough?" Let's look at the holes step by step to see why all three checks are necessary.
❌ WRONG WAY: Validation That Only Checks PoW
def is_chain_valid_wrong(self):
"""Only checks PoW — dangerous implementation!"""
for i in range(1, len(self.chain)):
current = self.chain[i]
# Only checks PoW: just looks at whether hash starts with "00..."
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"Block #{current.index}: Invalid PoW"
return True, "Chain is valid"
# 🚨 Attack scenario:
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
# Attacker manipulates data, then re-mines to satisfy PoW too
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"]
chain.chain[1].nonce = 0
chain.chain[1].hash = chain.chain[1].calculate_hash()
chain.chain[1].mine_block(chain.difficulty)
# Check PoW only? → It passes!
# Hash starts with "00..." so it's judged as fine.
# But it's already mismatching block #2's prev_hash!
Why is this dangerous? If an attacker tampers with a block and re-mines it, the PoW condition can be satisfied. Since chain links (prev_hash) and hash integrity aren't checked, the tampered block passes as legitimate.
🤔 BETTER: Hash Recalculation + PoW Check
def is_chain_valid_better(self):
"""Hash recalculation + PoW — improved but still has gaps"""
for i in range(1, len(self.chain)):
current = self.chain[i]
# Check hash recalculation match
if current.hash != current.calculate_hash():
return False, f"Block #{current.index}: Hash mismatch"
# Check PoW
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"Block #{current.index}: Invalid PoW"
return True, "Chain is valid"
# 🚨 Attack scenario:
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
# Attacker tampers with block #1 and re-mines it
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"]
chain.chain[1].nonce = 0
chain.chain[1].hash = chain.chain[1].calculate_hash()
chain.chain[1].mine_block(chain.difficulty)
# Hash recalculation + PoW → Block #1 itself passes!
# (Data, hash, and PoW are all internally consistent)
# But block #2's prev_hash still points to the old hash.
# Since we don't check the prev_hash link... we miss the broken chain!
Why is this insufficient? It only checks the internal consistency of each block, not the links between blocks. Even if an attacker completely replaces a middle block (as long as hash and PoW match), it won't be detected. Because it doesn't verify whether the blocks are actually connected.
✅ BEST: Hash Recalculation + Chain Link + PoW — Triple Verification
def is_chain_valid(self):
"""Complete validation — all three must be checked to be safe"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# Check 1: Block internal integrity
if current.hash != current.calculate_hash():
return False, f"Block #{current.index}: Hash mismatch"
# Check 2: Inter-block link integrity ← This is the key!
if current.prev_hash != previous.hash:
return False, f"Block #{current.index}: Chain link broken"
# Check 3: Proof of work validity
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"Block #{current.index}: Invalid PoW"
return True, "Chain is valid"
# ✅ Now any attack can be detected:
# - Change only data → caught by Check 1
# - Change data and re-mine → caught by Check 2 (prev_hash mismatch)
# - Insert/delete blocks → caught by Check 2
# - Add block without PoW → caught by Check 3
Summary: In security, "isn't this good enough?" is the most dangerous thought. Leave out one check and an attack that targets exactly that gap becomes possible. Each of the three checks blocks a different type of attack:
| Omitted Check | Attack That Becomes Possible |
|---|---|
| Hash recalculation | Tamper with data while leaving the hash field unchanged |
| prev_hash link | Tamper/replace a block and disguise it by re-mining |
| PoW | Insert arbitrary blocks without mining |
Step 4: Adding Blocks to the Chain
Implementation is done — let's actually run it.
# chain_test.py — Adding blocks and printing them
from blockchain import Blockchain
# Create blockchain with difficulty 2
my_chain = Blockchain(difficulty=2)
print("=== Blockchain created ===")
print(f"Genesis block hash: {my_chain.chain[0].hash[:16]}...")
# Add 2 blocks
my_chain.add_block(["Alice → Bob: 50 BTC", "Bob → Charlie: 25 BTC"])
my_chain.add_block(["Charlie → Dave: 10 BTC"])
# Print the entire chain
for block in my_chain.chain:
print(f"\n--- Block #{block.index} ---")
print(f" Transactions: {block.transactions}")
print(f" Prev Hash: {block.prev_hash[:16]}...")
print(f" Curr Hash: {block.hash[:16]}...")
print(f" Nonce: {block.nonce}")
# Validity check
valid, msg = my_chain.is_chain_valid()
print(f"\nChain validity: {msg}")
# Expected output:
=== Blockchain created ===
Genesis block hash: 00a1b2c3d4e5f678...
--- Block #0 ---
Transactions: ['Genesis Block']
Prev Hash: 0000000000000000...
Curr Hash: 00a1b2c3d4e5f678...
Nonce: 127
--- Block #1 ---
Transactions: ['Alice → Bob: 50 BTC', 'Bob → Charlie: 25 BTC']
Prev Hash: 00a1b2c3d4e5f678...
Curr Hash: 0045de891fba2c37...
Nonce: 83
--- Block #2 ---
Transactions: ['Charlie → Dave: 10 BTC']
Prev Hash: 0045de891fba2c37...
Curr Hash: 009f12e5c3a7d8b1...
Nonce: 214
Chain validity: Chain is valid
Look at the output carefully. Block #1's Prev Hash exactly matches Block #0's Curr Hash. Block #2's Prev Hash is the same as Block #1's Curr Hash. This is the "chain." A hash-linked list — nothing more, nothing less.
Step 5: Tampering Experiment — The Moment the Chain Breaks
This is the highlight of today's lesson. What happens when a malicious attacker tampers with Block #1's transactions?
# tamper_demo.py — Tampering detection experiment
from blockchain import Blockchain
# 1. Build a normal chain
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
chain.add_block(["Charlie → Dave: 10 BTC"])
print("=== Before Tampering ===")
valid, msg = chain.is_chain_valid()
print(f"Chain validity: {msg}")
print(f"Block #1 original transaction: {chain.chain[1].transactions}")
print(f"Block #1 original hash: {chain.chain[1].hash[:20]}...")
# 2. Attacker tampers with Block #1's transaction!
print("\n🚨 Attacker is tampering with the amount in Block #1...")
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"] # 50 → 5000!
print(f"Block #1 tampered transaction: {chain.chain[1].transactions}")
print(f"Block #1 stored hash: {chain.chain[1].hash[:20]}...")
print(f"Block #1 recalculated hash: {chain.chain[1].calculate_hash()[:20]}...")
# 3. Run validation
print("\n=== Validation After Tampering ===")
valid, msg = chain.is_chain_valid()
print(f"Chain validity: {valid}")
print(f"Detail message: {msg}")
# Expected output:
=== Before Tampering ===
Chain validity: Chain is valid
Block #1 original transaction: ['Alice → Bob: 50 BTC']
Block #1 original hash: 0045de891fba2c37ab...
🚨 Attacker is tampering with the amount in Block #1...
Block #1 tampered transaction: ['Alice → Bob: 5000 BTC']
Block #1 stored hash: 0045de891fba2c37ab...
Block #1 recalculated hash: 8f2a1bc3e7d94560f1...
=== Validation After Tampering ===
Chain validity: False
Detail message: Block #1: Hash mismatch
The stored hash is unchanged, but the recalculated hash is completely different. Because the data changed. SHA-256's avalanche effect. When "50 BTC" changed to "5000 BTC," the hash became a completely different value, and it's caught immediately at is_chain_valid()'s first check — hash != calculate_hash().
What If You Recalculate the Hash Too?
"Can't the attacker just recalculate Block #1's hash too?" Good question.
# tamper_rehash.py — Why the chain breaks even after recalculating the hash
from blockchain import Blockchain
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
# Attacker: tampers transaction + recalculates hash + even re-mines!
print("🚨 Attacker: recalculating hash after tampering transaction...")
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"]
chain.chain[1].mine_block(chain.difficulty) # Re-mine!
print(f"Block #1 new hash: {chain.chain[1].hash[:20]}...")
print(f"Block #2's prev_hash: {chain.chain[2].prev_hash[:20]}...")
print(f"Match: {chain.chain[1].hash == chain.chain[2].prev_hash}")
valid, msg = chain.is_chain_valid()
print(f"\nChain validity: {valid}")
print(f"Detail message: {msg}")
# Expected output:
🚨 Attacker: recalculating hash after tampering transaction...
Block #1 new hash: 00f7c8a912b3e456...
Block #2's prev_hash: 0045de891fba2c37...
Match: False
Chain validity: False
Detail message: Block #2: Chain link broken
Block #1 was re-mined, but Block #2 remembers the prev_hash of the original Block #1. It doesn't match the newly mined hash, so it's caught at the second check — prev_hash != previous.hash.
Let's look at this chain reaction in a diagram:
This is the reality of Immutability. It doesn't mean "cannot be changed." It means "to change it, you'd have to re-mine the entire remainder faster than the network, and the cost of that is astronomical." We'll cover 51% attacks in depth in Lesson 9 — just get a feel for it now.
🤔 Think about it: Bitcoin's current hash rate is about 600 EH/s (exahashes per second). An attacker would need about 300 EH/s to secure 51%. How many of the latest ASIC miners (Antminer S21, 200 TH/s) would that require?
View Answer
300 EH/s = 300,000,000 TH/s. One unit does 200 TH/s, so that's 1.5 million units. If one unit costs about $5,000, the total equipment cost is $7.5 billion (roughly 10 trillion KRW). Electricity costs are separate. This is the economic security that PoW + hash chains create. Hacking isn't "technically impossible" — it's "economically pointless."
Cost Analysis: How Much Does It Cost to Alter One Block?
Talking about immutability in words alone doesn't sink in. Let's feel it in numbers.
# cost_analysis.py — Estimating the cost of tampering
import time
from blockchain import Blockchain
# Build a chain of 10 blocks at difficulty 4
chain = Blockchain(difficulty=4)
for i in range(1, 11):
chain.add_block([f"Transaction {i}"])
print(f"Chain of 10 blocks created")
# Tampering with block #3 requires re-mining 8 blocks (#3~#10)
start = time.time()
blocks_to_remine = 8
for i in range(3, 3 + blocks_to_remine):
block = chain.chain[i]
if i == 3:
block.transactions = ["Tampered transaction!"]
block.prev_hash = chain.chain[i-1].hash
block.nonce = 0
block.hash = block.calculate_hash()
block.mine_block(chain.difficulty)
elapsed = time.time() - start
print(f"Blocks re-mined: {blocks_to_remine}")
print(f"Time elapsed: {elapsed:.2f}s")
print(f"Average per block: {elapsed/blocks_to_remine:.2f}s")
print(f"\nBitcoin equivalent (difficulty ~80 digits):")
print(f" 1 block mining: ~10 minutes")
print(f" 8 blocks re-mined: ~80 minutes")
print(f" Meanwhile the honest network adds 8 more blocks!")
# Expected output (varies by computer performance):
Chain of 10 blocks created
Blocks re-mined: 8
Time elapsed: 3.47s
Average per block: 0.43s
Bitcoin equivalent (difficulty ~80 digits):
1 block mining: ~10 minutes
8 blocks re-mined: ~80 minutes
Meanwhile the honest network adds 8 more blocks!
Let's note what these numbers mean in practice. One of the first things I check during a DeFi security audit is chain finality. On Ethereum, it's generally recommended to wait for 12 confirmations. Because to reverse a transaction 12 blocks back, you'd need to re-mine 12 blocks faster than the network. Bitcoin uses 6 confirmations as the standard, and larger amounts warrant waiting for more.
Not knowing this principle is dangerous. You end up making the mistake of processing payments with "1 block confirmation" in smart contracts — there are real cases where tens of millions of dollars were stolen through exactly this vulnerability.
Full Flow Summary
From chain creation to validation — let's summarize the entire process in a sequence diagram:
Self-Review Checklist
Once you've written the code, check the following:
- Is the genesis block's
prev_hashequal to"0" * 64? - Does
add_block()use the last block's hash asprev_hash? - Does
is_chain_valid()verify all three: hash recalculation, chain link, and PoW? - Does validation return
Falsewhen a transaction is tampered? - Is
mine_block()called insideadd_block()?
🔍 Deep Dive: How Senior Developers Do It Differently
1. Add Merkle root verification: Right now, we stringify the whole transaction list and include it in the hash. Real Bitcoin summarizes transactions with a Merkle tree (covered in Lesson 8) and only puts the root hash in the block header. This allows verifying whether a specific transaction is included in a block without downloading the whole thing (SPV verification).
2. Fork handling: What if two miners find a block simultaneously? The chain temporarily splits (a fork). Real blockchain nodes resolve this with the rule "choose the longest chain." Our Blockchain class doesn't have this logic yet.
3. Serialization: To send a blockchain over a file or network, it needs to be serialized as JSON. Add to_dict() and from_dict() methods. We'll implement this in the Lesson 10 REST API.
🔨 Project Update
Here's the complete project code built so far. The concepts from Lessons 1–5 all come together here. Copy and run it directly.
blockchain.py — Core module (added in this lesson)
# blockchain.py — Integrated Block + Blockchain module
import hashlib
import time
class Block:
"""
Block structure designed in Lesson 4 + PoW mining method from Lesson 5
Block header: index, timestamp, prev_hash, nonce, hash
Block body: transactions
"""
def __init__(self, index, transactions, prev_hash):
self.index = index
self.timestamp = time.time()
self.transactions = transactions # Transaction list (digital checks from Lesson 3)
self.prev_hash = prev_hash # Previous block hash (key to chain linking!)
self.nonce = 0 # Nonce for PoW (Lesson 5)
self.hash = self.calculate_hash()
def calculate_hash(self):
"""Hash all block data with SHA-256 (fingerprint from Lesson 1)"""
data = (f"{self.index}{self.timestamp}{self.transactions}"
f"{self.prev_hash}{self.nonce}")
return hashlib.sha256(data.encode()).hexdigest()
def mine_block(self, difficulty):
"""Proof of work: search nonces until leading zero bits match (Lesson 5)"""
target = "0" * difficulty
while self.hash[:difficulty] != target:
self.nonce += 1
self.hash = self.calculate_hash()
return self.hash
def __repr__(self):
return (f"Block(#{self.index}, txs={len(self.transactions)}, "
f"hash={self.hash[:12]}...)")
class Blockchain:
"""
[Newly added in Lesson 6]
The core class that connects blocks into a chain and verifies integrity
"""
def __init__(self, difficulty=2):
self.difficulty = difficulty
self.pending_transactions = [] # Waiting transactions (mempool)
self.chain = [self._create_genesis_block()]
def _create_genesis_block(self):
"""The chain's first block — starts with zeros instead of a previous hash"""
genesis = Block(0, ["Genesis Block"], "0" * 64)
genesis.mine_block(self.difficulty)
return genesis
def get_latest_block(self):
"""The last block in the chain"""
return self.chain[-1]
def add_block(self, transactions):
"""New block: link previous hash → PoW mining → add to chain"""
prev_hash = self.get_latest_block().hash
new_block = Block(len(self.chain), transactions, prev_hash)
new_block.mine_block(self.difficulty)
self.chain.append(new_block)
return new_block
def is_chain_valid(self):
"""
Full chain integrity verification — 3 steps:
1) Hash recalculation match
2) Previous hash link match
3) PoW validity
"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# Check 1: Has the block data been tampered?
if current.hash != current.calculate_hash():
return False, f"Block #{current.index}: Hash mismatch (data tampering detected)"
# Check 2: Is the chain link correct?
if current.prev_hash != previous.hash:
return False, f"Block #{current.index}: Chain link broken"
# Check 3: Is the proof of work valid?
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"Block #{current.index}: Invalid PoW"
return True, "✅ Chain is valid"
def print_chain(self):
"""Print the entire chain in a readable format"""
for block in self.chain:
print(f"\n{'='*50}")
print(f"Block #{block.index}")
print(f" Timestamp: {time.ctime(block.timestamp)}")
print(f" Transactions: {block.transactions}")
print(f" Prev Hash: {block.prev_hash[:24]}...")
print(f" Curr Hash: {block.hash[:24]}...")
print(f" Nonce: {block.nonce}")
print(f"\n{'='*50}")
# === Demo when run directly ===
if __name__ == "__main__":
print("🔗 Creating mini blockchain...\n")
bc = Blockchain(difficulty=2)
bc.add_block(["Alice → Bob: 50 BTC", "Bob → Charlie: 25 BTC"])
bc.add_block(["Charlie → Dave: 10 BTC"])
bc.add_block(["Dave → Eve: 5 BTC", "Eve → Frank: 2 BTC"])
bc.print_chain()
valid, msg = bc.is_chain_valid()
print(f"\nChain validation result: {msg}")
print(f"Total blocks: {len(bc.chain)}")
tamper_demo.py — Tampering detection demo (added in this lesson)
# tamper_demo.py — Blockchain tampering detection demo
from blockchain import Blockchain
def run_tamper_demo():
print("=" * 60)
print(" 🔒 Blockchain Tampering Detection Demo")
print("=" * 60)
# Step 1: Build a normal chain
print("\n[Step 1] Building normal chain (difficulty 2)")
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
chain.add_block(["Charlie → Dave: 10 BTC"])
print(f" Block count: {len(chain.chain)}")
valid, msg = chain.is_chain_valid()
print(f" Validation result: {msg}")
# Step 2: Tamper with Block #1 data
print("\n[Step 2] 🚨 Attack: Changing Block #1 amount from 50 → 5000!")
original_tx = chain.chain[1].transactions[:]
original_hash = chain.chain[1].hash
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"]
print(f" Original transaction: {original_tx}")
print(f" Tampered transaction: {chain.chain[1].transactions}")
print(f" Stored hash: {original_hash[:24]}...")
print(f" Recalculated hash: {chain.chain[1].calculate_hash()[:24]}...")
# Step 3: Validation
print("\n[Step 3] Running validation")
valid, msg = chain.is_chain_valid()
print(f" Result: {msg}")
# Step 4: What if the hash is recalculated too?
print("\n[Step 4] 🚨 Attacker re-mines the hash too!")
chain.chain[1].nonce = 0
chain.chain[1].hash = chain.chain[1].calculate_hash()
chain.chain[1].mine_block(chain.difficulty)
print(f" Block #1 new hash: {chain.chain[1].hash[:24]}...")
print(f" Block #2 prev_hash: {chain.chain[2].prev_hash[:24]}...")
valid, msg = chain.is_chain_valid()
print(f" Result: {msg}")
# Conclusion
print("\n" + "=" * 60)
print(" 💡 Conclusion: Tampering with one block breaks the")
print(" prev_hash of every block that follows. Unless you")
print(" re-mine all of them, tampering is detected instantly!")
print("=" * 60)
if __name__ == "__main__":
run_tamper_demo()
Run the project you've built so far:
# Run the blockchain module
python blockchain.py
# Run the tampering detection demo
python tamper_demo.py
# blockchain.py expected output:
🔗 Creating mini blockchain...
==================================================
Block #0
Timestamp: Thu Mar 27 14:32:01 2026
Transactions: ['Genesis Block']
Prev Hash: 000000000000000000000000...
Curr Hash: 00a1b2c3d4e5f67890abcdef...
Nonce: 127
==================================================
Block #1
Timestamp: Thu Mar 27 14:32:01 2026
Transactions: ['Alice → Bob: 50 BTC', 'Bob → Charlie: 25 BTC']
Prev Hash: 00a1b2c3d4e5f67890abcdef...
Curr Hash: 0045de891fba2c37ab012345...
Nonce: 83
...
Chain validation result: ✅ Chain is valid
Total blocks: 4
# tamper_demo.py expected output:
============================================================
🔒 Blockchain Tampering Detection Demo
============================================================
[Step 1] Building normal chain (difficulty 2)
Block count: 4
Validation result: ✅ Chain is valid
[Step 2] 🚨 Attack: Changing Block #1 amount from 50 → 5000!
Original transaction: ['Alice → Bob: 50 BTC']
Tampered transaction: ['Alice → Bob: 5000 BTC']
Stored hash: 0045de891fba2c37ab01...
Recalculated hash: 8f2a1bc3e7d94560f1de...
[Step 3] Running validation
Result: Block #1: Hash mismatch (data tampering detected)
[Step 4] 🚨 Attacker re-mines the hash too!
Block #1 new hash: 00f7c8a912b3e456d789...
Block #2 prev_hash: 0045de891fba2c37ab01...
Result: Block #2: Chain link broken
============================================================
💡 Conclusion: Tampering with one block breaks the
prev_hash of every block that follows. Unless you
re-mine all of them, tampering is detected instantly!
============================================================
Summary Diagram
Everything learned today on one page:
Next Lesson Preview: With the chain structure complete, it's time to answer the question "How much is Alice's balance?" Bitcoin has no account balance. Instead, it tracks balances using a unique model called UTXO (Unspent Transaction Output). The Input and Output structure of transactions from Lesson 3 shines here.
Difficulty Fork
🟢 If it was easy
Congratulations. Just the key points:
- Blockchain = list of blocks connected by hashes
is_chain_valid()verifies three things: hash recalculation, chain link, PoW- Tamper one block → need to re-mine all blocks after it → economically impossible
In the next lesson we'll track balances with the UTXO model. blockchain.py will be getting proper transaction management features added.
🟡 If it was difficult
Let me explain the chain structure with a different analogy.
Sealed letter analogy: Imagine including a photo of the previous letter inside each envelope. Envelope #1 contains a photo of letter #0, envelope #2 contains a photo of letter #1. If someone opens letter #1 and changes its contents? It would immediately be caught because it differs from the "original photo of letter #1" inside envelope #2.
is_chain_valid() is the automated process of comparing these photos:
- Re-take the photo (
calculate_hash()) and check if it matches the photo written on the envelope (hash) - Check if the photo inside the next envelope (
prev_hash) matches the actual photo of the previous envelope (previous.hash)
Extra practice: Add 5 blocks to blockchain.py and change the data in the 3rd block. Verify that is_chain_valid() reports the error at exactly the 3rd block.
🔴 Challenge
Interview question: "Is blockchain immutability absolute? Under what conditions can it be broken?"
Model answer points:
- 51% attack: If you secure more than half of the network's hash power, you can theoretically rewrite the chain
- Chain reorganization (reorg): Since a longer chain takes precedence over a shorter one, an attacker who builds a longer secret chain can replace the existing one
- Economic limits: At Bitcoin's scale the cost would be tens of trillions of KRW, making it practically impossible — but on smaller PoW coins (ETC, BTG, etc.) 51% attacks have actually occurred
Coding challenge: Add a replace_chain(new_chain) method to the Blockchain class. Implement the "longest chain rule" that only replaces the current chain when the new chain is longer and valid. This is the fundamental mechanism for reaching consensus in a distributed network.