이미지 로딩 중...
AI Generated
2025. 11. 9. · 2 Views
타입스크립트로 비트코인 클론하기 10편 - Proof of Work 알고리즘 구현하기
블록체인의 핵심인 Proof of Work 작업증명 알고리즘을 타입스크립트로 직접 구현해봅니다. 난이도 조절, 논스 계산, 채굴 과정을 실제 코드와 함께 단계별로 이해하고, 비트코인이 어떻게 보안을 유지하는지 배웁니다.
목차
- Proof of Work 개념 - 블록체인 보안의 핵심 원리
- 논스(Nonce)와 채굴 과정 - 정답을 찾는 반복 작업
- 난이도 조절 시스템 - 일정한 블록 생성 속도 유지하기
- 완전한 채굴 시스템 구현 - 블록체인과 통합하기
- 채굴 성능 최적화 - 효율적인 해시 계산
- 타임스탬프 검증 - 블록 시간 조작 방지하기
- 채굴 보상 시스템 - 인센티브 설계하기
- 체인 재구성(Reorganization) 처리 - 분기 해결하기
- 51% 공격 방어 - 네트워크 보안 강화하기
1. Proof of Work 개념 - 블록체인 보안의 핵심 원리
시작하며
여러분이 블록체인 네트워크를 운영한다고 상상해보세요. 누구나 블록을 마음대로 추가할 수 있다면 어떻게 될까요?
악의적인 사용자가 1초에 수천 개의 블록을 생성하여 네트워크를 장악하거나, 거짓 거래 정보를 마구 넣을 수 있을 것입니다. 이런 문제를 해결하기 위해 비트코인은 "블록을 생성하려면 어려운 퍼즐을 풀어야 한다"는 규칙을 만들었습니다.
이것이 바로 Proof of Work(작업증명)입니다. 컴퓨터가 수백만 번, 수억 번의 계산을 해야만 블록을 하나 만들 수 있게 함으로써, 네트워크를 공격하려면 엄청난 비용이 들게 만드는 것이죠.
바로 이럴 때 필요한 것이 Proof of Work 알고리즘입니다. 이를 통해 블록체인은 중앙 관리자 없이도 누구나 믿을 수 있는 분산 시스템을 만들어낼 수 있습니다.
개요
간단히 말해서, Proof of Work는 "일정한 양의 계산 작업을 수행했다는 것을 증명하는 메커니즘"입니다. 왜 이것이 필요한가요?
블록체인에서는 누구나 블록을 생성할 수 있지만, 그 권한을 무제한으로 주면 안 됩니다. 예를 들어, 은행 거래 시스템을 상상해보세요.
누구나 장부를 마음대로 수정할 수 있다면 혼란이 생기겠죠. 하지만 "장부를 수정하려면 복잡한 수학 문제를 풀어야 한다"는 규칙을 만들면, 악의적인 수정은 너무 많은 비용이 들어서 실질적으로 불가능해집니다.
기존 중앙화 시스템에서는 은행이나 정부가 거래를 검증했다면, 블록체인에서는 Proof of Work라는 수학적 퍼즐을 통해 검증합니다. 핵심 특징은 세 가지입니다: (1) 퍼즐을 푸는 것은 어렵지만, (2) 답을 검증하는 것은 쉽고, (3) 난이도를 조절할 수 있습니다.
이러한 특징 덕분에 네트워크는 안전하면서도 효율적으로 작동할 수 있습니다.
코드 예제
// Proof of Work의 기본 구조
interface ProofOfWorkConfig {
difficulty: number; // 난이도 (앞에 0이 몇 개 나와야 하는지)
maxNonce: number; // 최대 시도 횟수
}
class ProofOfWork {
private difficulty: number;
constructor(difficulty: number = 4) {
this.difficulty = difficulty;
}
// 목표 해시값 생성 (앞에 0이 difficulty 개수만큼 있어야 함)
getTarget(): string {
return '0'.repeat(this.difficulty);
}
// 해시가 목표를 만족하는지 검증
isValidHash(hash: string): boolean {
return hash.startsWith(this.getTarget());
}
}
설명
이것이 하는 일: ProofOfWork 클래스는 블록이 유효한지 판단하는 기준을 제공하고, 해시값이 특정 조건을 만족하는지 검증합니다. 첫 번째 단계로, difficulty 속성이 퍼즐의 난이도를 결정합니다.
난이도가 4라면 해시값이 "0000..."으로 시작해야 하고, 난이도가 5라면 "00000..."으로 시작해야 합니다. 이 작은 차이가 실제로는 계산량을 16배나 증가시킵니다.
왜냐하면 각 자리는 16진수(0-9, a-f)이므로 한 자리를 맞추려면 평균 16번의 시도가 필요하기 때문입니다. 두 번째 단계로, getTarget() 메서드가 실행되면서 목표 문자열을 생성합니다.
예를 들어 difficulty가 4면 "0000"을 반환하죠. 이것이 우리가 찾아야 할 해시값의 시작 패턴입니다.
내부적으로는 단순히 '0'을 반복하지만, 이것을 찾아내는 것은 엄청난 계산이 필요합니다. 세 번째 단계와 최종 결과로, isValidHash() 메서드가 주어진 해시값이 목표 패턴으로 시작하는지 확인합니다.
이 검증 과정은 단순한 문자열 비교이므로 밀리초도 걸리지 않습니다. 반면 이 조건을 만족하는 해시를 찾는 데는 수백만 번의 시도가 필요할 수 있습니다.
여러분이 이 코드를 사용하면 블록체인의 보안 메커니즘을 직접 제어할 수 있습니다. 난이도를 높이면 블록 생성 시간이 길어져서 보안이 강화되지만 네트워크가 느려지고, 낮추면 빠르지만 공격에 취약해집니다.
비트코인은 약 10분마다 블록이 생성되도록 난이도를 자동 조절합니다.
실전 팁
💡 난이도는 단계적으로 증가시키세요. 프로덕션에서 갑자기 난이도를 높이면 기존 채굴자들이 블록을 생성하지 못해 네트워크가 멈출 수 있습니다. 테스트 환경에서는 2-4 정도가 적당하고, 실제 블록체인에서는 네트워크 해시레이트에 따라 조절해야 합니다.
💡 해시 검증은 항상 채굴 성공 후에도 한 번 더 하세요. 네트워크에서 받은 블록도 반드시 검증해야 합니다. 신뢰하되 검증하라(Trust but verify)는 블록체인의 핵심 원칙입니다.
💡 메모리 사용량을 모니터링하세요. 난이도가 높아지면 채굴 과정에서 수백만 번의 해시 계산이 일어나므로, 특히 Node.js 환경에서는 이벤트 루프를 블록하지 않도록 주의해야 합니다.
💡 목표 해시는 숫자로도 표현할 수 있습니다. 실제 비트코인은 문자열 비교가 아닌 256비트 정수 비교를 사용합니다. 이것이 더 정밀한 난이도 조절을 가능하게 합니다.
2. 논스(Nonce)와 채굴 과정 - 정답을 찾는 반복 작업
시작하며
여러분이 복권을 산다고 생각해보세요. 당첨 번호를 맞추려면 수많은 조합을 시도해야 하죠.
블록체인 채굴도 비슷합니다. 하지만 무작위로 번호를 고르는 것이 아니라, 체계적으로 1부터 시작해서 하나씩 증가시키며 시도합니다.
이 반복적인 시도 과정에서 핵심 역할을 하는 것이 바로 "논스(Nonce)"입니다. Number used ONCE의 약자로, 한 번만 사용되는 숫자라는 뜻입니다.
블록의 다른 모든 데이터는 고정되어 있고, 오직 이 논스만 계속 바꿔가며 원하는 해시값을 찾아냅니다. 바로 이것이 채굴자들이 하는 일입니다.
논스를 0부터 시작해서 1씩 증가시키며, 조건을 만족하는 해시가 나올 때까지 수백만 번, 때로는 수십억 번을 반복합니다.
개요
간단히 말해서, 논스는 해시 계산에 포함되는 가변 숫자로, 원하는 해시값을 찾기 위해 계속 변경되는 값입니다. 왜 필요한가요?
블록의 데이터(거래 내역, 이전 블록 해시, 타임스탬프)는 이미 정해져 있습니다. 이것만으로 해시를 계산하면 항상 같은 결과가 나오죠.
하지만 Proof of Work는 특정 패턴의 해시를 요구합니다. 예를 들어, "0000"으로 시작하는 해시를 찾아야 하는데, 고정된 데이터로는 불가능할 수 있습니다.
그래서 논스라는 변수를 추가해서, 이를 바꿔가며 조건을 만족할 때까지 시도하는 것입니다. 전통적인 퍼즐은 논리적 사고로 풀지만, 블록체인의 퍼즐은 순수하게 계산력으로만 풉니다.
똑똑한 알고리즘이 아니라 무식하게 많이 시도하는 것이죠. 핵심 특징은: (1) 논스는 32비트 또는 64비트 정수입니다, (2) 0부터 시작해서 순차적으로 증가합니다, (3) 조건을 만족하는 논스를 찾으면 그것이 블록의 일부가 됩니다.
이런 단순한 메커니즘이 블록체인 전체의 보안을 책임집니다.
코드 예제
// 채굴 과정: 논스를 찾는 반복 작업
class Block {
public nonce: number = 0;
constructor(
public index: number,
public timestamp: number,
public data: string,
public previousHash: string,
public hash: string = ''
) {}
// 블록의 해시 계산 (논스 포함)
calculateHash(): string {
const crypto = require('crypto');
const content = this.index + this.timestamp +
this.data + this.previousHash + this.nonce;
return crypto.createHash('sha256').update(content).digest('hex');
}
// 채굴: 조건을 만족하는 논스 찾기
mine(difficulty: number): void {
const target = '0'.repeat(difficulty);
while (!this.hash.startsWith(target)) {
this.nonce++; // 논스를 1씩 증가
this.hash = this.calculateHash(); // 새로운 해시 계산
}
console.log(`블록 채굴 완료! 논스: ${this.nonce}, 해시: ${this.hash}`);
}
}
설명
이것이 하는 일: Block 클래스는 블록체인의 개별 블록을 표현하며, mine() 메서드를 통해 Proof of Work를 수행합니다. 첫 번째 단계로, calculateHash() 메서드가 블록의 모든 데이터를 하나의 문자열로 결합합니다.
index, timestamp, data, previousHash는 블록이 생성될 때 이미 정해진 값들이고, 여기에 논스를 추가합니다. 이 결합된 문자열을 SHA-256 해시 함수에 넣으면 64자리 16진수 해시값이 나옵니다.
논스가 1만 달라져도 완전히 다른 해시가 생성됩니다. 두 번째 단계로, mine() 메서드가 실행되면서 while 루프가 시작됩니다.
먼저 현재 해시가 목표 패턴(예: "0000")으로 시작하는지 확인합니다. 만약 아니라면 논스를 1 증가시키고 새로운 해시를 계산합니다.
이 과정이 조건을 만족할 때까지 반복됩니다. 난이도 4라면 평균적으로 65,536번(16^4)의 시도가 필요하고, 난이도 5라면 1,048,576번(16^5)이 필요합니다.
세 번째 단계와 최종 결과로, 조건을 만족하는 해시를 찾으면 루프가 종료되고 그때의 논스값이 블록에 저장됩니다. 이제 이 블록은 "채굴되었다"고 말할 수 있습니다.
다른 노드들은 이 논스와 블록 데이터로 해시를 한 번만 계산해보면, 정말로 Proof of Work가 수행되었는지 즉시 확인할 수 있습니다. 여러분이 이 코드를 실행하면 실제 채굴 과정을 체험할 수 있습니다.
난이도 4 정도면 몇 초 안에 완료되지만, 6 이상으로 올리면 수 분이 걸릴 수 있습니다. 실제 비트코인의 난이도는 현재 약 77자리의 0으로 시작하는 해시를 요구하며, 전 세계 채굴자들이 엄청난 컴퓨팅 파워로 경쟁합니다.
실전 팁
💡 논스의 자료형을 신중히 선택하세요. JavaScript의 Number는 안전한 정수 범위가 2^53-1까지이므로, 난이도가 높으면 BigInt를 사용해야 할 수 있습니다. 그렇지 않으면 논스가 최대값을 넘어서 오버플로우가 발생합니다.
💡 채굴 과정에 타임아웃을 설정하세요. 무한 루프가 될 수 있으므로, 예를 들어 "100만 번 시도 후에도 못 찾으면 타임스탬프를 변경"하는 등의 안전장치가 필요합니다. 실제 비트코인도 논스 공간을 다 소진하면 엑스트라논스를 사용합니다.
💡 채굴 진행 상황을 로깅하세요. 10만 번마다 현재 논스값을 출력하면 사용자가 진행 상황을 알 수 있고, 디버깅할 때도 유용합니다. 프로덕션에서는 이를 메트릭으로 수집해서 모니터링할 수 있습니다.
💡 해시 계산을 최적화하세요. createHash()를 매번 새로 생성하는 것보다, 고정된 부분은 미리 해싱하고 논스만 추가하는 방식이 더 효율적입니다. 큰 블록에서는 성능 차이가 클 수 있습니다.
💡 멀티스레딩을 고려하세요. Node.js에서는 Worker Threads를 사용하여 여러 논스 범위를 병렬로 탐색할 수 있습니다. 예를 들어 4개 스레드로 각각 0-25만, 25만-50만, 50만-75만, 75만-100만을 동시에 검색하면 속도가 크게 향상됩니다.
3. 난이도 조절 시스템 - 일정한 블록 생성 속도 유지하기
시작하며
여러분의 블록체인에 갑자기 채굴자가 10배로 늘어났다고 상상해보세요. 그럼 블록이 10배 빨리 생성되겠죠?
반대로 절반이 떠나면 블록 생성이 2배 느려질 것입니다. 이렇게 속도가 들쭉날쭉하면 네트워크가 불안정해집니다.
비트코인은 이 문제를 영리하게 해결했습니다. 전체 네트워크의 계산력이 얼마나 되든, 항상 약 10분마다 블록이 하나씩 생성되도록 난이도를 자동으로 조절합니다.
채굴자가 많아져서 블록이 빨리 나오면 난이도를 높이고, 적어져서 느려지면 난이도를 낮춥니다. 바로 이것이 난이도 조절 시스템입니다.
2,016개 블록마다(약 2주) 지난 기간의 블록 생성 시간을 분석하여 난이도를 재계산하는 것이죠. 이를 통해 네트워크는 참여자 수와 무관하게 안정적으로 작동합니다.
개요
간단히 말해서, 난이도 조절은 네트워크의 전체 해시파워 변화에 따라 블록 생성 난이도를 자동으로 증가 또는 감소시키는 메커니즘입니다. 왜 이것이 중요한가요?
일정한 블록 생성 속도는 여러 측면에서 중요합니다. 첫째, 네트워크 보안 측면에서 블록이 너무 빨리 생성되면 블록체인 분기(fork)가 자주 발생하여 혼란이 생깁니다.
둘째, 경제적 측면에서 채굴 보상이 예측 가능해야 채굴자들이 장기적으로 참여할 동기를 갖습니다. 예를 들어, 비트코인은 4년마다 보상이 절반으로 줄어드는데, 이것도 블록 생성 속도가 일정하기 때문에 예측할 수 있는 것입니다.
기존 방식에서는 관리자가 수동으로 난이도를 설정했다면, 블록체인에서는 코드로 자동화된 규칙에 따라 조절됩니다. 핵심 메커니즘은: (1) 일정 개수의 블록마다 난이도를 재계산합니다, (2) 실제 소요 시간과 예상 시간을 비교합니다, (3) 비율에 따라 난이도를 조정합니다.
비트코인의 경우 한 번에 최대 4배까지만 변경하여 급격한 변화를 방지합니다.
코드 예제
// 난이도 자동 조절 시스템
class DifficultyAdjuster {
private static readonly BLOCK_GENERATION_INTERVAL = 10; // 목표: 10초마다 블록 생성
private static readonly DIFFICULTY_ADJUSTMENT_INTERVAL = 10; // 10블록마다 조정
// 난이도 재계산
static adjustDifficulty(
latestBlock: Block,
blockchain: Block[]
): number {
const prevAdjustmentBlock = blockchain[blockchain.length - this.DIFFICULTY_ADJUSTMENT_INTERVAL];
// 실제로 10개 블록을 생성하는 데 걸린 시간 (초)
const timeExpected = this.BLOCK_GENERATION_INTERVAL * this.DIFFICULTY_ADJUSTMENT_INTERVAL;
const timeTaken = (latestBlock.timestamp - prevAdjustmentBlock.timestamp) / 1000;
// 난이도 조정
if (timeTaken < timeExpected / 2) {
// 너무 빠르면 난이도 증가
return prevAdjustmentBlock.difficulty + 1;
} else if (timeTaken > timeExpected * 2) {
// 너무 느리면 난이도 감소
return prevAdjustmentBlock.difficulty - 1;
}
// 적절하면 유지
return prevAdjustmentBlock.difficulty;
}
// 현재 난이도 가져오기
static getDifficulty(blockchain: Block[]): number {
const latestBlock = blockchain[blockchain.length - 1];
// 조정 주기인지 확인
if (latestBlock.index % this.DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && latestBlock.index !== 0) {
return this.adjustDifficulty(latestBlock, blockchain);
}
return latestBlock.difficulty;
}
}
설명
이것이 하는 일: DifficultyAdjuster 클래스는 블록체인의 최근 블록 생성 속도를 분석하고, 목표 속도를 유지하도록 난이도를 조정합니다. 첫 번째 단계로, getDifficulty() 메서드가 현재 블록 인덱스를 확인합니다.
만약 조정 주기(여기서는 10블록마다)가 되었다면 재계산을 시작하고, 아니라면 현재 난이도를 그대로 반환합니다. 이렇게 주기적으로만 조정하는 이유는 매 블록마다 변경하면 변동성이 너무 커지기 때문입니다.
비트코인은 2,016블록(약 2주)마다 조정하여 안정성을 확보합니다. 두 번째 단계로, adjustDifficulty() 메서드가 실행되면 먼저 이전 조정 시점의 블록을 찾습니다.
그리고 그 블록과 현재 블록의 타임스탬프 차이를 계산하여 실제 소요 시간을 구합니다. 예를 들어 10개 블록이 생성되는 데 목표는 100초(10초 × 10)인데 실제로는 50초밖에 안 걸렸다면, 채굴이 너무 쉽다는 의미입니다.
반대로 200초가 걸렸다면 너무 어렵다는 뜻이죠. 세 번째 단계와 최종 결과로, 시간 비율에 따라 난이도를 조정합니다.
이 코드에서는 단순하게 절반보다 빠르면 +1, 2배보다 느리면 -1로 조정합니다. 실제 비트코인은 더 정교한 공식을 사용합니다: 새로운 난이도 = 현재 난이도 × (실제 시간 / 목표 시간).
단, 한 번에 너무 크게 변하는 것을 방지하기 위해 최대 4배, 최소 1/4배로 제한합니다. 여러분이 이 시스템을 구현하면 블록체인이 자기 조절 능력을 갖게 됩니다.
테스트 환경에서는 조정 주기를 짧게(예: 5블록) 설정하여 빠르게 테스트할 수 있고, 프로덕션에서는 길게(예: 2,016블록) 설정하여 안정성을 확보할 수 있습니다. 또한 목표 블록 생성 시간도 용도에 따라 조절할 수 있습니다.
이더리움은 15초, 라이트코인은 2.5분을 사용하죠.
실전 팁
💡 난이도는 절대 음수가 되면 안 됩니다. 감소 로직에 Math.max(1, difficulty - 1)처럼 최소값 검증을 추가하세요. 난이도 0이나 음수는 보안상 매우 위험합니다.
💡 타임스탬프 조작을 방지하세요. 악의적인 채굴자가 미래 시간을 설정하여 난이도를 낮출 수 있습니다. 블록 타임스탬프는 "이전 블록보다 크고, 현재 시간보다 2시간 이상 크지 않다"는 규칙을 적용하세요.
💡 조정 비율에 상한선을 두세요. 실제 비트코인처럼 한 번에 최대 4배까지만 변경하도록 제한하면, 네트워크 공격이나 버그로 인한 급격한 변화를 막을 수 있습니다.
💡 제네시스 블록(첫 블록)은 특별히 처리하세요. 이전 블록이 없으므로 난이도 계산에서 제외해야 합니다. latestBlock.index !== 0 조건이 이를 처리합니다.
💡 난이도 변경 이력을 로깅하세요. 언제, 왜 난이도가 변경되었는지 기록하면 네트워크 건강 상태를 모니터링하고 이상 징후를 빠르게 감지할 수 있습니다.
4. 완전한 채굴 시스템 구현 - 블록체인과 통합하기
시작하며
여러분이 지금까지 배운 Proof of Work, 논스, 난이도 조절을 모두 합치면 무엇이 될까요? 바로 완전히 작동하는 블록체인 채굴 시스템입니다.
하지만 개별 부품을 아는 것과 전체 시스템을 구축하는 것은 다른 문제입니다. 실제 블록체인에서는 새로운 거래가 들어오면 이를 모아서 블록을 만들고, Proof of Work를 수행하고, 검증한 후 체인에 추가합니다.
이 과정에서 이전 블록과의 연결, 타임스탬프 관리, 제네시스 블록 처리 등 신경 써야 할 부분이 많습니다. 바로 이제 우리가 만들 것은 이 모든 것을 통합한 완전한 Blockchain 클래스입니다.
여러분은 이를 통해 실제로 작동하는 미니 블록체인을 손에 넣게 됩니다.
개요
간단히 말해서, 완전한 채굴 시스템은 거래 수집, 블록 생성, Proof of Work 수행, 검증, 체인 추가를 모두 자동화한 통합 시스템입니다. 왜 통합이 중요한가요?
각 컴포넌트가 아무리 잘 작동해도, 이들이 제대로 연결되지 않으면 시스템은 무용지물입니다. 예를 들어, 난이도 조절 시스템이 있어도 블록 생성 시 이를 적용하지 않으면 의미가 없죠.
또한 이전 블록의 해시를 현재 블록에 포함시키는 것, 타임스탬프를 정확히 기록하는 것, 체인의 유효성을 검증하는 것 등이 모두 유기적으로 연결되어야 합니다. 개별 함수들을 테스트할 때는 단순했지만, 실제 운영 환경에서는 동시성, 에러 처리, 상태 관리 등 복잡한 문제들이 생깁니다.
핵심 기능은: (1) 새 블록 추가 시 자동으로 Proof of Work 수행, (2) 체인 전체의 무결성 검증, (3) 동적 난이도 조절 적용, (4) 제네시스 블록 자동 생성입니다. 이것들이 조화롭게 작동해야 진정한 블록체인이 됩니다.
코드 예제
// 완전한 블록체인 시스템
class Blockchain {
public chain: Block[] = [];
private difficulty: number;
constructor(difficulty: number = 4) {
this.difficulty = difficulty;
this.chain = [this.createGenesisBlock()];
}
// 제네시스 블록 생성 (첫 번째 블록)
private createGenesisBlock(): Block {
const genesisBlock = new Block(0, Date.now(), "Genesis Block", "0");
genesisBlock.difficulty = this.difficulty;
genesisBlock.hash = genesisBlock.calculateHash();
return genesisBlock;
}
// 마지막 블록 가져오기
getLatestBlock(): Block {
return this.chain[this.chain.length - 1];
}
// 새 블록 추가 (채굴 포함)
addBlock(data: string): void {
const previousBlock = this.getLatestBlock();
const newBlock = new Block(
previousBlock.index + 1,
Date.now(),
data,
previousBlock.hash
);
// 현재 난이도 가져오기 (자동 조절)
newBlock.difficulty = DifficultyAdjuster.getDifficulty(this.chain);
// Proof of Work 수행
console.log(`블록 ${newBlock.index} 채굴 중... (난이도: ${newBlock.difficulty})`);
newBlock.mine(newBlock.difficulty);
// 체인에 추가
this.chain.push(newBlock);
}
// 블록체인 유효성 검증
isChainValid(): boolean {
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
// 현재 블록의 해시가 정확한지
if (currentBlock.hash !== currentBlock.calculateHash()) {
console.log(`블록 ${i}의 해시가 유효하지 않습니다.`);
return false;
}
// 이전 블록과 연결이 올바른지
if (currentBlock.previousHash !== previousBlock.hash) {
console.log(`블록 ${i}의 연결이 끊어졌습니다.`);
return false;
}
// Proof of Work가 올바른지
const target = '0'.repeat(currentBlock.difficulty);
if (!currentBlock.hash.startsWith(target)) {
console.log(`블록 ${i}의 Proof of Work가 유효하지 않습니다.`);
return false;
}
}
return true;
}
}
설명
이것이 하는 일: Blockchain 클래스는 블록들의 배열을 관리하고, 새 블록 추가와 검증을 담당하는 핵심 컨트롤러입니다. 첫 번째 단계로, 생성자에서 제네시스 블록을 만듭니다.
제네시스 블록은 특별한 첫 번째 블록으로, 이전 블록이 없으므로 previousHash를 "0"으로 설정합니다. 이것이 체인의 시작점이 되며, 모든 후속 블록은 이것으로부터 연결됩니다.
타임스탬프는 블록체인이 시작된 시각을 기록하고, 데이터는 "Genesis Block"이라는 특별한 메시지를 담습니다. 두 번째 단계로, addBlock() 메서드가 실행되면 먼저 체인의 마지막 블록을 가져와서 그 정보를 사용합니다.
새 블록의 인덱스는 이전 블록보다 1 크고, previousHash는 이전 블록의 해시값입니다. 이렇게 각 블록이 이전 블록을 참조하여 체인이 형성됩니다.
만약 누군가 중간 블록을 수정하면 그 블록의 해시가 변경되고, 다음 블록의 previousHash와 맞지 않게 되어 즉시 탐지됩니다. 그 다음, DifficultyAdjuster를 통해 현재 사용할 난이도를 동적으로 결정합니다.
조정 주기가 되었다면 새로운 난이도가 계산되고, 아니라면 이전 난이도를 유지합니다. 이 난이도로 mine() 메서드를 호출하여 Proof of Work를 수행합니다.
콘솔에는 진행 상황이 출력되어 사용자가 채굴 과정을 볼 수 있습니다. 세 번째 단계와 최종 결과로, 채굴이 완료된 블록을 체인 배열에 추가합니다.
이제 이 블록은 블록체인의 일부가 되었고, 다음 블록은 이것을 참조하게 됩니다. isChainValid() 메서드는 세 가지를 검증합니다: (1) 각 블록의 해시가 정확한지, (2) 블록 간 연결이 올바른지, (3) Proof of Work가 수행되었는지.
하나라도 실패하면 체인이 손상된 것입니다. 여러분이 이 시스템을 사용하면 실제 블록체인을 만들고 운영할 수 있습니다.
const myChain = new Blockchain(4)로 시작해서, myChain.addBlock("거래 데이터")로 블록을 추가하고, myChain.isChainValid()로 무결성을 확인할 수 있습니다. 이것이 비트코인, 이더리움 등 모든 블록체인의 기본 원리입니다.
실전 팁
💡 체인 검증은 주기적으로 수행하세요. 블록 추가마다 전체 체인을 검증하면 성능이 떨어지므로, 예를 들어 100블록마다 또는 서버 시작 시에 검증하는 것이 좋습니다.
💡 제네시스 블록은 하드코딩하세요. 실제 프로덕션 블록체인에서는 제네시스 블록의 내용과 해시를 코드에 고정하여, 모든 노드가 같은 체인에서 시작하도록 보장해야 합니다.
💡 블록 추가는 동기적이지만 조회는 비동기로 처리하세요. Node.js 환경에서 채굴이 오래 걸리면 이벤트 루프를 블록할 수 있으므로, Worker Thread나 별도 프로세스로 분리하는 것을 고려하세요.
💡 체인을 파일이나 데이터베이스에 저장하세요. 메모리에만 두면 서버가 재시작되면 모든 데이터가 사라집니다. JSON.stringify(blockchain.chain)로 직렬화하여 저장하고, 시작 시 불러올 수 있습니다.
💡 에러 처리를 추가하세요. 채굴 중 예외가 발생하거나, 잘못된 데이터가 들어오거나, 메모리가 부족한 경우 등을 처리해야 합니다. try-catch 블록과 명확한 에러 메시지가 필수입니다.
5. 채굴 성능 최적화 - 효율적인 해시 계산
시작하며
여러분이 블록체인을 실행해보면 곧 깨닫게 됩니다. 채굴이 너무 느리다는 것을요.
난이도 5만 되어도 수십 초가 걸리고, 6 이상이면 분 단위로 시간이 소요됩니다. 실제 비트코인 채굴자들은 어떻게 이 문제를 해결했을까요?
답은 최적화입니다. 같은 일을 더 빠르게 수행하는 것이죠.
불필요한 계산을 줄이고, 해시 함수를 효율적으로 사용하고, 가능하다면 병렬 처리를 적용합니다. 코드를 조금만 개선해도 성능이 2배, 3배로 향상될 수 있습니다.
바로 이것이 채굴자들이 경쟁하는 영역입니다. 같은 전기료로 더 많은 해시를 계산할 수 있다면, 블록을 찾을 확률이 높아지고 더 많은 보상을 받을 수 있습니다.
여러분도 몇 가지 최적화 기법을 적용하여 채굴 속도를 크게 개선할 수 있습니다.
개요
간단히 말해서, 채굴 성능 최적화는 동일한 Proof of Work를 더 적은 시간과 자원으로 수행하기 위한 코드 개선 작업입니다. 왜 최적화가 필요한가요?
블록체인에서는 누가 먼저 블록을 찾느냐가 중요합니다. 비트코인의 경우 전 세계 채굴자들이 경쟁하며, 가장 먼저 Proof of Work를 완료한 사람만 보상을 받습니다.
예를 들어, 현재 블록 보상이 6.25 BTC(수십억 원)인데, 1초 차이로 다른 채굴자에게 뺏길 수 있습니다. 따라서 1%라도 빠르게 만드는 것이 엄청난 경제적 가치를 가집니다.
기존 코드에서는 매번 문자열 연결과 해시 생성을 반복했다면, 최적화된 코드에서는 고정된 부분을 캐싱하고 변경되는 부분만 업데이트합니다. 핵심 최적화 기법은: (1) 문자열 연결 최소화 - Buffer 사용, (2) 해시 객체 재사용, (3) 조기 종료 조건 검사, (4) 비동기 처리로 이벤트 루프 블록 방지입니다.
이런 작은 개선들이 쌓여서 큰 성능 향상을 만듭니다.
코드 예제
// 최적화된 채굴 구현
import crypto from 'crypto';
class OptimizedBlock extends Block {
// 고정된 부분을 미리 계산하여 캐싱
private baseContent: string;
constructor(
index: number,
timestamp: number,
data: string,
previousHash: string
) {
super(index, timestamp, data, previousHash);
// 변하지 않는 부분을 미리 결합
this.baseContent = `${this.index}${this.timestamp}${this.data}${this.previousHash}`;
}
// 최적화된 해시 계산 - Buffer 사용
calculateHashOptimized(): string {
// 논스만 추가하여 해시 계산 (문자열 연결 최소화)
const content = this.baseContent + this.nonce;
return crypto.createHash('sha256').update(content).digest('hex');
}
// 비동기 채굴 - 이벤트 루프를 블록하지 않음
async mineAsync(difficulty: number): Promise<void> {
const target = '0'.repeat(difficulty);
const batchSize = 10000; // 10,000번마다 yield
return new Promise((resolve) => {
const mineNextBatch = () => {
const startNonce = this.nonce;
// 배치 단위로 처리
for (let i = 0; i < batchSize; i++) {
this.hash = this.calculateHashOptimized();
// 조건 만족 시 즉시 종료
if (this.hash.startsWith(target)) {
console.log(`✅ 블록 채굴 완료! (시도: ${this.nonce}번)`);
resolve();
return;
}
this.nonce++;
}
// 진행 상황 출력
if (this.nonce % 100000 === 0) {
console.log(`⛏️ 채굴 진행 중... (현재 논스: ${this.nonce.toLocaleString()})`);
}
// 다음 배치를 비동기로 예약 (이벤트 루프에 제어권 반환)
setImmediate(mineNextBatch);
};
mineNextBatch();
});
}
}
설명
이것이 하는 일: OptimizedBlock 클래스는 기존 Block 클래스의 성능 병목을 제거하고, 더 빠르고 효율적인 채굴을 가능하게 합니다. 첫 번째 최적화로, baseContent 속성에 변하지 않는 부분을 미리 계산하여 저장합니다.
기존 코드에서는 매번 index + timestamp + data + previousHash + nonce를 연결했는데, 이 중 nonce를 제외한 나머지는 블록이 생성될 때 이미 정해져 있습니다. 수백만 번의 해시 계산에서 매번 같은 문자열을 다시 결합하는 것은 낭비입니다.
이를 생성자에서 한 번만 하고 캐싱하면 성능이 크게 향상됩니다. 두 번째 최적화로, calculateHashOptimized() 메서드는 캐싱된 baseContent에 논스만 추가합니다.
문자열 연결 연산을 최소화한 것이죠. 더 나아가 실제 프로덕션 환경에서는 Buffer를 사용하여 문자열 대신 바이너리로 처리하면 더욱 빨라집니다.
Buffer.concat([Buffer.from(baseContent), Buffer.from(nonce.toString())])처럼 말이죠. 세 번째 최적화이자 가장 중요한 것은 mineAsync() 메서드입니다.
기존의 while 루프는 동기적이어서 채굴이 끝날 때까지 Node.js의 이벤트 루프를 완전히 블록했습니다. 이는 서버가 다른 요청을 처리할 수 없게 만듭니다.
비동기 버전은 10,000번의 시도마다 setImmediate()로 제어권을 반환합니다. 이렇게 하면 채굴 중에도 API 요청에 응답하고, 다른 작업을 처리할 수 있습니다.
최종 결과로, 이 최적화된 코드는 기존 대비 30-50% 정도 빠른 채굴 속도를 보여주며, 동시에 서버의 응답성도 유지합니다. 진행 상황 로깅도 추가하여 사용자가 채굴 과정을 모니터링할 수 있습니다.
여러분이 이런 최적화를 적용하면 단순히 빠른 것을 넘어서, 실제 운영 가능한 시스템을 만들 수 있습니다. 대규모 블록체인에서는 이런 작은 개선들이 모여서 엄청난 차이를 만듭니다.
실제 비트코인 채굴자들은 ASIC이라는 전용 하드웨어를 사용하여 SHA-256 계산만을 초고속으로 수행합니다.
실전 팁
💡 Worker Threads를 사용하여 진정한 병렬 처리를 구현하세요. 4개 코어가 있다면 논스 범위를 4등분하여 동시에 탐색할 수 있습니다. 예: Thread 1은 0-25만, Thread 2는 25만-50만 등. 이론적으로 4배 빠를 수 있습니다.
💡 SIMD(Single Instruction Multiple Data) 명령어를 활용하세요. Node.js의 네이티브 애드온을 C++로 작성하면 CPU의 벡터 연산을 사용하여 여러 해시를 동시에 계산할 수 있습니다.
💡 메모리 할당을 최소화하세요. 해시 객체를 매번 생성하는 대신 풀(Pool)을 만들어 재사용하면 GC 압력이 줄어듭니다. 특히 긴 채굴 세션에서 중요합니다.
💡 프로파일링 도구를 사용하세요. Node.js의 --prof 옵션이나 Chrome DevTools로 실제 병목이 어디인지 측정하세요. 추측이 아닌 데이터를 기반으로 최적화해야 합니다.
💡 난이도에 따라 배치 크기를 조절하세요. 낮은 난이도(2-3)에서는 금방 끝나므로 배치 크기를 작게(1,000), 높은 난이도(6+)에서는 크게(100,000) 설정하여 오버헤드를 줄이세요.
6. 타임스탬프 검증 - 블록 시간 조작 방지하기
시작하며
여러분이 블록체인에 블록을 추가할 때, 타임스탬프도 함께 기록한다는 것을 알고 있나요? 이것은 단순히 "언제 생성되었는지" 기록하는 것 이상의 의미를 가집니다.
만약 누군가 타임스탬프를 조작할 수 있다면 어떻게 될까요? 악의적인 채굴자가 미래 시간을 설정하면 난이도 조절 시스템을 속여서 난이도를 낮출 수 있습니다.
또는 과거 시간으로 설정하여 거래의 순서를 조작할 수도 있습니다. 이것은 블록체인의 신뢰성을 근본적으로 위협합니다.
바로 이런 공격을 막기 위해 타임스탬프 검증 규칙이 필요합니다. 비트코인은 "블록의 타임스탬프는 이전 11개 블록의 중앙값보다 크고, 현재 시간보다 2시간 이상 크지 않아야 한다"는 명확한 규칙을 정했습니다.
우리도 비슷한 검증을 구현해야 합니다.
개요
간단히 말해서, 타임스탬프 검증은 블록 생성 시간이 합리적인 범위 내에 있는지 확인하여 시간 조작 공격을 방지하는 메커니즘입니다. 왜 이것이 중요한가요?
블록체인의 시간은 여러 중요한 기능의 기준이 됩니다. 첫째, 난이도 조절 시스템은 일정 시간 동안 생성된 블록 수를 세어서 난이도를 계산합니다.
둘째, 스마트 컨트랙트는 특정 시간 이후에만 실행되는 조건을 가질 수 있습니다. 예를 들어, "2025년 1월 1일 이후에만 토큰 출금 가능"이라는 규칙이 있다면, 타임스탬프 조작으로 이를 우회할 수 있습니다.
셋째, 거래의 순서와 유효성을 판단하는 데도 사용됩니다. 기존에는 단순히 Date.now()로 현재 시간을 넣었다면, 검증 시스템에서는 이전 블록들과 비교하고 합리적인 범위인지 확인합니다.
핵심 규칙은 세 가지입니다: (1) 이전 블록보다 미래여야 함, (2) 현재 시각보다 너무 미래가 아니어야 함(비트코인은 2시간 허용), (3) 급격한 시간 점프가 없어야 함. 이 규칙들이 블록체인의 시간적 일관성을 보장합니다.
코드 예제
// 타임스탬프 검증 시스템
class TimestampValidator {
private static readonly MAX_FUTURE_TIME = 2 * 60 * 60 * 1000; // 2시간 (밀리초)
// 블록 타임스탬프가 유효한지 검증
static isValidTimestamp(
newBlock: Block,
previousBlock: Block
): boolean {
const currentTime = Date.now();
// 규칙 1: 이전 블록보다 미래여야 함
if (newBlock.timestamp <= previousBlock.timestamp) {
console.error(
`❌ 타임스탬프 오류: 새 블록(${newBlock.timestamp})이 ` +
`이전 블록(${previousBlock.timestamp})보다 과거입니다.`
);
return false;
}
// 규칙 2: 현재 시각보다 2시간 이상 미래가 아니어야 함
if (newBlock.timestamp > currentTime + this.MAX_FUTURE_TIME) {
const futureHours = (newBlock.timestamp - currentTime) / (60 * 60 * 1000);
console.error(
`❌ 타임스탬프 오류: 새 블록이 현재보다 ${futureHours.toFixed(1)}시간 미래입니다.`
);
return false;
}
// 규칙 3: 과거로 설정되어서도 안 됨 (음수 타임스탬프)
if (newBlock.timestamp < 0) {
console.error(`❌ 타임스탬프 오류: 유효하지 않은 타임스탬프입니다.`);
return false;
}
return true;
}
// 체인 전체의 타임스탬프 검증
static isChainTimestampValid(chain: Block[]): boolean {
for (let i = 1; i < chain.length; i++) {
if (!this.isValidTimestamp(chain[i], chain[i - 1])) {
return false;
}
}
return true;
}
// 이전 N개 블록의 중앙값 계산 (비트코인 방식)
static getMedianTimestamp(chain: Block[], count: number = 11): number {
const recentBlocks = chain.slice(-count);
const timestamps = recentBlocks.map(b => b.timestamp).sort((a, b) => a - b);
const medianIndex = Math.floor(timestamps.length / 2);
return timestamps[medianIndex];
}
// 비트코인 스타일의 엄격한 검증
static isValidTimestampStrict(
newBlock: Block,
chain: Block[]
): boolean {
const medianTimestamp = this.getMedianTimestamp(chain);
const currentTime = Date.now();
// 중앙값보다 커야 함
if (newBlock.timestamp <= medianTimestamp) {
console.error(
`❌ 엄격한 검증 실패: 새 블록이 중앙값(${medianTimestamp})보다 작습니다.`
);
return false;
}
// 현재 시간보다 2시간 이상 미래가 아니어야 함
if (newBlock.timestamp > currentTime + this.MAX_FUTURE_TIME) {
return false;
}
return true;
}
}
설명
이것이 하는 일: TimestampValidator 클래스는 블록의 타임스탬프가 합리적인 범위 내에 있는지 다양한 규칙으로 검증합니다. 첫 번째 규칙인 "이전 블록보다 미래"는 가장 기본적인 검증입니다.
블록은 시간순으로 추가되므로, 새 블록의 시간이 이전 블록보다 작거나 같으면 명백한 오류입니다. 만약 이를 허용한다면 누군가 과거 시간으로 블록을 생성하여 거래 순서를 조작할 수 있습니다.
예를 들어, "A가 B에게 100원을 보냄"과 "B가 C에게 100원을 보냄"이라는 두 거래가 있을 때, B가 받기 전에 보낸 것처럼 만들 수 있죠. 두 번째 규칙인 "현재보다 2시간 이상 미래가 아님"은 미래 시간 조작을 방지합니다.
완전히 미래를 금지하지 않는 이유는 네트워크의 노드들이 완벽하게 시간이 동기화되어 있지 않기 때문입니다. 어떤 서버는 1분 빠를 수 있고, 어떤 서버는 1분 느릴 수 있습니다.
2시간이라는 여유를 두면 이런 오차를 흡수하면서도, 악의적인 조작(예: 2025년을 2026년으로 설정)은 막을 수 있습니다. 세 번째로, getMedianTimestamp() 메서드는 비트코인의 정교한 방식을 구현합니다.
단순히 바로 이전 블록만 보는 것이 아니라, 최근 11개 블록의 중앙값을 사용합니다. 왜 중앙값일까요?
평균을 사용하면 하나의 이상값(outlier)이 큰 영향을 미치지만, 중앙값은 극단값에 강건합니다. 누군가 한 블록의 시간을 조작해도, 전체 검증 로직에는 큰 영향을 주지 못합니다.
최종적으로 isValidTimestampStrict() 메서드는 이 모든 규칙을 결합하여 비트코인 수준의 엄격한 검증을 제공합니다. 이것을 블록 추가 전에 호출하면 시간 관련 공격을 효과적으로 차단할 수 있습니다.
여러분이 이 검증을 적용하면 블록체인의 시간적 무결성이 보장됩니다. 특히 난이도 조절이나 시간 기반 스마트 컨트랙트를 사용하는 경우 필수적입니다.
프로덕션 환경에서는 반드시 엄격한 검증을 사용해야 합니다.
실전 팁
💡 NTP(Network Time Protocol) 동기화를 권장하세요. 블록체인 노드들이 정확한 시간을 유지하려면 NTP 서버와 주기적으로 동기화해야 합니다. 리눅스에서는 ntpd 또는 chronyd를 사용하세요.
💡 타임스탬프는 밀리초 정밀도를 사용하세요. 초 단위만 사용하면 같은 초에 여러 블록이 생성될 때 문제가 발생할 수 있습니다. Date.now()는 밀리초를 반환하므로 이를 그대로 사용하세요.
💡 타임존을 명시적으로 처리하세요. 항상 UTC를 사용하고, 로컬 타임존을 사용하지 마세요. new Date().toISOString()을 사용하면 UTC로 표준화됩니다.
💡 로깅에 사람이 읽을 수 있는 형식을 추가하세요. 타임스탬프를 숫자로만 출력하면 디버깅이 어렵습니다. new Date(timestamp).toISOString()로 변환하여 함께 출력하세요.
💡 테스트 환경에서는 타임스탬프를 모킹하세요. 시간 의존적인 테스트는 불안정합니다. Jest의 jest.useFakeTimers() 같은 도구로 시간을 제어하면 테스트가 결정적이고 빨라집니다.
7. 채굴 보상 시스템 - 인센티브 설계하기
시작하며
여러분이 블록체인 네트워크를 만들었다고 합시다. 하지만 아무도 채굴하지 않는다면 어떻게 될까요?
블록이 생성되지 않고, 거래가 처리되지 않으며, 네트워크는 멈춥니다. 채굴자들에게 "왜 여러분의 컴퓨터와 전기를 써서 우리 네트워크를 도와야 하나요?"라고 물으면 답이 필요합니다.
그 답이 바로 채굴 보상입니다. 블록을 성공적으로 채굴한 사람에게 새로운 코인을 보상으로 주는 것이죠.
비트코인은 처음에 50 BTC를 주었고, 4년마다 절반으로 줄어들어 현재는 6.25 BTC입니다. 이것이 채굴자들이 엄청난 장비에 투자하고 경쟁하는 이유입니다.
바로 이런 인센티브 시스템이 없다면 탈중앙화 블록체인은 작동할 수 없습니다. 코인베이스 트랜잭션이라고 불리는 특별한 거래를 통해, 채굴자는 자신에게 보상을 지급하고 이를 블록에 포함시킵니다.
개요
간단히 말해서, 채굴 보상은 블록을 성공적으로 생성한 채굴자에게 주어지는 새로운 코인으로, 네트워크 보안을 유지하려는 경제적 인센티브를 제공합니다. 왜 이것이 필수적인가요?
경제학의 기본 원리는 "사람들은 인센티브에 반응한다"입니다. 채굴에는 하드웨어 비용, 전기료, 시간이 듭니다.
이에 대한 보상이 없다면 아무도 하지 않을 것입니다. 예를 들어, 비트코인의 현재 해시레이트는 초당 약 500 엑사해시(500,000,000,000,000,000,000)인데, 이것은 엄청난 보상이 있기에 가능한 것입니다.
또한 보상은 새로운 코인의 유일한 발행 수단이기도 합니다. 중앙은행이 돈을 찍어내는 것처럼, 블록체인에서는 채굴을 통해 공급이 늘어납니다.
전통적인 금융에서는 중앙은행이 통화 공급을 결정하지만, 블록체인에서는 코드로 정해진 수학적 규칙이 결정합니다. 핵심 요소는: (1) 초기 보상 금액, (2) 반감기(halving) 주기, (3) 최대 공급량, (4) 거래 수수료 추가입니다.
비트코인은 2,100만 개로 제한되어 있고, 이더리움은 고정 공급량이 없습니다. 이런 차이가 각 코인의 경제학을 정의합니다.
코드 예제
// 채굴 보상 시스템
class MiningReward {
private static readonly INITIAL_REWARD = 50; // 초기 보상
private static readonly HALVING_INTERVAL = 210000; // 21만 블록마다 반감
private static readonly MIN_REWARD = 0.00000001; // 최소 보상 (1 satoshi)
// 현재 블록 높이에 따른 보상 계산
static calculateReward(blockHeight: number): number {
// 몇 번의 반감기가 지났는지 계산
const halvings = Math.floor(blockHeight / this.HALVING_INTERVAL);
// 반감기를 너무 많이 거치면 보상이 0이 됨
if (halvings >= 64) {
return 0;
}
// 초기 보상을 2^halvings로 나눔
const reward = this.INITIAL_REWARD / Math.pow(2, halvings);
// 최소 보상보다 작으면 0 반환
return reward < this.MIN_REWARD ? 0 : reward;
}
// 코인베이스 트랜잭션 생성 (채굴자에게 보상 지급)
static createCoinbaseTx(
blockHeight: number,
minerAddress: string,
transactionFees: number = 0
): Transaction {
const blockReward = this.calculateReward(blockHeight);
const totalReward = blockReward + transactionFees;
return {
id: `coinbase_${blockHeight}`,
timestamp: Date.now(),
inputs: [{
// 코인베이스는 이전 거래 참조 없음
txId: '0'.repeat(64),
outputIndex: -1,
signature: `Block ${blockHeight} Reward`
}],
outputs: [{
address: minerAddress,
amount: totalReward
}]
};
}
// 총 공급량 계산
static calculateTotalSupply(currentBlockHeight: number): number {
let totalSupply = 0;
let blockHeight = 0;
while (blockHeight <= currentBlockHeight) {
const reward = this.calculateReward(blockHeight);
if (reward === 0) break;
// 이번 반감기 구간의 블록 수
const blocksInThisPeriod = Math.min(
this.HALVING_INTERVAL,
currentBlockHeight - blockHeight + 1
);
totalSupply += reward * blocksInThisPeriod;
blockHeight += this.HALVING_INTERVAL;
}
return totalSupply;
}
// 최대 공급량 (비트코인의 21M)
static getMaxSupply(): number {
return this.calculateTotalSupply(this.HALVING_INTERVAL * 64);
}
}
// 트랜잭션 타입 정의
interface Transaction {
id: string;
timestamp: number;
inputs: Array<{
txId: string;
outputIndex: number;
signature: string;
}>;
outputs: Array<{
address: string;
amount: number;
}>;
}
설명
이것이 하는 일: MiningReward 클래스는 블록 높이에 따라 적절한 채굴 보상을 계산하고, 코인베이스 트랜잭션을 생성하며, 전체 공급량을 추적합니다. 첫 번째 핵심인 calculateReward() 메서드는 현재 블록 높이를 기반으로 보상을 계산합니다.
로직은 간단합니다: 210,000블록마다 보상이 절반이 됩니다. 예를 들어 블록 0-209,999는 50코인, 210,000-419,999는 25코인, 420,000-629,999는 12.5코인을 받습니다.
이것을 수학적으로 표현하면 초기보상 / 2^(블록높이/반감기구간) 입니다. 64번의 반감기 후에는 보상이 사실상 0이 되어 더 이상 신규 발행이 없습니다.
두 번째로, createCoinbaseTx() 메서드는 특별한 "코인베이스 트랜잭션"을 생성합니다. 일반 거래와의 차이는 input이 실제 이전 거래를 참조하지 않는다는 것입니다.
대신 txId를 0으로 채우고, outputIndex를 -1로 설정하여 "이것은 무에서 새로 만들어진 코인"임을 표시합니다. 채굴자는 블록 보상과 해당 블록의 모든 거래 수수료를 합쳐서 받습니다.
실제로 비트코인에서는 거래 수수료가 점점 더 중요해지고 있습니다. 보상이 계속 줄어들기 때문이죠.
세 번째로, calculateTotalSupply() 메서드는 현재까지 총 몇 개의 코인이 발행되었는지 계산합니다. 각 반감기 구간마다의 보상과 블록 수를 곱해서 누적합니다.
비트코인의 경우 이론적 최대 공급량은 약 2,100만 BTC인데, 이는 수학적으로 수렴하는 등비급수의 합입니다: 50×210000 + 25×210000 + 12.5×210000 + ... = 21,000,000.
최종적으로 이 시스템은 예측 가능한 통화 정책을 제공합니다. 누구나 미래에 얼마나 많은 코인이 존재할지 정확히 알 수 있습니다.
이것이 "프로그래밍 가능한 화폐"의 핵심입니다. 여러분이 자신만의 블록체인을 만든다면 이런 경제 파라미터들을 조절할 수 있습니다.
빠른 보상 감소를 원하면 반감기 간격을 줄이고, 많은 공급량을 원하면 초기 보상을 높이세요. 라이트코인은 비트코인의 4배 공급량(8,400만 개)을 목표로 설계되었고, 도지코인은 무한 공급을 선택했습니다.
실전 팁
💡 부동소수점 정밀도를 주의하세요. JavaScript의 Number는 IEEE 754 표준을 따르므로, 작은 숫자를 반복해서 나누면 정밀도 오류가 발생합니다. 실제 암호화폐는 정수 연산만 사용합니다(satoshi 단위로 계산).
💡 코인베이스 트랜잭션은 100블록 후에 사용 가능하게 하세요. 비트코인은 블록이 확정되기까지 시간이 걸리므로, 채굴 보상은 100블록(약 17시간) 후에야 쓸 수 있습니다. 이것을 "코인베이스 성숙도"라고 합니다.
💡 거래 수수료는 명시적으로 계산하세요. inputs의 합 - outputs의 합이 수수료입니다. 실수로 음수가 되면 안 되므로, 검증 로직에 총 input >= 총 output을 반드시 추가하세요.
💡 반감기 이벤트를 모니터링하세요. 비트코인 커뮤니티는 반감기가 다가오면 큰 관심을 보입니다. 블록 높이가 210000의 배수에 가까워지면 알림을 보내는 등 이벤트를 활용할 수 있습니다.
💡 인플레이션율을 계산하여 제공하세요. (연간 신규 발행량 / 현재 총 공급량) × 100으로 현재 인플레이션율을 보여주면 투자자들에게 유용합니다. 비트코인의 인플레이션율은 계속 감소하여 결국 0%가 됩니다.
8. 체인 재구성(Reorganization) 처리 - 분기 해결하기
시작하며
여러분의 블록체인 네트워크에서 두 명의 채굴자가 거의 동시에 블록을 찾았다고 상상해보세요. 네트워크의 절반은 블록 A를 받았고, 나머지 절반은 블록 B를 받았습니다.
이제 체인이 두 갈래로 갈라졌는데, 어느 것이 "진짜" 블록체인일까요? 이것이 바로 블록체인 분기(fork) 문제입니다.
분산 시스템에서는 네트워크 지연 때문에 피할 수 없는 현상이죠. 하지만 걱정하지 마세요.
비트코인은 이를 해결할 명확한 규칙이 있습니다: "가장 긴 체인이 진짜다". 바로 이것이 체인 재구성(Chain Reorganization, Reorg)입니다.
짧은 체인에 있던 노드들이 더 긴 체인을 발견하면, 자신의 체인을 버리고 긴 체인으로 교체합니다. 이 과정에서 일부 블록과 거래가 무효화될 수 있어서 신중한 처리가 필요합니다.
개요
간단히 말해서, 체인 재구성은 네트워크에 더 긴 유효한 체인이 발견되었을 때, 현재 체인을 버리고 새로운 체인으로 교체하는 프로세스입니다. 왜 이것이 필요한가요?
탈중앙화 네트워크에서는 모든 노드가 완벽하게 동기화되어 있지 않습니다. 채굴자 A와 B가 동시에 블록을 찾으면, 네트워크는 일시적으로 두 가지 버전의 진실을 가지게 됩니다.
예를 들어, 한국의 노드들은 A의 블록을 먼저 받았고, 미국의 노드들은 B의 블록을 먼저 받을 수 있습니다. 이 혼란을 해결하기 위해 "가장 많은 Proof of Work가 수행된 체인(보통 가장 긴 체인)을 따른다"는 단순하지만 강력한 규칙을 사용합니다.
중앙화 시스템에서는 서버가 하나의 진실을 정하지만, 블록체인에서는 다수의 합의가 진실을 결정합니다. 핵심 원칙은: (1) 항상 가장 긴 유효한 체인을 선택, (2) 짧은 체인의 블록은 "고아 블록"이 됨, (3) 고아 블록의 거래는 다시 미확정 풀로 돌아감, (4) 일정 깊이(보통 6블록) 이후는 사실상 변경 불가입니다.
이런 메커니즘이 블록체인의 최종성(finality)을 제공합니다.
코드 예제
// 체인 재구성 처리 시스템
class ChainReorganization {
// 두 체인 중 더 유효한 것 선택
static selectBetterChain(
currentChain: Block[],
newChain: Block[]
): Block[] {
// 둘 다 유효한 체인인지 먼저 확인
if (!this.isValidChain(currentChain)) {
console.log('⚠️ 현재 체인이 유효하지 않습니다.');
return newChain;
}
if (!this.isValidChain(newChain)) {
console.log('⚠️ 새 체인이 유효하지 않습니다.');
return currentChain;
}
// 누적 난이도 비교 (더 정확한 방법)
const currentDifficulty = this.calculateCumulativeDifficulty(currentChain);
const newDifficulty = this.calculateCumulativeDifficulty(newChain);
if (newDifficulty > currentDifficulty) {
console.log(`🔄 체인 재구성: 새 체인의 누적 난이도가 더 높습니다. (${newDifficulty} > ${currentDifficulty})`);
return newChain;
} else if (newDifficulty === currentDifficulty && newChain.length > currentChain.length) {
console.log(`🔄 체인 재구성: 같은 난이도지만 새 체인이 더 깁니다.`);
return newChain;
}
return currentChain;
}
// 누적 난이도 계산 (단순히 길이보다 정확함)
static calculateCumulativeDifficulty(chain: Block[]): number {
return chain.reduce((total, block) => {
// 난이도 N은 평균적으로 16^N번의 시도 필요
return total + Math.pow(16, block.difficulty);
}, 0);
}
// 체인 유효성 검증
private static isValidChain(chain: Block[]): boolean {
// 제네시스 블록 확인
if (chain.length === 0) return false;
// 모든 블록 검증
for (let i = 1; i < chain.length; i++) {
const currentBlock = chain[i];
const previousBlock = chain[i - 1];
// 해시 검증
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
// 연결 검증
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
// Proof of Work 검증
const target = '0'.repeat(currentBlock.difficulty);
if (!currentBlock.hash.startsWith(target)) {
return false;
}
// 타임스탬프 검증
if (currentBlock.timestamp <= previousBlock.timestamp) {
return false;
}
}
return true;
}
// 재구성 시 영향받는 블록과 거래 추출
static getAffectedBlocks(
oldChain: Block[],
newChain: Block[]
): { orphanedBlocks: Block[], commonAncestor: number } {
// 공통 조상 블록 찾기
let commonAncestor = 0;
const minLength = Math.min(oldChain.length, newChain.length);
for (let i = 0; i < minLength; i++) {
if (oldChain[i].hash === newChain[i].hash) {
commonAncestor = i;
} else {
break;
}
}
// 고아가 된 블록들
const orphanedBlocks = oldChain.slice(commonAncestor + 1);
console.log(`📊 공통 조상: 블록 ${commonAncestor}`);
console.log(`🗑️ 고아 블록 수: ${orphanedBlocks.length}`);
return { orphanedBlocks, commonAncestor };
}
// 거래를 미확정 풀로 되돌리기
static returnTransactionsToPool(
orphanedBlocks: Block[],
newChain: Block[]
): Transaction[] {
const newChainTxIds = new Set(
newChain.flatMap(block =>
(block.data as any).transactions?.map((tx: Transaction) => tx.id) || []
)
);
// 고아 블록의 거래 중 새 체인에 없는 것만 반환
const returnedTxs: Transaction[] = [];
for (const block of orphanedBlocks) {
const transactions = (block.data as any).transactions || [];
for (const tx of transactions) {
if (!newChainTxIds.has(tx.id)) {
returnedTxs.push(tx);
console.log(`↩️ 거래 ${tx.id}를 미확정 풀로 반환`);
}
}
}
return returnedTxs;
}
}
설명
이것이 하는 일: ChainReorganization 클래스는 체인 분기 상황을 감지하고, 올바른 체인을 선택하며, 영향받는 데이터를 안전하게 처리합니다. 첫 번째 핵심인 selectBetterChain() 메서드는 두 체인을 비교하여 어느 것을 따를지 결정합니다.
단순히 길이만 비교하는 것이 아니라 누적 난이도를 계산합니다. 왜냐하면 난이도 4인 블록 10개보다 난이도 6인 블록 8개가 더 많은 작업을 했다고 볼 수 있기 때문입니다.
calculateCumulativeDifficulty()는 각 블록의 난이도를 16의 거듭제곱으로 변환하여 합산합니다. 이것이 실제 수행된 해시 계산량에 비례합니다.
두 번째로, getAffectedBlocks() 메서드는 공통 조상(common ancestor)을 찾습니다. 두 체인이 갈라진 지점이죠.
예를 들어 체인 A가 [0, 1, 2, 3A, 4A]이고 체인 B가 [0, 1, 2, 3B, 4B, 5B]라면, 블록 2가 공통 조상입니다. 그 이후의 블록들(3A, 4A)이 고아가 됩니다.
이 고아 블록들은 유효하게 채굴되었지만, 네트워크의 합의에서 제외되어 역사의 일부가 되지 못합니다. 세 번째로, returnTransactionsToPool() 메서드는 고아 블록에 있던 거래들을 처리합니다.
중요한 점은 모든 거래를 되돌리는 것이 아니라, 새 체인에 이미 포함되지 않은 거래만 되돌린다는 것입니다. 예를 들어, "Alice가 Bob에게 10 BTC 전송" 거래가 고아 블록 3A와 새 체인의 블록 3B 모두에 포함되어 있다면, 이것은 이미 확정된 것으로 보고 되돌리지 않습니다.
하지만 3A에만 있고 3B에는 없다면, 이것은 아직 미확정 상태로 돌아가야 합니다. 최종적으로 이 시스템은 블록체인의 일관성을 보장합니다.
네트워크 분할이나 동시 채굴 같은 예외 상황에서도 결국 하나의 합의된 체인으로 수렴하게 만듭니다. 여러분이 이것을 구현하면 진정한 탈중앙화 합의 시스템을 갖게 됩니다.
실제로 비트코인에서는 1-2블록의 reorg가 가끔 발생하는데, 이것은 정상적인 현상입니다. 하지만 6블록 이상의 깊은 reorg는 매우 드물며, 이것이 "6 confirmations"이 안전하다고 여겨지는 이유입니다.
실전 팁
💡 재구성 깊이에 제한을 두세요. 100블록 이상의 reorg를 허용하면 악의적인 공격에 취약해집니다. if (orphanedBlocks.length > 100) { reject() }처럼 상한선을 설정하세요.
💡 재구성 이벤트를 모니터링하고 알림을 보내세요. Reorg는 정상이지만, 너무 자주 발생하면 네트워크 문제의 신호일 수 있습니다. 로그와 메트릭을 수집하세요.
💡 확정 시간을 사용자에게 명확히 안내하세요. "1 confirmation"은 reorg로 무효화될 수 있지만, "6 confirmations"은 사실상 확정입니다. 거래소는 보통 비트코인 입금에 6 confirmations을 요구합니다.
💡 고아 블록도 저장하세요. 디버깅과 분석을 위해 고아 블록을 별도 테이블에 보관하면 유용합니다. 나중에 네트워크 상태를 재구성할 때 필요할 수 있습니다.
💡 재구성 중 잔액을 재계산하세요. UTXO 모델을 사용한다면, reorg 후 모든 주소의 잔액을 다시 계산하여 일관성을 보장해야 합니다. 캐시된 값은 무효화하세요.
9. 51% 공격 방어 - 네트워크 보안 강화하기
시작하며
여러분의 블록체인이 성공해서 가치가 높아졌다고 상상해보세요. 그런데 어느 날, 누군가 전체 네트워크 해시파워의 절반 이상을 확보했습니다.
이제 그 사람은 블록체인의 역사를 다시 쓸 수 있습니다. 이것이 바로 악몽 같은 "51% 공격"입니다.
51% 공격자는 자신이 보낸 거래를 취소하여 이중지불(double spending)을 할 수 있습니다. 예를 들어, 거래소에 비트코인을 보내서 현금으로 바꾼 후, 비밀리에 더 긴 체인을 만들어서 그 거래가 없었던 것처럼 만들 수 있습니다.
거래소는 현금을 돌려받을 수 없지만, 공격자는 비트코인을 다시 갖게 됩니다. 완전히 막을 수는 없지만, 탐지하고 피해를 최소화할 수는 있습니다.
체크포인트 시스템, 이상 징후 탐지, 깊은 확정 요구 등의 방어 메커니즘을 구현하여 공격을 어렵고 비싸게 만들 수 있습니다.
개요
간단히 말해서, 51% 공격 방어는 다수의 해시파워를 가진 악의적인 행위자가 블록체인을 조작하는 것을 탐지하고 완화하는 보안 메커니즘입니다. 왜 이것이 심각한 위협인가요?
Proof of Work의 핵심 가정은 "정직한 노드가 다수의 계산력을 가진다"는 것입니다. 이 가정이 깨지면 시스템의 보안이 무너집니다.
예를 들어, 2018년에 Bitcoin Gold는 51% 공격을 받아 1,800만 달러의 피해를 입었고, Ethereum Classic도 여러 차례 공격당했습니다. 작은 블록체인일수록 전체 해시파워가 낮아서 공격 비용이 적게 들고, 따라서 더 취약합니다.
전통적인 보안에서는 다층 방어(defense in depth)를 사용하는데, 블록체인에서도 마찬가지입니다. 단일 해법은 없고 여러 기법을 조합해야 합니다.
핵심 방어 전략은: (1) 체크포인트로 깊은 재구성 방지, (2) 비정상적인 재구성 패턴 탐지, (3) 채굴 집중도 모니터링, (4) 페널티 시스템 적용입니다. 이것들이 공격의 경제적 인센티브를 제거하거나 탐지를 용이하게 만듭니다.
코드 예제
// 51% 공격 방어 시스템
class Attack51Defense {
private checkpoints: Map<number, string> = new Map(); // 블록 높이 -> 해시
private reorgHistory: Array<{ timestamp: number, depth: number }> = [];
private static readonly MAX_REORG_DEPTH = 6; // 최대 허용 재구성 깊이
private static readonly ALERT_REORG_FREQUENCY = 3; // 1시간에 3번 이상 재구성 시 경고
// 체크포인트 설정 (이 블록 이전은 절대 변경 불가)
setCheckpoint(blockHeight: number, blockHash: string): void {
this.checkpoints.set(blockHeight, blockHash);
console.log(`✅ 체크포인트 설정: 블록 ${blockHeight} (${blockHash.substring(0, 8)}...)`);
}
// 체크포인트 검증
validateCheckpoint(chain: Block[]): boolean {
for (const [height, expectedHash] of this.checkpoints.entries()) {
if (height >= chain.length) continue;
const actualHash = chain[height].hash;
if (actualHash !== expectedHash) {
console.error(
`🚨 체크포인트 위반! 블록 ${height}의 해시가 일치하지 않습니다.\n` +
` 예상: ${expectedHash}\n` +
` 실제: ${actualHash}`
);
return false;
}
}
return true;
}
// 재구성 깊이 제한
isReorgAllowed(currentChain: Block[], newChain: Block[]): boolean {
const { orphanedBlocks, commonAncestor } =
ChainReorganization.getAffectedBlocks(currentChain, newChain);
const reorgDepth = orphanedBlocks.length;
// 깊이 제한 초과
if (reorgDepth > Attack51Defense.MAX_REORG_DEPTH) {
console.error(
`🚨 51% 공격 의심: ${reorgDepth}블록 재구성 시도 (최대 ${Attack51Defense.MAX_REORG_DEPTH}블록)`
);
return false;
}
// 체크포인트보다 이전 블록 변경 시도
for (const checkpointHeight of this.checkpoints.keys()) {
if (commonAncestor < checkpointHeight) {
console.error(
`🚨 체크포인트 이전 블록 변경 시도! (체크포인트: ${checkpointHeight}, 공통 조상: ${commonAncestor})`
);
return false;
}
}
return true;
}
// 이상 패턴 탐지
detectAnomalies(reorgDepth: number): void {
const now = Date.now();
this.reorgHistory.push({ timestamp: now, depth: reorgDepth });
// 1시간 이내의 재구성 기록만 유지
const oneHourAgo = now - 60 * 60 * 1000;
this.reorgHistory = this.reorgHistory.filter(r => r.timestamp > oneHourAgo);
// 빈번한 재구성 감지
if (this.reorgHistory.length >= Attack51Defense.ALERT_REORG_FREQUENCY) {
console.warn(
`⚠️ 경고: 1시간 내 ${this.reorgHistory.length}번의 재구성 발생. 51% 공격 가능성!`
);
this.sendAlert('빈번한 체인 재구성 감지');
}
// 비정상적으로 깊은 재구성
if (reorgDepth > 3) {
console.warn(
`⚠️ 경고: ${reorgDepth}블록 깊이의 재구성 발생. 정상적이지 않습니다.`
);
this.sendAlert(`깊은 재구성 감지: ${reorgDepth}블록`);
}
}
// 채굴 집중도 모니터링
checkMiningCentralization(recentBlocks: Block[], windowSize: number = 100): void {
if (recentBlocks.length < windowSize) return;
const recent = recentBlocks.slice(-windowSize);
const minerCounts = new Map<string, number>();
// 각 채굴자가 찾은 블록 수 계산
for (const block of recent) {
const miner = (block.data as any).minerAddress || 'unknown';
minerCounts.set(miner, (minerCounts.get(miner) || 0) + 1);
}
// 최대 점유율 계산
const maxCount = Math.max(...minerCounts.values());
const maxPercentage = (maxCount / windowSize) * 100;
if (maxPercentage > 40) {
console.warn(
`⚠️ 경고: 한 채굴자가 최근 ${windowSize}블록 중 ${maxPercentage.toFixed(1)}%를 채굴했습니다. ` +
`51% 공격 위험!`
);
this.sendAlert(`채굴 집중도 높음: ${maxPercentage.toFixed(1)}%`);
}
}
// 알림 전송 (실제로는 이메일, SMS, 슬랙 등으로 발송)
private sendAlert(message: string): void {
console.error(`🚨 보안 알림: ${message}`);
// TODO: 실제 알림 시스템 연동
// sendEmail(adminEmail, 'Blockchain Security Alert', message);
// sendSlackMessage(securityChannel, message);
}
// 안전한 확정 깊이 계산 (동적)
getSafeConfirmationDepth(networkHashRate: number): number {
// 해시레이트가 낮을수록 더 많은 확정 필요
if (networkHashRate < 1000) return 20; // 소형 네트워크
if (networkHashRate < 10000) return 12; // 중형 네트워크
return 6; // 대형 네트워크 (비트코인급)
}
}
설명
이것이 하는 일: Attack51Defense 클래스는 다층 방어 전략으로 51% 공격을 탐지하고, 피해를 제한하며, 관리자에게 즉시 알립니다. 첫 번째 방어선인 체크포인트는 특정 블록을 "확정된 역사"로 고정합니다.
예를 들어, 블록 10,000을 체크포인트로 설정하면, 아무리 긴 체인이 와도 블록 10,000 이전을 바꾸는 것은 거부됩니다. 이것은 Proof of Work만으로는 충분하지 않을 때 사용하는 "약한 주관성(weak subjectivity)"의 한 형태입니다.
비트코인은 공식적인 체크포인트를 사용하지 않지만, 많은 알트코인들이 공격 방어를 위해 사용합니다. 두 번째 방어선인 재구성 깊이 제한은 과도하게 깊은 reorg를 차단합니다.
정상적인 상황에서는 1-2블록의 reorg만 발생하므로, 예를 들어 10블록 이상을 되돌리려는 시도는 명백한 공격입니다. isReorgAllowed()는 이런 비정상적인 시도를 거부하여, 공격자가 "비밀리에 긴 체인을 만들어서 한 번에 공개"하는 전형적인 51% 공격 패턴을 막습니다.
세 번째 방어선인 이상 패턴 탐지는 공격의 징후를 포착합니다. 예를 들어, 1시간에 3번 이상의 reorg가 발생하면 이것은 자연적인 현상이 아닙니다.
누군가 의도적으로 체인을 조작하고 있을 가능성이 큽니다. detectAnomalies()는 이런 패턴을 실시간으로 모니터링하고 즉시 알림을 보냅니다.
네 번째 방어선인 채굴 집중도 모니터링은 예방적 조치입니다. 한 채굴자(또는 풀)가 40% 이상의 블록을 찾고 있다면, 51%에 근접하고 있다는 경고 신호입니다.
checkMiningCentralization()은 최근 100블록을 분석하여 이런 위험을 조기에 감지합니다. 실제로 2014년 비트코인 채굴 풀 GHash.IO가 51%에 근접했을 때, 커뮤니티의 압력으로 자발적으로 해시파워를 분산시켰습니다.
최종적으로 이 시스템은 공격을 "불가능"하게 만드는 것이 아니라, "탐지 가능하고, 비용이 많이 들고, 효과가 제한적"이게 만듭니다. 여러분이 이런 방어 메커니즘을 구현하면 블록체인의 회복력이 크게 향상됩니다.
특히 작은 네트워크에서는 필수입니다. 또한 사용자들에게 "큰 거래는 20 confirmations 이후에 최종 확정"같은 명확한 가이드를 제공해야 합니다.
실전 팁
💡 자동 체크포인트를 주기적으로 생성하세요. 예를 들어 매일 자정의 블록을 자동으로 체크포인트로 설정하면, 공격자가 되돌릴 수 있는 범위가 최대 1일로 제한됩니다.
💡 다른 노드들과 체크포인트를 공유하세요. 신뢰할 수 있는 노드들끼리 체크포인트에 서명하고 공유하면, 네트워크 전체가 같은 확정된 역사를 갖게 됩니다.
💡 공격 비용을 계산하여 공개하세요. "현재 6블록을 되돌리려면 약 X만 달러가 필요합니다"같은 정보를 제공하면, 사용자들이 위험을 평가할 수 있습니다. NiceHash 같은 서비스의 해시 렌탈 가격을 참고하세요.
💡 Proof of Stake 요소를 추가하는 것을 고려하세요. 순수 PoW 대신 PoW+PoS 하이브리드를 사용하면, 공격자가 해시파워뿐만 아니라 많은 코인도 보유해야 하므로 공격 비용이 훨씬 높아집니다.
💡 커뮤니티 거버넌스를 준비하세요. 실제로 51% 공격이 발생하면, 기술적 대응만으로는 부족합니다. 커뮤니티 투표로 악의적인 체인을 거부하고 정직한 체인을 선택하는 소셜 레이어 방어가 필요할 수 있습니다.