이미지 로딩 중...

타입스크립트로 비트코인 클론하기 3편 블록 데이터 구조 설계하기 - 슬라이드 1/9
A

AI Generated

2025. 11. 9. · 2 Views

타입스크립트로 비트코인 클론하기 3편 블록 데이터 구조 설계하기

블록체인의 핵심인 블록 데이터 구조를 타입스크립트로 설계하는 방법을 배웁니다. 블록 헤더, 트랜잭션 리스트, 해시 연결 구조를 실제 비트코인과 유사하게 구현하며 타입 안정성을 확보하는 실전 가이드입니다.


목차

  1. 블록 인터페이스 정의
  2. 블록 헤더 구조체
  3. 제네시스 블록 생성
  4. 블록 해시 계산
  5. 이전 블록 연결
  6. 타임스탬프 관리
  7. 난이도 타겟 시스템
  8. Nonce 값 설계

1. 블록 인터페이스 정의

시작하며

여러분이 블록체인을 처음 구현할 때 이런 고민을 하신 적 있나요? "블록에는 정확히 어떤 데이터가 들어가야 하지?

타입을 어떻게 정의해야 안전할까?" 이런 질문은 블록체인 개발의 첫 걸음에서 누구나 겪는 고민입니다. 블록의 구조를 명확하게 정의하지 않으면 나중에 버그가 발생하거나, 다른 개발자와 협업할 때 혼란이 생깁니다.

특히 타입스크립트를 사용한다면 타입 안정성을 최대한 활용해야 합니다. 바로 이럴 때 필요한 것이 명확한 블록 인터페이스 정의입니다.

타입스크립트의 강력한 타입 시스템을 활용하면 컴파일 타임에 오류를 잡아내고, IDE의 자동완성 기능도 최대한 활용할 수 있습니다.

개요

간단히 말해서, 블록 인터페이스는 블록체인에서 각 블록이 가져야 할 데이터 구조를 타입으로 정의한 것입니다. 실제 비트코인 블록은 인덱스, 타임스탬프, 트랜잭션 데이터, 이전 블록 해시, 현재 블록 해시, 난이도, nonce 등의 정보를 담고 있습니다.

이러한 정보를 타입스크립트 인터페이스로 명확히 정의하면 런타임 에러를 사전에 방지할 수 있습니다. 기존 자바스크립트에서는 객체 속성을 자유롭게 추가하거나 삭제할 수 있었지만, 타입스크립트 인터페이스를 사용하면 정해진 구조만 허용됩니다.

핵심 특징은 첫째, 타입 안정성 보장, 둘째, IDE 자동완성 지원, 셋째, 리팩토링 시 안전성입니다. 이러한 특징들이 대규모 블록체인 프로젝트에서 코드 품질을 유지하는 데 매우 중요합니다.

코드 예제

// 블록의 기본 구조를 정의하는 인터페이스
interface Block {
  index: number;                    // 블록 번호 (0부터 시작)
  timestamp: number;                // 블록 생성 시간 (Unix timestamp)
  data: string;                     // 블록에 저장될 데이터
  previousHash: string;             // 이전 블록의 해시값
  hash: string;                     // 현재 블록의 해시값
  difficulty: number;               // 채굴 난이도
  nonce: number;                    // 작업 증명을 위한 임의의 숫자
}

// 사용 예시
const block: Block = {
  index: 1,
  timestamp: Date.now(),
  data: "Transaction data",
  previousHash: "0000abc...",
  hash: "0000def...",
  difficulty: 4,
  nonce: 12345
};

설명

이것이 하는 일: 블록 인터페이스는 블록체인의 각 블록이 반드시 포함해야 하는 속성들과 그 타입을 명시적으로 정의하여, 런타임 에러를 컴파일 타임에 방지합니다. 첫 번째로, index와 timestamp는 블록의 순서와 시간 정보를 관리합니다.

index는 0부터 시작하는 블록 번호로, 블록체인에서 해당 블록의 위치를 나타냅니다. timestamp는 Unix 타임스탬프 형식으로 블록이 생성된 정확한 시간을 기록하는데, 이는 블록의 시간 순서를 보장하고 이중 지불 공격을 방지하는 데 필수적입니다.

그 다음으로, data 필드는 실제 블록에 저장될 정보를 담습니다. 비트코인에서는 여기에 트랜잭션 정보가 들어가지만, 우리 구현에서는 간단히 문자열로 시작합니다.

나중에 Transaction[] 타입으로 확장할 수 있습니다. previousHash와 hash는 블록체인의 연결 고리를 만드는 핵심 요소입니다.

세 번째로, difficulty와 nonce는 작업 증명(Proof of Work) 메커니즘의 핵심입니다. difficulty는 채굴 난이도를 나타내며, 이 값이 높을수록 유효한 해시를 찾기 어렵습니다.

nonce는 채굴자가 유효한 해시를 찾을 때까지 계속 변경하는 값입니다. 여러분이 이 인터페이스를 사용하면 블록을 생성할 때 실수로 필수 속성을 빠뜨리거나 잘못된 타입의 값을 할당하는 것을 방지할 수 있습니다.

또한 VSCode 같은 IDE에서 자동완성이 완벽하게 작동하여 개발 생산성이 크게 향상됩니다. 리팩토링 시에도 타입 체크 덕분에 안전하게 코드를 수정할 수 있습니다.

실전 팁

💡 readonly 키워드를 활용하여 블록 생성 후 변경 불가능하도록 만들면 블록체인의 불변성을 타입 레벨에서 보장할 수 있습니다

💡 data 필드는 나중에 제네릭 타입으로 만들어 다양한 데이터 타입을 지원하도록 확장할 수 있습니다 (interface Block<T> 형태)

💡 옵셔널 체이닝과 null 체크를 활용하여 previousHash가 없는 제네시스 블록도 타입 안전하게 처리하세요

💡 인터페이스 대신 type을 사용할 수도 있지만, 확장성과 선언 병합(declaration merging) 기능을 고려하면 interface가 더 적합합니다

💡 Zod나 io-ts 같은 런타임 타입 검증 라이브러리를 함께 사용하면 외부에서 들어오는 데이터도 안전하게 검증할 수 있습니다


2. 블록 헤더 구조체

시작하며

