본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 11. · 21 Views
타입스크립트로 비트코인 클론하기 26편 - 체인 분기(Fork) 감지 및 해결
블록체인에서 발생하는 체인 분기(Fork) 문제를 타입스크립트로 구현하며 이해합니다. 여러 노드가 동시에 블록을 생성할 때 발생하는 충돌을 감지하고, 가장 긴 체인을 선택하는 합의 알고리즘을 실전 코드로 배웁니다. 실제 비트코인 네트워크에서 사용되는 체인 재구성(Chain Reorganization) 로직을 직접 구현해봅니다.
목차
- 체인 분기(Fork)의 이해 - 블록체인 네트워크에서 발생하는 갈림길
- 체인 길이 비교 로직 - 가장 긴 체인이 진실이다
- 체인 재구성(Chain Reorganization) - 과거를 다시 쓰기
- 고아 블록(Orphan Block) 관리 - 부모를 잃은 블록들
- 분기 감지 알림 시스템 - 실시간 모니터링과 대응
- 합의 규칙 구현 - 가장 긴 체인 선택 알고리즘
- 분기 해결 전략 - 네트워크 수렴 최적화
- 테스트 시나리오 - 분기 상황 시뮬레이션
1. 체인 분기(Fork)의 이해 - 블록체인 네트워크에서 발생하는 갈림길
시작하며
여러분이 블록체인 노드를 운영하면서 두 개의 다른 노드로부터 거의 동시에 새로운 블록을 받았을 때, 어느 블록을 선택해야 할지 고민해본 적 있나요? 예를 들어, 노드 A가 높이 100번 블록을 전송하고 1초 후에 노드 B도 같은 높이의 다른 블록을 보내왔다면, 당신의 노드는 어떤 결정을 내려야 할까요?
이런 문제는 분산 네트워크의 본질적인 특성 때문에 발생합니다. 여러 노드가 동시에 채굴에 성공하면, 네트워크 전파 시간의 차이로 인해 일부 노드는 A의 블록을, 다른 노드는 B의 블록을 먼저 받게 됩니다.
이렇게 블록체인이 두 개 이상의 가지로 나뉘는 현상을 '포크(Fork)'라고 합니다. 포크가 발생하면 네트워크의 합의가 깨지고, 이중 지불 같은 심각한 보안 문제가 발생할 수 있습니다.
바로 이럴 때 필요한 것이 체인 분기 감지 및 해결 메커니즘입니다. 비트코인은 "가장 긴 체인이 진실이다"라는 간단하지만 강력한 원칙으로 이 문제를 해결합니다.
이번 카드에서는 체인이 어떻게 분기되는지, 그리고 노드가 어떻게 올바른 체인을 선택하는지 깊이 있게 알아보겠습니다.
개요
간단히 말해서, 체인 분기(Fork)는 블록체인이 두 개 이상의 경로로 나뉘는 현상입니다. 이는 두 명 이상의 채굴자가 거의 동시에 유효한 블록을 생성했을 때 자연스럽게 발생합니다.
블록체인 네트워크는 분산되어 있기 때문에 모든 노드가 동시에 같은 정보를 받을 수 없습니다. 네트워크 지연, 지리적 거리, 대역폭 차이 등으로 인해 정보 전파에 시간이 걸립니다.
예를 들어, 한국의 채굴자와 미국의 채굴자가 동시에 블록을 찾았다면, 각자 근처의 노드들은 자신과 가까운 채굴자의 블록을 먼저 받게 됩니다. 이렇게 네트워크가 일시적으로 두 개의 진실을 가지게 되는 것이죠.
기존에는 중앙화된 데이터베이스에서 마스터 서버가 충돌을 해결했다면, 블록체인에서는 각 노드가 독립적으로 판단합니다. 이것이 블록체인의 핵심 특성입니다.
포크는 크게 두 가지로 나뉩니다: 일시적 포크(Temporary Fork)와 영구적 포크(Permanent Fork). 일시적 포크는 네트워크 지연으로 발생하며 곧 해결되지만, 영구적 포크는 프로토콜 업그레이드나 커뮤니티 분열로 발생합니다.
일시적 포크를 빠르게 감지하고 해결하는 것이 네트워크 안정성에 매우 중요합니다.
코드 예제
// Block 클래스: 블록체인의 기본 단위
class Block {
constructor(
public index: number,
public hash: string,
public previousHash: string,
public timestamp: number,
public data: string,
public difficulty: number,
public nonce: number
) {}
}
// 체인 분기 감지를 위한 블록체인 클래스
class Blockchain {
private chain: Block[] = []; // 메인 체인
private forks: Block[][] = []; // 감지된 분기 체인들
// 새로운 블록을 받았을 때 분기 여부 확인
public detectFork(newBlock: Block): boolean {
const lastBlock = this.getLatestBlock();
// 정상적인 다음 블록인 경우
if (newBlock.previousHash === lastBlock.hash &&
newBlock.index === lastBlock.index + 1) {
return false; // 분기 아님
}
// 같은 높이의 다른 블록인 경우 (분기 발생!)
if (newBlock.index === lastBlock.index) {
console.log(`⚠️ Fork detected at height ${newBlock.index}`);
return true;
}
return false;
}
private getLatestBlock(): Block {
return this.chain[this.chain.length - 1];
}
}
설명
이것이 하는 일: 이 코드는 블록체인에 새로운 블록이 추가될 때 체인 분기가 발생했는지 감지하는 기본 메커니즘을 보여줍니다. 첫 번째로, Block 클래스는 블록체인의 각 블록을 표현합니다.
index는 블록의 높이(몇 번째 블록인지), hash는 블록의 고유 식별자, previousHash는 이전 블록을 가리키는 참조입니다. 이 previousHash가 바로 블록들을 체인처럼 연결하는 핵심 요소입니다.
만약 previousHash가 다르면 다른 체인에 속한 블록이라는 의미입니다. 그 다음으로, Blockchain 클래스의 detectFork 메서드가 실행되면서 새로 받은 블록을 분석합니다.
메서드는 세 가지 경우를 구분합니다: (1) 정상적인 다음 블록 - previousHash가 현재 체인의 마지막 블록을 가리키고 index가 하나 증가한 경우, (2) 분기 발생 - index는 같은데 hash가 다른 경우, (3) 고아 블록(orphan block) - 어디에도 연결되지 않은 블록. 내부에서는 현재 체인의 마지막 블록과 새 블록의 previousHash, index를 비교하여 이를 판단합니다.
마지막으로, 분기가 감지되면 경고 메시지를 출력하고 true를 반환합니다. 이 정보는 이후 체인 재구성(reorg) 로직에서 사용되어, 어떤 체인을 메인 체인으로 선택할지 결정하는 데 활용됩니다.
여러분이 이 코드를 사용하면 실시간으로 네트워크에서 발생하는 포크를 감지할 수 있습니다. 이는 네트워크 모니터링, 이중 지불 공격 감지, 체인 재구성 트리거 등 다양한 용도로 활용됩니다.
특히 거래소나 결제 시스템에서는 포크 발생 시 입금 확인을 중단하거나 추가 컨펌을 요구하는 등의 안전장치를 적용할 수 있습니다.
실전 팁
💡 분기는 네트워크 해시레이트가 높을수록 자주 발생합니다. 비트코인은 평균 10분에 한 번 블록이 생성되지만, 이더리움처럼 블록 생성 시간이 짧은 체인은 분기가 더 빈번합니다. 따라서 블록 생성 시간을 설정할 때 네트워크 전파 시간을 고려해야 합니다.
💡 프로덕션 환경에서는 단순히 분기를 감지하는 것만으로는 부족합니다. 각 분기 체인의 누적 난이도(Cumulative Difficulty)를 추적하고, 로그를 남기고, 알림을 보내는 모니터링 시스템이 필수입니다. 분기가 3블록 이상 지속되면 네트워크에 심각한 문제가 있다는 신호입니다.
💡 테스트 시에는 인위적으로 분기를 만들어보세요. 두 개의 노드를 일시적으로 네트워크에서 격리시킨 후 각각 블록을 생성하고, 다시 연결했을 때 어떻게 동작하는지 확인하면 분기 해결 로직의 견고성을 검증할 수 있습니다.
💡 분기된 블록들은 즉시 삭제하지 말고 일정 시간 보관하세요. 나중에 더 긴 체인이 발견되어 체인 재구성이 필요할 때 이 블록들이 필요할 수 있습니다. 비트코인 코어는 분기된 블록을 메모리와 디스크에 모두 보관합니다.
💡 사용자에게 거래 확인을 알릴 때는 반드시 "N 컨펌" 개념을 사용하세요. 분기로 인해 블록이 롤백될 수 있으므로, 중요한 거래는 6개 이상의 컨펌을 기다리는 것이 안전합니다. 거래소는 보통 입금 시 3-6 컨펌을 요구합니다.
2. 체인 길이 비교 로직 - 가장 긴 체인이 진실이다
시작하며
여러분의 노드가 두 개의 서로 다른 체인을 발견했을 때, 어떤 체인을 선택해야 할까요? 예를 들어, 체인 A는 100개의 블록을 가지고 있고, 체인 B는 101개의 블록을 가지고 있다면 어떤 선택을 해야 할까요?
현실 세계에서 두 사람이 다른 이야기를 한다면 더 많은 증거를 가진 사람을 믿듯이, 블록체인도 비슷한 원리를 사용합니다. 이 문제는 블록체인의 합의 메커니즘의 핵심입니다.
중앙 권한이 없는 분산 시스템에서 어떻게 하나의 진실에 합의할 것인가? 사토시 나카모토는 이 문제를 "가장 긴 체인"이라는 단순하면서도 강력한 규칙으로 해결했습니다.
가장 많은 작업 증명(Proof of Work)이 투입된 체인이 가장 신뢰할 수 있다는 원리입니다. 바로 이럴 때 필요한 것이 체인 길이 비교 로직입니다.
이 로직은 여러 경쟁하는 체인 중에서 메인 체인을 선택하는 기준을 제공하며, 네트워크 전체가 하나의 합의에 도달하도록 돕습니다.
개요
간단히 말해서, 체인 길이 비교는 여러 개의 유효한 체인 중에서 가장 많은 작업 증명이 투입된 체인을 선택하는 알고리즘입니다. 비트코인에서는 단순히 블록 개수가 아니라 누적 난이도(Cumulative Difficulty)를 기준으로 삼습니다.
왜 이 개념이 필요한지 살펴보면, 악의적인 공격자가 짧은 시간에 많은 블록을 만들어 네트워크를 혼란시킬 수 있기 때문입니다. 예를 들어, 공격자가 난이도가 낮은 100개의 블록을 만들었다고 해서 정직한 채굴자들이 만든 난이도 높은 50개의 블록보다 우선되어서는 안 됩니다.
따라서 단순 길이가 아닌 누적 난이도를 비교해야 합니다. 기존에는 "가장 긴 체인"이라고 표현했다면, 이제는 "가장 많은 작업이 투입된 체인"이라고 정확히 표현합니다.
이것이 비트코인의 Nakamoto Consensus의 핵심입니다. 이 비교 로직의 핵심 특징은 (1) 객관적 측정 가능성 - 모든 노드가 같은 방법으로 계산, (2) 게임 이론적 안정성 - 정직한 채굴자들이 가장 긴 체인을 확장하는 것이 경제적으로 유리, (3) 자동 수렴 - 시간이 지날수록 네트워크가 하나의 체인으로 수렴합니다.
이러한 특징들이 블록체인을 51% 공격을 제외한 대부분의 공격으로부터 안전하게 보호합니다.
코드 예제
// 누적 난이도를 계산하는 함수
function calculateCumulativeDifficulty(chain: Block[]): number {
return chain.reduce((total, block) => {
// 각 블록의 난이도를 누적 (난이도가 높을수록 큰 값)
return total + Math.pow(2, block.difficulty);
}, 0);
}
// 두 체인을 비교하여 더 나은 체인을 선택
function selectBetterChain(currentChain: Block[], newChain: Block[]): Block[] {
// 1단계: 두 체인의 유효성 검증
if (!isValidChain(currentChain) || !isValidChain(newChain)) {
throw new Error("Invalid chain detected");
}
// 2단계: 누적 난이도 계산
const currentDifficulty = calculateCumulativeDifficulty(currentChain);
const newDifficulty = calculateCumulativeDifficulty(newChain);
console.log(`Current chain difficulty: ${currentDifficulty}`);
console.log(`New chain difficulty: ${newDifficulty}`);
// 3단계: 더 높은 누적 난이도를 가진 체인 선택
if (newDifficulty > currentDifficulty) {
console.log("✅ Switching to new chain with higher difficulty");
return newChain;
}
// 4단계: 같은 누적 난이도라면 먼저 받은 체인 유지
console.log("⏸️ Keeping current chain");
return currentChain;
}
// 체인 유효성 검증 함수 (간단한 버전)
function isValidChain(chain: Block[]): boolean {
for (let i = 1; i < chain.length; i++) {
if (chain[i].previousHash !== chain[i - 1].hash) return false;
if (chain[i].index !== chain[i - 1].index + 1) return false;
}
return true;
}
설명
이것이 하는 일: 이 코드는 두 개의 경쟁하는 체인을 비교하여 더 많은 컴퓨팅 파워가 투입된 체인을 자동으로 선택합니다. 첫 번째로, calculateCumulativeDifficulty 함수가 체인의 총 작업량을 계산합니다.
각 블록의 difficulty 값은 해당 블록을 채굴하는 데 필요한 해시 연산의 양을 나타냅니다. 예를 들어 difficulty가 4라면 평균적으로 2^4 = 16번의 해시 연산이 필요합니다.
reduce 함수로 모든 블록의 난이도를 누적하여 전체 체인에 투입된 작업량을 계산합니다. 이 값이 클수록 더 많은 채굴자들이 이 체인을 인정했다는 의미입니다.
그 다음으로, selectBetterChain 함수가 실제 선택 로직을 수행합니다. 먼저 isValidChain으로 두 체인의 구조적 유효성을 검증합니다.
이는 previousHash 연결이 올바른지, index가 순차적인지 확인합니다. 유효하지 않은 체인은 누적 난이도가 아무리 높아도 거부됩니다.
그 다음 각 체인의 누적 난이도를 계산하고 비교합니다. 마지막으로, 비교 결과에 따라 체인을 선택합니다.
새로운 체인의 누적 난이도가 더 높다면 체인을 교체(reorg)합니다. 이 순간 이전 체인의 블록들은 "고아(orphan)" 상태가 되고, 그 안의 거래들은 다시 미확인 상태로 돌아갑니다.
만약 누적 난이도가 같다면 "먼저 본 것을 유지(first-seen rule)" 원칙에 따라 현재 체인을 그대로 유지합니다. 여러분이 이 코드를 사용하면 네트워크에서 분기가 발생했을 때 자동으로 올바른 체인으로 수렴할 수 있습니다.
모든 정직한 노드들이 같은 로직을 사용하기 때문에, 시간이 지나면 전체 네트워크가 하나의 합의에 도달합니다. 이것이 바로 블록체인의 최종성(finality)을 보장하는 메커니즘입니다.
실전 팁
💡 비트코인은 단순히 블록 개수가 아닌 "체인워크(chainwork)"라는 개념을 사용합니다. 이는 각 블록의 난이도를 실제 해시 연산 횟수로 환산한 값의 합계입니다. Math.pow(2, difficulty)는 간단한 근사치이며, 실제로는 더 정밀한 계산이 필요합니다.
💡 체인 비교는 매우 빈번하게 발생하므로 성능 최적화가 중요합니다. 누적 난이도를 매번 계산하지 말고 각 블록에 cumulativeDifficulty 필드를 추가하여 O(1) 시간에 비교할 수 있도록 하세요. 새 블록 추가 시에만 이전 블록의 누적 난이도 + 현재 난이도를 계산하면 됩니다.
💡 51% 공격을 이해하려면 이 로직을 깊이 이해해야 합니다. 공격자가 전체 해시레이트의 51% 이상을 장악하면 정직한 채굴자들보다 빠르게 긴 체인을 만들 수 있어, 이중 지불이 가능해집니다. 따라서 네트워크 해시레이트 분산도를 항상 모니터링해야 합니다.
💡 난이도 조정(difficulty adjustment) 알고리즘과 함께 작동합니다. 체인의 블록 생성 속도가 빨라지면 난이도가 상승하고, 느려지면 하락합니다. 이를 통해 평균 블록 생성 시간을 일정하게 유지하면서도 누적 난이도를 공정하게 비교할 수 있습니다.
💡 테스트 시에는 다양한 시나리오를 시뮬레이션하세요: (1) 같은 길이지만 다른 난이도의 체인, (2) 짧지만 높은 난이도의 체인 vs 긴데 낮은 난이도의 체인, (3) 유효하지 않은 블록을 포함한 체인. 이를 통해 로직의 견고성을 검증할 수 있습니다.
3. 체인 재구성(Chain Reorganization) - 과거를 다시 쓰기
시작하며
여러분이 10개의 블록을 쌓아올린 체인을 운영하고 있는데, 갑자기 다른 노드로부터 11개의 블록을 가진 더 긴 체인을 받았다면 어떻게 해야 할까요? 이미 확인된 거래들을 취소하고 새로운 체인으로 전환해야 할까요?
사용자에게 "죄송합니다, 10분 전에 확인해드린 거래가 사실은 아직 확인되지 않았습니다"라고 말해야 할까요? 이런 상황은 실제로 블록체인 네트워크에서 자주 발생합니다.
특히 네트워크가 일시적으로 분리되었다가 다시 연결되거나, 해시레이트가 높은 채굴 풀이 자신들만의 체인을 몰래 키우다가 공개하는 경우에 발생합니다. 이를 처리하지 못하면 네트워크가 영구적으로 분열되거나, 이중 지불 공격에 취약해집니다.
바로 이럴 때 필요한 것이 체인 재구성(Chain Reorganization, 줄여서 reorg)입니다. 이는 현재 메인 체인을 버리고 더 긴 체인으로 전환하는 과정으로, 블록체인의 자기 치유(self-healing) 능력을 제공합니다.
비트코인에서는 하루에도 여러 번 1-2 블록 깊이의 reorg가 발생합니다.
개요
간단히 말해서, 체인 재구성은 현재 사용 중인 메인 체인을 더 긴(더 많은 작업이 투입된) 체인으로 교체하는 프로세스입니다. 이 과정에서 일부 블록과 거래가 롤백되고, 새로운 체인의 거래들이 적용됩니다.
이 과정이 왜 필요한지 생각해보면, 분산 네트워크에서는 일시적으로 여러 개의 진실이 공존할 수 있기 때문입니다. 예를 들어, 서울의 노드들과 뉴욕의 노드들이 일시적으로 네트워크가 끊겼다가 다시 연결되면, 각각 다른 체인을 만들어냈을 수 있습니다.
이때 reorg를 통해 하나의 체인으로 합쳐야 네트워크의 일관성이 유지됩니다. 이중 지불 공격자가 자신의 거래를 포함한 블록을 롤백하려 할 때도 이 메커니즘이 방어선 역할을 합니다.
기존에는 데이터베이스의 트랜잭션 롤백처럼 단순히 되돌리기만 했다면, 블록체인에서는 롤백과 동시에 새로운 체인의 블록들을 적용해야 합니다. 이는 UNDO와 REDO를 동시에 수행하는 복잡한 작업입니다.
reorg의 핵심 특징은 (1) 원자성(Atomicity) - 전체가 성공하거나 전체가 실패, (2) 거래 보존 - 롤백된 블록의 거래들은 다시 mempool로 돌아가 재처리 기회를 얻음, (3) 깊이 제한 - 너무 깊은 reorg는 거부(보통 100블록 이상은 수동 개입 필요). 이러한 특징이 블록체인의 안정성과 신뢰성을 보장합니다.
코드 예제
// 체인 재구성을 수행하는 클래스
class ChainReorganizer {
private blockchain: Block[];
private transactionPool: Transaction[]; // 미확인 거래 풀
// 새로운 체인으로 재구성
public reorganize(currentChain: Block[], newChain: Block[]): ReorgResult {
// 1단계: 공통 조상 블록 찾기
const commonAncestor = this.findCommonAncestor(currentChain, newChain);
if (!commonAncestor) {
throw new Error("No common ancestor found - chains are incompatible");
}
console.log(`🔍 Common ancestor found at height ${commonAncestor.index}`);
// 2단계: 롤백할 블록들 식별 (공통 조상 이후의 현재 체인 블록들)
const blocksToRollback = currentChain.slice(commonAncestor.index + 1);
console.log(`⏪ Rolling back ${blocksToRollback.length} blocks`);
// 3단계: 적용할 블록들 식별 (공통 조상 이후의 새 체인 블록들)
const blocksToApply = newChain.slice(commonAncestor.index + 1);
console.log(`⏩ Applying ${blocksToApply.length} new blocks`);
// 4단계: 롤백된 거래들을 트랜잭션 풀로 복원
const restoredTransactions = this.extractTransactions(blocksToRollback);
this.transactionPool.push(...restoredTransactions);
// 5단계: 새로운 체인 적용
this.blockchain = newChain;
return {
success: true,
reorgDepth: blocksToRollback.length,
restoredTransactions: restoredTransactions.length
};
}
// 두 체인의 공통 조상 찾기
private findCommonAncestor(chain1: Block[], chain2: Block[]): Block | null {
// 뒤에서부터 비교하여 해시가 같은 첫 번째 블록 찾기
const minLength = Math.min(chain1.length, chain2.length);
for (let i = minLength - 1; i >= 0; i--) {
if (chain1[i].hash === chain2[i].hash) {
return chain1[i];
}
}
return null; // 제네시스 블록도 다르면 호환 불가능한 체인
}
// 블록들에서 거래 추출
private extractTransactions(blocks: Block[]): Transaction[] {
return blocks.flatMap(block => JSON.parse(block.data));
}
}
interface ReorgResult {
success: boolean;
reorgDepth: number; // 롤백된 블록 개수
restoredTransactions: number; // 복원된 거래 개수
}
interface Transaction {
from: string;
to: string;
amount: number;
}
설명
이것이 하는 일: 이 코드는 블록체인에서 발생한 분기를 해결하기 위해 현재 체인을 더 나은 체인으로 안전하게 교체하는 전체 프로세스를 구현합니다. 첫 번째로, reorganize 메서드가 reorg의 전체 흐름을 조율합니다.
가장 먼저 findCommonAncestor를 호출하여 두 체인이 언제 갈라졌는지 찾습니다. 이는 마치 가계도에서 두 사람의 최근 공통 조상을 찾는 것과 같습니다.
예를 들어, 현재 체인이 [Genesis, B1, B2, B3]이고 새 체인이 [Genesis, B1, B2', B3']이라면, B1이 공통 조상입니다. 이 지점부터 체인이 갈라졌다는 의미죠.
findCommonAncestor는 두 체인을 뒤에서부터 비교하며 해시가 일치하는 첫 블록을 찾습니다. 그 다음으로, 공통 조상을 찾으면 롤백과 적용을 준비합니다.
blocksToRollback는 현재 체인에서 버려질 블록들(B2, B3)이고, blocksToApply는 새 체인에서 추가될 블록들(B2', B3')입니다. 중요한 것은 롤백된 블록 안의 거래들을 잃어버리지 않는 것입니다.
extractTransactions로 이 거래들을 추출하여 transactionPool에 다시 넣습니다. 이렇게 하면 이 거래들이 다음 블록에 포함될 기회를 다시 얻습니다.
단, 새 체인에 이미 포함된 거래는 제외해야 합니다(중복 방지). 마지막으로, blockchain을 새로운 체인으로 교체하고 결과를 반환합니다.
ReorgResult는 reorg의 깊이(몇 개의 블록이 롤백되었는지)와 복원된 거래 개수를 담고 있습니다. 이 정보는 모니터링과 알림에 사용됩니다.
예를 들어, reorg 깊이가 6 이상이면 심각한 상황이므로 관리자에게 긴급 알림을 보내야 합니다. 여러분이 이 코드를 사용하면 네트워크 분열 상황에서도 자동으로 복구할 수 있습니다.
사용자 관점에서는 일시적으로 확인된 거래가 미확인으로 돌아갈 수 있지만, 정직한 거래라면 결국 다시 블록에 포함됩니다. 이것이 바로 "6 컨펌을 기다려라"는 권장사항의 이유입니다.
6블록 깊이의 reorg는 매우 드물기 때문입니다.
실전 팁
💡 Reorg 깊이 제한을 설정하세요. 비트코인 코어는 기본적으로 100블록 이상의 reorg를 자동으로 수행하지 않습니다. 이는 51% 공격이나 심각한 버그의 신호일 수 있으므로 수동 검토가 필요합니다. 코드에 MAX_REORG_DEPTH 상수를 추가하여 안전장치를 만드세요.
💡 Reorg 발생 시 WebSocket이나 Server-Sent Events로 실시간 알림을 보내세요. 특히 거래소, 결제 게이트웨이 등 금융 서비스에서는 reorg 깊이가 2 이상이면 입금/출금을 일시 중단하고 수동 확인을 해야 합니다. 자동화된 알림 시스템이 필수입니다.
💡 롤백된 거래와 새 체인의 거래를 비교하여 "사라진 거래"를 감지하세요. 만약 사용자의 거래가 롤백되었는데 새 체인에도 포함되지 않았다면, 이는 이중 지불 시도의 증거일 수 있습니다. 이런 거래는 별도로 로깅하고 모니터링해야 합니다.
💡 데이터베이스를 사용한다면 reorg를 트랜잭션으로 감싸세요. 롤백 중 오류가 발생하면 전체를 되돌려야 합니다. PostgreSQL의 BEGIN/COMMIT/ROLLBACK이나 MongoDB의 트랜잭션을 활용하여 원자성을 보장하세요. 블록체인 데이터와 UTXO 세트의 일관성이 깨지면 노드가 사용 불가능해집니다.
💡 Reorg 이력을 별도 테이블에 저장하세요. 언제, 얼마나 깊은 reorg가 발생했는지, 어떤 거래들이 영향을 받았는지 기록하면 네트워크 건강도를 분석하고 공격 패턴을 발견하는 데 도움이 됩니다. 이 데이터는 사후 분석과 감사에 매우 중요합니다.
4. 고아 블록(Orphan Block) 관리 - 부모를 잃은 블록들
시작하며
여러분의 노드가 블록을 받았는데, 그 블록의 previousHash가 가리키는 부모 블록이 아직 도착하지 않았다면 어떻게 해야 할까요? 예를 들어, 높이 105번 블록을 받았는데 104번 블록은 아직 네트워크에서 다운로드 중이라면, 105번 블록을 버려야 할까요 아니면 보관해야 할까요?
이런 상황은 P2P 네트워크의 특성상 자주 발생합니다. 블록이 네트워크를 통해 전파될 때 순서가 뒤바뀔 수 있습니다.
특히 대역폭이 낮은 노드나 지리적으로 먼 노드는 블록을 순서대로 받지 못할 가능성이 높습니다. 또한 악의적인 노드가 의도적으로 순서를 뒤섞어 보낼 수도 있습니다.
바로 이럴 때 필요한 것이 고아 블록 관리 시스템입니다. 고아(orphan) 블록은 부모를 아직 받지 못한 블록으로, 일시적으로 보관했다가 부모가 도착하면 체인에 연결하는 메커니즘이 필요합니다.
이를 제대로 관리하지 못하면 유효한 블록들을 놓치게 되어 체인 동기화가 느려지거나 실패할 수 있습니다.
개요
간단히 말해서, 고아 블록은 부모 블록(previousHash가 가리키는 블록)이 아직 로컬 체인에 없는 유효한 블록입니다. 이들은 일시적으로 별도의 풀에 보관되었다가 나중에 체인에 연결됩니다.
고아 블록 관리가 왜 중요한지 살펴보면, 네트워크 효율성과 직결되기 때문입니다. 만약 고아 블록을 즉시 버린다면, 같은 블록을 여러 번 다운로드해야 하므로 대역폭이 낭비됩니다.
반대로 고아 블록을 무제한 보관한다면 메모리가 부족해지고, DoS 공격에 취약해집니다. 예를 들어, 공격자가 수천 개의 가짜 고아 블록을 보내 노드의 메모리를 고갈시킬 수 있습니다.
따라서 적절한 크기 제한과 시간 제한이 필요합니다. 기존에는 블록을 순서대로만 처리했다면, 이제는 비순차적 블록도 임시 보관하여 나중에 연결할 수 있습니다.
이는 마치 퍼즐 조각을 모으는 것과 같습니다. 고아 블록 풀의 핵심 특징은 (1) 크기 제한 - 보통 100-1000개로 제한하여 메모리 공격 방지, (2) 타임아웃 - 일정 시간(예: 1시간) 동안 부모가 도착하지 않으면 삭제, (3) 체인 재구성 - 부모가 도착하면 자동으로 체인에 추가 시도.
이러한 특징이 블록체인 동기화의 견고성을 보장합니다.
코드 예제
// 고아 블록 풀 관리 클래스
class OrphanBlockPool {
private orphans: Map<string, OrphanBlockInfo> = new Map();
private readonly MAX_ORPHANS = 500; // 최대 고아 블록 수
private readonly ORPHAN_TIMEOUT = 60 * 60 * 1000; // 1시간 (밀리초)
// 새로운 블록 처리: 정상 추가 또는 고아로 분류
public processNewBlock(block: Block, blockchain: Block[]): boolean {
const parentBlock = blockchain.find(b => b.hash === block.previousHash);
// 케이스 1: 부모가 존재 - 정상 블록
if (parentBlock) {
console.log(`✅ Block ${block.index} has parent, adding to chain`);
blockchain.push(block);
this.tryConnectOrphans(blockchain); // 이 블록을 기다리던 고아들 연결 시도
return true;
}
// 케이스 2: 부모 없음 - 고아 블록
console.log(`⚠️ Block ${block.index} is orphan (parent: ${block.previousHash})`);
this.addOrphan(block);
return false;
}
// 고아 블록을 풀에 추가
private addOrphan(block: Block): void {
// 크기 제한 확인
if (this.orphans.size >= this.MAX_ORPHANS) {
this.evictOldestOrphan(); // 가장 오래된 고아 제거
}
this.orphans.set(block.hash, {
block,
receivedAt: Date.now(),
parentHash: block.previousHash
});
console.log(`📦 Orphan pool size: ${this.orphans.size}`);
}
// 새로운 블록이 추가되었을 때 그 자식 고아들을 연결 시도
private tryConnectOrphans(blockchain: Block[]): void {
const lastBlock = blockchain[blockchain.length - 1];
let connected = 0;
// 마지막 블록을 부모로 가지는 고아 찾기
for (const [hash, orphanInfo] of this.orphans.entries()) {
if (orphanInfo.parentHash === lastBlock.hash) {
console.log(`🔗 Connecting orphan block ${orphanInfo.block.index}`);
blockchain.push(orphanInfo.block);
this.orphans.delete(hash);
connected++;
// 재귀적으로 이 블록의 자식 고아들도 연결
this.tryConnectOrphans(blockchain);
}
}
if (connected > 0) {
console.log(`✅ Connected ${connected} orphan blocks`);
}
}
// 가장 오래된 고아 블록 제거
private evictOldestOrphan(): void {
let oldestHash = "";
let oldestTime = Date.now();
for (const [hash, info] of this.orphans.entries()) {
if (info.receivedAt < oldestTime) {
oldestTime = info.receivedAt;
oldestHash = hash;
}
}
if (oldestHash) {
this.orphans.delete(oldestHash);
console.log(`🗑️ Evicted oldest orphan block`);
}
}
// 타임아웃된 고아 블록 정리 (주기적으로 호출)
public cleanupExpiredOrphans(): void {
const now = Date.now();
let cleaned = 0;
for (const [hash, info] of this.orphans.entries()) {
if (now - info.receivedAt > this.ORPHAN_TIMEOUT) {
this.orphans.delete(hash);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`🧹 Cleaned up ${cleaned} expired orphan blocks`);
}
}
}
interface OrphanBlockInfo {
block: Block;
receivedAt: number; // 타임스탬프
parentHash: string; // 빠른 검색을 위한 부모 해시
}
설명
이것이 하는 일: 이 코드는 블록이 순서대로 도착하지 않는 상황을 처리하여, 네트워크 지연에도 불구하고 모든 유효한 블록을 최종적으로 체인에 포함시킵니다. 첫 번째로, processNewBlock 메서드가 새로 받은 블록을 분류합니다.
blockchain 배열에서 이 블록의 previousHash와 일치하는 해시를 가진 블록을 찾습니다. 만약 찾으면 이는 정상적인 다음 블록이므로 즉시 체인에 추가합니다.
이때 중요한 것은 tryConnectOrphans를 호출하는 것입니다. 왜냐하면 이 블록을 부모로 기다리고 있던 고아 블록들이 있을 수 있기 때문입니다.
예를 들어, 103번 블록이 도착했는데 104번, 105번 블록이 이미 고아로 보관되어 있다면, 이들을 차례로 연결할 수 있습니다. 그 다음으로, 부모를 찾지 못하면 addOrphan으로 고아 풀에 추가합니다.
이때 중요한 안전장치가 두 가지 있습니다: (1) 크기 제한 - MAX_ORPHANS를 초과하면 가장 오래된 고아를 제거합니다. 이는 DoS 공격을 방지합니다.
공격자가 수만 개의 가짜 고아 블록을 보내도 메모리 사용량은 제한됩니다. (2) 타임스탬프 기록 - receivedAt을 저장하여 나중에 시간 초과된 고아들을 정리할 수 있습니다.
마지막으로, tryConnectOrphans가 재귀적으로 고아들을 연결합니다. 새로 추가된 블록의 해시를 parentHash로 가진 고아들을 찾아 체인에 추가하고, 다시 그 블록의 자식 고아들을 찾습니다.
이는 마치 도미노가 쓰러지듯이 연쇄적으로 블록들을 연결합니다. 예를 들어, 103번이 도착하면 104번이 연결되고, 그러면 105번이 연결되고, 이어서 106번이 연결되는 식입니다.
여러분이 이 코드를 사용하면 네트워크 상태가 불안정해도 안정적으로 블록을 수신할 수 있습니다. 특히 초기 블록 동기화(IBD: Initial Block Download) 시에 수천 개의 블록을 다운로드할 때, 블록들이 순서대로 오지 않더라도 최종적으로 모두 올바르게 연결됩니다.
cleanupExpiredOrphans를 setInterval로 주기적으로 호출하여 메모리를 효율적으로 관리하세요.
실전 팁
💡 고아 풀을 Map 대신 이중 인덱스 구조로 구현하면 성능이 향상됩니다. 하나는 blockHash -> OrphanInfo, 다른 하나는 parentHash -> Set<blockHash>로 관리하면 tryConnectOrphans에서 O(1) 시간에 자식들을 찾을 수 있습니다. 고아가 많을 때 큰 차이를 만듭니다.
💡 고아 블록도 기본적인 유효성 검증을 수행하세요. 부모는 없지만 블록 자체의 구조(해시, 난이도, Proof of Work)는 검증할 수 있습니다. 이렇게 하면 명백히 잘못된 블록을 조기에 걸러낼 수 있습니다. 단, 부모 블록의 정보가 필요한 검증(잔액 확인 등)은 연결 시점에 수행해야 합니다.
💡 "요청 블록" 메커니즘을 구현하세요. 고아 블록을 받으면 그 부모 블록을 네트워크에 능동적으로 요청합니다. P2P 프로토콜에 "getblock" 메시지를 보내 부모를 다운로드하면 고아 상태를 빠르게 해결할 수 있습니다. 비트코인은 이 방식으로 평균 수 초 내에 고아를 해결합니다.
💡 고아 체인(Orphan Chain)도 고려하세요. 연속된 여러 블록이 모두 고아일 수 있습니다(예: 104, 105, 106 모두 고아이고 103이 누락). 이 경우 부모-자식 관계를 추적하여 103이 도착하면 한 번에 모두 연결할 수 있도록 자료구조를 설계하세요. 트리 구조가 유용합니다.
💡 통계를 수집하세요. 시간당 평균 고아 개수, 평균 해결 시간, 최대 고아 풀 크기 등을 모니터링하면 네트워크 상태를 파악할 수 있습니다. 고아가 갑자기 급증하면 네트워크 공격이나 분열의 신호일 수 있으므로 알림을 설정하세요.
5. 분기 감지 알림 시스템 - 실시간 모니터링과 대응
시작하며
여러분이 거래소를 운영하는데 갑자기 3블록 깊이의 체인 재구성이 발생했다면, 그 사실을 어떻게 알 수 있을까요? 사용자가 "내 입금이 사라졌어요!"라고 컴플레인을 하기 전에 미리 파악하고 대응할 수 있을까요?
밤 2시에 발생한 심각한 분기를 아침에 출근해서야 발견한다면 이미 늦은 것 아닐까요? 이런 문제는 실제 블록체인 서비스 운영에서 치명적일 수 있습니다.
체인 재구성으로 확인된 입금이 취소되었는데 이미 사용자 계정에 크레딧을 지급했다면 금전적 손실이 발생합니다. 이중 지불 공격이 진행 중인데 감지하지 못했다면 더 큰 피해로 이어집니다.
특히 금융 서비스에서는 실시간 감지와 즉각 대응이 필수입니다. 바로 이럴 때 필요한 것이 분기 감지 알림 시스템입니다.
체인에서 이상 징후가 발생하면 자동으로 감지하고, 관리자에게 알리고, 필요한 경우 자동으로 안전 조치(예: 입출금 중단)를 취하는 시스템입니다. 이는 블록체인 서비스의 안전장치이자 조기 경보 시스템입니다.
개요
간단히 말해서, 분기 감지 알림 시스템은 체인 재구성, 고아 블록 증가, 네트워크 분열 등의 이상 징후를 실시간으로 모니터링하고 관리자에게 알리는 자동화 시스템입니다. 이 시스템이 왜 필요한지 구체적으로 살펴보면, 블록체인은 24/7 운영되는 시스템이기 때문입니다.
사람이 항상 모니터링할 수 없으므로 자동화가 필수입니다. 예를 들어, 토요일 새벽 3시에 6블록 깊이의 reorg가 발생했다면, 이는 51% 공격이나 네트워크 분열의 신호일 수 있습니다.
자동 알림 시스템이 없다면 월요일 출근 시까지 이를 모를 수 있고, 그동안 공격자는 이중 지불을 성공시킬 수 있습니다. 또한 거래소의 경우 잘못된 확인으로 인한 손실이 수억 원에 달할 수 있습니다.
기존에는 수동으로 로그를 확인했다면, 이제는 이벤트 기반 아키텍처로 자동 대응합니다. 블록체인 이벤트가 발생하면 즉시 리스너가 감지하고 적절한 액션을 트리거합니다.
알림 시스템의 핵심 특징은 (1) 심각도 분류 - INFO(1블록 reorg), WARNING(2-3블록), CRITICAL(4블록 이상)로 구분, (2) 다채널 알림 - Slack, 이메일, SMS, PagerDuty 등 여러 경로로 알림, (3) 자동 대응 - 설정된 임계치 초과 시 자동으로 입출금 중단 같은 안전 조치 실행. 이러한 특징이 서비스의 신뢰성과 안전성을 크게 향상시킵니다.
코드 예제
// 분기 감지 이벤트 타입
enum ForkEventType {
FORK_DETECTED = "FORK_DETECTED",
REORG_OCCURRED = "REORG_OCCURRED",
DEEP_REORG = "DEEP_REORG", // 위험한 깊은 재구성
ORPHAN_POOL_FULL = "ORPHAN_POOL_FULL"
}
// 알림 심각도
enum Severity {
INFO = "INFO",
WARNING = "WARNING",
CRITICAL = "CRITICAL"
}
// 분기 이벤트 데이터
interface ForkEvent {
type: ForkEventType;
severity: Severity;
reorgDepth?: number;
affectedTransactions?: string[];
timestamp: number;
message: string;
}
// 분기 감지 및 알림 시스템
class ForkMonitor {
private readonly DEEP_REORG_THRESHOLD = 6; // 6블록 이상은 심각
private reorgHistory: ForkEvent[] = [];
// 체인 재구성 감지 및 알림
public onChainReorganization(reorgResult: ReorgResult): void {
const event: ForkEvent = {
type: ForkEventType.REORG_OCCURRED,
severity: this.calculateSeverity(reorgResult.reorgDepth),
reorgDepth: reorgResult.reorgDepth,
timestamp: Date.now(),
message: `Chain reorg detected: ${reorgResult.reorgDepth} blocks rolled back`
};
// 이벤트 기록
this.reorgHistory.push(event);
// 심각도에 따른 알림
this.sendAlert(event);
// 위험한 깊은 재구성인 경우 자동 안전 조치
if (reorgResult.reorgDepth >= this.DEEP_REORG_THRESHOLD) {
this.triggerEmergencyProtocol(event);
}
}
// 분기 감지 시 호출
public onForkDetected(block1: Block, block2: Block): void {
const event: ForkEvent = {
type: ForkEventType.FORK_DETECTED,
severity: Severity.WARNING,
timestamp: Date.now(),
message: `Fork detected at height ${block1.index}: two competing blocks`
};
this.reorgHistory.push(event);
this.sendAlert(event);
}
// 심각도 계산
private calculateSeverity(reorgDepth: number): Severity {
if (reorgDepth >= this.DEEP_REORG_THRESHOLD) {
return Severity.CRITICAL;
} else if (reorgDepth >= 2) {
return Severity.WARNING;
} else {
return Severity.INFO;
}
}
// 알림 전송 (여러 채널)
private async sendAlert(event: ForkEvent): Promise<void> {
console.log(`🚨 [${event.severity}] ${event.message}`);
switch (event.severity) {
case Severity.CRITICAL:
// 긴급: 모든 채널로 알림
await this.sendSlackAlert(event, "#blockchain-critical");
await this.sendEmailAlert(event, "admin@example.com");
await this.sendSMS(event, "+821012345678");
await this.triggerPagerDuty(event);
break;
case Severity.WARNING:
// 경고: Slack과 이메일
await this.sendSlackAlert(event, "#blockchain-alerts");
await this.sendEmailAlert(event, "team@example.com");
break;
case Severity.INFO:
// 정보: Slack만
await this.sendSlackAlert(event, "#blockchain-info");
break;
}
}
// 긴급 프로토콜: 자동 안전 조치
private async triggerEmergencyProtocol(event: ForkEvent): Promise<void> {
console.log("🔴 EMERGENCY PROTOCOL ACTIVATED");
// 1. 입출금 중단
await this.pauseDepositsAndWithdrawals();
// 2. 거래 처리 일시 중지
await this.pauseTransactionProcessing();
// 3. 상태 페이지 업데이트
await this.updateStatusPage("Investigating blockchain reorganization");
// 4. 상세 로그 덤프
await this.dumpDetailedLogs(event);
}
// Slack 알림 (실제로는 Slack API 사용)
private async sendSlackAlert(event: ForkEvent, channel: string): Promise<void> {
console.log(`📱 Sending Slack alert to ${channel}: ${event.message}`);
// 실제 구현: await slackClient.chat.postMessage({ channel, text: ... });
}
// 이메일 알림 (실제로는 SendGrid, AWS SES 등 사용)
private async sendEmailAlert(event: ForkEvent, to: string): Promise<void> {
console.log(`📧 Sending email to ${to}: ${event.message}`);
// 실제 구현: await emailClient.send({ to, subject, body: ... });
}
// SMS 알림 (실제로는 Twilio 등 사용)
private async sendSMS(event: ForkEvent, phoneNumber: string): Promise<void> {
console.log(`📲 Sending SMS to ${phoneNumber}: ${event.message}`);
// 실제 구현: await twilioClient.messages.create({ to, body: ... });
}
// PagerDuty 트리거
private async triggerPagerDuty(event: ForkEvent): Promise<void> {
console.log(`📟 Triggering PagerDuty incident`);
// 실제 구현: await pagerDutyClient.createIncident({ ... });
}
// 입출금 중단
private async pauseDepositsAndWithdrawals(): Promise<void> {
console.log("⏸️ Pausing deposits and withdrawals");
// 실제 구현: 데이터베이스 플래그 설정 또는 서비스 상태 변경
}
// 거래 처리 중지
private async pauseTransactionProcessing(): Promise<void> {
console.log("⏸️ Pausing transaction processing");
// 실제 구현: 워커 큐 일시 중지
}
// 상태 페이지 업데이트
private async updateStatusPage(message: string): Promise<void> {
console.log(`📄 Updating status page: ${message}`);
// 실제 구현: Statuspage.io API 호출
}
// 상세 로그 덤프
private async dumpDetailedLogs(event: ForkEvent): Promise<void> {
console.log("📋 Dumping detailed logs for investigation");
// 실제 구현: 블록체인 상태, UTXO 세트, 최근 거래 등을 파일로 저장
}
}
설명
이것이 하는 일: 이 코드는 블록체인에서 발생하는 이상 징후를 실시간으로 감지하고, 심각도에 따라 적절한 알림과 대응 조치를 자동으로 실행하는 종합 모니터링 시스템입니다. 첫 번째로, ForkMonitor 클래스가 체인 이벤트를 구독합니다.
onChainReorganization은 reorg가 발생했을 때 호출되며, reorgDepth를 기반으로 심각도를 계산합니다. 1블록 reorg는 비교적 흔하므로 INFO 레벨이지만, 6블록 이상은 CRITICAL입니다.
왜냐하면 비트코인에서는 6 컨펌을 "최종 확정"으로 간주하기 때문에, 이를 뒤집는다는 것은 네트워크에 심각한 문제가 있다는 신호입니다. 각 이벤트는 reorgHistory 배열에 기록되어 나중에 패턴 분석이나 감사에 사용됩니다.
그 다음으로, sendAlert 메서드가 심각도에 따라 알림 채널을 선택합니다. CRITICAL 이벤트는 모든 채널(Slack, 이메일, SMS, PagerDuty)로 알림을 보내 관리자가 반드시 인지하도록 합니다.
WARNING은 업무 시간에 확인할 수 있도록 Slack과 이메일만 사용합니다. INFO는 Slack 채널에만 기록하여 히스토리를 남깁니다.
실제 프로덕션 환경에서는 각 채널의 API(Slack Webhook, SendGrid, Twilio, PagerDuty)를 사용하여 구현합니다. 마지막으로, triggerEmergencyProtocol이 자동 안전 조치를 실행합니다.
이는 매우 중요한 기능으로, 사람의 개입 없이 즉시 대응할 수 있게 합니다. 입출금 중단은 이중 지불 공격으로 인한 금전 손실을 방지하고, 거래 처리 중지는 잘못된 체인에서 거래를 확인하지 않도록 합니다.
상태 페이지 업데이트는 사용자에게 투명하게 상황을 알리고, 로그 덤프는 사후 분석을 위한 증거를 보존합니다. 이 모든 것이 자동으로 수 초 내에 실행됩니다.
여러분이 이 시스템을 사용하면 블록체인 인프라를 안전하게 운영할 수 있습니다. 특히 거래소, 결제 게이트웨이, DeFi 프로토콜 같은 금융 서비스에서는 이런 모니터링 시스템이 필수입니다.
실제로 많은 거래소가 2016-2017년 이더리움 클래식 51% 공격, 2019년 비트코인 골드 51% 공격 등을 이런 시스템으로 감지하고 손실을 최소화했습니다.
실전 팁
💡 알림 피로(Alert Fatigue)를 방지하세요. 1블록 reorg는 너무 자주 발생하므로 매번 알림을 보내면 관리자가 무시하게 됩니다. INFO 레벨은 Slack의 저우선순위 채널에만 보내고, 하루에 한 번 요약 리포트로 정리하는 것이 효과적입니다. 중요한 알림만 즉시 보내야 신뢰성이 유지됩니다.
💡 False Positive를 줄이기 위해 시간 윈도우를 설정하세요. 예를 들어, 1분 내에 3번 이상의 분기가 감지되면 WARNING으로 승격시키는 식입니다. 단일 이벤트보다 패턴이 더 정확한 신호입니다. 이는 일시적 네트워크 문제와 실제 공격을 구분하는 데 도움이 됩니다.
💡 알림에 즉시 실행 가능한 정보를 포함하세요. "Reorg detected"만 보내지 말고, "6 blocks rolled back, 15 transactions affected, estimated loss: $5,000, Action required: Check exchange hot wallet"처럼 구체적으로 작성하면 관리자가 빠르게 대응할 수 있습니다. 알림 템플릿을 미리 작성하세요.
💡 On-Call 로테이션과 통합하세요. PagerDuty나 OpsGenie를 사용하여 CRITICAL 알림은 현재 당직자에게 에스컬레이션되도록 설정합니다. 1차 담당자가 15분 내에 응답하지 않으면 2차 담당자에게 자동으로 전달되는 식입니다. 이렇게 하면 24/7 대응 체계를 구축할 수 있습니다.
💡 플레이북을 문서화하고 자동화하세요. "CRITICAL reorg 발생 시 대응 절차"를 명확히 정의하고, 가능한 부분은 스크립트로 자동화합니다. 예를 들어, "1. 입출금 중단, 2. 최근 입금 목록 조회, 3. 영향받은 사용자에게 이메일 발송"을 Runbook으로 만들어 한 번의 클릭으로 실행할 수 있게 합니다.
6. 합의 규칙 구현 - 가장 긴 체인 선택 알고리즘
시작하며
여러분의 노드가 네트워크에서 여러 개의 경쟁하는 체인을 발견했을 때, 어떤 기준으로 "진짜" 체인을 선택해야 할까요? 만약 각 노드가 제멋대로 다른 체인을 선택한다면 네트워크는 어떻게 될까요?
중앙 서버 없이 수천 개의 독립적인 노드가 하나의 합의에 도달한다는 것이 정말 가능할까요? 이 문제는 분산 시스템의 가장 오래된 난제 중 하나입니다.
비잔틴 장군 문제(Byzantine Generals Problem)로 알려진 이 문제를 사토시 나카모토는 천재적으로 해결했습니다. 바로 "가장 많은 작업 증명이 투입된 체인이 진실"이라는 간단한 규칙입니다.
이 규칙은 객관적이고, 검증 가능하며, 게임 이론적으로 안정적입니다. 바로 이럴 때 필요한 것이 합의 규칙(Consensus Rules)의 명확한 구현입니다.
모든 노드가 같은 코드로 같은 규칙을 실행하면, 중앙 조정 없이도 자연스럽게 하나의 체인으로 수렴합니다. 이것이 바로 블록체인의 마법이자 탈중앙화의 핵심입니다.
개요
간단히 말해서, 합의 규칙은 네트워크의 모든 노드가 동의하는 체인 선택 기준입니다. 비트코인에서는 "가장 높은 누적 난이도(chainwork)를 가진 유효한 체인"이 메인 체인으로 선택됩니다.
이 규칙이 왜 작동하는지 경제적 인센티브 관점에서 보면, 채굴자는 블록을 찾으면 보상(블록 리워드 + 수수료)을 받습니다. 그런데 이 보상은 그 블록이 메인 체인에 포함되어야만 유효합니다.
따라서 이성적인 채굴자는 항상 가장 긴 체인 위에 새 블록을 쌓으려 합니다. 자신만의 짧은 체인을 만들면 아무도 인정하지 않아 보상을 받을 수 없기 때문입니다.
이렇게 경제적 인센티브가 정직한 행동을 유도합니다. 기존에는 중앙 서버의 타임스탬프에 의존했다면, 블록체인에서는 작업 증명(Proof of Work)이라는 물리적 자원(전기, 하드웨어)에 의존합니다.
작업 증명은 위조할 수 없고, 모든 노드가 독립적으로 검증할 수 있습니다. 합의 규칙의 핵심 요소는 (1) 결정론적(Deterministic) - 같은 입력에 항상 같은 결과, (2) 검증 가능(Verifiable) - 누구나 독립적으로 검증 가능, (3) 인센티브 호환(Incentive-Compatible) - 정직한 행동이 경제적으로 유리.
이 세 가지가 충족되어야 탈중앙화된 합의가 가능합니다.
코드 예제
// 합의 규칙을 구현하는 클래스
class ConsensusEngine {
private readonly BLOCK_REWARD = 50; // 블록 보상 (BTC)
private readonly HALVING_INTERVAL = 210000; // 반감기 블록 수
// 여러 체인 중 최선의 체인 선택
public selectBestChain(chains: Block[][]): Block[] {
let bestChain: Block[] | null = null;
let highestChainwork = 0;
for (const chain of chains) {
// 규칙 1: 체인 유효성 검증
if (!this.isValidChain(chain)) {
console.log(`❌ Chain rejected: invalid`);
continue;
}
// 규칙 2: 누적 난이도 계산
const chainwork = this.calculateChainwork(chain);
console.log(`📊 Chain height ${chain.length}: chainwork = ${chainwork}`);
// 규칙 3: 가장 높은 chainwork를 가진 체인 선택
if (chainwork > highestChainwork) {
highestChainwork = chainwork;
bestChain = chain;
} else if (chainwork === highestChainwork && bestChain) {
// 규칙 4: Chainwork가 같으면 먼저 본 체인 유지 (First-Seen Rule)
console.log(`⚖️ Tie detected, keeping first-seen chain`);
}
}
if (!bestChain) {
throw new Error("No valid chain found");
}
console.log(`✅ Best chain selected: height ${bestChain.length}, chainwork ${highestChainwork}`);
return bestChain;
}
// 체인 전체 유효성 검증
private isValidChain(chain: Block[]): boolean {
// 제네시스 블록 검증
if (!this.isValidGenesisBlock(chain[0])) {
return false;
}
// 각 블록 검증
for (let i = 1; i < chain.length; i++) {
const currentBlock = chain[i];
const previousBlock = chain[i - 1];
// 블록 구조 검증
if (!this.isValidBlock(currentBlock, previousBlock)) {
return false;
}
// 작업 증명 검증
if (!this.isValidProofOfWork(currentBlock)) {
return false;
}
// 블록 보상 검증
if (!this.isValidBlockReward(currentBlock, i)) {
return false;
}
}
return true;
}
// Chainwork 계산 (누적 난이도)
private calculateChainwork(chain: Block[]): number {
let totalWork = 0;
for (const block of chain) {
// 각 블록의 난이도를 해시 연산 횟수로 환산
// difficulty 4 = 평균 2^4 = 16회 해시 시도 필요
totalWork += Math.pow(2, block.difficulty);
}
return totalWork;
}
// 제네시스 블록 검증
private isValidGenesisBlock(block: Block): boolean {
// 제네시스 블록은 하드코딩된 값과 일치해야 함
const GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
return block.index === 0 && block.previousHash === "0";
// 실제로는 block.hash === GENESIS_HASH도 확인
}
// 블록 구조 검증
private isValidBlock(block: Block, previousBlock: Block): boolean {
// 인덱스 연속성
if (block.index !== previousBlock.index + 1) {
return false;
}
// 이전 해시 연결
if (block.previousHash !== previousBlock.hash) {
return false;
}
// 타임스탬프 검증 (이전 블록보다 미래)
if (block.timestamp <= previousBlock.timestamp) {
return false;
}
return true;
}
// 작업 증명 검증
private isValidProofOfWork(block: Block): boolean {
// 블록 해시가 난이도 목표를 만족하는지 확인
const target = "0".repeat(block.difficulty);
return block.hash.startsWith(target);
}
// 블록 보상 검증
private isValidBlockReward(block: Block, blockHeight: number): boolean {
// 반감기 계산 (210,000블록마다 절반으로)
const halvings = Math.floor(blockHeight / this.HALVING_INTERVAL);
const expectedReward = this.BLOCK_REWARD / Math.pow(2, halvings);
// 실제로는 block.data에서 코인베이스 거래를 파싱하여 검증
// 여기서는 개념적 구현
return true; // 간단한 예제에서는 생략
}
}
설명
이것이 하는 일: 이 코드는 블록체인의 핵심인 나카모토 합의(Nakamoto Consensus)를 구현하여, 여러 경쟁 체인 중에서 네트워크가 따라야 할 정규(canonical) 체인을 자동으로 선택합니다. 첫 번째로, selectBestChain 메서드가 여러 개의 후보 체인을 받아 최선의 체인을 선택합니다.
각 체인에 대해 먼저 isValidChain으로 유효성을 검증합니다. 이는 매우 중요한데, 아무리 chainwork가 높아도 유효하지 않은 블록을 포함한 체인은 즉시 거부됩니다.
예를 들어, 이중 지불을 포함한 블록, 잘못된 작업 증명을 가진 블록, 과도한 블록 보상을 주장하는 블록이 하나라도 있으면 전체 체인이 무효가 됩니다. 이렇게 엄격한 검증이 블록체인의 보안을 보장합니다.
그 다음으로, 유효한 체인들에 대해 calculateChainwork로 누적 작업량을 계산합니다. 이는 각 블록의 difficulty를 2의 거듭제곱으로 환산한 값의 합계입니다.
왜 단순 합계가 아니라 지수 함수를 사용할까요? 난이도가 1 증가하면 필요한 해시 연산이 2배로 증가하기 때문입니다.
따라서 difficulty 10인 블록 1개는 difficulty 9인 블록 2개와 대략 같은 작업량을 나타냅니다. 이 정확한 측정이 공정한 비교를 가능하게 합니다.
마지막으로, chainwork가 가장 높은 체인을 선택하고, 동점인 경우 먼저 받은 체인을 유지합니다(First-Seen Rule). 이 규칙은 네트워크 분열을 방지합니다.
만약 동점일 때 랜덤하게 선택하면 노드들이 서로 다른 체인을 선택할 수 있습니다. 하지만 first-seen 규칙을 사용하면 대부분의 노드가 같은 블록을 먼저 받았을 것이므로 자연스럽게 다수가 같은 체인으로 수렴합니다.
이것이 바로 P2P 네트워크의 마법입니다. 여러분이 이 합의 엔진을 사용하면 완전히 탈중앙화된 블록체인을 운영할 수 있습니다.
중앙 서버, 투표, 리더 선출 같은 메커니즘 없이도 수만 개의 노드가 자동으로 하나의 진실에 합의합니다. 이것이 비트코인이 13년 넘게 단 한 번의 다운타임 없이 작동할 수 있었던 이유입니다.
코드가 곧 법입니다(Code is Law).
실전 팁
💡 검증은 항상 철저하게 수행하세요. "빠르게 검증하고 천천히 생성하라(Verify quickly, generate slowly)"는 블록체인의 황금률입니다. 새 블록을 받으면 즉시 모든 규칙을 검증하고, 하나라도 위반하면 거부합니다. 관대한 검증은 공격자에게 기회를 줍니다.
💡 난이도 조정 알고리즘을 합의 규칙에 포함시키세요. 비트코인은 2016블록(약 2주)마다 평균 블록 생성 시간이 10분이 되도록 난이도를 조정합니다. 이는 네트워크 해시레이트가 변해도 일정한 블록 생성 속도를 유지하는 핵심 메커니즘입니다. 코드로 명확히 구현하고 모든 노드가 같은 로직을 사용해야 합니다.
💡 소프트 포크와 하드 포크를 이해하고 대비하세요. 합의 규칙을 변경할 때 기존 노드와 호환되는 방식(소프트 포크)으로 할지, 호환되지 않는 방식(하드 포크)으로 할지 결정해야 합니다. SegWit은 소프트 포크, 비트코인 캐시는 하드 포크의 예시입니다. 업그레이드 전략을 신중히 수립하세요.
💡 체크포인트(Checkpoint)를 활용하세요. 매우 오래된 블록(예: 1년 전)은 재검증할 필요가 없으므로, 특정 높이의 블록 해시를 하드코딩하여 빠른 동기화가 가능합니다. 비트코인 코어는 주요 마일스톤 블록을 체크포인트로 설정합니다. 단, 너무 최근 블록을 체크포인트로 하면 탈중앙화가 훼손될 수 있으니 주의하세요.
💡 합의 규칙은 절대 임의로 변경하지 마세요. 규칙을 변경하려면 네트워크 전체의 합의가 필요합니다. BIP(Bitcoin Improvement Proposal) 같은 공식 프로세스를 거쳐 커뮤니티의 동의를 얻어야 합니다. 일방적인 규칙 변경은 네트워크 분열을 초래합니다.
7. 분기 해결 전략 - 네트워크 수렴 최적화
시작하며
여러분의 노드가 분기를 감지한 후 올바른 체인을 선택했다면, 이제 어떻게 해야 할까요? 그냥 기다리면 네트워크가 자동으로 수렴할까요, 아니면 적극적으로 다른 노드들에게 정보를 전파해야 할까요?
만약 네트워크의 절반은 체인 A를, 나머지 절반은 체인 B를 따르고 있다면 이 교착 상태를 어떻게 깨야 할까요? 이 문제는 단순히 기다리는 것만으로는 해결되지 않을 수 있습니다.
특히 네트워크가 물리적으로 분리되어 있거나(예: 해저 케이블 단절), 일부 노드들이 악의적으로 잘못된 정보를 전파하는 경우에는 능동적인 전략이 필요합니다. 빠른 수렴은 네트워크 안정성과 사용자 경험에 직접적인 영향을 미칩니다.
바로 이럴 때 필요한 것이 분기 해결 전략(Fork Resolution Strategy)입니다. 이는 올바른 체인을 선택한 후, 그 정보를 네트워크에 효율적으로 전파하고, 다른 노드들도 같은 결론에 도달하도록 돕는 메커니즘입니다.
비트코인 네트워크가 10분마다 새 블록이 생성되는데도 대부분의 분기가 수 초 내에 해결되는 이유가 바로 이 전략 덕분입니다.
개요
간단히 말해서, 분기 해결 전략은 체인 분기가 발생했을 때 네트워크가 빠르게 하나의 체인으로 수렴하도록 돕는 블록 전파 및 피어 관리 기법입니다. 왜 빠른 수렴이 중요한지 구체적으로 살펴보면, 분기가 오래 지속될수록 이중 지불 공격의 위험이 커지기 때문입니다.
예를 들어, 공격자가 체인 A에서 상품을 구매하고, 동시에 체인 B에서 같은 코인을 다른 곳에 사용한 후, 체인 B가 메인 체인으로 선택되면 첫 번째 거래는 무효가 됩니다. 따라서 분기를 빠르게 해결하여 한 거래만 유효하도록 만드는 것이 중요합니다.
또한 긴 분기는 사용자 혼란과 불신을 야기합니다. 기존에는 단순히 새 블록을 받으면 모든 피어에게 전달했다면, 이제는 더 긴 체인을 발견하면 우선순위를 높여 전파합니다.
이를 "Unsolicited Block Push"라고 합니다. 분기 해결 전략의 핵심 요소는 (1) 우선 전파 - 더 긴 체인의 블록을 즉시 모든 피어에게 전파, (2) 피어 선택 - 잘 연결된 피어에게 먼저 전파하여 네트워크 전체로 빠르게 확산, (3) 재동기화 - 짧은 체인에 있는 피어를 감지하고 긴 체인으로 안내.
이러한 전략이 네트워크의 자기 치유 능력을 강화합니다.
코드 예제
// 분기 해결 및 체인 전파 전략
class ForkResolutionStrategy {
private peers: Peer[] = []; // 연결된 피어들
private currentChain: Block[];
constructor(initialChain: Block[]) {
this.currentChain = initialChain;
}
// 새로운 체인 발견 시 분기 해결 프로토콜 실행
public async resolveAndPropagate(newChain: Block[]): Promise<void> {
const currentChainwork = this.calculateChainwork(this.currentChain);
const newChainwork = this.calculateChainwork(newChain);
// 새 체인이 더 좋은 체인인 경우
if (newChainwork > currentChainwork) {
console.log(`🔄 Switching to better chain (chainwork: ${newChainwork} > ${currentChainwork})`);
// 1단계: 로컬 체인 교체
this.currentChain = newChain;
// 2단계: 즉시 모든 피어에게 새 체인 정보 전파
await this.broadcastNewChain(newChain);
// 3단계: 뒤처진 피어 동기화 지원
await this.helpSyncLaggingPeers();
console.log(`✅ Fork resolved and propagated to ${this.peers.length} peers`);
} else {
console.log(`⏸️ Keeping current chain (chainwork: ${currentChainwork} >= ${newChainwork})`);
}
}
// 새 체인을 모든 피어에게 브로드캐스트
private async broadcastNewChain(chain: Block[]): Promise<void> {
// 우선순위가 높은 피어부터 전파 (잘 연결된 노드)
const sortedPeers = this.sortPeersByImportance();
console.log(`📡 Broadcasting to ${sortedPeers.length} peers`);
// 병렬로 전파 (빠른 확산)
const broadcastPromises = sortedPeers.map(async (peer) => {
try {
// 피어가 모르는 블록만 전송 (대역폭 절약)
const unknownBlocks = await this.getUnknownBlocks(peer, chain);
if (unknownBlocks.length > 0) {
await peer.sendBlocks(unknownBlocks);
console.log(`✅ Sent ${unknownBlocks.length} blocks to peer ${peer.id}`);
}
} catch (error) {
console.error(`❌ Failed to send to peer ${peer.id}:`, error);
}
});
await Promise.all(broadcastPromises);
}
// 피어를 중요도에 따라 정렬 (잘 연결된 피어 우선)
private sortPeersByImportance(): Peer[] {
return [...this.peers].sort((a, b) => {
// 정렬 기준: 연결 개수, 업타임, 레이턴시
return b.connectionCount - a.connectionCount;
});
}
// 피어가 아직 모르는 블록 찾기
private async getUnknownBlocks(peer: Peer, chain: Block[]): Promise<Block[]> {
// 피어의 최신 블록 높이 조회
const peerHeight = await peer.getChainHeight();
// 피어가 가지지 않은 블록들 반환
return chain.slice(peerHeight + 1);
}
// 뒤처진 피어들을 찾아 동기화 지원
private async helpSyncLaggingPeers(): Promise<void> {
const currentHeight = this.currentChain.length - 1;
for (const peer of this.peers) {
try {
const peerHeight = await peer.getChainHeight();
const heightDiff = currentHeight - peerHeight;
// 10블록 이상 뒤처진 피어 발견
if (heightDiff >= 10) {
console.log(`🔍 Peer ${peer.id} is ${heightDiff} blocks behind, helping sync`);
// "Headers-First" 방식: 헤더만 먼저 보내고 요청 시 블록 전송
const headers = this.currentChain.slice(peerHeight + 1).map(b => ({
index: b.index,
hash: b.hash,
previousHash: b.previousHash
}));
await peer.sendHeaders(headers);
}
} catch (error) {
console.error(`❌ Failed to help peer ${peer.id}:`, error);
}
}
}
// Chainwork 계산
private calculateChainwork(chain: Block[]): number {
return chain.reduce((total, block) => total + Math.pow(2, block.difficulty), 0);
}
// 피어로부터 체인 정보 수신 시 호출
public async onPeerChainAnnouncement(peer: Peer, theirChain: Block[]): Promise<void> {
console.log(`📥 Received chain announcement from peer ${peer.id}`);
// 피어의 체인이 더 좋은지 확인하고 필요시 전환
await this.resolveAndPropagate(theirChain);
}
// 새 피어 연결 시 현재 체인 공유
public async onNewPeerConnected(peer: Peer): Promise<void> {
console.log(`🤝 New peer connected: ${peer.id}`);
this.peers.push(peer);
// 새 피어에게 현재 최선의 체인 정보 전송
await peer.sendChainInfo({
height: this.currentChain.length - 1,
bestBlockHash: this.currentChain[this.currentChain.length - 1].hash,
chainwork: this.calculateChainwork(this.currentChain)
});
}
}
// 피어 인터페이스 (실제로는 네트워크 계층 구현)
interface Peer {
id: string;
connectionCount: number; // 이 피어가 가진 다른 연결 개수
sendBlocks(blocks: Block[]): Promise<void>;
sendHeaders(headers: BlockHeader[]): Promise<void>;
sendChainInfo(info: ChainInfo): Promise<void>;
getChainHeight(): Promise<number>;
}
interface BlockHeader {
index: number;
hash: string;
previousHash: string;
}
interface ChainInfo {
height: number;
bestBlockHash: string;
chainwork: number;
}
설명
이것이 하는 일: 이 코드는 체인 분기 상황에서 네트워크가 빠르게 합의에 도달하도록 블록 정보를 전략적으로 전파하고, 뒤처진 노드들을 동기화시키는 종합 시스템입니다. 첫 번째로, resolveAndPropagate 메서드가 새로운 체인을 평가하고 필요시 전환합니다.
chainwork를 비교하여 새 체인이 더 좋다고 판단되면 로컬 체인을 교체하고, 즉시 broadcastNewChain을 호출합니다. 이 "즉시"가 중요한데, 몇 초라도 지연되면 네트워크의 다른 부분이 다른 체인을 따를 수 있기 때문입니다.
마치 선거 개표처럼, 결과가 나오면 빠르게 전파해야 모두가 같은 결론에 도달합니다. 그 다음으로, broadcastNewChain이 효율적인 전파를 수행합니다.
여기서 두 가지 최적화가 적용됩니다: (1) 피어 우선순위 - sortPeersByImportance로 잘 연결된 피어(connectionCount가 높은)에게 먼저 전송합니다. 이런 "허브" 노드들이 빠르게 정보를 받으면 네트워크 전체로 빠르게 확산됩니다.
(2) 선택적 전송 - getUnknownBlocks로 각 피어가 이미 가진 블록은 제외하고 새 블록만 보냅니다. 이렇게 하면 대역폭을 크게 절약할 수 있습니다.
Promise.all로 모든 전송을 병렬로 수행하여 시간도 단축합니다. 마지막으로, helpSyncLaggingPeers가 뒤처진 노드들을 찾아 도와줍니다.
각 피어의 체인 높이를 조회하여 10블록 이상 뒤처졌으면 "Headers-First" 방식으로 블록 헤더를 전송합니다. 헤더는 매우 작기 때문에(80바이트) 빠르게 전송할 수 있고, 피어는 헤더를 보고 어떤 블록이 필요한지 판단한 후 요청할 수 있습니다.
이는 비트코인 0.10에서 도입된 최적화 기법으로, 초기 동기화를 몇 배 빠르게 만들었습니다. 여러분이 이 전략을 사용하면 네트워크 분기가 발생해도 대부분 몇 초 내에 해결됩니다.
실제 비트코인 네트워크에서 1블록 분기는 평균 30초 내에 해결되며, 이 시스템 덕분입니다. 또한 새로운 노드가 네트워크에 참여했을 때도 빠르게 최신 체인으로 동기화할 수 있습니다.
실전 팁
💡 Compact Block Relay (BIP 152)를 구현하세요. 전체 블록을 보내는 대신, 블록 헤더 + 거래 ID 목록만 보냅니다. 대부분의 거래는 이미 mempool에 있으므로 받는 쪽에서 재구성할 수 있습니다. 이렇게 하면 대역폭을 90% 이상 줄일 수 있습니다.
💡 피어 다양성을 유지하세요. 모든 피어가 같은 ISP나 지역에 있으면 네트워크 분열 시 편향된 정보만 받을 수 있습니다. 지리적, 네트워크적으로 다양한 피어들과 연결하여 균형 잡힌 정보를 받도록 하세요. 비트코인 코어는 최소 8개의 아웃바운드 피어를 권장합니다.
💡 Block-Only 연결을 활용하세요. 일부 피어와는 블록만 교환하고 거래는 교환하지 않는 연결을 유지합니다. 이렇게 하면 거래 전파 공격(특정 거래를 의도적으로 전파하지 않음)을 방어하면서도 블록 정보는 빠르게 받을 수 있습니다.
💡 전파 시간을 측정하고 최적화하세요. 블록을 받은 시간과 모든 피어에게 전파 완료한 시간의 차이를 로깅합니다. 이 시간이 길어지면 네트워크 설정을 점검하거나, 느린 피어를 교체하거나, 대역폭을 증설해야 합니다. 목표는 1초 이내 전파입니다.
💡 Eclipse Attack을 방어하세요. 공격자가 당신의 노드를 악의적인 피어들로만 둘러싸서 잘못된 체인 정보만 주는 공격입니다. 방어 방법: (1) 신뢰할 수 있는 DNS 시드 사용, (2) 주기적으로 새 피어 발견, (3) 피어 IP 다양성 확인. 한 IP 대역에서 너무 많은 연결을 받지 않도록 제한하세요.
8. 테스트 시나리오 - 분기 상황 시뮬레이션
시작하며
여러분이 지금까지 구현한 분기 감지 및 해결 로직이 실제로 제대로 작동하는지 어떻게 확인할 수 있을까요? 프로덕션 환경에서 실제 분기가 발생하기를 기다렸다가 테스트할 수는 없는 노릇입니다.
만약 코드에 버그가 있다면 실제 사용자의 자금이 위험에 처할 수 있습니다. 어떻게 안전하게 테스트할 수 있을까요?
이 문제는 많은 개발자들이 간과하는 부분입니다. 블록체인 코드는 한 번 배포되면 되돌리기 어렵고, 특히 합의 규칙 관련 버그는 네트워크 분열이나 자금 손실로 이어질 수 있습니다.
2013년 비트코인의 BDB 락 제한 버그, 2018년 이더리움의 Geth 합의 버그 등 실제 사례들이 있습니다. 이런 버그들은 모두 충분한 테스트로 예방할 수 있었습니다.
바로 이럴 때 필요한 것이 포괄적인 테스트 시나리오입니다. 다양한 분기 상황을 인위적으로 만들어 코드가 올바르게 대응하는지 검증하는 것입니다.
단위 테스트, 통합 테스트, 시뮬레이션을 통해 프로덕션에 배포하기 전에 모든 엣지 케이스를 확인할 수 있습니다.
개요
간단히 말해서, 분기 테스트 시나리오는 실제 네트워크 환경을 시뮬레이션하여 체인 분기, 재구성, 고아 블록 등의 상황을 인위적으로 생성하고 시스템의 대응을 검증하는 자동화된 테스트 스위트입니다. 테스트가 왜 이렇게 중요한지 구체적으로 보면, 블록체인은 돈을 다루는 시스템이기 때문입니다.
일반 웹 애플리케이션의 버그는 사용자 불편을 초래하지만, 블록체인 버그는 직접적인 금전 손실로 이어집니다. 예를 들어, reorg 로직에 버그가 있어 이미 지출된 코인을 다시 사용할 수 있다면, 이는 무한 자금 생성 버그가 됩니다.
또한 합의 버그는 네트워크를 영구적으로 분열시킬 수 있습니다. 2016년 이더리움과 이더리움 클래식의 분열처럼요.
기존에는 수동으로 테스트 노드를 실행하고 블록을 만들어야 했다면, 이제는 자동화된 테스트 프레임워크로 수십 가지 시나리오를 몇 초 만에 검증할 수 있습니다. 테스트 시나리오의 핵심 요소는 (1) 결정론적 환경 - 같은 테스트를 반복 실행해도 같은 결과, (2) 엣지 케이스 커버리지 - 일반적 상황부터 극단적 상황까지, (3) 성능 측정 - 분기 해결 속도, 메모리 사용량 등 모니터링.
이러한 테스트가 프로덕션 배포 전 안전망 역할을 합니다.
코드 예제
// 분기 상황 테스트 스위트
describe("Fork Detection and Resolution", () => {
let blockchain: Blockchain;
let orphanPool: OrphanBlockPool;
let consensusEngine: ConsensusEngine;
beforeEach(() => {
// 각 테스트 전에 깨끗한 상태로 초기화
blockchain = new Blockchain();
orphanPool = new OrphanBlockPool();
consensusEngine = new ConsensusEngine();
});
// 테스트 1: 기본 1블록 분기 감지
test("Should detect simple 1-block fork", () => {
// Given: 공통 조상이 있는 체인
const commonAncestor = createBlock(0, "genesis", "0");
blockchain.addBlock(commonAncestor);
// When: 같은 높이에 두 개의 다른 블록 생성
const block1A = createBlock(1, "block1A", commonAncestor.hash);
const block1B = createBlock(1, "block1B", commonAncestor.hash);
blockchain.addBlock(block1A);
const forkDetected = blockchain.detectFork(block1B);
// Then: 분기가 감지되어야 함
expect(forkDetected).toBe(true);
});
// 테스트 2: 체인 재구성 - 더 긴 체인으로 교체
test("Should reorganize to longer chain", () => {
// Given: 짧은 체인 A (높이 3)
const chainA = createChain([
{ index: 0, hash: "genesis", prevHash: "0" },
{ index: 1, hash: "A1", prevHash: "genesis" },
{ index: 2, hash: "A2", prevHash: "A1" },
]);
// And: 더 긴 체인 B (높이 5)
const chainB = createChain([
{ index: 0, hash: "genesis", prevHash: "0" },
{ index: 1, hash: "B1", prevHash: "genesis" },
{ index: 2, hash: "B2", prevHash: "B1" },
{ index: 3, hash: "B3", prevHash: "B2" },
{ index: 4, hash: "B4", prevHash: "B3" },
]);
// When: 더 긴 체인 발견
blockchain.replaceChain(chainA);
const result = blockchain.replaceChain(chainB);
// Then: 체인이 교체되어야 함
expect(result.reorgOccurred).toBe(true);
expect(blockchain.getHeight()).toBe(4); // 높이 5 = index 4
expect(blockchain.getLatestBlock().hash).toBe("B4");
});
// 테스트 3: 높은 누적 난이도 체인 선택 (짧지만 어려운 체인)
test("Should select chain with higher cumulative difficulty", () => {
// Given: 긴 체인 (5블록, 낮은 난이도)
const easyChain = createChainWithDifficulty([2, 2, 2, 2, 2]); // 총 난이도 10
// And: 짧은 체인 (3블록, 높은 난이도)
const hardChain = createChainWithDifficulty([4, 4, 4]); // 총 난이도 12
// When: 최선의 체인 선택
const bestChain = consensusEngine.selectBestChain([easyChain, hardChain]);
// Then: 누적 난이도가 높은 짧은 체인이 선택되어야 함
expect(bestChain).toBe(hardChain);
});
// 테스트 4: 고아 블록 관리
test("Should handle orphan blocks and connect when parent arrives", () => {
// Given: 블록 0만 있는 체인
const block0 = createBlock(0, "genesis", "0");
blockchain.addBlock(block0);
// When: 블록 2를 먼저 받음 (부모인 블록 1 없음)
const block2 = createBlock(2, "block2", "block1_hash");
const added = orphanPool.processNewBlock(block2, blockchain.getChain());
// Then: 고아로 분류되어야 함
expect(added).toBe(false);
expect(orphanPool.getOrphanCount()).toBe(1);
// When: 부모 블록 1이 도착
const block1 = createBlock(1, "block1", block0.hash);
orphanPool.processNewBlock(block1, blockchain.getChain());
// Then: 블록 2가 자동으로 연결되어야 함
expect(blockchain.getHeight()).toBe(2);
expect(orphanPool.getOrphanCount()).toBe(0);
});
// 테스트 5: 깊은 재구성 시 거래 복원
test("Should restore transactions from rolled back blocks", () => {
// Given: 거래를 포함한 체인 A
const tx1 = { from: "Alice", to: "Bob", amount: 50 };
const tx2 = { from: "Bob", to: "Charlie", amount: 30 };
const chainA = createChainWithTransactions([
{ index: 0, txs: [] },
{ index: 1, txs: [tx1] },
{ index: 2, txs: [tx2] },
]);
// And: 거래를 포함한 더 긴 체인 B (tx1은 없고 tx3만 있음)
const tx3 = { from: "Alice", to: "Dave", amount: 40 };
const chainB = createChainWithTransactions([
{ index: 0, txs: [] },
{ index: 1, txs: [] },
{ index: 2, txs: [tx3] },
{ index: 3, txs: [] },
]);
blockchain.replaceChain(chainA);
const mempool = new TransactionPool();
// When: 체인 B로 재구성
const reorgResult = blockchain.reorgToChain(chainB, mempool);
// Then: 롤백된 거래들이 mempool로 복원되어야 함
expect(reorgResult.restoredTransactions).toContain(tx1);
expect(reorgResult.restoredTransactions).toContain(tx2);
expect(mempool.size()).toBe(2);
});
// 테스트 6: 고아 풀 크기 제한
test("Should enforce orphan pool size limit", () => {
const MAX_ORPHANS = 10;
orphanPool.setMaxSize(MAX_ORPHANS);
// When: 제한보다 많은 고아 블록 추가
for (let i = 0; i < 15; i++) {
const orphan = createBlock(i + 100, `orphan${i}`, `missing_parent${i}`);
orphanPool.processNewBlock(orphan, blockchain.getChain());
}
// Then: 최대 크기로 제한되어야 함
expect(orphanPool.getOrphanCount()).toBe(MAX_ORPHANS);
});
// 테스
댓글 (0)
함께 보면 좋은 카드 뉴스
Context Fundamentals - AI 컨텍스트의 기본 원리
AI 에이전트 개발의 핵심인 컨텍스트 관리를 다룹니다. 시스템 프롬프트 구조부터 Attention Budget, Progressive Disclosure까지 실무에서 바로 적용할 수 있는 컨텍스트 최적화 전략을 배웁니다.
API Integration 도구 완벽 가이드
외부 API를 호출하고 통합하는 방법을 처음부터 차근차근 배워봅니다. REST API 호출부터 인증, Rate Limiting, 실전 GitHub API 활용까지 실무에서 바로 쓸 수 있는 내용을 담았습니다.
File System 도구 완벽 가이드
LLM과 AI 에이전트가 파일 시스템을 다루는 방법을 알아봅니다. 읽기, 쓰기, 삭제부터 경로 검증, 파일 타입 처리까지 실무에서 바로 활용할 수 있는 도구 사용법을 배웁니다.
도구 실행 결과 처리 완벽 가이드
LLM 애플리케이션에서 도구 실행 결과를 안정적으로 처리하는 방법을 다룹니다. 성공과 실패를 구분하고, 재시도 전략을 세우며, 견고한 폴백 메커니즘까지 구현하는 실전 기법을 배웁니다.
Tool Schema 설계 완벽 가이드
LLM 기반 AI 에이전트에서 도구를 호출할 때 필수적인 Tool Schema 설계 방법을 다룹니다. JSON Schema 작성부터 파라미터 정의, 타입 최적화, 복잡한 도구 스키마 실습까지 초급 개발자도 쉽게 따라할 수 있도록 설명합니다.