Rust로 만든 비트코인 엔진을 WebAssembly로 컴파일해 브라우저에서 직접 돌려보는 학습 도구
아래 6개 부품이 어떻게 맞물리는지 차례대로 직접 만져보면, "비트코인이 왜 위조가 안 되는가, 주인 없이도 왜 돌아가는가"가 손에 잡혀요.
src/ 의 Rust 엔진이 합니다.cargo run 으로 돌던 그 엔진이 그대로 웹에서 돕니다.block.rs(해시·채굴) · transaction.rs(거래) · blockchain.rs(체인·잔액·검증) · utxo.rs(UTXO·입력별 서명) · wallet.rs(키·ECDSA) · merkle.rs(머클트리) · header.rs(80B 헤더) · network.rs(P2P 합의) · attack.rs(이중지불) · wasm_api.rs(브라우저 다리)아무 글자나 입력해 보세요. 입력이 1바이트든 1메가든, 결과는 항상 64자리 16진수(256비트)입니다.
한 글자만 바꿔도 결과의 절반 가까운 비트가 뒤집힙니다. 두 입력을 비교해 보세요.
비트코인의 핵심 아이디어예요. 각 블록은 자기 데이터뿐 아니라 바로 앞 블록의 해시까지 함께 SHA-256에 넣습니다. 그래서 블록들이 사슬(chain)처럼 이어져요 — 이게 블록체인이라는 이름의 유래죠.
👉 아래 블록의 데이터를 고치거나 ⛏ 채굴을 눌러보세요. 그 블록 해시가 바뀌면, 그걸 품고 있던 뒤 블록들의 해시가 줄줄이 다시 계산돼요(주황색 반짝임). 채굴해둔 뒤 블록이 있었다면 0으로 시작하던 게 깨지는 것도 보입니다.
자주 나오는 얘기예요. 근데 두 가지 위협을 나눠서 봐야 정확합니다.
그러니까 양자 컴퓨터가 노리는 건 "해시 연결"보다 "서명" 쪽이에요. 대비책으로 양자내성 서명(post-quantum)으로 갈아타자는 논의가 이미 진행 중이고, 무엇보다 그만한 규모의 양자 컴퓨터는 아직 존재하지 않습니다.
블록 데이터는 그대로 두고 nonce(숫자)만 0,1,2,… 바꿔가며 해시를 다시 계산해요. 해시가 0이 N개로 시작하면 성공. 정답을 찾는 빠른 방법은 없고 오직 무차별 대입뿐입니다.
한 줄 = 한 번의 시도예요. 데이터 + nonce 를 SHA-256에 넣어 본 결과죠. 앞에 0이 모자라면 ❌, 목표만큼 0으로 시작하면 ✅. 정답을 미리 알 방법이 없어서 0번부터 하나씩 무작정 대입하는 게 전부입니다.
여긴 브라우저에서 즉시 체감하려고 난이도를 확 낮췄어요(1~6). 실제 비트코인은 이렇게 다릅니다:
그래서 이 실험실 속도(초당 수만~수십만)로는 우주 나이로도 못 찾아요. 전 세계 채굴기를 합치면 특수 장비(ASIC)로 초당 약 10²⁰번(= 1억 × 1조) 규모의 해시를 돌려서, 그걸로 평균 10분마다 블록 하나를 캡니다.
→ target · double SHA-256 · 80바이트 헤더의 정밀한 버전은 탭 5 · 블록 해부에서 실제 hex까지 직접 볼 수 있어요.
프리셋을 고르거나 값을 직접 바꾼 뒤 엔진을 시작하세요. (제네시스 블록이 채굴되며 시작됩니다)
보내는 사람의 잔액이 부족하면 엔진이 거부해요(이중지불 방지).
채굴 보상(코인베이스)과 거래로 잔액이 바뀝니다.
멤풀의 거래들을 모아 새 블록을 채굴합니다. 맨 앞에는 채굴 보상(코인베이스) 거래가 자동으로 들어가요.
위에서 블록을 조작하면 검증에 걸리죠. 그럼 공격자가 조작한 블록부터 다시 채굴하면 되지 않을까요? 문제는 그러는 동안에도 정직한 네트워크는 계속 블록을 쌓는다는 거예요. 그래서 공격자는 따라잡는 게 아니라 추월해야 합니다.
CLI에서 println! 으로 찍히던 그 메시지를 브라우저로 그대로 가져온 거예요.
3번 탭은 계좌식(Alice=20, Bob=10처럼 잔액을 기억)이었어요. 하지만 진짜 비트코인은 잔액을 저장하지 않고, "아직 안 쓴 거래 출력(UTXO)" 조각들만 둡니다. 지갑이 그걸 다 더해 잔액을 계산할 뿐이죠.
발행/송금에 처음 등장하는 이름마다 개인키·공개키 한 쌍이 자동 생성돼요. 주소는 공개키를 해시해 만든 값이고, 동전은 이 주소에 잠깁니다. 개인키는 비밀(엔진 내부에만 존재).
공개키·개인키는 타원곡선 위에서 하는 "점 점프 놀이"예요. 시작점 G에서 정해진 규칙(선 긋고 → 만나는 점 → 위아래 뒤집기)으로 점프하는데 — 몇 번 점프했는지 = 개인키(비밀), 최종 착지점 = 공개키(공개). 버튼을 눌러 한 번씩 더해보세요.
위 "점프" 그림이 바로 ②단계(개인키→공개키)예요. 전체를 이으면 난수(개인키) → 타원곡선 곱 → 공개키 → 해시 → 주소. 이더리움도 같은 secp256k1 곡선을 써서 ①②는 완전히 같고, 주소 만들 때 해시 함수만 달라요.
| 단계 | 비트코인 | 이더리움 |
|---|---|---|
| ① 개인키 | 256비트 난수 | 256비트 난수 동일 |
| ② 공개키 | 개인키 × G (secp256k1) | 개인키 × G 같은 곡선 |
| ③ 주소 | RIPEMD160(SHA256(pub)) → Base58 | Keccak256(pub) 끝 20B → 0x… 해시만 다름 |
| 서명 | ECDSA (secp256k1) | ECDSA 동일 |
| 시드 문구 | BIP-39 (12/24단어) | BIP-39 동일 |
| 계정 경로 | m/44'/0'/… | m/44'/60'/… 숫자만 다름 |
| 저장 위치 | wallet.dat · 하드웨어 칩 | 브라우저 확장(암호화) · 하드웨어 칩 |
개인키는 블록체인에 없어요. 체인엔 UTXO·주소·트랜잭션·서명만 올라갑니다. 개인키는 내 지갑 안에만 있어요 — 노드도 내 개인키를 모릅니다(알면 누구나 내 돈을 훔치니까).
wallet.dat 파일에 보관(암호화 가능).서명은 "내 기기 안에서" 일어나요. 지갑이 로컬에서 개인키로 거래에 서명 → 체인엔 서명 + 거래만 전송 → 노드는 공개키로 검증만. "서명은 나만, 검증은 누구나"의 비대칭이죠. (이더리움도 완전히 동일한 방식이에요.)
UTXO가 있어야 보낼 수 있어요. 먼저 누군가에게 새 코인을 만들어 줍니다.
큰 동전부터 골라 목표+수수료 이상이 될 때까지 소비하고, 거스름돈을 돌려받아요.
공격자가 피해자의 UTXO를 자기 앞으로 보내는 거래를 만들어 봅니다. 하지만 공격자는 피해자의 개인키가 없어요. 자기 키로 서명할 수밖에 없고, 그 키의 주소는 피해자 주소와 다르므로 검증에서 거부됩니다.
주소별로 묶었어요. 각 칩 = UTXO 한 개. 파란 테두리는 거스름돈으로 생긴 UTXO예요. 잔액은 칩들의 합으로 계산됩니다(저장 아님).
2·3번 탭에선 설명을 위해 블록 전체를 문자열로 이어붙여 해시했어요. 하지만 진짜 비트코인은 다릅니다. 거래 수천 건은 머클트리로 32바이트 머클루트 하나로 요약하고, 채굴자는 딱 80바이트 헤더만 double SHA-256으로 반복 해시하죠. 그리고 난이도는 "0의 개수"가 아니라 target이라는 256비트 숫자보다 작은가로 판정해요.
거래를 한 줄에 하나씩 적어요(내용은 자유). 각 거래를 double SHA-256해 잎(leaf)을 만들고, 둘씩 이어붙여 해시하길 반복해 꼭대기의 루트 하나로 압축합니다. 노드 수가 홀수면 마지막 노드를 복제해 짝을 맞춰요.
머클루트와 직전 블록 해시, 난이도를 넣어 헤더를 만듭니다. 헤더는 정확히 6필드 · 80바이트예요.
지금까지의 탭은 전부 노드 1대의 내부 동작이었어요. 하지만 진짜 비트코인은 수만 대의 노드가 각자 자기 체인을 들고, 서로 블록을 주고받아요. 여기서 '합의'는 노드끼리 투표하거나 서로 승인해 주는 게 아니에요 — 각 노드가 받은 블록을 혼자서 규칙대로 검증하고(위반이면 그냥 버림), 통과한 체인들 중 가장 긴(누적 작업량이 큰) 걸 스스로 채택할 뿐이죠. 중앙 서버도, 심판도, 투표도 없어요. 딱 하나의 규칙으로 정리됩니다 — 가장 길고 유효한 체인이 이긴다.
방금 봤듯 합의는 '투표'가 아니에요. 그런데 왜 투표로 하면 안 되고, 굳이 전기까지 태우는 채굴이어야 했을까요? 여기에 비트코인의 진짜 발명이 숨어 있어요.
각 노드에서 채굴해 블록을 붙이고, 방송으로 이웃에게 퍼뜨려 보세요. 두 노드에서 각각 캐면 포크가 생기고, 한쪽을 더 길게 만든 뒤 방송하면 재구성(reorg)이 일어납니다.
한마디로 "돈 내고 물건 받은 뒤, 그 결제 기록을 몰래 지워버리는" 사기예요. 같은 코인을 두 번 쓰는 거죠.
아래 시뮬레이터에서 공개 체인(모두가 아는 진짜 기록)과 공격자의 비밀 체인을 직접 경쟁시켜, 결제가 어떻게 증발하는지 볼 수 있어요.
컨펌 = 내 결제가 담긴 블록 위에 쌓인 블록 수. 판매자가 물건을 보내기 전에 이걸 더 기다릴수록, 공격자가 결제를 뒤집으려고 따라잡아야 할 거리가 멀어져요. 공격자의 비밀 체인이 정직한 체인보다 더 길어져야 뒤집을 수 있는데, 기다리는 동안 정직한 체인이 계속 앞서 나가기 때문이죠.
51% 공격에도 한계가 있어요. 결론부터: 갓 결제된 최근 몇 블록은 노려볼 수 있어도, 깊은 과거(제네시스 근처)는 사실상 절대 불가능합니다. 아래에서 공격자 해시파워와 컨펌 수를 바꿔보면 왜 그런지 숫자로 보여요. (모든 계산은 Rust 엔진이 합니다.)
결론부터: "하루아침에 붕괴"는 아니지만, 특정 조건의 코인엔 실질 위협이에요. 핵심은 딱 두 알고리즘이에요 — 쇼어(Shor)는 서명을 깨고(치명적), 그로버(Grover)는 해시를 조금 빠르게 할 뿐(제한적). 이 사이트에서 본 secp256k1 서명(탭 4)이 쇼어의 표적, SHA-256(탭 1·2·5)이 그로버의 표적입니다.
가르는 기준은 딱 하나 — 공개키가 체인에 드러나 있느냐. 일반 주소는 공개키를 해시 뒤에 숨겨 두기 때문에, "한 번도 출금 안 한" 주소는 (현재로선) 안전해요.
비트코인이 돈의 장부라면, 이더리움은 거기에 프로그램(스마트 컨트랙트)을 올려 두는 장부입니다. 한번 배포된 코드는 아무도 못 바꾸고, 조건이 맞으면 자동으로 실행돼요. 합의는 The Merge 이후 Proof of Stake — validator가 32 ETH를 걸고 블록을 제안·검증합니다.
이더리움에서 만나는 거의 모든 id — 주소·함수 셀렉터·이벤트 topic·컨트랙트 주소 — 는 이 해시 하나에서 나옵니다. 비트코인의 SHA-256과 같은 자리인데, 이더는 Keccak-256을 써요. 먼저 직접 해시해 보세요.
이더리움이 쓰는 건 표준 확정 이전의 원조 Keccak이라, NIST가 확정한 SHA3-256과 패딩 바이트 하나가 다릅니다. 같은 입력인데 결과가 완전히 달라요.
Keccak-256("") = c5d2460186f7233c…5d85a470 (패딩 0x01)
SHA3-256("") = a7ffc6f8bf1ed766…c5f8dd9a (패딩 0x06)
라이브러리에서 sha3_256을 부르면 이더리움 주소가 안 나오는 이유예요. 반드시 keccak256을 써야 합니다.
지갑 주소는 공개키를 Keccak한 뒤 끝 20바이트만 잘라 만듭니다. (이 데모는 공개키 대신 이름을 해시 — 규칙은 동일)
지갑 만들기는 난수(개인키) → 타원곡선 곱 → 공개키 → 해시 → 주소가 전부예요. 앞 두 단계(개인키→공개키)는 비트코인과 완전히 동일하고(같은 secp256k1 곡선), 주소 만들 때 해시 함수만 달라요. 이 "타원곡선 곱(점프)"을 그림으로 보고 싶으면 Bitcoin 탭 4를 보세요.
| 단계 | 비트코인 | 이더리움 |
|---|---|---|
| ① 개인키 | 256비트 난수 | 256비트 난수 동일 |
| ② 공개키 | 개인키 × G (secp256k1) | 개인키 × G 같은 곡선 |
| ③ 주소 | RIPEMD160(SHA256(pub)) → Base58 | Keccak256(pub) 끝 20B → 0x… 해시만 다름 |
| 서명 | ECDSA (secp256k1) | ECDSA 동일 |
| 시드 문구 | BIP-39 (12/24단어) | BIP-39 동일 |
| 계정 경로 | m/44'/0'/… | m/44'/60'/… 숫자만 다름 |
| 저장 위치 | wallet.dat · 하드웨어 칩 | 브라우저 확장(암호화) · 하드웨어 칩 |
개인키는 블록체인에 없어요. 체인엔 주소·잔액·트랜잭션·서명만 올라갑니다. 개인키는 내 지갑 안에만 있어요 — 노드도 내 개인키를 모릅니다(알면 누구나 내 돈을 훔치니까).
서명은 "내 기기 안에서" 일어나요. 지갑이 로컬에서 개인키로 tx에 서명 → 체인엔 서명 + tx만 전송 → 노드는 공개키로 검증만. "서명은 나만, 검증은 누구나"의 비대칭이죠.
이더리움은 여러 곳에서 Keccak을 씁니다. 쓰임새마다 입력이 다르고, 해시한 결과에서 필요한 부분만 잘라 써요. 지금은 아래를 눌러 보며 "아, 여기저기서 Keccak을 쓰는구나" 정도만 느끼면 충분합니다.
컨트랙트 호출 때 "어떤 함수?"를 가리키는 id. 탭 3에서 buy()·transfer()를 부를 때 앞에 붙는 값이에요.
로그에서 "이건 Transfer 이벤트"라고 구분하는 값. 탭 3·4 이벤트 로그의 topic0가 바로 이거예요.
배포할 때마다 nonce가 올라가서 주소가 매번 달라집니다. 탭 3에서 자판기를 배포하면 이 규칙으로 주소가 정해져요.
mapping 저장 위치: balanceOf[Alice]가 storage 어디에 놓이는지도 keccak(key ‖ slot번호)로 정해집니다 — 탭 4 "토큰 storage 표"의 실제 주소 계산 방식이에요.
CREATE2: nonce 대신 keccak(0xff ‖ 배포자 ‖ salt ‖ keccak(코드))로 주소를 미리 계산 — 배포 전에 주소를 알 수 있어 L2·지갑에서 많이 씁니다.
체크섬 주소(EIP-55): 주소의 대소문자는 주소 자체를 keccak한 값으로 정해져요. 오타를 잡는 체크섬이라, 0xAbC… 처럼 섞인 대소문자에 의미가 있습니다.
비트코인은 동전 조각(UTXO)을 모아 잔액을 계산했죠. 이더리움은 계좌처럼 잔액 숫자를 직접 저장해요. 보내면 내 숫자가 줄고 상대 숫자가 늘 뿐 — 거스름돈도, UTXO 조각도 없습니다. 직접 보내 보세요.
| 항목 | 비트코인 (UTXO) | 이더리움 (계정) |
|---|---|---|
| 잔액 | 동전(UTXO) 조각들의 합 — 직접 저장 안 함 | 숫자로 직접 저장 |
| 보낼 때 | UTXO 통째 소비 + 거스름돈 새로 생성 | 내 잔액 −, 상대 잔액 + (거스름돈 없음) |
| 재사용 방지 | UTXO가 한 번 쓰면 소멸 (자동) | nonce 번호로 방지 (아래 카드) |
| 병렬성 | 서로 다른 UTXO는 동시 처리 쉬움 | 한 계정은 nonce 순서라 직렬 |
| 컨트랙트 상태 | 표현이 까다로움 | 잔액+storage로 자연스러움 |
"직접 저장"이라 했지만 블록 안에 잔액이 적히는 건 아니에요. 각 노드가 관리하는 월드 스테이트(world state) — 주소 → 계정 객체 라는 거대한 맵 — 안에 있습니다. 계정 하나는 딱 4개 필드예요.
| 필드 | 뜻 |
|---|---|
| nonce | 그 계정이 보낸 tx 수 (아래 카드의 그 번호) |
| balance | 잔액 (wei 단위) ← 여기 |
| storageRoot | 컨트랙트 storage의 요약 해시 (지갑 EOA는 빈 값) |
| codeHash | 컨트랙트 코드의 해시 (지갑 EOA는 빈 값) |
지갑(EOA)이든 컨트랙트든 구조는 똑같고, 지갑은 뒤 두 필드가 비어 있을 뿐이에요.
블록에는 뭐가 들어가나: 이 맵 전체를 해시 트리로 묶은 꼭대기 해시 하나 = state root 만 블록 헤더에 박힙니다. 잔액 숫자 자체는 노드가 트랜잭션을 실행하며 자기 DB를 갱신해 유지해요. 즉 블록 = "이 시점 상태의 지문(state root)" + "그 블록의 tx들".
↔ 비트코인: 비트코인은 잔액을 저장하는 곳이 아예 없고, 노드가 UTXO 집합을 들고 있다가 "내 주소로 쓸 수 있는 UTXO 합"으로 그때그때 계산. 이더리움은 주소별 balance 숫자를 직접 들고 있어 조회 한 번이면 끝.
nonce는 계정마다 따로 매기는 트랜잭션 순번(0, 1, 2, …)이에요. 왜 이게 꼭 필요한지 먼저 보죠.
stuck tx: nonce 5를 낮은 수수료로 보내 안 처리되면, nonce 6·7은 아무리 급해도 대기해요(순서 때문). 해결은 같은 nonce 5를 더 높은 수수료로 다시 보내 덮어쓰기.
nonce gap: 5 다음에 실수로 7을 보내면, 6이 올 때까지 7은 영원히 대기. mempool에서 6을 기다려요.
컨트랙트 주소와의 관계: 탭 1에서 봤듯 컨트랙트 주소 = keccak(배포자 주소 + nonce). 그래서 같은 사람이 배포해도 nonce가 올라가 매번 다른 주소가 나와요.
보낼 때 tip 값을 바꾸면 위 미리보기(소각·tip·차감)가 실시간으로 바뀌어요. 그 숫자가 어디서 오는지 정리합니다.
gas used = 21,000 · base fee = 10 Gwei/gas · tip = 입력 T Gwei/gas
소각 = 21,000 × 10 × 10⁻⁹ ETH tip = 21,000 × T × 10⁻⁹ ETH → 제안자 차감 = value + 소각 + tip
아래는 진짜 Solidity로 쓴 자판기(SnackMachine)입니다. 배포하면 코드가 체인에 박제되고, 누구든 buy()를 호출할 수 있어요. owner도 규칙을 못 바꿉니다.
to·data가 뭐냐로 이름만 갈려요. 그래서 배포도 전송처럼 nonce를 먹고, 그 nonce가 위 컨트랙트 주소의 재료가 됩니다.
| 하는 일 | tx 생김새 | nonce |
|---|---|---|
| 단순 전송 | to=받는 사람 · value · data 없음 | +1 |
| 컨트랙트 배포 | to=비어있음(null) · data=바이트코드 | +1 |
| 컨트랙트 호출 | to=컨트랙트 주소 · data=함수+인자 | +1 |
storage = 이 컨트랙트 전용 서랍. 오른쪽 Solidity 코드 위쪽에 선언된 변수들(price, stock…)의 현재 값이 여기 담깁니다. 함수를 호출해야만 바뀌어요.
볼펜으로 쓰는 장부라고 생각하세요. 이전 기록을 지우거나 덮어쓸 수 없고, 트랜잭션이 성공할 때만 다음 줄(새 버전 v번호)이 추가됩니다. 조건이 안 맞아 revert되면 새 버전은 안 생기고, 아래엔 ⛔로 '시도했지만 거부됨'만 남아요 — 상태가 반쯤 바뀌는 일은 없습니다(원자성). 대신 gas는 나갑니다.
withdraw()를 Bob으로 호출해 보세요 — revert되지만 gas는 나갑니다.
은행 예치: 약관·직원·법원이 개입 가능. 컨트랙트 예치: 배포된 코드 조건 말고는 그 무엇도 돈을 못 움직입니다.
주소가 keccak(배포자, nonce)로 결정되는 것, 이벤트의 topic0 = keccak(시그니처)인 것 전부 1번 탭에서 본 해시 규칙 그대로예요.
단점도 같은 원리에서 나옵니다 — 코드에 버그가 있어도 못 고칩니다 (The DAO 해킹이 그 사례).
방금 만든 코드·storage·잔액이 블록 안에 적힐 것 같지만, 블록에는 "주문서"만 적힙니다. 결과물은 각 노드가 직접 계산해서 자기 DB에 보관해요.
이 시뮬 세계의 모든 계정이 나옵니다 — 다른 탭에서 쓰는 컨트랙트(SAND는 4번 토큰 탭, ETH/USD Feed는 5번 오라클 탭)는 시작할 때 미리 배포돼 있어요. 실제 이더리움 상태 DB에 USDT·Uniswap이 이미 들어 있는 것과 같습니다.
ETH는 1번 탭처럼 프로토콜이 계정 객체의 balance로 직접 관리합니다 (비트코인 UTXO가 아니라 계좌 숫자). The Sandbox의 SAND나 USDT는 그 필드가 아니라, 이더리움 위 그 토큰 컨트랙트 storage의 표(주소→수량)예요. ERC-20은 그 표의 표준이고, 아래 SAND는 그 구조를 만져 보는 교육용 미니 토큰입니다.
balance(ETH). SAND·USDT 같은 앱 토큰은 자기 storage 표. (실제 SAND는 gas 때문에 Polygon에서도 많이 거래되지만, 토큰 자체는 이더리움 ERC-20 계열입니다.)
The Sandbox의 SAND 장부가 이렇게 생겼다고 보면 됩니다. 오른쪽 mapping(address → uint256) balanceOf — 지갑이 보여주는 "토큰 잔액"의 정체예요. ETH 계정 balance와는 다른 칸입니다. (교육용 미니 버전 · 메인넷 SAND 컨트랙트 아님)
컨트랙트는 인터넷도 API도 못 씁니다. 모든 노드가 같은 계산을 재현해야 하니까요 — 예를 들어 날씨 API를 직접 치면, 어떤 노드는 성공·어떤 노드는 실패하거나, 어떤 노드는 A·어떤 노드는 B를 받을 수 있어 결과가 갈라집니다. 그래서 바깥 데이터(가격·날씨·경기 결과)는 oracle 노드들이 트랜잭션으로 넣어줘야 합니다. Chainlink가 이 방식이에요.
각 노드가 거래소 API에서 본 가격을 tx로 올린다고 생각하세요. (실제 Chainlink 노드는 거래소 자체가 아니라, 여러 거래소·집계 API를 조회하는 독립 운영자입니다 — 여기선 출처 이름으로 표시)
“ETH가 3,000달러 아래로 떨어지면 1 ETH를 지급한다”는 보험을 배포해 보세요. 지급 여부는 사람이 아니라 feed의 median이 결정합니다. 아래 Bob 잔액이 settle 후 늘어나는지 보세요.
가입·정산할 때마다 Bob ETH가 어떻게 바뀌는지 처음부터 보여 줍니다.
오라클 문제가 뭐였나: 온체인 데이터는 구성원들이 서로 검증하며 신뢰를 유지하지만, 외부(오프체인) 데이터는 인체인 방식으로 참·거짓을 검증할 방법이 없습니다. 외국 여행객이 입국 심사를 거치듯, 바깥 데이터도 심사자가 필요한데 — 그 심사를 한 업체에 맡기면 다시 중앙화가 되죠. 이 딜레마가 오라클 문제입니다.
써드파티 오라클 (Chainlink·Band): 여러 검증 노드가 각자 데이터를 가져와 비교·합의합니다(위 median이 그 축소판). 정확히 보고한 노드는 보상(LINK)을 받고, 틀리게 보고한 노드는 스테이킹한 코인을 잃고 평판이 깎여 다음 검증 기회가 줄어요. 탈중앙적이지만 노드가 많아 느리고 보상이 분산됩니다.
퍼스트파티 오라클 (PYTH 등): 거래소·데이터 제공자가 직접 서명해 올립니다. 스테이킹으로 거짓 보고를 억제해요. 빠르고 효율적이지만 제공자를 믿어야 해서 중앙화 쪽으로 기웁니다.
왜 중요한가: 오라클이 신뢰를 확보해줘야 부동산·주식 같은 실물 자산(RWA)도 스마트 컨트랙트로 다룰 수 있게 됩니다. LINK 코인 자체가 기술이 아니라, 검증 노드에게 주는 보상 수단이라는 점도 포인트.
The Merge(2022) 이후 이더리움엔 비트코인 같은 채굴이 없습니다. 시간이 slot(실제 12초짜리 칸)으로 잘게 나뉘고, 매 slot마다 지분 비례 무작위 추첨(실제는 RANDAO)으로 validator 한 명이 뽑혀 블록을 제안합니다. 나머지 validator들은 그 블록이 맞는지 확인하고 attest(찬성 투표)를 던져요. slot 여러 개(여기선 8개, 실제 32개)를 묶은 게 epoch이고, epoch 단위로 투표를 집계해 확정을 진행합니다.
읽는 법: Slot = 지금 몇 번째 시간 칸인지 · Epoch = slot 묶음 번호 · Justified/Finalized = 아래 "2단계 확정"이 어느 epoch까지 진행됐는지 (— = 아직 확정된 epoch 없음).
블록마다 stateRoot(그 시점 world state의 지문)가 실립니다. 다른 탭에서 송금·컨트랙트 호출을 한 뒤 slot을 진행해 보세요 — 지문이 바뀝니다. 검증자가 attest 전에 재실행으로 대조하는 값이 바로 이거예요. 블록의 attest 줄은 이 블록에 찬성 투표한 validator 명단입니다 (✓ 투표 · ✗ 오프라인).
블록을 제안·투표하려면 32 ETH를 staking하고 활성화해야 합니다. 이중서명 같은 위반은 slashing으로 담보가 깎여요 — PoW의 전기 대신 담보로 시빌을 막는 구조입니다.
제안자 당번이 된 validator가 같은 slot에 서로 다른 블록 2개를 서명해 절반씩 다른 버전을 보여주는 공격입니다(이중지불 시도 — BTC 탭 7의 "비밀 체인"의 PoS 버전). 결과: ① 체인이 두 갈래로 갈라지지만 ② 정직한 다수의 투표 가중치가 원래 체인을 헤드로 지키고 ③ 같은 slot에 대한 서명 2개 = 수학적 증거라 변명 불가 — 자동으로 slashing됩니다. BTC와 달리 실패한 공격은 담보를 잃어 재시도가 불가능해요.
지금까지 배운 걸 하나의 실제 거래로 꿰어 봅니다. 전통 부동산은 에스크로 회사·등기소·중개인이 신뢰를 맡죠. 여기선 코드(컨트랙트)가 그 역할을 대신해요 — 대금을 잠그고, 조건이 확인돼야만 매도자에게 넘깁니다. 단계마다 tx·nonce·gas·storage·블록·stateRoot가 어떻게 움직이는지 보세요.
왼쪽은 실제 잔액·컨트랙트 storage·이벤트, 오른쪽은 그 규칙을 정한 Solidity 코드예요. 방금 호출한 함수가 코드에서 빛납니다.
각 단계는 트랜잭션이고, 블록에 담기면 전체 상태의 지문 stateRoot가 바뀝니다. git 커밋처럼 이전 걸 고치는 게 아니라 새 블록이 앞으로 쌓여요. (stateRoot는 이 시뮬의 계정+컨트랙트 스냅샷을 keccak-256 한 값입니다.)
stateRoot가 보여요. tx가 상태를 바꾸면 stateRoot가 완전히 달라지고, revert된 tx는 상태를 안 바꾸니 stateRoot도 그대로입니다.
| 역할 | 전통 부동산 거래 | 이더리움 에스크로 |
|---|---|---|
| 대금 보관 | 에스크로 회사·은행 계좌 | 컨트랙트가 잠금 (아무도 못 빼감) |
| 조건 확인 | 등기소·법무사·중개인 | 확인자 tx (confirm) |
| 지급 실행 | 사람이 송금 처리 (지연·실수) | 코드가 자동 (조건 충족 시만) |
| 파기·환불 | 분쟁·소송 가능 | refund로 원자적 환불 |
| 신뢰 주체 | 여러 기관·사람 | 배포된 코드 (변경 불가) |
| 기록 | 기관별 장부 (분산·불투명) | 블록·stateRoot (공개·검증가능) |
스마트 컨트랙트는 결국 바이트코드고, 그걸 한 명령(opcode)씩 실행하는 가상 CPU가 EVM이에요. world state(데이터)에 tx(명령)를 넣으면, EVM이 gas를 태우며 실행해 새 상태를 만듭니다. JVM처럼 스택 기반 가상머신이고, JS 이벤트 루프처럼 단일 스레드로 끝까지 실행돼요.
| 컴퓨터 | 이더리움 | 뭐냐 |
|---|---|---|
| 디스크 / DB (전체) | world state | 모든 계정 잔액 + 모든 컨트랙트 storage |
| 그중 한 파일·테이블 | storage | 컨트랙트 하나분의 영구 칸 (world state의 일부) |
| RAM (휘발) | stack · memory | tx 끝나면 사라짐 |
| 실행할 프로그램 | tx | 누가·어떤 함수·인자 (동작) |
| CPU | EVM | 바이트코드를 opcode씩 실행 |
| 전기요금·클럭 | gas | 실행 비용·상한 |
| 전체 DB 체크섬 | stateRoot | world state 전체의 지문 |
PUSH 3, PUSH 4, ADD → 스택에 7.
JS 이벤트 루프 닮은 점: 단일 스레드로 끝까지 실행(중간에 안 끊김), 끝나거나 통째로 revert.
random·현재시각·네트워크 I/O가 없음. 전 세계 노드가 같은 tx를 각자 실행해 똑같은 스택·스토리지·gas가 나와야 합의가 되니까요. gas도 이래서 존재해요(무한루프·DoS 방지).
프로그램을 고르고 다음 스텝을 누르면, opcode 하나가 실행될 때마다 스택·스토리지·gas가 어떻게 변하는지 보여줘요. 이벤트 루프 시각화처럼요.
왼쪽이 지금 storage 내용(working tree), 오른쪽이 그걸 해시한 stateRoot(커밋 해시)예요. SSTORE로 값이 바뀌면 새 커밋이 쌓이고, 프로그램을 바꿔도 히스토리는 남습니다.
new_state = EVM.execute(state, tx) 하고 stateRoot = hash(new_state). 이 상태 전이 기계를 이해하면, 나머지는 전부 "이 기계 위의 인스턴스들"로 보입니다.
"모두가 재실행한다"는 건 state를 들고 있어야 가능해요. 근데 노드 종류마다 얼마나 들고 있느냐가 달라요. 특히 라이트 클라이언트는 state를 안 들고 있어서, 아래에서 볼 머클 증명으로 값을 확인해요.
| 노드 종류 | world state 보유 | 설명 |
|---|---|---|
| 풀 노드 (가장 흔함) | ✅ 현재(최근) state | 최신 world state + 모든 블록 보관. 아주 오래된 중간 state는 잘라냄(prune). 필요하면 제네시스부터 재실행해 복원. |
| 아카이브 노드 | ✅ 모든 과거 state 전부 | 블록 1번부터 지금까지 모든 시점의 state 스냅샷을 다 보관. 용량 수 TB(탐색기·인프라용). |
| 라이트 클라이언트 | ❌ 안 들고 있음 | 블록 헤더만 보관. 잔액 등이 필요하면 풀 노드에 요청하고, 헤더의 stateRoot로 머클 증명만 검증. |
라이트 클라이언트는 블록 헤더의 stateRoot 하나만 신뢰해요. "이 계정 잔액이 진짜야?"를 확인하려고, state를 가진 풀 노드가 경로의 형제 해시 몇 개만 보내주면, 그걸로 잎에서 꼭대기까지 해시를 직접 재계산해 stateRoot와 대조해요. 아래에서 계정을 고르고 증명을 재생해 보세요 — 경로가 깜빡이며 stateRoot까지 타고 올라가요.
Web2.0에는 24시간 혼자 도는 중앙 서버 한 대가 있어요. 이더리움엔 그런 서버가 없어요. 대신 slot마다 다른 노드가 바통(블록)을 이어받아 상태를 한 칸 전진시키고, 나머지 노드들이 각자 재검산해서 맞는지 확인해요. "쉬지 않는 서버"라는 겉모습은, 번갈아 이어달리는 수천 개의 노드가 합성해내는 착시예요.
각 slot(12초)마다 랜덤 추첨된 제안자 노드 한 명이 tx를 실행해 새 world state와 stateRoot를 만들고(바통 생성), 그걸 다음 slot으로 넘겨요. 나머지 검증자들은 그 바통을 받아 각자 재실행해서 stateRoot가 같은지 채점하고 투표해요. 32 slot(=1 epoch)마다 충분히 표가 쌓이면 finalized — 되돌릴 수 없게 굳어요.