여러분이 블록체인 데이터를 효율적으로 관리하려고 할 때 이런 상황을 겪어본 적 있나요? 모든 블록 데이터를 다 읽어야 해서 네트워크 부하가 심하거나, 블록 검증에 시간이 너무 오래 걸리는 경우 말이죠.

실제 비트코인 네트워크에서는 수십만 개의 블록이 존재하고, 각 블록에는 수천 개의 트랜잭션이 포함될 수 있습니다. 매번 모든 데이터를 읽는다면 성능이 심각하게 저하됩니다.

바로 이럴 때 필요한 것이 블록 헤더입니다. 블록의 메타데이터만 분리하여 관리하면 경량화된 클라이언트(SPV)를 구현할 수 있고, 블록 검증 속도도 크게 향상됩니다.

개요

간단히 말해서, 블록 헤더는 블록의 메타데이터만 모아놓은 구조체로, 실제 트랜잭션 데이터 없이도 블록을 식별하고 검증할 수 있게 해줍니다. 비트코인의 SPV(Simplified Payment Verification) 지갑은 블록 헤더만 다운로드하여 작동합니다.

80바이트의 헤더만으로 블록의 유효성을 검증할 수 있어, 모바일 기기처럼 저장 공간과 대역폭이 제한된 환경에서 매우 유용합니다. 기존에는 전체 블록을 다운로드하여 검증했다면, 이제는 헤더만으로 머클 증명을 통해 특정 트랜잭션의 존재 여부를 확인할 수 있습니다.

핵심 특징은 첫째, 데이터 크기가 작아 네트워크 효율성이 높고, 둘째, 블록 검증에 필요한 모든 정보를 포함하며, 셋째, 머클 루트를 통해 트랜잭션 무결성을 보장합니다. 이러한 특징들이 경량 클라이언트 구현의 핵심입니다.

코드 예제

// 블록 헤더만 분리한 구조체
interface BlockHeader {
  version: number;                  // 블록 버전
  index: number;                    // 블록 인덱스
  previousHash: string;             // 이전 블록 해시
  merkleRoot: string;               // 트랜잭션 머클 루트
  timestamp: number;                // 블록 생성 시간
  difficulty: number;               // 난이도 타겟
  nonce: number;                    // 작업 증명 값
}

// 헤더에서 해시를 계산하는 함수
function calculateHeaderHash(header: BlockHeader): string {
  const headerString = `${header.version}${header.index}${header.previousHash}${header.merkleRoot}${header.timestamp}${header.difficulty}${header.nonce}`;
  return sha256(headerString);
}

// 사용 예시
const header: BlockHeader = {
  version: 1,
  index: 100,
  previousHash: "0000abc...",
  merkleRoot: "xyz123...",
  timestamp: Date.now(),
  difficulty: 4,
  nonce: 0
};

설명

이것이 하는 일: 블록 헤더는 블록의 핵심 메타데이터만 추출하여, 전체 트랜잭션 데이터 없이도 블록의 유효성을 검증하고 블록체인의 연결성을 확인할 수 있게 합니다. 첫 번째로, version 필드는 블록 구조의 버전을 나타냅니다.

비트코인은 소프트웨어가 업그레이드되면서 블록 구조가 변경될 수 있는데, version을 통해 어떤 규칙으로 블록을 해석해야 하는지 알 수 있습니다. 이는 하위 호환성을 유지하는 데 필수적입니다.

그 다음으로, merkleRoot는 블록에 포함된 모든 트랜잭션의 해시를 머클 트리로 만들었을 때의 루트 해시입니다. 이 값 하나만으로 블록 내 모든 트랜잭션의 무결성을 검증할 수 있습니다.

SPV 클라이언트는 이 merkleRoot와 머클 증명을 사용하여 특정 트랜잭션이 블록에 포함되어 있는지 확인합니다. 세 번째로, calculateHeaderHash 함수는 헤더의 모든 필드를 연결하여 SHA-256 해시를 계산합니다.

실제 비트코인은 이중 SHA-256을 사용하며, 이 해시값이 난이도 타겟보다 작아야 유효한 블록으로 인정됩니다. 채굴자는 이 조건을 만족하는 nonce 값을 찾기 위해 수십억 번의 해시 계산을 수행합니다.

여러분이 이 블록 헤더 구조를 사용하면 모바일 지갑이나 IoT 디바이스처럼 제한된 환경에서도 블록체인을 검증할 수 있습니다. 전체 블록체인이 수백 GB인 경우에도 헤더만 다운로드하면 수십 MB로 충분합니다.

또한 네트워크 대역폭을 크게 절약하여 동기화 속도가 수백 배 빨라집니다.

실전 팁

💡 실제 비트코인은 헤더를 직렬화할 때 80바이트 고정 크기를 사용하므로, Buffer나 ArrayBuffer를 활용한 바이너리 직렬화를 구현하면 더 효율적입니다

💡 헤더만 저장하는 별도의 데이터베이스 테이블을 만들면 블록 검증 쿼리 성능이 크게 향상됩니다

💡 merkleRoot 계산 시 트랜잭션 순서가 매우 중요하므로, 배열 순서를 보장하는 자료구조를 사용하세요

💡 비트코인은 리틀 엔디안 바이트 순서를 사용하므로, 다른 구현체와 호환성을 원한다면 엔디안 처리에 주의해야 합니다

💡 헤더 체인만 먼저 다운로드하고 검증한 후, 필요한 블록의 전체 데이터를 나중에 받는 "헤더 우선 동기화" 전략을 사용하면 초기 동기화 시간을 크게 단축할 수 있습니다


3. 제네시스 블록 생성

시작하며

여러분이 블록체인을 처음 시작할 때 이런 딜레마에 빠진 적 있나요? "첫 번째 블록은 이전 해시가 없는데 어떻게 만들지?" 이것은 모든 블록체인 개발자가 처음에 겪는 닭이 먼저냐 달걀이 먼저냐의 문제입니다.

첫 번째 블록(제네시스 블록)은 특별한 처리가 필요합니다. 일반 블록처럼 이전 블록을 참조할 수 없고, 모든 노드가 동일한 제네시스 블록을 가져야 네트워크가 합의에 도달할 수 있습니다.

