블록들을 잇다: 체인 구조 구현과 무결성 검증
학습 목표
- ✓블록을 체인으로 연결하는 add_block() 로직을 구현할 수 있다
- ✓체인 전체의 유효성을 검증하는 is_chain_valid() 함수를 작성하고 테스트할 수 있다
- ✓블록 데이터 위·변조 시 검증이 실패하는 과정을 실험으로 입증하고 그 원리를 설명할 수 있다
블록들을 잇다: 체인 구조 구현과 무결성 검증
장부 한 페이지가 아니라, 묶인 장부 전체
레슨 5에서 우리는 넌스를 돌려가며 선행 0 비트를 맞추는 작업 증명(PoW)을 구현했다. 난이도 4에서 평균 6만 번 이상의 해시 계산. 난이도가 1 오를 때마다 약 16배씩 폭증하는 시간. "풀기 어렵고 검증은 쉽다" — 스도쿠 비유, 기억나시죠?
그런데 하나 빠졌다. 블록 하나를 아무리 잘 채굴해봤자, 그건 '장부 한 페이지'에 불과하다. 레슨 4에서 블록을 '공증된 거래 장부 한 페이지'에 비유했었는데, 장부가 의미를 가지려면 페이지들이 순서대로 묶여 있어야 한다. 뽑아서 끼워넣거나 바꿔치기할 수 없게.
오늘 드디어 그 "묶음"을 만든다. 블록 하나하나를 **체인(chain)**으로 연결하고, 누군가 중간 블록을 몰래 조작하면 즉시 탐지되는 시스템을 완성한다.
솔직히 고백하자면, 내가 이더리움 스마트 컨트랙트 보안 감사를 처음 했을 때 "해시 체인"의 위력을 과소평가했다. "그냥 해시 링크드 리스트 아닌가?"라고 생각했는데 — 맞다. 구조적으로는 그게 전부다. 하지만 이 단순한 구조가 수조 원의 가치를 보호한다. 단순함이 곧 강력함이다.
오늘의 미션
📦 최종 산출물:
├── blockchain.py ← Blockchain 클래스 (add_block, is_chain_valid)
└── tamper_demo.py ← 위변조 탐지 데모 스크립트
구체적으로:
Blockchain클래스를 설계하고 구현한다- 제네시스 블록을 자동 생성한다
add_block()으로 블록을 체인에 추가한다 (PoW 통합)is_chain_valid()로 전체 체인의 무결성을 검증한다- 중간 블록을 위변조하고, 검증이 실패하는 걸 실험으로 확인한다
체인의 핵심 원리: 해시 링크
체인 구조의 비밀은 놀라울 정도로 간단하다. 각 블록이 바로 이전 블록의 해시를 품고 있다.
이 구조를 처음 본 사람은 "그래서 뭐?"라고 할 수 있다. 진짜 위력은 여기서 드러난다:
블록 #1의 데이터를 바꾸면 → 블록 #1의 해시가 바뀐다 → 블록 #2의 prev_hash와 불일치 → 블록 #2도 다시 채굴해야 한다 → 블록 #3도... → 끝까지 전부.
레슨 1에서 배운 SHA-256의 "눈사태 효과"를 떠올려 보자. 입력이 1비트만 바뀌어도 해시가 완전히 달라진다. 이 성질이 체인 구조와 만나면, 한 블록의 변조가 뒤따르는 모든 블록의 무효화로 이어진다. 한 장의 도미노가 쓰러지면 끝까지 쓰러지는 것과 같다.
🤔 생각해보세요: 체인에 블록이 1,000개 있고, 500번째 블록의 트랜잭션을 조작하려면 몇 개의 블록을 다시 채굴해야 할까?
답변 보기
500번째 블록부터 1,000번째 블록까지, 총 501개를 다시 채굴해야 한다. 500번째 블록의 해시가 바뀌면 501번째의 prev_hash가 불일치하고, 501번째를 고치면 502번째가 깨지고... 연쇄 반응이다. 난이도 4만 해도 블록 하나 채굴에 수초가 걸렸는데, 501개를 다시 채굴하는 동안 정직한 네트워크는 계속 앞서 나간다. 이것이 블록체인 불변성의 실체다.
Step 1: 제네시스 블록 — 모든 체인의 시작
모든 블록체인에는 **제네시스 블록(Genesis Block)**이 있다. 첫 번째 블록이니까 이전 해시(prev_hash)가 없다. 관례적으로 "0" * 64 (64자리 0)을 넣는다.
비트코인의 제네시스 블록에는 사토시 나카모토가 숨겨놓은 메시지가 있다: "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks". 은행 시스템에 대한 불신을 블록체인의 첫 페이지에 새긴 거다. 꽤 멋있지 않은가?
우리도 제네시스 블록을 만들어보자. 레슨 4에서 만든 Block 클래스를 사용한다.
# genesis_demo.py — 제네시스 블록 생성
import hashlib
import time
class Block:
"""레슨 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
# 제네시스 블록: 이전 해시가 없으니 0으로 채운다
genesis = Block(0, ["제네시스 블록"], "0" * 64)
genesis.mine_block(2)
print(f"인덱스: {genesis.index}")
print(f"해시: {genesis.hash}")
print(f"이전 해시: {genesis.prev_hash}")
print(f"넌스: {genesis.nonce}")
# 예상 출력 (해시값은 매 실행마다 다름):
인덱스: 0
해시: 00a1b2c3d4e5f6... (00으로 시작)
이전 해시: 0000000000000000000000000000000000000000000000000000000000000000
넌스: 127
제네시스 블록의 prev_hash가 64개의 0인 것이 보이시죠? 이게 체인의 "닻(anchor)"이다. 모든 것이 여기서부터 시작된다.
Step 2: Blockchain 클래스 설계
제네시스 블록이라는 닻을 내렸으니, 이제 그 위에 블록들을 쌓아 올릴 차례다. 블록들을 관리하는 Blockchain 클래스를 설계하자.
Blockchain 클래스가 가져야 할 속성:
chain: 블록 리스트 — 체인의 본체difficulty: 채굴 난이도 — PoW와 연동pending_transactions: 아직 블록에 포함되지 않은 대기 트랜잭션
메서드:
create_genesis_block(): 체인 초기화 시 자동 호출add_block(): 새 블록 생성 → 이전 해시 연결 → 채굴 → 체인에 추가get_latest_block(): 마지막 블록 반환 (새 블록의prev_hash를 알아내기 위해)is_chain_valid(): 전체 체인 순회하며 무결성 검증
🤔 생각해보세요:
pending_transactions는 왜 필요할까? 트랜잭션이 생길 때마다 바로 블록을 만들면 안 될까?
답변 보기
안 된다. 이유는 두 가지다. 첫째, 블록 채굴에는 시간과 연산 비용이 들기 때문에 트랜잭션 하나마다 블록을 만들면 비효율적이다. 비트코인은 평균 10분에 한 블록을 만들고, 그 안에 수천 개의 트랜잭션을 묶는다. 둘째, 레슨 3에서 트랜잭션을 '디지털 수표'에 비유했던 걸 기억하시죠? 수표를 모아뒀다가 한꺼번에 은행에 가져가는 것과 같다. 이 "모아두는 공간"이 바로 pending_transactions다. 실제 비트코인에서는 이것을 **멤풀(mempool)**이라고 부른다.
Step 3: Blockchain 클래스 구현
설계를 마쳤으니 코드로 옮기자. 한 줄 한 줄 의미를 새기면서.
# blockchain.py — Blockchain 클래스 핵심 구현
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):
"""체인의 첫 블록 — 이전 해시는 0으로 채운다"""
genesis = Block(0, ["제네시스 블록"], "0" * 64)
genesis.mine_block(self.difficulty)
return genesis
def get_latest_block(self):
"""체인의 마지막 블록 반환"""
return self.chain[-1]
def add_block(self, transactions):
"""새 블록 생성 → 이전 해시 연결 → 채굴 → 체인에 추가"""
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):
"""전체 체인 순회하며 무결성 검증"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# 검증 1: 현재 블록의 해시가 정확한가?
if current.hash != current.calculate_hash():
return False, f"블록 #{current.index}: 해시 불일치"
# 검증 2: 이전 블록 해시 연결이 올바른가?
if current.prev_hash != previous.hash:
return False, f"블록 #{current.index}: 체인 연결 끊김"
# 검증 3: PoW가 유효한가?
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"블록 #{current.index}: PoW 무효"
return True, "체인이 유효합니다"
is_chain_valid()가 세 가지를 검증하는 구조에 주목하자:
| 검증 단계 | 확인 내용 | 실패 의미 |
|---|---|---|
| 해시 재계산 | hash == calculate_hash() | 블록 데이터가 변조됨 |
| 체인 연결 | prev_hash == previous.hash | 블록 순서가 변조됨 |
| PoW 확인 | 해시가 00...으로 시작 | 채굴 없이 블록 삽입됨 |
레슨 2에서 디지털 서명이 "내가 보냈다"를 수학으로 증명했듯, is_chain_valid()는 "이 체인은 변조되지 않았다"를 수학으로 증명한다. 서명이 개별 트랜잭션을 보호하고, 해시 체인이 전체 거래 기록을 보호한다. 두 레이어가 겹치면서 블록체인의 보안이 완성된다.
흔한 실수: is_chain_valid() 검증을 대충 구현하면 생기는 일
체인 검증을 처음 구현할 때, "해시만 확인하면 되지 않나?" 혹은 "PoW만 보면 충분하지 않나?"라고 생각하기 쉽다. 세 가지 검증이 왜 전부 필요한지, 단계별로 구멍을 보자.
❌ WRONG WAY: PoW만 확인하는 검증
def is_chain_valid_wrong(self):
"""PoW만 확인 — 위험한 구현!"""
for i in range(1, len(self.chain)):
current = self.chain[i]
# PoW만 확인: 해시가 "00..."으로 시작하는지만 본다
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"블록 #{current.index}: PoW 무효"
return True, "체인이 유효합니다"
# 🚨 공격 시나리오:
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
# 공격자가 데이터를 조작한 뒤, 재채굴해서 PoW도 맞춰버린다
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)
# PoW만 확인하면? → 통과해버린다!
# 해시가 "00..."으로 시작하니까 문제없다고 판단.
# 하지만 블록 #2의 prev_hash와는 이미 불일치 상태!
왜 위험한가? 공격자가 블록 하나를 조작하고 재채굴만 하면, PoW 조건은 만족시킬 수 있다. 체인 연결(prev_hash)이나 해시 무결성은 확인하지 않으니까, 조작된 블록이 정상으로 통과한다.
🤔 BETTER: 해시 재계산 + PoW 확인
def is_chain_valid_better(self):
"""해시 재계산 + PoW — 개선되었지만 여전히 빈틈이 있다"""
for i in range(1, len(self.chain)):
current = self.chain[i]
# 해시 재계산 일치 확인
if current.hash != current.calculate_hash():
return False, f"블록 #{current.index}: 해시 불일치"
# PoW 확인
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"블록 #{current.index}: PoW 무효"
return True, "체인이 유효합니다"
# 🚨 공격 시나리오:
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
# 공격자가 블록 #1을 조작하고 재채굴한다
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)
# 해시 재계산 + PoW → 블록 #1 자체는 통과!
# (데이터, 해시, PoW가 모두 일관적이니까)
# 하지만 블록 #2의 prev_hash가 옛날 해시를 가리키고 있다.
# prev_hash 연결을 안 보니까... 체인이 끊어진 걸 모른다!
왜 부족한가? 각 블록 내부의 일관성만 확인하고, 블록 간의 연결을 확인하지 않는다. 공격자가 중간 블록을 통째로 교체해도 (해시와 PoW만 맞추면) 탐지하지 못한다. 블록들이 실제로 연결되어 있는지를 확인하지 않기 때문이다.
✅ BEST: 해시 재계산 + 체인 연결 + PoW — 3중 검증
def is_chain_valid(self):
"""완전한 검증 — 세 가지를 모두 확인해야 안전하다"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# 검증 1: 블록 내부 무결성
if current.hash != current.calculate_hash():
return False, f"블록 #{current.index}: 해시 불일치"
# 검증 2: 블록 간 연결 무결성 ← 이게 핵심!
if current.prev_hash != previous.hash:
return False, f"블록 #{current.index}: 체인 연결 끊김"
# 검증 3: 작업 증명 유효성
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"블록 #{current.index}: PoW 무효"
return True, "체인이 유효합니다"
# ✅ 이제 어떤 공격에도 탐지 가능:
# - 데이터만 바꾸면 → 검증 1에서 걸림
# - 데이터 바꾸고 재채굴하면 → 검증 2에서 걸림 (prev_hash 불일치)
# - 블록을 삽입/삭제하면 → 검증 2에서 걸림
# - PoW 없이 블록 추가하면 → 검증 3에서 걸림
요약: 보안에서는 "이 정도면 충분하지 않을까?"가 가장 위험한 생각이다. 검증 하나를 빼면 그 빈틈을 정확히 노리는 공격이 가능해진다. 세 검증이 각각 다른 유형의 공격을 막는다:
| 빠뜨린 검증 | 가능해지는 공격 |
|---|---|
| 해시 재계산 생략 | 데이터 변조 후 해시 필드만 그대로 두기 |
| prev_hash 연결 생략 | 블록을 조작·교체하고 재채굴로 위장 |
| PoW 생략 | 채굴 없이 임의의 블록 삽입 |
Step 4: 체인에 블록 추가하기
구현을 끝냈으니 실제로 돌려보자.
# chain_test.py — 블록 추가 및 출력
from blockchain import Blockchain
# 난이도 2로 블록체인 생성
my_chain = Blockchain(difficulty=2)
print("=== 블록체인 생성 완료 ===")
print(f"제네시스 블록 해시: {my_chain.chain[0].hash[:16]}...")
# 블록 2개 추가
my_chain.add_block(["Alice → Bob: 50 BTC", "Bob → Charlie: 25 BTC"])
my_chain.add_block(["Charlie → Dave: 10 BTC"])
# 체인 전체 출력
for block in my_chain.chain:
print(f"\n--- 블록 #{block.index} ---")
print(f" 트랜잭션: {block.transactions}")
print(f" 이전 해시: {block.prev_hash[:16]}...")
print(f" 현재 해시: {block.hash[:16]}...")
print(f" 넌스: {block.nonce}")
# 유효성 검증
valid, msg = my_chain.is_chain_valid()
print(f"\n체인 유효성: {msg}")
# 예상 출력:
=== 블록체인 생성 완료 ===
제네시스 블록 해시: 00a1b2c3d4e5f678...
--- 블록 #0 ---
트랜잭션: ['제네시스 블록']
이전 해시: 0000000000000000...
현재 해시: 00a1b2c3d4e5f678...
넌스: 127
--- 블록 #1 ---
트랜잭션: ['Alice → Bob: 50 BTC', 'Bob → Charlie: 25 BTC']
이전 해시: 00a1b2c3d4e5f678...
현재 해시: 0045de891fba2c37...
넌스: 83
--- 블록 #2 ---
트랜잭션: ['Charlie → Dave: 10 BTC']
이전 해시: 0045de891fba2c37...
현재 해시: 009f12e5c3a7d8b1...
넌스: 214
체인 유효성: 체인이 유효합니다
출력을 자세히 보자. 블록 #1의 이전 해시가 블록 #0의 현재 해시와 정확히 일치한다. 블록 #2의 이전 해시는 블록 #1의 현재 해시와 같다. 이것이 "체인"이다. 해시로 연결된 링크드 리스트, 그 이상도 이하도 아니다.
Step 5: 위변조 실험 — 체인이 깨지는 순간
여기가 오늘 레슨의 하이라이트다. 악의적인 공격자가 블록 #1의 트랜잭션을 조작하면 어떻게 될까?
# tamper_demo.py — 위변조 탐지 실험
from blockchain import Blockchain
# 1. 정상 체인 구축
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("=== 위변조 전 ===")
valid, msg = chain.is_chain_valid()
print(f"체인 유효성: {msg}")
print(f"블록 #1 원본 트랜잭션: {chain.chain[1].transactions}")
print(f"블록 #1 원본 해시: {chain.chain[1].hash[:20]}...")
# 2. 공격자가 블록 #1의 트랜잭션을 조작!
print("\n🚨 공격자가 블록 #1의 금액을 조작합니다...")
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"] # 50 → 5000!
print(f"블록 #1 조작 트랜잭션: {chain.chain[1].transactions}")
print(f"블록 #1 저장된 해시: {chain.chain[1].hash[:20]}...")
print(f"블록 #1 재계산 해시: {chain.chain[1].calculate_hash()[:20]}...")
# 3. 검증 실행
print("\n=== 위변조 후 검증 ===")
valid, msg = chain.is_chain_valid()
print(f"체인 유효성: {valid}")
print(f"상세 메시지: {msg}")
# 예상 출력:
=== 위변조 전 ===
체인 유효성: 체인이 유효합니다
블록 #1 원본 트랜잭션: ['Alice → Bob: 50 BTC']
블록 #1 원본 해시: 0045de891fba2c37ab...
🚨 공격자가 블록 #1의 금액을 조작합니다...
블록 #1 조작 트랜잭션: ['Alice → Bob: 5000 BTC']
블록 #1 저장된 해시: 0045de891fba2c37ab...
블록 #1 재계산 해시: 8f2a1bc3e7d94560f1...
=== 위변조 후 검증 ===
체인 유효성: False
상세 메시지: 블록 #1: 해시 불일치
저장된 해시는 그대로인데, 재계산한 해시가 완전히 달라졌다. 데이터가 바뀌었으니까. SHA-256의 눈사태 효과다. "50 BTC"가 "5000 BTC"로 바뀌자 해시가 전혀 다른 값이 되었고, is_chain_valid()의 첫 번째 검증 — hash != calculate_hash() — 에서 바로 걸린다.
그래도 해시를 다시 계산하면?
"공격자가 블록 #1의 해시도 다시 계산하면 되지 않나?" 좋은 질문이다.
# tamper_rehash.py — 해시 재계산해도 체인이 깨지는 이유
from blockchain import Blockchain
chain = Blockchain(difficulty=2)
chain.add_block(["Alice → Bob: 50 BTC"])
chain.add_block(["Bob → Charlie: 25 BTC"])
# 공격자: 트랜잭션 조작 + 해시 재계산 + 재채굴까지!
print("🚨 공격자: 트랜잭션 조작 후 해시도 다시 계산합니다...")
chain.chain[1].transactions = ["Alice → Bob: 5000 BTC"]
chain.chain[1].mine_block(chain.difficulty) # 다시 채굴!
print(f"블록 #1 새 해시: {chain.chain[1].hash[:20]}...")
print(f"블록 #2의 prev_hash: {chain.chain[2].prev_hash[:20]}...")
print(f"일치 여부: {chain.chain[1].hash == chain.chain[2].prev_hash}")
valid, msg = chain.is_chain_valid()
print(f"\n체인 유효성: {valid}")
print(f"상세 메시지: {msg}")
# 예상 출력:
🚨 공격자: 트랜잭션 조작 후 해시도 다시 계산합니다...
블록 #1 새 해시: 00f7c8a912b3e456...
블록 #2의 prev_hash: 0045de891fba2c37...
일치 여부: False
체인 유효성: False
상세 메시지: 블록 #2: 체인 연결 끊김
블록 #1을 다시 채굴했지만, 블록 #2가 기억하고 있는 prev_hash는 원래 블록 #1의 해시다. 새로 채굴한 해시와 맞지 않으니, 두 번째 검증 — prev_hash != previous.hash — 에서 걸린다.
이 연쇄 반응의 흐름을 다이어그램으로 보자:
이것이 **불변성(Immutability)**의 실체다. "바꿀 수 없다"가 아니다. "바꾸려면 나머지 전체를 네트워크보다 빠르게 재채굴해야 하는데, 그 비용이 천문학적이다"라는 뜻이다. 레슨 9에서 51% 공격을 깊이 다룰 텐데, 미리 감만 잡아두자.
🤔 생각해보세요: 비트코인의 현재 해시레이트는 약 600 EH/s(엑사해시/초)이다. 공격자가 51%를 확보하려면 약 300 EH/s가 필요하다. 최신 ASIC 채굴기(Antminer S21, 200 TH/s)로 이걸 달성하려면 몇 대가 필요할까?
답변 보기
300 EH/s = 300,000,000 TH/s. 한 대가 200 TH/s이니까 150만 대가 필요하다. 한 대 가격이 약 5,000달러라면 총 **75억 달러(약 10조 원)**의 장비 비용이 든다. 전기료는 별도. 이것이 PoW + 해시 체인이 만들어내는 경제적 보안이다. 해킹이 "기술적으로 불가능"한 게 아니라, "경제적으로 무의미"한 거다.
비용 분석: 한 블록 고치면 얼마나 드는가
불변성을 말로만 설명하면 와닿지 않는다. 숫자로 체감해보자.
# cost_analysis.py — 위변조 비용 추정
import time
from blockchain import Blockchain
# 블록 10개짜리 체인을 난이도 4로 생성
chain = Blockchain(difficulty=4)
for i in range(1, 11):
chain.add_block([f"트랜잭션 {i}"])
print(f"블록 10개 체인 생성 완료")
# 블록 #3을 조작하면 #3~#10까지 8개를 재채굴해야 한다
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 = ["조작된 트랜잭션!"]
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_to_remine}")
print(f"소요 시간: {elapsed:.2f}초")
print(f"블록당 평균: {elapsed/blocks_to_remine:.2f}초")
print(f"\n비트코인 환산 (난이도 ~80자리):")
print(f" 블록 1개 채굴: ~10분")
print(f" 8개 재채굴: ~80분")
print(f" 그 사이 정직한 네트워크는 8블록 추가 진행!")
# 예상 출력 (컴퓨터 성능에 따라 다름):
블록 10개 체인 생성 완료
재채굴 블록 수: 8
소요 시간: 3.47초
블록당 평균: 0.43초
비트코인 환산 (난이도 ~80자리):
블록 1개 채굴: ~10분
8개 재채굴: ~80분
그 사이 정직한 네트워크는 8블록 추가 진행!
이 숫자가 실전에서 어떤 의미를 갖는지 짚고 넘어가자. DeFi 보안 감사를 할 때 가장 먼저 확인하는 것 중 하나가 **체인의 확정성(finality)**이다. 이더리움에서는 보통 12 확인(confirmation)을 기다리라고 권장한다. 12블록 뒤의 트랜잭션을 뒤집으려면 12개 블록을 네트워크보다 빠르게 재채굴해야 하니까. 비트코인은 6 확인이 표준이고, 큰 금액일수록 더 많은 확인을 기다린다.
이 원리를 모르면 위험하다. 스마트 컨트랙트에서 "블록 1개 확인"만으로 결제를 처리하는 실수를 하게 되는데 — 실제로 이런 취약점으로 수천만 달러가 탈취된 사례가 있다.
전체 흐름 정리
체인의 생성부터 검증까지, 전체 프로세스를 시퀀스 다이어그램으로 정리하자:
자가 검토 체크리스트
코드를 작성했다면 다음을 확인하자:
- 제네시스 블록의
prev_hash가"0" * 64인가? -
add_block()이 마지막 블록의 해시를prev_hash로 사용하는가? -
is_chain_valid()가 해시 재계산, 체인 연결, PoW 세 가지를 모두 검증하는가? - 트랜잭션 변조 시 검증이
False를 반환하는가? -
mine_block()이add_block()안에서 호출되는가?
🔍 심화 학습: 시니어는 이렇게 다르게 한다
1. 머클 루트 검증 추가: 지금은 트랜잭션 리스트를 통째로 문자열화해서 해시에 포함시킨다. 실제 비트코인은 레슨 8에서 배울 머클 트리로 트랜잭션을 요약한 뒤, 그 루트 해시만 블록 헤더에 넣는다. 이렇게 하면 특정 트랜잭션이 블록에 포함되어 있는지를 전체를 다운로드하지 않고도 검증할 수 있다(SPV 검증).
2. 포크 처리: 두 채굴자가 동시에 블록을 찾으면? 체인이 일시적으로 갈라진다(포크). 실제 블록체인 노드는 "가장 긴 체인을 택한다"는 규칙으로 이를 해결한다. 우리 Blockchain 클래스에는 아직 이 로직이 없다.
3. 직렬화: 블록체인을 파일이나 네트워크로 전송하려면 JSON으로 직렬화해야 한다. to_dict(), from_dict() 메서드를 추가하면 된다. 레슨 10의 REST API에서 이걸 구현한다.
🔨 프로젝트 업데이트
지금까지 만든 전체 프로젝트 코드다. 레슨 1~5의 개념이 여기서 하나로 합쳐진다. 복사해서 바로 실행할 수 있다.
blockchain.py — 핵심 모듈 (이번 레슨에서 추가)
# blockchain.py — 블록 + 블록체인 통합 모듈
import hashlib
import time
class Block:
"""
레슨 4에서 설계한 블록 구조 + 레슨 5의 PoW 채굴 메서드
블록 헤더: index, timestamp, prev_hash, nonce, hash
블록 바디: transactions
"""
def __init__(self, index, transactions, prev_hash):
self.index = index
self.timestamp = time.time()
self.transactions = transactions # 트랜잭션 리스트 (레슨 3의 디지털 수표들)
self.prev_hash = prev_hash # 이전 블록 해시 (체인 연결 핵심!)
self.nonce = 0 # PoW용 넌스 (레슨 5)
self.hash = self.calculate_hash()
def calculate_hash(self):
"""블록 데이터 전체를 SHA-256으로 해싱 (레슨 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):
"""작업 증명: 선행 0 비트 맞출 때까지 넌스 탐색 (레슨 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:
"""
[레슨 6에서 새로 추가]
블록들을 체인으로 연결하고 무결성을 검증하는 핵심 클래스
"""
def __init__(self, difficulty=2):
self.difficulty = difficulty
self.pending_transactions = [] # 대기 중인 트랜잭션 (멤풀)
self.chain = [self._create_genesis_block()]
def _create_genesis_block(self):
"""체인의 첫 블록 — 이전 해시 없이 0으로 시작"""
genesis = Block(0, ["제네시스 블록"], "0" * 64)
genesis.mine_block(self.difficulty)
return genesis
def get_latest_block(self):
"""체인의 마지막 블록"""
return self.chain[-1]
def add_block(self, transactions):
"""새 블록: 이전 해시 연결 → PoW 채굴 → 체인에 추가"""
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):
"""
전체 체인 무결성 검증 — 3단계:
1) 해시 재계산 일치 여부
2) 이전 해시 연결 일치 여부
3) PoW 유효성
"""
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# 검증 1: 블록 데이터가 변조되지 않았는가?
if current.hash != current.calculate_hash():
return False, f"블록 #{current.index}: 해시 불일치 (데이터 변조 감지)"
# 검증 2: 체인 연결이 올바른가?
if current.prev_hash != previous.hash:
return False, f"블록 #{current.index}: 체인 연결 끊김"
# 검증 3: 작업 증명이 유효한가?
if current.hash[:self.difficulty] != "0" * self.difficulty:
return False, f"블록 #{current.index}: PoW 무효"
return True, "✅ 체인이 유효합니다"
def print_chain(self):
"""체인 전체를 보기 좋게 출력"""
for block in self.chain:
print(f"\n{'='*50}")
print(f"블록 #{block.index}")
print(f" 타임스탬프: {time.ctime(block.timestamp)}")
print(f" 트랜잭션: {block.transactions}")
print(f" 이전 해시: {block.prev_hash[:24]}...")
print(f" 현재 해시: {block.hash[:24]}...")
print(f" 넌스: {block.nonce}")
print(f"\n{'='*50}")
# === 직접 실행 시 데모 ===
if __name__ == "__main__":
print("🔗 미니 블록체인 생성 중...\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"\n체인 검증 결과: {msg}")
print(f"총 블록 수: {len(bc.chain)}")
tamper_demo.py — 위변조 탐지 데모 (이번 레슨에서 추가)
# tamper_demo.py — 블록체인 위변조 탐지 데모
from blockchain import Blockchain
def run_tamper_demo():
print("=" * 60)
print(" 🔒 블록체인 위변조 탐지 데모")
print("=" * 60)
# 1단계: 정상 체인 구축
print("\n[1단계] 정상 체인 구축 (난이도 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" 블록 수: {len(chain.chain)}")
valid, msg = chain.is_chain_valid()
print(f" 검증 결과: {msg}")
# 2단계: 블록 #1 데이터 위변조
print("\n[2단계] 🚨 공격: 블록 #1의 금액을 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_tx}")
print(f" 조작 트랜잭션: {chain.chain[1].transactions}")
print(f" 저장된 해시: {original_hash[:24]}...")
print(f" 재계산 해시: {chain.chain[1].calculate_hash()[:24]}...")
# 3단계: 검증
print("\n[3단계] 검증 실행")
valid, msg = chain.is_chain_valid()
print(f" 결과: {msg}")
# 4단계: 해시까지 다시 계산해도?
print("\n[4단계] 🚨 공격자가 해시도 다시 채굴!")
chain.chain[1].nonce = 0
chain.chain[1].hash = chain.chain[1].calculate_hash()
chain.chain[1].mine_block(chain.difficulty)
print(f" 블록 #1 새 해시: {chain.chain[1].hash[:24]}...")
print(f" 블록 #2 prev_hash: {chain.chain[2].prev_hash[:24]}...")
valid, msg = chain.is_chain_valid()
print(f" 결과: {msg}")
# 결론
print("\n" + "=" * 60)
print(" 💡 결론: 한 블록을 조작하면 뒤따르는 모든 블록의")
print(" prev_hash가 깨진다. 전부 재채굴하지 않는 한")
print(" 위변조는 즉시 탐지된다!")
print("=" * 60)
if __name__ == "__main__":
run_tamper_demo()
지금까지 만든 프로젝트를 실행해보세요:
# 블록체인 모듈 실행
python blockchain.py
# 위변조 탐지 데모 실행
python tamper_demo.py
# blockchain.py 예상 출력:
🔗 미니 블록체인 생성 중...
==================================================
블록 #0
타임스탬프: Thu Mar 27 14:32:01 2026
트랜잭션: ['제네시스 블록']
이전 해시: 000000000000000000000000...
현재 해시: 00a1b2c3d4e5f67890abcdef...
넌스: 127
==================================================
블록 #1
타임스탬프: Thu Mar 27 14:32:01 2026
트랜잭션: ['Alice → Bob: 50 BTC', 'Bob → Charlie: 25 BTC']
이전 해시: 00a1b2c3d4e5f67890abcdef...
현재 해시: 0045de891fba2c37ab012345...
넌스: 83
...
체인 검증 결과: ✅ 체인이 유효합니다
총 블록 수: 4
# tamper_demo.py 예상 출력:
============================================================
🔒 블록체인 위변조 탐지 데모
============================================================
[1단계] 정상 체인 구축 (난이도 2)
블록 수: 4
검증 결과: ✅ 체인이 유효합니다
[2단계] 🚨 공격: 블록 #1의 금액을 50 → 5000으로 조작!
원본 트랜잭션: ['Alice → Bob: 50 BTC']
조작 트랜잭션: ['Alice → Bob: 5000 BTC']
저장된 해시: 0045de891fba2c37ab01...
재계산 해시: 8f2a1bc3e7d94560f1de...
[3단계] 검증 실행
결과: 블록 #1: 해시 불일치 (데이터 변조 감지)
[4단계] 🚨 공격자가 해시도 다시 채굴!
블록 #1 새 해시: 00f7c8a912b3e456d789...
블록 #2 prev_hash: 0045de891fba2c37ab01...
결과: 블록 #2: 체인 연결 끊김
============================================================
💡 결론: 한 블록을 조작하면 뒤따르는 모든 블록의
prev_hash가 깨진다. 전부 재채굴하지 않는 한
위변조는 즉시 탐지된다!
============================================================
정리 다이어그램
오늘 배운 모든 것을 한 장으로:
다음 레슨 예고: 체인 구조를 완성했으니, 이제 "Alice의 잔고는 얼마인가?"라는 질문에 답할 차례다. 비트코인에는 계좌 잔액이 없다. 대신 **UTXO(미사용 트랜잭션 출력)**라는 독특한 모델로 잔고를 추적한다. 레슨 3에서 배운 트랜잭션의 입력(Input)과 출력(Output) 구조가 여기서 빛을 발한다.
난이도 포크
🟢 쉬웠다면
축하한다. 핵심만 짚자:
- 블록체인 = 해시로 연결된 블록 리스트
is_chain_valid()는 해시 재계산, 체인 연결, PoW 세 가지를 검증- 한 블록 변조 → 뒤의 모든 블록 재채굴 필요 → 경제적으로 불가능
다음 레슨에서는 UTXO 모델로 잔고를 추적한다. blockchain.py에 트랜잭션을 제대로 관리하는 기능이 추가될 예정이다.
🟡 어려웠다면
체인 구조를 다른 비유로 풀어보겠다.
봉인된 편지 비유: 편지 봉투에 이전 편지의 사진을 동봉한다고 상상하자. 편지 #1에는 편지 #0의 사진이 들어있고, 편지 #2에는 편지 #1의 사진이 들어있다. 누군가 편지 #1을 열어서 내용을 바꾸면? 편지 #2 안에 있는 "편지 #1의 원본 사진"과 달라지니까 즉시 발각된다.
is_chain_valid()는 이 사진 대조 작업을 자동으로 수행하는 것이다:
- 편지를 다시 찍어보고(
calculate_hash()), 봉투에 적힌 사진(hash)과 같은지 확인 - 다음 편지 안에 든 사진(
prev_hash)이 실제 이전 편지의 사진(previous.hash)과 같은지 확인
추가 연습: blockchain.py에 블록을 5개 추가하고, 3번째 블록의 데이터를 바꿔보자. is_chain_valid()가 정확히 3번째 블록에서 에러를 보고하는지 확인하자.
🔴 도전 과제
면접 문제: "블록체인의 불변성은 절대적인가? 어떤 조건에서 깨질 수 있는가?"
모범 답변 포인트:
- 51% 공격: 네트워크 해시파워의 과반을 확보하면 이론적으로 체인을 재작성할 수 있다
- 체인 재구성(reorg): 짧은 체인보다 긴 체인이 우선되므로, 공격자가 더 긴 비밀 체인을 만들면 기존 체인을 대체할 수 있다
- 경제적 한계: 비트코인 규모에서는 비용이 수십조 원이므로 실질적으로 불가능하지만, 작은 PoW 코인(ETC, BTG 등)에서는 실제로 51% 공격이 발생한 사례가 있다
코딩 과제: Blockchain 클래스에 replace_chain(new_chain) 메서드를 추가하라. 새 체인이 현재 체인보다 길고 유효할 때만 교체하는 "가장 긴 체인 규칙"을 구현하라. 이것이 분산 네트워크에서 합의를 이루는 기본 메커니즘이다.