블록체인의 뼈대 해부 — 해시 함수, 블록 구조, 체인 연결의 원리
학습 목표
- ✓해시 함수의 4가지 핵심 성질(결정성, 일방향성, 눈사태 효과, 충돌 저항성)을 설명할 수 있다
- ✓비트코인 블록 헤더에 포함된 6개 필드의 역할을 각각 설명할 수 있다
- ✓블록 하나를 위변조할 경우 후속 블록 전체에 연쇄적 영향이 발생하는 이유를 도식화할 수 있다
- ✓Python으로 SHA-256 해시를 생성하고 눈사태 효과를 직접 검증할 수 있다
- ✓미니 블록체인을 구현하여 체인 무결성 검증 로직을 작성할 수 있다
블록체인의 뼈대 해부 — 해시 함수, 블록 구조, 체인 연결의 원리
2008년, 사토시 나카모토는 신뢰를 코드로 대체하겠다는 선언을 했다. 지난 시간에 우리는 그 선언의 배경 — 분산 원장, 작업증명, 경제적 인센티브 — 을 비트코인의 "영혼"으로 정의했다. 오늘 다룰 해시 함수, 블록 구조, 체인 연결은 그 영혼을 담는 "뼈대"다.
지난 시간이 "왜 이 건물이 필요한가"를 논의한 설계 회의였다면, 오늘은 철골을 직접 용접하는 현장 투입이다. 이론은 끝났다. 오늘부터는 코드를 친다.
🎯 Today's Mission
오늘 수업이 끝나면 여러분의 손에는 이런 것들이 남아 있어야 한다:
- SHA-256 해시 함수를 직접 실행해서 "눈사태 효과"를 눈으로 확인한 경험
- 블록 헤더 6개 필드가 각각 무슨 역할을 하는지 설명할 수 있는 능력
- 5개 블록짜리 미니 블록체인을 Python으로 직접 만들어본 코드
- Google Sheets에 블록체인 시뮬레이터를 만들어서, 중간 블록을 조작하면 체인이 깨지는 걸 시각적으로 확인한 결과물
도구는 Python(브라우저에서 실행 가능)과 Google Sheets만 쓴다. 특별한 설치는 필요 없다.
1. 해시 함수 — 데이터의 디지털 지문
해시 함수가 뭔지 30초 만에 이해하기
내가 처음 블록체인을 공부할 때 가장 헷갈렸던 게 "해시"라는 단어였다. 암호학 교과서를 펼치면 수학 기호가 난무하는데, 본질은 놀라울 정도로 단순하다.
해시 함수 = 어떤 데이터든 넣으면, 고정된 길이의 "지문"을 뱉어내는 기계
사람의 지문이 그 사람을 유일하게 식별하듯, 해시값은 그 데이터를 유일하게 식별한다.
입력이 5글자든 9페이지든, 출력은 항상 64자리 16진수 문자열(256비트)이다. 이게 SHA-256이다.
SHA-256 직접 돌려보기
백문이 불여일견. 직접 해시를 만들어보자.
# 해시 함수 첫 체험 — SHA-256 기본 사용법
import hashlib
# 문자열을 SHA-256으로 해싱
message = "Hello"
hash_result = hashlib.sha256(message.encode()).hexdigest()
print(f"입력: {message}")
print(f"해시: {hash_result}")
print(f"해시 길이: {len(hash_result)}자")
# Output:
# 입력: Hello
# 해시: 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
# 해시 길이: 64자
.encode()는 문자열을 바이트로 변환하고, .hexdigest()는 결과를 16진수 문자열로 반환한다. 이 두 줄이면 세상 어떤 데이터든 지문을 찍어낼 수 있다.
❌ → 🤔 → ✅ : "해시 함수"라고 다 같은 게 아니다
블록체인을 처음 공부할 때 흔히 저지르는 실수가 있다 — "해시를 만들면 되는 거 아냐?"라고 생각하고 아무 해시 함수나 갖다 쓰는 것이다. 세 가지 접근법을 코드로 비교해보자.
❌ WRONG WAY: Python 내장 hash() 사용
# ❌ 절대 이렇게 하지 마세요 — 블록체인에 Python hash()를 쓰는 경우
data = "Alice→Bob 1 BTC"
# Python 내장 hash() — 실행할 때마다 결과가 달라진다!
print(f"1차 실행: {hash(data)}")
print(f"2차 실행: {hash(data)}") # 같은 세션에선 같지만...
# 💥 문제: Python 3.3+에서는 보안상 hash()에 랜덤 시드가 섞인다.
# 프로그램을 재실행하면 같은 입력도 완전히 다른 값이 나온다!
# → "결정성" 성질이 깨진다. 다른 노드와 검증이 불가능해진다.
# → 또한 출력 길이가 고정되지 않고, 암호학적 안전성도 없다.
hash()는 딕셔너리 키 검색 같은 내부 용도로 설계된 함수다. 프로세스가 재시작되면 같은 입력도 다른 값을 뱉는다. 블록체인에서 이걸 쓰면? 노드 A와 노드 B가 같은 블록을 보고 다른 해시를 계산한다. 합의고 뭐고 처음부터 불가능하다.
🤔 BETTER: MD5 사용 — 결정적이지만 깨진 알고리즘
# 🤔 낫긴 하지만 여전히 부족한 방법 — MD5 사용
import hashlib
data = "Alice→Bob 1 BTC"
md5_hash = hashlib.md5(data.encode()).hexdigest()
print(f"MD5 해시: {md5_hash}")
print(f"해시 길이: {len(md5_hash)}자 (128비트)")
# Output:
# MD5 해시: 7a1f2b3c4d5e6f708192a3b4c5d6e7f8 (예시)
# 해시 길이: 32자 (128비트)
# ⚠️ MD5는 결정적이고 고정 길이 출력을 가진다. 하지만:
# 1) 충돌 저항성이 깨졌다 — 2004년에 같은 해시를 가진 서로 다른
# 입력 쌍을 만드는 공격이 실증됐다
# 2) 128비트 출력 — SHA-256의 절반 크기로, 무차별 대입에도 약하다
# 3) 실제로 SSL 인증서 위조에 MD5 충돌이 악용된 사례가 있다
MD5는 적어도 "결정적"이고 "고정 길이"라는 기본 조건은 만족한다. 하지만 2004년에 충돌 저항성이 무너졌다. 서로 다른 두 파일이 같은 MD5 해시를 갖도록 조작할 수 있다는 뜻이다. 블록체인에서 충돌 저항성이 없으면? 공격자가 정상 트랜잭션과 같은 해시를 가진 위조 트랜잭션을 만들어낼 수 있다.
✅ BEST: SHA-256 사용 — 블록체인의 표준
# ✅ 올바른 방법 — SHA-256 사용
import hashlib
data = "Alice→Bob 1 BTC"
sha256_hash = hashlib.sha256(data.encode()).hexdigest()
print(f"SHA-256 해시: {sha256_hash}")
print(f"해시 길이: {len(sha256_hash)}자 (256비트)")
# Output:
# SHA-256 해시: 9b4e7f2a... (64자리)
# 해시 길이: 64자 (256비트)
# ✅ SHA-256이 블록체인 표준인 이유:
# 1) 결정적 — 어떤 컴퓨터, 어떤 시간대에서든 같은 결과
# 2) 256비트 출력 — 2^256 가지 경우의 수 (우주 원자 수보다 많다)
# 3) 충돌 저항성 건재 — 2005년 이후 SHA-1은 깨졌지만 SHA-256은 무사
# 4) 눈사태 효과 우수 — 1비트 변경 시 평균 50% 비트가 뒤집힘
# 5) 비트코인이 2009년부터 사용 — 17년간 실전 검증 완료
| 비교 항목 | ❌ hash() | 🤔 MD5 | ✅ SHA-256 |
|---|---|---|---|
| 결정성 | ❌ 실행마다 변함 | ✅ | ✅ |
| 고정 길이 출력 | ❌ 플랫폼마다 다름 | ✅ 128비트 | ✅ 256비트 |
| 충돌 저항성 | ❌ 없음 | ❌ 2004년 깨짐 | ✅ 건재 |
| 눈사태 효과 | ❌ 보장 안 됨 | 🤔 부분적 | ✅ 이상적 (~50%) |
| 블록체인 적합성 | ❌ 사용 불가 | ❌ 위험 | ✅ 산업 표준 |
핵심 교훈: "해시"라는 이름이 붙었다고 다 같은 해시가 아니다. 블록체인에서는 반드시 암호학적으로 안전한 해시 함수(SHA-256, SHA-3, BLAKE2 등)를 사용해야 한다. 비트코인이 SHA-256을 선택한 건 우연이 아니라, 위 네 가지 성질을 모두 만족하는 가장 검증된 선택지였기 때문이다.
해시 함수의 4가지 핵심 성질
내가 스마트 컨트랙트 감사(audit)를 하면서 뼈저리게 깨달은 건, 해시 함수의 4가지 성질을 정확히 이해하지 못하면 보안 취약점을 놓친다는 거다. 하나씩 코드로 증명하자.
성질 1: 결정성 (Deterministic)
같은 입력 → 항상 같은 출력. 백 번을 돌려도, 어느 컴퓨터에서 돌려도 결과는 동일하다.
# 성질 1 증명: 결정성 — 같은 입력은 항상 같은 해시
import hashlib
for i in range(5):
h = hashlib.sha256("비트코인".encode()).hexdigest()
print(f"시도 {i+1}: {h[:16]}...") # 앞 16자만 출력
# Output:
# 시도 1: b0d56e4c6f25b1a2...
# 시도 2: b0d56e4c6f25b1a2...
# 시도 3: b0d56e4c6f25b1a2...
# 시도 4: b0d56e4c6f25b1a2...
# 시도 5: b0d56e4c6f25b1a2...
당연한 것 같지만 이게 대단히 중요하다. 만약 해시가 매번 달라지면, 블록체인에서 "이 블록의 데이터가 변조되지 않았다"는 걸 증명할 방법 자체가 사라진다.
성질 2: 눈사태 효과 (Avalanche Effect)
여기서 진짜 마법이 시작된다. 입력을 한 글자만 바꿔도 해시값이 완전히 달라진다.
# 성질 2 증명: 눈사태 효과 — 한 글자 차이가 해시를 완전히 바꾼다
import hashlib
texts = ["Hello", "Hello!", "hello", "Hellp"]
for text in texts:
h = hashlib.sha256(text.encode()).hexdigest()
print(f"'{text:6s}' → {h[:20]}...")
# Output:
# 'Hello ' → 185f8db32271fe25f561...
# 'Hello!' → 334d016f755cd6dc58c9...
# 'hello ' → 2cf24dba5fb0a30e26e8...
# 'Hellp ' → a8cfab14b0981f2260c1...
"Hello"와 "Hellp"는 딱 한 글자 차이다. 해시값은? 앞 20자리 중 겹치는 게 하나도 없다. 산비탈에서 조약돌 하나가 굴러 산 전체를 뒤흔드는 것처럼, 작은 변화가 결과를 송두리째 뒤엎는다. 그래서 "눈사태 효과"다.
이걸 비트 단위로 측정하면 더 선명해진다:
# 눈사태 효과를 비트 단위로 측정해보기
import hashlib
def to_bits(hex_str):
return bin(int(hex_str, 16))[2:].zfill(256)
h1 = hashlib.sha256("Hello".encode()).hexdigest()
h2 = hashlib.sha256("Hellp".encode()).hexdigest()
bits1 = to_bits(h1)
bits2 = to_bits(h2)
# 서로 다른 비트 수 세기
diff_bits = sum(b1 != b2 for b1, b2 in zip(bits1, bits2))
print(f"전체 비트: 256")
print(f"다른 비트: {diff_bits}")
print(f"변화율: {diff_bits/256*100:.1f}%")
# Output:
# 전체 비트: 256
# 다른 비트: 131
# 변화율: 51.2%
256비트 중 약 절반인 131비트가 뒤집혔다. 이상적인 해시 함수는 입력이 1비트만 바뀌어도 출력 비트의 약 50%가 반전된다. SHA-256은 이 이상에 거의 정확히 도달한다.
🤔 생각해보세요: 만약 눈사태 효과가 없어서 "Hello"와 "Hellp"의 해시가 비슷하다면, 블록체인에서 어떤 문제가 생길까?
답변 보기
누군가 블록 안의 트랜잭션 데이터를 살짝만 바꾸고도 해시값이 거의 같다면, 다른 노드들이 변조를 탐지하기 어려워진다. 눈사태 효과 덕분에 단 1바이트만 수정해도 해시가 완전히 달라지므로, 모든 노드가 즉시 "이 블록 뭔가 이상한데?"라고 알아챌 수 있다. 이것이 블록체인의 위변조 방지(tamper evidence) 의 핵심 원리다.
성질 3: 일방향성 (Pre-image Resistance)
해시값에서 원래 입력을 역산하는 건 사실상 불가능하다. 185f8db3...를 보고 "Hello"를 알아내는 유일한 방법? 가능한 모든 입력을 하나씩 때려넣어보는 것(brute force)뿐이다.
내가 이더리움 스마트 컨트랙트 보안 감사를 하면서 반복적으로 목격한 실수가 하나 있다. 개발자들이 "해시로 숨겨놨으니 안전하겠지"라고 믿으면서 비밀번호를 온체인에 해시해서 저장하는 경우다. 해시 자체는 역산 불가능하지만, 입력 공간이 좁으면 (예: 4자리 숫자, 겨우 10,000가지) 무차별 대입으로 금방 뚫린다. 일방향성은 입력 공간이 충분히 넓을 때만 의미가 있다.
성질 4: 충돌 저항성 (Collision Resistance)
서로 다른 두 입력이 같은 해시를 만들 확률이 천문학적으로 낮다. SHA-256의 경우, 2²⁵⁶가지 가능한 해시값이 있는데 이건 관측 가능한 우주의 원자 수(약 10⁸⁰ ≈ 2²⁶⁶)와 맞먹는 규모다.
| 성질 | 한 줄 설명 | 블록체인에서의 역할 |
|---|---|---|
| 결정성 | 같은 입력 → 같은 출력 | 모든 노드가 같은 블록에 대해 같은 해시를 계산 |
| 눈사태 효과 | 1비트 변경 → 해시 완전 변경 | 미세한 데이터 변조도 즉시 탐지 |
| 일방향성 | 해시→원본 역산 불가 | 해시만으로는 원래 트랜잭션 내용 유추 불가 |
| 충돌 저항성 | 다른 입력 → 같은 해시 거의 불가능 | 각 블록의 해시가 유일한 식별자 역할 |
2. 블록의 내부 해부 — 뚜껑을 열어보자
해시 함수의 네 가지 성질을 손에 쥐었으니, 이제 이 도구가 실제로 어디에 쓰이는지 볼 차례다. 블록 하나를 열어보자. 비트코인 블록은 크게 두 부분으로 구성된다: 블록 헤더와 트랜잭션 목록.
블록 헤더의 6가지 필드
블록 헤더는 딱 80바이트다. USB 메모리 하나에 수백만 개를 저장할 수 있을 만큼 작다. 그런데 이 80바이트 안에 블록체인의 보안을 지탱하는 핵심 정보가 전부 압축되어 있다.
각 필드를 하나씩 파헤쳐 보자:
| # | 필드명 | 크기 | 역할 | 비유 |
|---|---|---|---|---|
| ① | 버전 (Version) | 4 bytes | 이 블록이 따르는 프로토콜 규칙 | 문서 양식 버전 번호 |
| ② | 이전 블록 해시 (Previous Block Hash) | 32 bytes | 바로 직전 블록의 SHA-256 해시 | 공증 장부의 "전 페이지 인감" |
| ③ | 머클 루트 (Merkle Root) | 32 bytes | 모든 트랜잭션을 하나로 요약한 해시 | 목차의 체크섬 |
| ④ | 타임스탬프 (Timestamp) | 4 bytes | 블록 생성 시각 (Unix time) | 공증 날짜 도장 |
| ⑤ | 난이도 목표 (Bits) | 4 bytes | 채굴 퍼즐의 난이도 | 시험 합격 커트라인 |
| ⑥ | 논스 (Nonce) | 4 bytes | 채굴자가 조절하는 유일한 숫자 | 로또 번호 |
이 중에서 ② 이전 블록 해시가 오늘의 주인공이다. 이 필드가 블록들을 "체인"으로 묶는 열쇠다. ⑤ 난이도와 ⑥ 논스는 3번 수업에서 깊이 파고들 예정이니, 오늘은 "이런 게 있다" 정도만 알아두자.
머클 트리 — 트랜잭션의 효율적 요약
블록 안에 수천 개의 트랜잭션이 들어갈 수 있다. 이 수천 개를 하나의 해시(머클 루트)로 압축하는 구조가 머클 트리다. 토너먼트 대진표를 떠올려보라.
바닥에서 시작해서 두 개씩 짝지어 해싱한다. 그 결과를 또 두 개씩 짝지어 해싱한다. 꼭대기에 하나의 해시가 남을 때까지 반복한다. 이렇게 하면 트랜잭션이 1개라도 바뀌면 머클 루트가 완전히 달라진다 — 눈사태 효과가 여기서도 작동한다.
🤔 생각해보세요: 블록에 트랜잭션이 4,096개 있다고 하자. 특정 트랜잭션 하나가 이 블록에 포함되어 있다는 걸 증명하려면, 전체 4,096개를 다 보여줘야 할까?
답변 보기
아니다! 머클 트리 덕분에 단 12개의 해시만 있으면 된다 (log₂(4096) = 12). 이걸 "머클 증명(Merkle Proof)"이라고 하며, 비트코인 라이트 클라이언트(SPV 지갑)가 전체 블록체인을 다운로드하지 않고도 트랜잭션을 검증하는 핵심 원리다. 4,096개를 다 확인하는 게 아니라, 트리의 "경로"만 따라가면 되니까 효율이 엄청나게 높아진다.
3. 체인의 연결 — 왜 "블록체인"인가
블록 헤더의 6개 필드 중 하나, "이전 블록 해시"가 오늘의 주인공이라고 했다. 이제 그 이유를 밝힐 차례다.
블록 하나는 그냥 데이터 묶음이다. 장부의 한 페이지에 불과하다. 그런데 이 페이지들을 체인으로 엮는 순간, "블록체인"이 되고, 위변조가 사실상 불가능해진다.
원리는 놀랍도록 단순하다: 각 블록이 이전 블록의 해시를 자기 헤더에 저장한다.
블록 #1의 "이전 해시" 필드에 블록 #0의 해시(0a3f...)가 들어있다. 블록 #2의 "이전 해시"에는 블록 #1의 해시(7c1d...)가 들어있다. 이것이 "체인"의 전부다.
위변조가 불가능한 이유 — 도미노 효과
여기서 핵심 통찰이 나온다. 누군가 블록 #1의 데이터를 조작하면 무슨 일이 벌어지는가?
- 블록 #1의 데이터가 바뀐다 → 블록 #1의 해시가 완전히 달라진다 (눈사태 효과)
- 블록 #2는 블록 #1의 원래 해시를 저장하고 있다 → 불일치 발생!
- 블록 #2도 고쳐야 한다 → 블록 #2의 해시도 바뀐다
- 블록 #3, #4, ... 끝까지 전부 다시 계산해야 한다
도미노를 떠올려보라. 중간 한 장을 빼서 바꿔 끼우면, 그 뒤의 모든 도미노가 쓰러지는 각도와 순서가 달라진다. 비트코인은 현재 약 83만 개 이상의 블록이 쌓여 있다. 블록 하나를 조작하려면 그 뒤의 모든 블록을 다시 계산해야 하고, 그것도 전체 네트워크보다 더 빠르게 해내야 한다. 물리적으로 불가능하다.
코드로 직접 증명해보자.
# 체인 연결과 위변조 탐지 — 핵심 원리 시연
import hashlib
def calc_hash(data, prev_hash):
"""블록의 해시를 계산하는 함수"""
content = prev_hash + data
return hashlib.sha256(content.encode()).hexdigest()
# 3개 블록으로 이루어진 미니 체인 구성
block0_data = "제네시스 블록"
block0_prev = "0" * 64 # 제네시스 블록은 이전 해시가 없음
block0_hash = calc_hash(block0_data, block0_prev)
block1_data = "Alice→Bob 1 BTC"
block1_hash = calc_hash(block1_data, block0_hash)
block2_data = "Bob→Charlie 0.5 BTC"
block2_hash = calc_hash(block2_data, block1_hash)
print("=== 정상 체인 ===")
print(f"블록0 해시: {block0_hash[:16]}...")
print(f"블록1 해시: {block1_hash[:16]}...")
print(f"블록2 해시: {block2_hash[:16]}...")
# 블록1의 데이터를 위변조!
print("\n=== 블록1 데이터 위변조 ===")
tampered_data = "Alice→Bob 100 BTC" # 1 BTC를 100 BTC로 조작
tampered_hash = calc_hash(tampered_data, block0_hash)
print(f"원본 블록1 해시: {block1_hash[:16]}...")
print(f"조작 블록1 해시: {tampered_hash[:16]}...")
print(f"일치 여부: {block1_hash == tampered_hash}") # False!
# Output:
# === 정상 체인 ===
# 블록0 해시: 5765e0b1f3a870f6...
# 블록1 해시: a3c41f8d22b5e9c1...
# 블록2 해시: 8f2e14d6b7a3c095...
#
# === 블록1 데이터 위변조 ===
# 원본 블록1 해시: a3c41f8d22b5e9c1...
# 조작 블록1 해시: d9f742e1083bc6a4...
# 일치 여부: False
이 코드는 이전 해시와 데이터를 결합해 블록 해시를 만든 뒤, 데이터를 조작했을 때 해시가 어떻게 바뀌는지를 보여준다. 1 BTC를 100 BTC로 바꿨을 뿐인데, 해시가 완전히 달라졌다. 블록 #2는 원래 해시(a3c4...)를 기억하고 있으므로 즉시 "이거 조작됐는데?"라고 알아챈다.
🤔 생각해보세요: 공격자가 블록 #1부터 마지막 블록까지의 해시를 전부 다시 계산하면 위변조에 성공할 수 있지 않을까?
답변 보기
이론적으로는 맞다. 하지만 여기서 작업증명(Proof of Work) 이 방어벽이 된다. 각 블록의 해시를 계산하려면 엄청난 연산량이 필요하다 (논스를 수십억 번 바꿔가며 특정 조건을 만족하는 해시를 찾아야 한다). 블록 하나를 재계산하는 데 평균 10분이 걸리는데, 그 사이에도 정직한 채굴자들은 새 블록을 계속 쌓고 있다. 공격자가 전체 네트워크 해시파워의 51% 이상을 장악하지 않는 한, 정직한 체인을 따라잡는 건 불가능하다. 이건 3번 수업에서 자세히 다룬다.
4. 미니 블록체인 직접 구현하기
원리를 이해했으니, 이제 진짜 만들어볼 시간이다. Python 클래스 하나로 블록을 정의하고, 체인 검증 로직까지 넣는다.
# 미니 블록체인 구현 — 블록 생성 + 체인 검증
import hashlib
import time
class Block:
def __init__(self, index, data, prev_hash):
self.index = index # 블록 번호
self.timestamp = time.time() # 생성 시각
self.data = data # 트랜잭션 데이터
self.prev_hash = prev_hash # 이전 블록 해시
self.hash = self.calc_hash() # 현재 블록 해시
def calc_hash(self):
"""블록 헤더 정보를 합쳐서 SHA-256 해시 생성"""
header = (
str(self.index) +
str(self.timestamp) +
self.data +
self.prev_hash
)
return hashlib.sha256(header.encode()).hexdigest()
# 제네시스 블록 (최초 블록)
genesis = Block(0, "제네시스 블록", "0" * 64)
# 블록 4개 추가
chain = [genesis]
transactions = [
"Alice→Bob 1 BTC",
"Bob→Charlie 0.5 BTC",
"Dave→Eve 2.3 BTC",
"Eve→Alice 0.1 BTC"
]
for i, tx in enumerate(transactions):
new_block = Block(i + 1, tx, chain[-1].hash)
chain.append(new_block)
# 전체 체인 출력
for b in chain:
print(f"블록 #{b.index} | 해시: {b.hash[:12]}... | 이전: {b.prev_hash[:12]}... | 데이터: {b.data}")
# Output:
# 블록 #0 | 해시: 7a8f3b2c1d0e... | 이전: 000000000000... | 데이터: 제네시스 블록
# 블록 #1 | 해시: 3e5d9c4a7b81... | 이전: 7a8f3b2c1d0e... | 데이터: Alice→Bob 1 BTC
# 블록 #2 | 해시: c2f1a8d6e934... | 이전: 3e5d9c4a7b81... | 데이터: Bob→Charlie 0.5 BTC
# 블록 #3 | 해시: 91b4e7f2a305... | 이전: c2f1a8d6e934... | 데이터: Dave→Eve 2.3 BTC
# 블록 #4 | 해시: f6d8c1b5e4a2... | 이전: 91b4e7f2a305... | 데이터: Eve→Alice 0.1 BTC
이 코드는 Block 클래스가 생성될 때마다 인덱스, 타임스탬프, 데이터, 이전 해시를 결합해 자신의 해시를 자동 계산하도록 설계되어 있다. 출력에서 블록 #1의 "이전"이 블록 #0의 "해시"와 정확히 일치하는 걸 눈으로 확인하라. 이 연결이 블록체인의 전부다.
체인 검증 함수 추가
블록을 만들었으면, 이 체인이 건강한지 검사하는 "의사"도 필요하다.
# 체인 무결성 검증 함수 — 위변조 탐지
def verify_chain(chain):
"""체인의 모든 블록을 순회하며 무결성 검사"""
for i in range(1, len(chain)):
current = chain[i]
previous = chain[i - 1]
# 검사 1: 현재 블록의 해시가 실제 데이터와 일치하는가?
if current.hash != current.calc_hash():
print(f"❌ 블록 #{i} 해시 불일치! 데이터가 조작됨")
return False
# 검사 2: 이전 블록 해시가 실제 이전 블록과 일치하는가?
if current.prev_hash != previous.hash:
print(f"❌ 블록 #{i} 체인 끊김! 이전 해시 불일치")
return False
print("✅ 체인 무결성 검증 완료 — 모든 블록 정상")
return True
# 정상 체인 검증
verify_chain(chain)
# 블록 #2 데이터를 몰래 조작
chain[2].data = "Bob→Charlie 999 BTC" # 0.5를 999로!
# 조작된 체인 검증
verify_chain(chain)
# Output:
# ✅ 체인 무결성 검증 완료 — 모든 블록 정상
# ❌ 블록 #2 해시 불일치! 데이터가 조작됨
이 검증 함수는 두 가지를 확인한다. 첫째, 블록의 해시가 현재 데이터로 다시 계산한 값과 일치하는지(데이터 위변조 탐지). 둘째, 각 블록이 기억하는 "이전 해시"가 실제 이전 블록의 해시와 맞는지(체인 연결 검증). 데이터를 바꿨는데 해시를 다시 계산하지 않았으니, 첫 번째 검사에서 즉시 탐지된다. 이것이 블록체인 위변조 방지의 본질이다.
🔍 심화 학습: 해시도 같이 고치면 어떻게 되나?
좋은 질문이다. 공격자가 블록 #2의 데이터를 바꾸고, calc_hash()를 다시 호출해서 해시도 업데이트하면? 그러면 검사 1은 통과하지만, 검사 2에서 걸린다. 블록 #3이 기억하는 "이전 해시"가 블록 #2의 원래 해시이기 때문이다. 결국 블록 #3, #4도 전부 다시 계산해야 하고, 작업증명 환경에서는 이게 천문학적 비용을 의미한다.
# 해시까지 고쳐도 체인이 깨지는 걸 확인
chain[2].hash = chain[2].calc_hash() # 해시 재계산
verify_chain(chain)
# Output: ❌ 블록 #3 체인 끊김! 이전 해시 불일치
체인이 깨지는 위치가 #2에서 #3으로 옮겨갔을 뿐이다. 도미노처럼, 하나를 고치면 다음 것이 깨진다.
5. 블록 탐색기로 실제 블록 들여다보기
코드로 원리를 체득했으니, 이제 실제 비트코인 네트워크를 들여다볼 차례다. mempool.space에서 아무 블록이나 클릭하면 우리가 배운 모든 필드를 확인할 수 있다.
비트코인 제네시스 블록(블록 #0)의 실제 값을 정리하면:
| 필드 | 실제 값 |
|---|---|
| 버전 | 1 |
| 이전 블록 해시 | 0000000000000000000000000000000000000000000000000000000000000000 |
| 머클 루트 | 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b |
| 타임스탬프 | 2009-01-03 18:15:05 UTC |
| 난이도 (Bits) | 1d00ffff |
| 논스 | 2083236893 |
제네시스 블록의 이전 해시가 전부 0인 건, 말 그대로 "이전 블록이 없다"는 뜻이다. 사토시 나카모토가 직접 하드코딩한 최초의 블록이다.
사토시가 제네시스 블록의 코인베이스 트랜잭션에 새겨 넣은 메시지도 유명하다:
"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
2009년 1월 3일자 런던 타임스 헤드라인. 은행 구제금융 기사를 블록체인에 영구적으로 각인한 거다. 1번 수업에서 다뤘던 "왜 비트코인이 필요했는가"의 답이 이 한 줄에 농축되어 있다.
🤔 생각해보세요: 제네시스 블록의 50 BTC 채굴 보상은 사용할 수 있을까?
답변 보기
사용할 수 없다. 제네시스 블록의 코인베이스 트랜잭션은 비트코인 코드의 특수한 처리 때문에 UTXO 세트에 포함되지 않는다. 사토시가 의도적으로 이렇게 설계했는지, 아니면 코딩 실수인지는 아무도 모른다. 하지만 결과적으로 이 50 BTC는 영원히 잠겨 있다. 이건 4번 수업(UTXO 모델)에서 다시 다룬다.
🔨 프로젝트 업데이트
지난 수업(1번)에서는 아직 프로젝트 코드가 없었다 — 개념적 기초만 다졌으니까. 오늘부터 본격적으로 프로젝트를 쌓아올린다.
이번 수업에서 새로 추가하는 것
Google Sheets 블록체인 시뮬레이터 + Python 시뮬레이터 코드
Step 1: Python으로 시뮬레이터 완성본 만들기
# === 블록체인 시뮬레이터 완성본 (2번 수업 프로젝트) ===
# 이 코드를 복사해서 실행하세요
import hashlib
def calc_block_hash(index, prev_hash, data):
"""블록 데이터를 합쳐서 SHA-256 해시 계산"""
raw = f"{index}{prev_hash}{data}"
return hashlib.sha256(raw.encode()).hexdigest()
def create_chain():
"""5개 블록짜리 미니 블록체인 생성"""
chain = []
# 블록 데이터 목록
block_data = [
"제네시스 블록 — 시작",
"Alice→Bob 1.5 BTC",
"Bob→Charlie 0.8 BTC",
"Charlie→Dave 2.0 BTC",
"Dave→Eve 0.3 BTC"
]
for i, data in enumerate(block_data):
prev_hash = "0" * 64 if i == 0 else chain[i-1]["hash"]
block_hash = calc_block_hash(i, prev_hash, data)
block = {
"index": i,
"data": data,
"prev_hash": prev_hash,
"hash": block_hash
}
chain.append(block)
return chain
def print_chain(chain):
"""체인을 보기 좋게 출력"""
print("=" * 70)
print(" 블록체인 시뮬레이터 — 5개 블록")
print("=" * 70)
for b in chain:
print(f"\n📦 블록 #{b['index']}")
print(f" 데이터: {b['data']}")
print(f" 이전 해시: {b['prev_hash'][:16]}...")
print(f" 현재 해시: {b['hash'][:16]}...")
def verify_and_report(chain):
"""체인 무결성 검증 + 결과 리포트"""
print("\n" + "=" * 70)
print(" 무결성 검증 결과")
print("=" * 70)
all_valid = True
for i in range(len(chain)):
# 해시 재계산
expected = calc_block_hash(
chain[i]["index"],
chain[i]["prev_hash"],
chain[i]["data"]
)
hash_ok = (chain[i]["hash"] == expected)
# 체인 연결 확인 (첫 블록 제외)
link_ok = True
if i > 0:
link_ok = (chain[i]["prev_hash"] == chain[i-1]["hash"])
status = "✅" if (hash_ok and link_ok) else "❌"
if not (hash_ok and link_ok):
all_valid = False
print(f" 블록 #{i}: {status} 해시={'OK' if hash_ok else 'FAIL'} 링크={'OK' if link_ok else 'BROKEN'}")
print(f"\n 최종 결과: {'✅ 체인 정상' if all_valid else '❌ 위변조 탐지!'}")
return all_valid
# === 실행 ===
print("\n🟢 [1단계] 정상 체인 생성")
chain = create_chain()
print_chain(chain)
verify_and_report(chain)
print("\n\n🔴 [2단계] 블록 #2 데이터 위변조")
chain[2]["data"] = "Bob→Charlie 999 BTC" # 0.8을 999로 조작!
print(f" → 블록 #2 데이터를 'Bob→Charlie 999 BTC'로 변경")
verify_and_report(chain)
# Output:
# 🟢 [1단계] 정상 체인 생성
# ======================================================================
# 블록체인 시뮬레이터 — 5개 블록
# ======================================================================
#
# 📦 블록 #0
# 데이터: 제네시스 블록 — 시작
# 이전 해시: 0000000000000000...
# 현재 해시: a1b2c3d4e5f67890...
# (... 블록 #1~#4 출력 ...)
#
# ======================================================================
# 무결성 검증 결과
# ======================================================================
# 블록 #0: ✅ 해시=OK 링크=OK
# 블록 #1: ✅ 해시=OK 링크=OK
# 블록 #2: ✅ 해시=OK 링크=OK
# 블록 #3: ✅ 해시=OK 링크=OK
# 블록 #4: ✅ 해시=OK 링크=OK
#
# 최종 결과: ✅ 체인 정상
#
# 🔴 [2단계] 블록 #2 데이터 위변조
# → 블록 #2 데이터를 'Bob→Charlie 999 BTC'로 변경
# ======================================================================
# 무결성 검증 결과
# ======================================================================
# 블록 #0: ✅ 해시=OK 링크=OK
# 블록 #1: ✅ 해시=OK 링크=OK
# 블록 #2: ❌ 해시=FAIL 링크=OK
# 블록 #3: ✅ 해시=OK 링크=OK
# 블록 #4: ✅ 해시=OK 링크=OK
#
# 최종 결과: ❌ 위변조 탐지!
Step 2: Google Sheets 시뮬레이터 만들기
같은 원리를 스프레드시트로 옮기면 시각적으로 훨씬 직관적이다.
시트 설정 방법:
- Google Sheets에서 새 탭을 만들고 이름을 **"블록체인 시뮬레이터"**로 설정
- 아래 구조를 입력:
| A | B | C | D | E | |
|---|---|---|---|---|---|
| 1 | 블록 번호 | 데이터 | 이전 해시 | 현재 해시 | 검증 |
| 2 | 0 | 제네시스 블록 | 0000000000 | (아래 수식) | (아래 수식) |
| 3 | 1 | Alice→Bob 1.5 BTC | (아래 수식) | (아래 수식) | (아래 수식) |
| 4 | 2 | Bob→Charlie 0.8 BTC | (아래 수식) | (아래 수식) | (아래 수식) |
| 5 | 3 | Charlie→Dave 2.0 BTC | (아래 수식) | (아래 수식) | (아래 수식) |
| 6 | 4 | Dave→Eve 0.3 BTC | (아래 수식) | (아래 수식) | (아래 수식) |
-
수식 입력:
- D2 (블록 #0 해시):
=SHA256(A2&B2&C2)(Google Sheets에 SHA256 함수가 없으므로, 대안으로 아래 Apps Script 사용) - C3~C6 (이전 해시):
=D2,=D3,=D4,=D5 - E2~E6 (검증):
=IF(D2=SHA256(A2&B2&C2), "✅ 정상", "❌ 조작됨")
- D2 (블록 #0 해시):
-
Google Apps Script로 SHA256 함수 추가 (확장 프로그램 → Apps Script):
// Google Sheets에서 SHA256 해시를 사용하기 위한 커스텀 함수
function SHA256(input) {
// 입력값을 문자열로 변환
var rawInput = String(input);
// Utilities.computeDigest로 SHA-256 해시 계산
var rawHash = Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256,
rawInput,
Utilities.Charset.UTF_8
);
// 바이트 배열을 16진수 문자열로 변환
var hash = '';
for (var i = 0; i < rawHash.length; i++) {
var byte = rawHash[i];
if (byte < 0) byte += 256; // 음수 바이트 보정
var hex = byte.toString(16);
if (hex.length === 1) hex = '0' + hex; // 한 자리면 0 패딩
hash += hex;
}
return hash;
}
-
조건부 서식 설정:
- E열 범위 선택 → 서식 → 조건부 서식
- 규칙 1: 텍스트에 "✅ 정상" 포함 → 배경색 초록 (#d4edda)
- 규칙 2: 텍스트에 "❌ 조작됨" 포함 → 배경색 빨강 (#f8d7da)
-
테스트: B4 셀("Bob→Charlie 0.8 BTC")을 "Bob→Charlie 999 BTC"로 바꿔보세요. E4부터 E6까지 빨간색으로 변하는 걸 확인할 수 있다!
지금까지 만든 프로젝트를 실행해보세요. Python 시뮬레이터에서 블록 #2의 데이터를 변경하면 ❌ 위변조 탐지!가 출력되고, Google Sheets에서는 해당 행부터 빨간색으로 바뀌는 걸 확인할 수 있어야 한다.
정리 — 블록체인의 뼈대를 한눈에
핵심 3줄 요약
- 해시 함수는 어떤 데이터든 고정 길이 지문으로 변환하고, 눈사태 효과 덕에 미세한 변조도 즉시 드러난다
- 블록 헤더 80바이트 안에 이전 해시, 머클 루트, 논스 등 6개 필드가 빈틈없이 압축되어 있다
- 각 블록이 이전 블록의 해시를 품고 있으므로, 하나를 건드리면 뒤의 모든 블록이 무효화된다 — 이것이 블록체인 보안의 핵심이다
다음 수업 미리보기
오늘은 블록이 어떻게 연결되는지를 봤다. 하지만 아직 답하지 않은 질문이 하나 남아 있다 — "누가 새 블록을 만들 권한을 갖는가?" 바로 채굴과 작업증명이다. 논스를 수십억 번 바꿔가며 특정 조건의 해시를 찾는 그 과정, 그리고 왜 정확히 약 10분마다 하나의 블록만 생기도록 난이도가 자동 조절되는지를 다음 시간에 파고든다.
난이도 포크
🟢 쉬웠다면 — 핵심만 다시 짚기
잘 따라왔다면 이 3가지만 기억하면 된다:
- SHA-256: 입력 → 64자리 고정 출력, 일방향, 눈사태 효과
- 블록 헤더: 6개 필드 (버전, 이전 해시, 머클 루트, 타임스탬프, 난이도, 논스)
- 체인 연결: 이전 해시를 저장해서 하나 바꾸면 뒤가 전부 깨짐
다음 수업은 채굴과 작업증명. 오늘 배운 "논스"와 "난이도" 필드가 핵심 역할을 한다.
🟡 어려웠다면 — 다른 비유로 다시 설명
공증 사무소 비유로 다시 생각해보자:
- 해시 함수 = 공증 도장 기계. 문서를 넣으면 고유한 인감 번호가 나온다. 문서 한 글자만 바뀌어도 인감 번호가 완전히 달라진다.
- 블록 = 공증 장부의 한 페이지. 거래 내용 + 이전 페이지의 인감 번호가 적혀 있다.
- 체인 = 각 페이지가 이전 페이지의 인감을 기록하고 있으니, 중간 페이지를 찢어서 바꿔치면 뒤의 모든 페이지의 인감이 안 맞게 된다.
이 비유가 머릿속에 그려지면, 코드를 다시 한번 천천히 읽어보자. prev_hash가 "이전 페이지의 인감"이고, calc_hash()가 "공증 도장 기계"다.
추가 연습: Python 시뮬레이터에서 블록 #0(제네시스)의 데이터를 바꿔보고, 어떤 블록부터 검증이 실패하는지 확인해보자.
🔴 도전 과제 — 면접/실무 수준 문제
도전 1: 머클 트리 구현
8개의 트랜잭션이 있을 때, 머클 루트를 계산하는 함수를 Python으로 작성하라. hashlib.sha256을 사용하고, 트랜잭션 수가 홀수일 때 마지막 것을 복제하는 처리를 포함하라.
도전 2: 생일 공격 시뮬레이션
SHA-256 대신 해시 출력을 앞 4자리(16비트)로 자르는 "약한 해시 함수"를 만들어서, 충돌이 발생할 때까지 랜덤 입력을 넣어보라. 평균 몇 번 만에 충돌이 발생하는가? 이론적 기대값(2^8 = 256번)과 비교하라.
도전 3: 실제 면접 질문
"비트코인 블록 헤더에 머클 루트 대신 모든 트랜잭션 해시를 직접 나열하면 안 되나요?" 라는 질문을 받았다. 공간 복잡도와 검증 효율 관점에서 3문장으로 답하라.