바로 이럴 때 필요한 것이 하드코딩된 제네시스 블록입니다. 비트코인도 2009년 1월 3일의 제네시스 블록을 코드에 직접 포함하고 있으며, 이는 전체 블록체인의 신뢰 앵커 역할을 합니다.

개요

간단히 말해서, 제네시스 블록은 블록체인의 첫 번째 블록으로, 이전 해시 대신 고정된 값을 사용하며 모든 노드에서 동일하게 생성됩니다. 비트코인의 제네시스 블록에는 "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"라는 메시지가 담겨 있습니다.

이는 블록이 특정 날짜 이전에 생성될 수 없음을 증명하는 타임스탬프 역할을 합니다. 기존 일반 블록이 이전 블록의 해시를 참조한다면, 제네시스 블록은 "0"이나 특정 고정 문자열을 previousHash로 사용합니다.

핵심 특징은 첫째, 하드코딩된 고정 값으로 생성되어 모든 노드에서 동일하고, 둘째, 체인의 시작점이므로 절대 변경되지 않으며, 셋째, 역사적 의미나 타임스탬프 증거를 담을 수 있습니다. 이러한 특징들이 블록체인의 신뢰성과 투명성을 보장합니다.

코드 예제

import * as crypto from 'crypto';

// SHA-256 해시 함수
function sha256(data: string): string {
  return crypto.createHash('sha256').update(data).digest('hex');
}

// 제네시스 블록을 생성하는 함수
function createGenesisBlock(): Block {
  const genesisBlock: Block = {
    index: 0,
    timestamp: 1704067200000,  // 2024-01-01 00:00:00 UTC
    data: "Genesis Block - The beginning of our blockchain",
    previousHash: "0",         // 제네시스 블록은 이전 해시가 없음
    hash: "",                  // 계산 후 할당
    difficulty: 0,             // 제네시스 블록은 채굴 불필요
    nonce: 0
  };

  // 제네시스 블록의 해시 계산
  genesisBlock.hash = calculateHash(genesisBlock);

  return genesisBlock;
}

function calculateHash(block: Block): string {
  const dataString = `${block.index}${block.timestamp}${block.data}${block.previousHash}${block.difficulty}${block.nonce}`;
  return sha256(dataString);
}

설명

이것이 하는 일: 제네시스 블록은 블록체인의 시작점을 정의하고, 모든 후속 블록이 참조할 수 있는 최초의 유효한 블록을 생성합니다. 첫 번째로, createGenesisBlock 함수는 고정된 값들로 첫 번째 블록을 생성합니다.

index는 0으로 시작하고, timestamp는 프로젝트 시작 시점의 특정 시간을 하드코딩합니다. 이 타임스탬프는 블록체인 네트워크의 "탄생 시각"으로 역사적 의미를 가집니다.

data 필드에는 의미 있는 메시지를 넣을 수 있는데, 비트코인처럼 신문 헤드라인을 넣으면 블록 생성 시점을 증명할 수 있습니다. 그 다음으로, previousHash를 "0"으로 설정하는 것이 중요합니다.

이는 "이전 블록이 존재하지 않음"을 명시적으로 나타내는 관례입니다. 일부 구현에서는 64개의 0("0000...0000")을 사용하기도 합니다.

difficulty와 nonce를 0으로 설정하는 이유는 제네시스 블록은 채굴 과정 없이 즉시 생성되기 때문입니다. 세 번째로, calculateHash 함수는 블록의 모든 필드를 문자열로 연결한 후 SHA-256 해시를 계산합니다.

제네시스 블록의 해시는 두 번째 블록의 previousHash 값이 되어 체인을 시작합니다. 이 해시 계산 로직은 모든 블록에서 동일하게 사용되어 일관성을 보장합니다.

여러분이 이 제네시스 블록을 사용하면 모든 노드가 동일한 시작점에서 블록체인을 구축할 수 있습니다. 네트워크에 새로 참여한 노드는 제네시스 블록부터 현재까지의 모든 블록을 검증하여 전체 체인의 무결성을 확인합니다.

또한 data 필드에 의미 있는 메시지를 넣으면 프로젝트의 정체성과 투명성을 표현할 수 있습니다.

실전 팁

💡 제네시스 블록의 timestamp, data, previousHash를 상수로 정의하여 실수로 변경되는 것을 방지하세요 (const GENESIS_TIMESTAMP = 1704067200000)

💡 테스트넷과 메인넷의 제네시스 블록을 다르게 만들면 두 네트워크가 분리되어 안전하게 테스트할 수 있습니다

💡 제네시스 블록을 생성할 때 Object.freeze()를 사용하여 런타임에도 불변성을 보장하면 더 안전합니다

💡 블록체인 초기화 시 제네시스 블록의 해시를 검증하는 로직을 추가하면 네트워크 분열을 방지할 수 있습니다

💡 data 필드에 JSON 형식으로 초기 설정 정보(난이도 조정 주기, 보상 반감기 등)를 저장하면 체인 파라미터를 투명하게 관리할 수 있습니다


4. 블록 해시 계산

시작하며

여러분이 블록의 고유 식별자를 만들려고 할 때 이런 고민을 한 적 있나요? "블록 내용이 조금이라도 변경되면 즉시 알아챌 수 있는 방법이 없을까?" 블록체인의 핵심은 데이터 무결성인데, 이를 보장하는 것이 바로 해시 함수입니다.

만약 누군가가 과거 블록의 데이터를 몰래 수정한다면 전체 블록체인의 신뢰성이 무너집니다. 특히 금융 거래 같은 중요한 데이터를 다룰 때는 변조를 즉시 감지할 수 있어야 합니다.

바로 이럴 때 필요한 것이 암호학적 해시 함수 SHA-256입니다. 블록의 모든 데이터를 입력으로 받아 고유한 256비트 지문을 생성하며, 입력이 1비트만 바뀌어도 완전히 다른 해시가 나옵니다.

개요

간단히 말해서, 블록 해시는 블록의 모든 내용을 SHA-256 함수에 넣어 생성한 고유 식별자로, 블록의 무결성을 보장하고 체인의 연결을 만듭니다. 비트코인은 이중 SHA-256(SHA-256을 두 번 적용)을 사용하여 보안을 강화합니다.

해시 함수의 특징은 같은 입력은 항상 같은 출력을, 다른 입력은 완전히 다른 출력을 만들어낸다는 것입니다. 또한 해시에서 원본 데이터를 역추적하는 것이 사실상 불가능합니다.

기존에는 단순히 블록 번호로 블록을 식별했다면, 이제는 내용 기반 주소 지정(content-addressed)을 사용하여 데이터 자체가 식별자가 됩니다. 핵심 특징은 첫째, 결정론적(동일 입력 → 동일 출력), 둘째, 역계산 불가능(해시에서 원본 복원 불가), 셋째, 충돌 저항성(서로 다른 입력이 같은 해시를 만들 확률이 극히 낮음)입니다.

이러한 특징들이 블록체인을 변조 불가능하게 만듭니다.

코드 예제

import * as crypto from 'crypto';

// SHA-256 해시 계산 함수
function calculateBlockHash(
  index: number,
  previousHash: string,
  timestamp: number,
  data: string,
  difficulty: number,
  nonce: number
): string {
  // 모든 블록 데이터를 하나의 문자열로 결합
  const blockData = index + previousHash + timestamp + data + difficulty + nonce;

  // SHA-256 해시 계산
  return crypto.createHash('sha256')
    .update(blockData)
    .digest('hex');
}

// 비트코인처럼 이중 해시를 사용하는 버전
function calculateDoubleHash(blockData: string): string {
  const firstHash = crypto.createHash('sha256').update(blockData).digest('hex');
  const secondHash = crypto.createHash('sha256').update(firstHash).digest('hex');
  return secondHash;
}

// 사용 예시
const hash = calculateBlockHash(1, "0000abc...", Date.now(), "Transaction data", 4, 12345);
console.log(`Block hash: ${hash}`);

설명

이것이 하는 일: 블록 해시는 블록의 모든 필드를 암호학적 해시 함수에 넣어 고유한 지문을 생성하고, 이를 통해 블록의 무결성을 검증하고 체인의 연결을 만듭니다. 첫 번째로, calculateBlockHash 함수는 블록의 모든 중요한 필드를 순서대로 연결합니다.

index부터 nonce까지 모든 값을 문자열로 변환하여 하나로 이어붙이는데, 이 순서가 매우 중요합니다. 순서가 바뀌면 완전히 다른 해시가 나오므로 모든 노드가 동일한 순서를 사용해야 합니다.

그 다음으로, crypto.createHash('sha256')은 Node.js의 내장 암호화 모듈을 사용하여 SHA-256 해시를 계산합니다. update() 메서드에 데이터를 전달하고, digest('hex')로 16진수 문자열 형태의 해시를 얻습니다.

결과는 64자의 16진수 문자열(256비트 = 32바이트)입니다. 세 번째로, calculateDoubleHash 함수는 비트코인과 동일한 이중 해시를 구현합니다.

첫 번째 SHA-256의 결과를 다시 SHA-256에 넣어 보안을 강화합니다. 이는 길이 확장 공격(length extension attack) 같은 특정 암호학적 공격을 방지합니다.

실제 프로덕션 코드에서는 이중 해시를 사용하는 것이 권장됩니다. 여러분이 이 해시 계산을 사용하면 블록체인의 변조 감지가 자동으로 이루어집니다.

누군가 과거 블록을 수정하면 해당 블록의 해시가 바뀌고, 이는 다음 블록의 previousHash와 맞지 않아 체인이 끊어집니다. 또한 해시를 블록의 고유 ID로 사용하여 데이터베이스에 저장하거나 네트워크에서 블록을 요청할 때 활용할 수 있습니다.

실전 팁

💡 블록 데이터를 연결할 때 구분자(delimiter)를 넣지 않으면 "1,23" + "4"와 "12" + "34"가 같아지는 문제가 있으니, JSON.stringify()나 고정 길이 포맷을 사용하세요

💡 해시 계산은 CPU 집약적이므로, 프로덕션 환경에서는 Worker Thread나 WebAssembly를 활용한 최적화를 고려하세요

💡 해시를 비교할 때는 대소문자를 통일하세요 (일반적으로 소문자 hex 사용)

💡 TypeScript에서는 해시를 별도의 타입(type Hash = string)으로 정의하면 일반 문자열과 구분되어 타입 안정성이 높아집니다

💡 해시 함수를 의존성 주입 패턴으로 만들면 테스트 시 모의 해시 함수로 대체하여 결정론적 테스트가 가능합니다


5. 이전 블록 연결

시작하며

여러분이 블록들을 연결하여 체인을 만들 때 이런 의문을 가진 적 있나요? "단순히 배열에 블록을 추가하는 것과 무엇이 다르지?" 블록체인의 핵심은 바로 이 "체인" 부분에 있습니다.

일반 데이터베이스는 과거 레코드를 수정하거나 삭제할 수 있지만, 블록체인은 한 번 추가된 블록을 변경하면 이후 모든 블록이 무효화됩니다. 이것이 블록체인을 변조 불가능하게 만드는 핵심 메커니즘입니다.

바로 이럴 때 필요한 것이 previousHash를 통한 블록 연결입니다. 각 블록이 이전 블록의 해시를 저장함으로써, 도미노처럼 연쇄적인 무결성 검증이 가능해집니다.

개요

간단히 말해서, 이전 블록 연결은 현재 블록에 이전 블록의 해시를 저장하여 블록들을 암호학적으로 연결하는 메커니즘입니다. 이 연결 방식은 연결 리스트(Linked List)와 유사하지만, 포인터 대신 암호학적 해시를 사용한다는 점이 다릅니다.

만약 블록 N-1의 내용이 변경되면 그 해시가 바뀌고, 블록 N의 previousHash와 맞지 않게 되어 즉시 변조가 감지됩니다. 기존 연결 리스트에서는 포인터를 수정하여 중간 노드를 쉽게 교체할 수 있었다면, 블록체인에서는 한 블록을 수정하려면 이후 모든 블록을 다시 채굴해야 합니다.

핵심 특징은 첫째, 역방향 추적 가능(어떤 블록이든 제네시스 블록까지 거슬러 올라갈 수 있음), 둘째, 변조 감지 자동화(체인 검증만으로 모든 블록의 무결성 확인), 셋째, 분산 합의 기반(가장 긴 유효한 체인이 진실)입니다. 이러한 특징들이 신뢰할 수 있는 분산 원장을 만듭니다.

코드 예제

class Blockchain {
  private chain: Block[] = [];

  constructor() {
    // 제네시스 블록으로 체인 시작
    this.chain.push(this.createGenesisBlock());
  }

  private createGenesisBlock(): Block {
    return {
      index: 0,
      timestamp: Date.now(),
      data: "Genesis Block",
      previousHash: "0",
      hash: "genesis_hash",
      difficulty: 0,
      nonce: 0
    };
  }

  // 새 블록을 체인에 추가
  addBlock(data: string): Block {
    const previousBlock = this.getLatestBlock();
    const newBlock: Block = {
      index: previousBlock.index + 1,
      timestamp: Date.now(),
      data,
      previousHash: previousBlock.hash,  // 이전 블록의 해시를 저장
      hash: "",
      difficulty: 4,
      nonce: 0
    };

    // 새 블록의 해시 계산
    newBlock.hash = this.calculateHash(newBlock);
    this.chain.push(newBlock);
    return newBlock;
  }

  getLatestBlock(): Block {
    return this.chain[this.chain.length - 1];
  }

  // 체인의 무결성 검증
  isChainValid(): boolean {
    for (let i = 1; i < this.chain.length; i++) {
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      // 이전 블록과의 연결 확인
      if (currentBlock.previousHash !== previousBlock.hash) {
        return false;
      }
    }
    return true;
  }

  private calculateHash(block: Block): string {
    const data = `${block.index}${block.timestamp}${block.data}${block.previousHash}${block.difficulty}${block.nonce}`;
    return crypto.createHash('sha256').update(data).digest('hex');
  }
}

설명

이것이 하는 일: 이전 블록 연결은 각 블록이 이전 블록의 해시를 참조함으로써 블록들을 암호학적으로 묶어, 과거 데이터의 변조를 수학적으로 불가능하게 만듭니다. 첫 번째로, addBlock 메서드는 새 블록을 생성할 때 getLatestBlock()을 호출하여 체인의 마지막 블록을 가져옵니다.

이 마지막 블록의 hash 값을 새 블록의 previousHash에 저장합니다. 이렇게 하면 블록 N+1이 블록 N의 해시를 가지고 있어, N의 내용이 바뀌면 N+1부터 모든 후속 블록이 무효가 됩니다.

그 다음으로, isChainValid 메서드는 체인 전체를 순회하며 각 블록의 previousHash가 실제 이전 블록의 hash와 일치하는지 확인합니다. 만약 블록 50의 데이터를 누군가 변조했다면, 블록 50의 해시가 바뀌고, 블록 51의 previousHash와 맞지 않아 즉시 감지됩니다.

이 검증 로직은 O(n) 시간 복잡도로 전체 체인의 무결성을 보장합니다. 세 번째로, 이 연결 구조는 블록체인을 "append-only" 데이터 구조로 만듭니다.

새 블록은 끝에 추가할 수만 있고, 중간이나 과거 블록을 수정하려면 해당 블록부터 최신 블록까지 모든 블록을 다시 채굴해야 합니다. 네트워크의 51% 이상의 컴퓨팅 파워가 없다면 이는 사실상 불가능합니다.

여러분이 이 연결 메커니즘을 사용하면 중앙 권한 없이도 데이터의 무결성을 보장할 수 있습니다. 모든 참여자가 독립적으로 체인을 검증할 수 있고, 변조 시도는 즉시 발견됩니다.

또한 포크(fork) 상황에서도 가장 긴 유효한 체인을 선택하는 명확한 규칙이 생깁니다.

실전 팁

💡 체인 검증 시 매번 전체 블록을 검증하면 비효율적이므로, 마지막 검증 시점부터의 블록만 검증하는 증분 검증을 구현하세요

💡 previousHash를 readonly로 만들어 블록 생성 후 절대 변경할 수 없도록 타입 레벨에서 보장하세요

💡 체인이 분기(fork)되는 경우를 대비해 여러 체인을 관리하고 가장 긴 체인을 선택하는 로직을 추가하세요

💡 블록 검증 실패 시 정확히 어느 블록에서 문제가 발생했는지 로깅하면 디버깅이 훨씬 쉬워집니다

💡 대용량 블록체인의 경우 체크포인트(checkpoint) 기법을 사용하여 특정 블록 이전은 검증을 생략하고 성능을 개선할 수 있습니다


6. 타임스탬프 관리

시작하며

여러분이 블록체인에서 거래 순서를 증명해야 할 때 이런 문제를 겪어본 적 있나요? "두 거래 중 어느 것이 먼저 발생했는지 어떻게 증명하지?" 특히 이중 지불 공격을 방어하려면 시간 순서가 명확해야 합니다.

분산 시스템에서는 중앙 시계가 없기 때문에 시간 합의가 어렵습니다. 각 노드의 시계가 조금씩 다를 수 있고, 악의적인 노드는 의도적으로 시간을 조작할 수도 있습니다.

바로 이럴 때 필요한 것이 블록 타임스탬프입니다. 각 블록에 생성 시간을 기록하여 거래의 시간 순서를 확립하고, 네트워크 규칙을 통해 타임스탬프의 유효성을 검증합니다.

개요

간단히 말해서, 타임스탬프는 블록이 생성된 시점을 Unix 시간으로 기록하여 거래의 시간 순서를 증명하고 이중 지불을 방지합니다. 비트코인은 타임스탬프를 이용해 난이도 조정, 블록 보상 반감기 등의 시간 기반 규칙을 구현합니다.

또한 타임스탬프는 블록이 특정 시점 이전에 생성될 수 없었음을 증명하는 역할도 합니다. 기존 중앙화 시스템에서는 중앙 서버의 시계를 신뢰했다면, 블록체인에서는 타임스탬프 검증 규칙을 통해 분산된 시간 합의를 달성합니다.

핵심 특징은 첫째, 시간 순서 증명(어떤 거래가 먼저 발생했는지 확인), 둘째, 이중 지불 방지(같은 코인을 두 번 사용하는 것 차단), 셋째, 네트워크 규칙 시행(난이도 조정, 보상 변경 등)입니다. 이러한 특징들이 분산 시스템에서 시간의 신뢰성을 보장합니다.

코드 예제

class Block {
  public index: number;
  public timestamp: number;
  public data: string;
  public previousHash: string;
  public hash: string;
  public difficulty: number;
  public nonce: number;

  constructor(index: number, data: string, previousHash: string, difficulty: number) {
    this.index = index;
    this.timestamp = this.getCurrentTimestamp();  // 현재 시간 자동 기록
    this.data = data;
    this.previousHash = previousHash;
    this.difficulty = difficulty;
    this.nonce = 0;
    this.hash = this.calculateHash();
  }

  private getCurrentTimestamp(): number {
    // Unix timestamp (밀리초 단위)
    return Date.now();
  }

  private calculateHash(): string {
    const data = `${this.index}${this.timestamp}${this.data}${this.previousHash}${this.difficulty}${this.nonce}`;
    return crypto.createHash('sha256').update(data).digest('hex');
  }
}

// 타임스탬프 검증 함수
function isTimestampValid(newBlock: Block, previousBlock: Block): boolean {
  // 1. 이전 블록보다 나중에 생성되었는지 확인
  if (newBlock.timestamp <= previousBlock.timestamp) {
    return false;
  }

  // 2. 미래 시간이 아닌지 확인 (현재 시간 + 2시간 이내)
  const currentTime = Date.now();
  const maxAllowedTime = currentTime + 2 * 60 * 60 * 1000;  // 2시간 허용
  if (newBlock.timestamp > maxAllowedTime) {
    return false;
  }

  return true;
}

설명

이것이 하는 일: 타임스탬프는 블록이 생성된 정확한 시간을 기록하고, 검증 규칙을 통해 시간 조작을 방지하며, 블록체인 전체의 시간 순서를 확립합니다. 첫 번째로, getCurrentTimestamp 메서드는 Date.now()를 사용하여 Unix 타임스탬프를 밀리초 단위로 가져옵니다.

Unix 타임스탬프는 1970년 1월 1일 00:00:00 UTC부터 현재까지 경과한 시간을 나타냅니다. 밀리초 단위를 사용하면 같은 초 내에 생성된 블록도 구분할 수 있어 정밀도가 높습니다.

그 다음으로, isTimestampValid 함수는 두 가지 중요한 검증을 수행합니다. 첫째, 새 블록의 타임스탬프가 이전 블록보다 나중이어야 합니다.

이는 블록의 시간 순서를 보장하여 이중 지불 공격을 방지합니다. 둘째, 타임스탬프가 현재 시간보다 너무 미래가 아닌지 확인합니다.

비트코인은 2시간의 오차를 허용하는데, 이는 네트워크 지연과 시계 오차를 고려한 것입니다. 세 번째로, 타임스탬프는 블록 해시 계산에 포함되어 블록의 고유성을 보장합니다.

같은 트랜잭션 데이터라도 시간이 다르면 완전히 다른 해시가 생성됩니다. 또한 타임스탬프를 기반으로 난이도 조정 알고리즘이 작동합니다.

비트코인은 2016개 블록마다 평균 블록 생성 시간을 계산하여 난이도를 조정합니다. 여러분이 이 타임스탬프 시스템을 사용하면 중앙 시계 없이도 분산 환경에서 시간 합의를 달성할 수 있습니다.

블록의 시간 순서가 명확해져 "A가 B보다 먼저 발생했음"을 수학적으로 증명할 수 있습니다. 또한 감사(audit)와 규제 준수를 위한 타임스탬프 증거를 제공할 수 있습니다.

실전 팁

💡 타임스탬프를 초 단위로 저장하면 블록 크기를 줄일 수 있지만, 밀리초 단위가 더 정밀하므로 트레이드오프를 고려하세요

💡 NTP(Network Time Protocol) 서버와 동기화하여 노드의 시계 정확도를 높이면 타임스탬프 관련 문제를 줄일 수 있습니다

💡 타임스탬프 검증 실패 로그를 수집하면 악의적인 노드나 시계 오류를 가진 노드를 식별할 수 있습니다

💡 블록 생성 평균 시간을 모니터링하여 네트워크 상태와 난이도 조정의 적절성을 판단하세요

💡 타임스탬프를 Date 객체로 변환하여 표시할 때는 항상 UTC를 사용하여 타임존 문제를 방지하세요


7. 난이도 타겟 시스템

시작하며

여러분이 블록 생성 속도를 조절해야 할 때 이런 고민을 한 적 있나요? "컴퓨터 성능이 계속 좋아지는데 블록 생성 시간을 어떻게 일정하게 유지하지?" 블록이 너무 빨리 생성되면 네트워크 동기화 문제가 생기고, 너무 느리면 거래 확정 시간이 길어집니다.

비트코인은 약 10분마다 한 블록이 생성되도록 설계되었습니다. 하지만 채굴에 참여하는 컴퓨팅 파워는 계속 변하므로, 이를 보정하는 메커니즘이 필요합니다.

바로 이럴 때 필요한 것이 난이도 타겟 시스템입니다. 블록 해시가 특정 값보다 작아야만 유효한 블록으로 인정하고, 네트워크 해시레이트에 따라 이 타겟을 주기적으로 조정합니다.

개요

간단히 말해서, 난이도 타겟은 유효한 블록 해시가 만족해야 하는 조건으로, 해시 앞에 0이 몇 개 있어야 하는지를 정의하여 블록 생성 속도를 제어합니다. 비트코인은 2016개 블록(약 2주)마다 난이도를 재조정합니다.

최근 2주간 블록이 너무 빨리 생성되었다면 난이도를 올리고, 너무 느렸다면 난이도를 내립니다. 이를 통해 전체 네트워크 해시레이트가 변해도 평균 10분 간격을 유지합니다.

기존에는 고정된 난이도를 사용했다면, 이제는 동적 난이도 조정으로 네트워크 변화에 자동으로 적응합니다. 핵심 특징은 첫째, 자동 균형 조절(해시레이트 증가 → 난이도 증가), 둘째, 예측 가능한 블록 생성 시간, 셋째, 작업 증명의 공정성(더 많은 계산을 한 사람이 더 많은 블록 생성)입니다.

이러한 특징들이 안정적인 블록체인 네트워크를 만듭니다.

코드 예제

class ProofOfWork {
  private difficulty: number;
  private target: string;

  constructor(difficulty: number) {
    this.difficulty = difficulty;
    // 난이도에 따른 타겟 계산 (앞에 difficulty개의 0이 필요)
    this.target = '0'.repeat(difficulty);
  }

  // 유효한 해시를 찾을 때까지 nonce를 증가시키며 채굴
  mineBlock(block: Block): Block {
    let nonce = 0;
    let hash = '';

    console.log(`Mining block with difficulty ${this.difficulty}...`);
    const startTime = Date.now();

    while (true) {
      block.nonce = nonce;
      hash = this.calculateHash(block);

      // 해시가 타겟 조건을 만족하는지 확인
      if (hash.startsWith(this.target)) {
        const endTime = Date.now();
        console.log(`Block mined! Hash: ${hash}`);
        console.log(`Nonce: ${nonce}, Time: ${(endTime - startTime) / 1000}s`);
        block.hash = hash;
        return block;
      }

      nonce++;
    }
  }

  private calculateHash(block: Block): string {
    const data = `${block.index}${block.timestamp}${block.data}${block.previousHash}${block.difficulty}${block.nonce}`;
    return crypto.createHash('sha256').update(data).digest('hex');
  }

  // 난이도 조정 함수
  static adjustDifficulty(blockchain: Block[], difficulty: number): number {
    const BLOCK_GENERATION_INTERVAL = 10;  // 목표: 10초마다 1블록
    const DIFFICULTY_ADJUSTMENT_INTERVAL = 10;  // 10블록마다 난이도 조정

    const lastBlock = blockchain[blockchain.length - 1];

    // 조정 주기가 아니면 기존 난이도 유지
    if (lastBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0) {
      return difficulty;
    }

    // 최근 10블록의 생성 시간 계산
    const prevAdjustmentBlock = blockchain[blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL];
    const timeExpected = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL;
    const timeTaken = (lastBlock.timestamp - prevAdjustmentBlock.timestamp) / 1000;

    // 너무 빠르면 난이도 증가, 너무 느리면 감소
    if (timeTaken < timeExpected / 2) {
      return difficulty + 1;
    } else if (timeTaken > timeExpected * 2) {
      return Math.max(0, difficulty - 1);
    }

    return difficulty;
  }
}

설명

이것이 하는 일: 난이도 타겟 시스템은 유효한 블록 해시의 조건을 설정하고, 채굴자가 이 조건을 만족하는 nonce를 찾도록 강제하며, 네트워크 상태에 따라 난이도를 자동 조정합니다. 첫 번째로, target 변수는 유효한 해시가 만족해야 하는 패턴을 정의합니다.

difficulty가 4라면 target은 "0000"이 되고, 해시가 "0000abc..."처럼 시작해야만 유효합니다. 난이도가 1 증가할 때마다 유효한 해시를 찾을 확률이 1/16로 줄어들어, 평균 채굴 시간이 16배 증가합니다.

비트코인의 현재 난이도는 약 20개의 0을 요구하며, 이는 천문학적인 계산량을 의미합니다. 그 다음으로, mineBlock 함수는 브루트 포스 방식으로 유효한 nonce를 찾습니다.

nonce를 0부터 시작하여 1씩 증가시키며 매번 해시를 계산하고, startsWith(this.target)으로 조건을 만족하는지 확인합니다. 평균적으로 16^difficulty번의 해시 계산이 필요하므로, 난이도가 높을수록 채굴 시간이 기하급수적으로 증가합니다.

세 번째로, adjustDifficulty 함수는 최근 블록들의 생성 시간을 분석하여 난이도를 조정합니다. 목표 시간의 절반보다 빠르면 난이도를 1 증가시키고, 2배보다 느리면 1 감소시킵니다.

이러한 점진적 조정 방식은 급격한 난이도 변화를 방지하여 네트워크를 안정화합니다. 비트코인은 최대 4배까지만 난이도를 변경할 수 있도록 제한합니다.

여러분이 이 난이도 시스템을 사용하면 네트워크 해시레이트가 10배 증가해도 블록 생성 시간이 일정하게 유지됩니다. 이는 거래 확정 시간을 예측 가능하게 만들고, 네트워크 동기화 문제를 방지합니다.

또한 악의적인 공격자가 블록을 빠르게 생성하여 네트워크를 혼란시키는 것도 방지합니다.

실전 팁

💡 난이도 조정 알고리즘을 테스트할 때는 BLOCK_GENERATION_INTERVAL을 1초로 설정하여 빠르게 여러 시나리오를 실험하세요

💡 채굴 중 Ctrl+C로 중단할 수 있도록 process.on('SIGINT') 핸들러를 추가하여 사용자 경험을 개선하세요

💡 난이도가 너무 낮으면 (0 또는 1) 보안이 약해지므로 최소 난이도를 설정하세요 (예: 최소 2)

💡 채굴 성능을 측정하여 해시레이트(hashes per second)를 계산하면 네트워크 보안 강도를 정량화할 수 있습니다

💡 프로덕션 환경에서는 Worker Thread를 사용하여 채굴을 백그라운드에서 실행하고 메인 스레드가 블록되지 않도록 하세요


8. Nonce 값 설계

시작하며

여러분이 작업 증명을 구현할 때 이런 의문을 가진 적 있나요? "블록의 다른 모든 데이터는 고정되어 있는데, 어떻게 해시를 변경하지?" 블록의 인덱스, 타임스탬프, 트랜잭션은 정해져 있으므로 해시도 고정될 것 같지만, 채굴은 계속 다른 해시를 시도해야 합니다.

작업 증명의 핵심은 난이도 조건을 만족하는 해시를 찾는 것입니다. 하지만 해시 함수는 결정론적이므로, 입력이 같으면 출력도 항상 같습니다.

그렇다면 어떻게 다른 해시를 계속 생성할까요? 바로 이럴 때 필요한 것이 nonce 값입니다.

블록에 추가적인 가변 필드를 넣어, 이 값을 변경하며 조건을 만족하는 해시를 찾습니다. "Number used ONCE"의 약자인 nonce는 단 한 번만 사용되는 숫자입니다.

개요

간단히 말해서, nonce는 블록 해시를 변경하기 위한 임의의 숫자로, 채굴자가 유효한 해시를 찾을 때까지 계속 증가시키는 변수입니다. 채굴 과정은 본질적으로 "nonce를 0부터 시작하여 증가시키며, 블록 해시가 난이도 조건을 만족할 때까지 반복"하는 것입니다.

비트코인에서 nonce는 32비트 정수이므로 최대 약 43억까지 가능하지만, 난이도가 높으면 이 범위로도 부족할 수 있습니다. 기존에는 블록 데이터만으로 해시가 결정되었다면, nonce를 추가하여 같은 블록 데이터로도 수십억 개의 다른 해시를 생성할 수 있습니다.

핵심 특징은 첫째, 작업 증명의 변수(실제로 계산 작업을 수행했음을 증명), 둘째, 검증 용이성(한 번의 해시 계산으로 검증 가능), 셋째, 무작위성(어떤 nonce가 성공할지 예측 불가)입니다. 이러한 특징들이 공정하고 안전한 채굴 메커니즘을 만듭니다.

코드 예제

class Block {
  public nonce: number = 0;

  // ... 다른 필드들

  // 작업 증명: 유효한 해시를 찾을 때까지 nonce 증가
  mineBlock(difficulty: number): void {
    const target = '0'.repeat(difficulty);

    console.log(`Mining block ${this.index}...`);
    const startTime = Date.now();
    let hashCount = 0;

    while (!this.hash.startsWith(target)) {
      this.nonce++;
      this.hash = this.calculateHash();
      hashCount++;

      // 진행 상황 표시 (10만 번마다)
      if (hashCount % 100000 === 0) {
        console.log(`Tried ${hashCount} hashes, current nonce: ${this.nonce}`);
      }
    }

    const endTime = Date.now();
    const elapsedTime = (endTime - startTime) / 1000;
    const hashRate = Math.round(hashCount / elapsedTime);

    console.log(`✓ Block mined!`);
    console.log(`  Hash: ${this.hash}`);
    console.log(`  Nonce: ${this.nonce}`);
    console.log(`  Hashes tried: ${hashCount.toLocaleString()}`);
    console.log(`  Time: ${elapsedTime}s`);
    console.log(`  Hash rate: ${hashRate.toLocaleString()} h/s`);
  }

  private calculateHash(): string {
    // nonce를 포함하여 해시 계산
    const data = `${this.index}${this.previousHash}${this.timestamp}${JSON.stringify(this.data)}${this.nonce}`;
    return crypto.createHash('sha256').update(data).digest('hex');
  }
}

// nonce 검증 함수
function verifyProofOfWork(block: Block, difficulty: number): boolean {
  const target = '0'.repeat(difficulty);

  // 블록의 해시를 재계산하여 nonce가 올바른지 확인
  const calculatedHash = calculateBlockHash(block);

  // 1. 블록에 저장된 해시와 재계산한 해시가 일치하는가?
  if (calculatedHash !== block.hash) {
    return false;
  }

  // 2. 해시가 난이도 조건을 만족하는가?
  if (!calculatedHash.startsWith(target)) {
    return false;
  }

  return true;
}

설명

이것이 하는 일: Nonce는 블록 해시의 가변 요소로서, 채굴자가 난이도 조건을 만족하는 해시를 찾기 위해 조작하는 유일한 변수이며, 동시에 작업 증명의 핵심 증거입니다. 첫 번째로, mineBlock 메서드는 nonce를 0부터 시작하여 1씩 증가시킵니다.

매번 nonce를 변경하고 calculateHash()를 호출하여 새로운 해시를 계산합니다. 해시 함수의 특성상 nonce가 1만 바뀌어도 해시는 완전히 달라지므로, 각 시도는 독립적인 확률 실험입니다.

난이도가 4라면 평균 16^4 = 65,536번의 시도가 필요합니다. 그 다음으로, 진행 상황 표시 로직은 사용자 경험을 개선합니다.

10만 번마다 현재 시도 횟수와 nonce 값을 출력하여 채굴이 진행 중임을 보여줍니다. 또한 최종적으로 해시레이트(초당 해시 계산 횟수)를 계산하여 채굴 성능을 정량화합니다.

이는 네트워크 보안 강도를 측정하는 중요한 지표입니다. 세 번째로, verifyProofOfWork 함수는 채굴 결과를 검증합니다.

블록에 저장된 nonce를 사용하여 해시를 재계산하고, 이것이 블록의 hash 필드와 일치하며 난이도 조건을 만족하는지 확인합니다. 검증은 단 한 번의 해시 계산으로 즉시 완료되는 반면, 채굴은 평균 16^difficulty번의 계산이 필요합니다.

이러한 비대칭성이 작업 증명의 핵심입니다. 여러분이 이 nonce 시스템을 사용하면 채굴 경쟁이 공정해집니다.

아무도 미리 정답을 알 수 없고, 더 많은 해시 계산을 수행한 채굴자가 블록을 찾을 확률이 높아집니다. 이는 컴퓨팅 자원을 투자한 만큼 보상받는 공정한 시스템을 만듭니다.

또한 51% 공격의 비용을 극도로 높여 네트워크 보안을 강화합니다.

실전 팁

💡 32비트 nonce로 부족한 경우를 대비해 extraNonce 필드를 추가하거나 타임스탬프를 1초씩 증가시키는 전략을 사용하세요

💡 멀티 스레드 채굴을 구현할 때는 각 스레드가 다른 nonce 범위를 탐색하도록 분할하여 중복 작업을 방지하세요 (스레드 1: 0-1억, 스레드 2: 1억-2억...)

💡 채굴 중 새로운 블록이 네트워크에 전파되면 즉시 중단하고 새 블록을 기반으로 채굴을 시작하여 자원 낭비를 줄이세요

💡 GPU나 ASIC 채굴을 위해서는 WebAssembly나 네이티브 모듈을 사용하여 해시 계산을 최적화할 수 있습니다

💡 nonce를 BigInt로 구현하면 32비트 제한을 넘어설 수 있지만, 대부분의 경우 타임스탬프를 조정하는 것이 더 효율적입니다

이상으로 "타입스크립트로 비트코인 클론하기 3편 - 블록 데이터 구조 설계하기"에 대한 코드 카드 뉴스를 완성했습니다. 블록체인의 핵심 데이터 구조를 타입스크립트로 설계하는 방법을 배우셨기를 바랍니다!


#TypeScript#Blockchain#DataStructure#Bitcoin#CryptoHash

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.