이미지 로딩 중...

타입스크립트로 비트코인 클론하기 5편 블록 생성 로직 구현하기 - 슬라이드 1/8
A

AI Generated

2025. 11. 9. · 2 Views

타입스크립트로 비트코인 클론하기 5편 블록 생성 로직 구현하기

블록체인의 핵심인 블록 생성 로직을 타입스크립트로 직접 구현해봅니다. 블록 구조 설계부터 해시 계산, 블록 검증까지 실제 비트코인의 동작 원리를 코드로 이해할 수 있습니다.


목차

  1. Block 클래스
  2. 해시 계산 함수
  3. 제네시스 블록
  4. 새로운 블록 생성
  5. 블록 검증 로직
  6. Timestamp 관리
  7. 블록체인 클래스

1. Block 클래스

시작하며

여러분이 블록체인을 처음 공부할 때 가장 먼저 궁금한 것이 무엇인가요? 바로 "블록이 어떻게 생겼는지"일 겁니다.

블록체인은 이름 그대로 블록들이 체인처럼 연결된 구조인데, 그렇다면 각 블록은 어떤 데이터를 담고 있을까요? 실제 비트코인에서 블록은 매우 정교한 구조를 가지고 있습니다.

블록 헤더에는 이전 블록의 해시, 타임스탬프, 난이도, 논스 등이 포함되고, 블록 바디에는 트랜잭션 데이터가 들어갑니다. 이런 구조 덕분에 블록체인은 변조가 불가능한 특성을 갖게 됩니다.

바로 이럴 때 필요한 것이 Block 클래스입니다. 타입스크립트의 클래스를 사용하면 블록의 구조를 명확하게 정의하고, 타입 안정성을 보장하면서 블록을 생성할 수 있습니다.

개요

간단히 말해서, Block 클래스는 블록체인의 각 블록을 표현하는 데이터 구조입니다. 실무에서 블록체인을 개발할 때 가장 먼저 해야 할 일은 블록의 구조를 정의하는 것입니다.

블록에 어떤 데이터가 들어갈지, 각 필드의 타입은 무엇인지 명확히 해야 나중에 블록을 생성하고 검증하는 로직을 작성할 수 있습니다. 예를 들어, 암호화폐 거래소에서 자체 블록체인을 구축하거나, 공급망 관리 시스템에 블록체인을 도입할 때 반드시 거쳐야 하는 단계입니다.

기존에는 자바스크립트의 객체 리터럴로 블록을 표현했다면, 타입스크립트의 클래스를 사용하면 컴파일 타임에 타입 오류를 잡을 수 있고, IDE의 자동완성 기능도 활용할 수 있습니다. Block 클래스의 핵심 특징은 불변성(Immutability)과 연결성(Linkability)입니다.

한번 생성된 블록의 데이터는 변경되지 않아야 하고, 각 블록은 이전 블록의 해시를 참조하여 체인을 형성합니다. 이러한 특징들이 블록체인의 보안성과 투명성을 보장하는 핵심 요소입니다.

코드 예제

// Block.ts - 블록의 기본 구조를 정의하는 클래스
class Block {
  // 블록의 고유 식별자 - 블록 내용을 기반으로 계산된 해시값
  public hash: string;

  constructor(
    public index: number,          // 블록체인에서의 순서 (0부터 시작)
    public previousHash: string,   // 이전 블록의 해시 (체인 연결의 핵심)
    public timestamp: number,      // 블록 생성 시간 (Unix timestamp)
    public data: string,           // 블록에 저장할 실제 데이터 (트랜잭션 등)
  ) {
    // 블록 생성 시 자동으로 해시 계산
    this.hash = this.calculateHash();
  }

  // 블록의 해시를 계산하는 메서드 (나중에 구현)
  calculateHash(): string {
    return '';  // 임시 반환값
  }
}

설명

이것이 하는 일: Block 클래스는 블록체인의 개별 블록을 객체로 표현하고, 블록에 필요한 모든 정보를 타입 안전하게 관리합니다. 첫 번째로, constructor에서 블록의 기본 정보를 받아 초기화합니다.

index는 블록체인에서 몇 번째 블록인지를 나타내는 순서값이고, previousHash는 바로 앞 블록의 해시값입니다. 이 previousHash가 블록들을 체인처럼 연결하는 핵심 요소입니다.

왜 이렇게 하는지 궁금하시죠? 만약 누군가 과거의 블록을 변조하려고 하면, 그 블록의 해시값이 바뀌고, 다음 블록의 previousHash와 일치하지 않게 되어 변조를 즉시 감지할 수 있기 때문입니다.

그 다음으로, timestamp와 data 필드가 초기화됩니다. timestamp는 블록이 생성된 정확한 시간을 Unix timestamp(1970년 1월 1일부터의 밀리초)로 저장합니다.

이는 나중에 블록의 생성 순서를 검증하거나, 특정 시간대의 트랜잭션을 조회할 때 유용합니다. data 필드는 실제로 블록에 저장하고 싶은 데이터를 담습니다.

비트코인에서는 여기에 거래 정보가 들어가지만, 우리는 간단히 문자열로 시작합니다. 마지막으로, constructor 내부에서 calculateHash() 메서드를 호출하여 블록의 고유한 해시값을 생성합니다.

이 해시는 블록의 모든 데이터(index, previousHash, timestamp, data)를 입력값으로 하여 계산되므로, 블록의 내용이 조금이라도 바뀌면 완전히 다른 해시값이 나옵니다. 이것이 블록체인의 무결성을 보장하는 핵심 메커니즘입니다.

여러분이 이 코드를 사용하면 타입 안정성을 보장받으면서 블록을 생성할 수 있고, IDE의 자동완성과 타입 체크 기능을 활용할 수 있습니다. 실무에서는 필드가 잘못 할당되거나 누락되는 실수를 컴파일 단계에서 방지할 수 있어 개발 생산성이 크게 향상됩니다.

실전 팁

💡 블록의 모든 필드를 public readonly로 선언하여 불변성을 강제하세요. 한번 생성된 블록은 절대 수정되어서는 안 됩니다.

💡 큰 데이터를 저장할 때는 data 필드에 원본을 넣지 말고, 데이터의 해시만 저장하세요. 원본은 별도의 데이터베이스에 보관하는 것이 효율적입니다.

💡 Block 클래스에 isValid() 메서드를 추가하여 블록 자체의 유효성을 검증할 수 있게 만드세요. 해시가 올바르게 계산되었는지 확인하는 로직을 넣으면 좋습니다.

💡 타입스크립트의 interface를 먼저 정의하고 class가 이를 구현하도록 하면, 나중에 다른 블록 구현체로 교체하기 쉽습니다.

💡 실제 프로덕션에서는 data 필드의 타입을 string이 아닌 제네릭 타입으로 만들어서, 어떤 종류의 데이터든 저장할 수 있게 확장하세요.


2. 해시 계산 함수

시작하며

여러분이 블록체인 코드를 보다가 SHA256, 해시 함수 같은 용어를 접했을 때 "도대체 이게 뭐지?"라고 생각해본 적 있나요? 블록체인에서 해시는 단순한 암호화 도구가 아니라, 전체 시스템의 보안과 무결성을 책임지는 핵심 기술입니다.

실제로 비트코인은 SHA256 해시 함수를 사용하여 블록의 고유한 지문(fingerprint)을 만듭니다. 이 지문은 블록의 내용이 조금이라도 바뀌면 완전히 다른 값이 나오므로, 누군가 과거의 블록을 몰래 수정했는지 즉시 알 수 있습니다.

예를 들어, "Hello"와 "Hello."처럼 점 하나만 추가해도 해시값은 완전히 달라집니다. 바로 이럴 때 필요한 것이 calculateHash 함수입니다.

블록의 모든 데이터를 하나의 문자열로 결합하고, 이를 SHA256으로 해싱하여 블록의 고유한 식별자를 만들어냅니다.

개요

간단히 말해서, calculateHash는 블록의 모든 정보를 입력받아 고유한 해시값을 계산하는 함수입니다. 실무에서 해시 함수는 블록체인뿐만 아니라 비밀번호 저장, 파일 무결성 검증, 디지털 서명 등 다양한 곳에 사용됩니다.

블록체인에서는 특히 블록의 변조 여부를 확인하고, 작업 증명(Proof of Work)을 구현하는 데 필수적입니다. 예를 들어, 대용량 파일의 무결성을 검증할 때 전체 파일을 비교하는 대신 해시값만 비교하면 되므로 매우 효율적입니다.

기존에는 복잡한 암호화 라이브러리를 직접 다뤄야 했다면, 이제는 crypto-js 같은 검증된 라이브러리를 사용하여 간단하게 SHA256 해싱을 구현할 수 있습니다. 해시 함수의 핵심 특징은 일방향성(One-way)과 충돌 저항성(Collision Resistance)입니다.

해시값으로부터 원본을 역산할 수 없고, 서로 다른 입력이 같은 해시값을 만들 가능성이 극히 낮습니다. 이러한 특징들이 블록체인의 보안을 보장합니다.

코드 예제

// crypto-js 라이브러리에서 SHA256 함수 가져오기
import * as CryptoJS from 'crypto-js';

class Block {
  public hash: string;

  constructor(
    public index: number,
    public previousHash: string,
    public timestamp: number,
    public data: string,
  ) {
    this.hash = this.calculateHash();
  }

  // 블록의 모든 데이터를 조합하여 해시 계산
  calculateHash(): string {
    // index, previousHash, timestamp, data를 하나의 문자열로 결합
    const blockData = this.index + this.previousHash + this.timestamp + this.data;

    // SHA256 해시 함수로 해시값 생성하여 문자열로 반환
    return CryptoJS.SHA256(blockData).toString();
  }
}

설명

이것이 하는 일: calculateHash 함수는 블록의 모든 필드값을 하나로 결합한 후, SHA256 알고리즘으로 해싱하여 64자리 16진수 문자열을 반환합니다. 첫 번째로, 블록의 모든 데이터를 하나의 문자열로 결합합니다.

index, previousHash, timestamp, data를 순서대로 연결하는데, 이때 순서가 매우 중요합니다. 왜냐하면 "AB" + "CD"와 "A" + "BCD"는 모두 "ABCD"가 되어 같은 해시값을 만들기 때문입니다.

따라서 실무에서는 구분자를 추가하거나 JSON.stringify를 사용하는 것이 더 안전합니다. 그 다음으로, CryptoJS.SHA256() 함수에 결합된 문자열을 전달합니다.

SHA256은 어떤 길이의 입력이든 항상 256비트(32바이트)의 고정된 길이 출력을 생성하는 암호화 해시 함수입니다. 내부적으로 복잡한 수학적 연산을 수행하여, 입력의 단 1비트만 바뀌어도 출력의 절반 이상이 변하도록 설계되었습니다.

이를 "눈사태 효과(Avalanche Effect)"라고 부릅니다. 마지막으로, .toString() 메서드로 해시 객체를 16진수 문자열로 변환합니다.

결과는 "a5f8d..." 같은 형태의 64자리 문자열이 되며, 이것이 바로 블록의 고유한 지문이 됩니다. 이 해시값은 다음 블록의 previousHash로 사용되어 블록들을 체인처럼 연결합니다.

여러분이 이 코드를 사용하면 블록의 무결성을 수학적으로 보장할 수 있고, 변조 시도를 즉시 감지할 수 있습니다. 실무에서는 이 해시값을 데이터베이스 인덱스로 사용하거나, API 응답의 ETag로 활용하여 캐싱 최적화에도 사용할 수 있습니다.

실전 팁

💡 블록 데이터를 결합할 때는 JSON.stringify()를 사용하세요. 객체 필드의 순서나 타입 변환 문제를 방지할 수 있습니다.

💡 해시 계산은 CPU를 많이 사용하므로, 같은 블록의 해시를 여러 번 계산하지 말고 처음 한 번만 계산하여 캐시하세요.

💡 crypto-js 대신 Node.js의 내장 crypto 모듈을 사용하면 성능이 더 좋습니다. 하지만 브라우저 환경에서는 crypto-js가 더 호환성이 좋습니다.

💡 해시값을 데이터베이스에 저장할 때는 VARCHAR(64)로 충분합니다. SHA256은 항상 64자리 16진수 문자열을 생성합니다.

💡 개발 중에는 해시값의 앞 8자리만 로그에 출력하세요. 전체를 출력하면 로그가 너무 길어져 가독성이 떨어집니다.


3. 제네시스 블록

시작하며

여러분이 블록체인을 시작할 때 가장 먼저 마주치는 질문이 있습니다. "첫 번째 블록은 어떻게 만들지?" 모든 블록은 이전 블록의 해시를 참조해야 하는데, 첫 블록은 참조할 이전 블록이 없습니다.

이것은 "닭이 먼저냐 달걀이 먼저냐" 같은 순환 참조 문제입니다. 실제 비트코인 블록체인에서 첫 번째 블록은 2009년 1월 3일에 사토시 나카모토가 직접 생성했습니다.

이 블록에는 "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"라는 메시지가 담겨있어, 블록체인 생성 시점을 증명합니다. 이 특별한 첫 블록을 "제네시스 블록(Genesis Block)" 또는 "블록 0"이라고 부릅니다.

바로 이럴 때 필요한 것이 createGenesisBlock 함수입니다. 이전 블록이 없는 특수한 상황을 처리하여 블록체인의 시작점을 만들어줍니다.

개요

간단히 말해서, 제네시스 블록은 블록체인의 첫 번째 블록으로, 이전 블록에 대한 참조 없이 수동으로 생성되는 특수한 블록입니다. 모든 블록체인은 제네시스 블록으로 시작합니다.

이 블록은 체인의 근원이 되며, 네트워크의 모든 노드가 동일한 제네시스 블록을 공유해야 합니다. 만약 제네시스 블록이 다르면 완전히 다른 블록체인이 되는 것입니다.

예를 들어, 프라이빗 블록체인을 구축할 때 제네시스 블록에 회사의 설립 날짜나 창립 정신 같은 의미 있는 데이터를 넣어 블록체인의 정체성을 부여하기도 합니다. 기존에는 제네시스 블록도 일반 블록처럼 생성하려다가 previousHash를 어떻게 처리할지 고민했다면, 이제는 명시적으로 previousHash를 "0"이나 빈 문자열로 설정하여 이것이 첫 블록임을 명확히 표시합니다.

제네시스 블록의 핵심 특징은 유일성(Uniqueness)과 불변성(Immutability)입니다. 모든 블록체인 네트워크에서 제네시스 블록은 단 하나만 존재하며, 절대 변경되거나 삭제될 수 없습니다.

이것이 블록체인의 신뢰성을 보장하는 시작점입니다.

코드 예제

class Blockchain {
  // 블록들을 저장하는 배열
  public chain: Block[];

  constructor() {
    // 블록체인 초기화 시 제네시스 블록부터 생성
    this.chain = [this.createGenesisBlock()];
  }

  // 제네시스 블록을 생성하는 함수
  createGenesisBlock(): Block {
    // index: 0 (첫 번째 블록)
    // previousHash: "0" (이전 블록 없음을 표시)
    // timestamp: 0 (또는 특정 날짜의 timestamp)
    // data: 의미 있는 메시지 삽입
    return new Block(0, "0", 0, "Genesis Block");
  }
}

설명

이것이 하는 일: createGenesisBlock 함수는 블록체인의 첫 번째 블록을 특수한 매개변수로 생성하여, 체인의 시작점을 만듭니다. 첫 번째로, index를 0으로 설정합니다.

블록체인에서 블록의 순서는 0부터 시작하며, 제네시스 블록은 항상 인덱스 0을 갖습니다. 이는 프로그래밍 언어의 배열 인덱스가 0부터 시작하는 것과 같은 관례입니다.

나중에 특정 높이(height)의 블록을 조회할 때 인덱스를 직접 사용할 수 있어 편리합니다. 그 다음으로, previousHash를 "0"으로 설정합니다.

이것이 제네시스 블록의 가장 중요한 특징입니다. 일반 블록들은 실제 이전 블록의 해시값을 previousHash로 가지지만, 제네시스 블록은 참조할 이전 블록이 없으므로 관례적으로 "0"이나 "0000...0" 같은 특수값을 사용합니다.

일부 블록체인에서는 빈 문자열("")을 사용하기도 합니다. 세 번째로, timestamp를 설정합니다.

여기서는 간단히 0으로 했지만, 실제로는 블록체인이 시작된 의미 있는 시점의 timestamp를 사용하는 것이 좋습니다. 예를 들어, new Date('2024-01-01').getTime() 같은 방식으로 프로젝트 시작일을 기록할 수 있습니다.

비트코인의 제네시스 블록은 2009년 1월 3일의 timestamp를 가지고 있습니다. 마지막으로, data 필드에 "Genesis Block"이라는 메시지를 넣습니다.

실무에서는 여기에 블록체인의 목적이나 의미를 담은 메시지, 회사명, 프로젝트 버전 등을 넣어 블록체인의 정체성을 부여합니다. 비트코인의 제네시스 블록에는 당시 신문 헤드라인이 담겨있어, 블록 생성 시점을 증명하는 타임스탬프 역할을 합니다.

여러분이 이 코드를 사용하면 블록체인을 안전하게 초기화할 수 있고, 모든 노드가 동일한 시작점에서 출발하도록 보장할 수 있습니다. 실무에서는 제네시스 블록의 해시값을 설정 파일에 저장해두고, 새로운 노드가 네트워크에 참여할 때 제네시스 블록을 검증하여 올바른 네트워크인지 확인합니다.

실전 팁

💡 제네시스 블록의 timestamp에는 프로젝트의 의미 있는 날짜를 사용하세요. 나중에 블록체인의 역사를 추적할 때 유용합니다.

💡 제네시스 블록의 data에는 블록체인 네트워크의 버전 정보를 포함하세요. 하드포크 시 네트워크를 구분하는 데 도움이 됩니다.

💡 프로덕션 환경에서는 제네시스 블록을 환경변수나 설정 파일에서 읽어오도록 하세요. 테스트넷과 메인넷의 제네시스 블록을 쉽게 구분할 수 있습니다.

💡 제네시스 블록도 해시가 계산되므로, 이 해시값을 문서화해두세요. 네트워크 참여자들이 올바른 체인에 있는지 검증할 때 사용됩니다.

💡 제네시스 블록 생성 함수는 한 번만 호출되어야 하므로, Blockchain 클래스의 constructor에서만 호출되도록 접근 제어를 설정하세요.


4. 새로운 블록 생성

시작하며

여러분이 블록체인 애플리케이션을 만들 때 사용자가 새로운 데이터를 추가하려고 할 때마다 어떻게 해야 할까요? 예를 들어, 암호화폐 거래가 발생하거나, 공급망에서 제품이 이동하거나, 투표 시스템에서 새로운 표가 기록될 때 말이죠.

실제 블록체인 네트워크에서는 새로운 트랜잭션이 발생하면 이를 블록에 담아 체인에 추가합니다. 이때 가장 중요한 것은 새 블록이 기존 체인과 올바르게 연결되어야 한다는 점입니다.

만약 연결이 끊어지면 블록체인의 무결성이 깨지고, 데이터의 신뢰성을 보장할 수 없습니다. 바로 이럴 때 필요한 것이 addBlock 함수입니다.

새로운 데이터를 받아서 체인의 마지막 블록과 안전하게 연결된 새 블록을 생성하고 추가합니다.

개요

간단히 말해서, addBlock 함수는 새로운 데이터를 블록으로 만들어 블록체인의 끝에 추가하는 함수입니다. 블록체인에 데이터를 추가하는 것은 단순히 배열에 요소를 push하는 것과 다릅니다.

새 블록은 반드시 올바른 index, 이전 블록의 해시, 현재 시간을 가져야 하며, 이 모든 정보가 정확해야 블록체인의 무결성이 유지됩니다. 예를 들어, 의료 기록 블록체인에서 환자의 새로운 진료 기록을 추가하거나, 부동산 등기 블록체인에서 소유권 이전 기록을 추가할 때 이 함수가 사용됩니다.

기존에는 블록을 수동으로 생성하고 인덱스와 해시를 직접 계산해야 했다면, 이제는 addBlock 함수가 모든 것을 자동으로 처리하여 개발자가 데이터에만 집중할 수 있게 해줍니다. addBlock 함수의 핵심 특징은 자동화(Automation)와 일관성(Consistency)입니다.

개발자는 추가할 데이터만 제공하면 되고, 함수가 알아서 올바른 index, previousHash, timestamp를 설정합니다. 이를 통해 실수로 잘못된 블록이 추가되는 것을 방지합니다.

코드 예제

class Blockchain {
  public chain: Block[];

  constructor() {
    this.chain = [this.createGenesisBlock()];
  }

  createGenesisBlock(): Block {
    return new Block(0, "0", 0, "Genesis Block");
  }

  // 체인의 마지막 블록을 반환하는 헬퍼 함수
  getLatestBlock(): Block {
    return this.chain[this.chain.length - 1];
  }

  // 새로운 블록을 생성하고 체인에 추가
  addBlock(data: string): void {
    const previousBlock = this.getLatestBlock();
    const newIndex = previousBlock.index + 1;
    const newTimestamp = Date.now();  // 현재 시간 (밀리초)

    // 새 블록 생성
    const newBlock = new Block(
      newIndex,
      previousBlock.hash,  // 이전 블록의 해시로 체인 연결
      newTimestamp,
      data
    );

    // 체인에 추가
    this.chain.push(newBlock);
  }
}

설명

이것이 하는 일: addBlock 함수는 새로운 데이터를 입력받아, 체인의 마지막 블록과 올바르게 연결된 새 블록을 생성하고 배열에 추가합니다. 첫 번째로, getLatestBlock()을 호출하여 체인의 마지막 블록을 가져옵니다.

이 블록의 정보가 새 블록을 만드는 데 필요한 모든 것을 제공합니다. 특히 previousBlock.hash가 중요한데, 이것이 새 블록의 previousHash가 되어 블록들을 체인처럼 연결합니다.

배열의 마지막 요소를 가져오는 것은 O(1) 시간 복잡도로 매우 빠릅니다. 그 다음으로, 새 블록의 index를 계산합니다.

이전 블록의 index에 1을 더하면 되는데, 이렇게 하면 블록의 순서가 자동으로 관리됩니다. 예를 들어, 체인에 블록이 5개 있다면 (0, 1, 2, 3, 4), 다음 블록의 index는 5가 됩니다.

또한 Date.now()로 현재 시간을 밀리초 단위의 Unix timestamp로 가져옵니다. 이는 블록이 정확히 언제 생성되었는지 기록하며, 나중에 블록의 생성 순서를 검증하는 데 사용됩니다.

세 번째로, 계산된 정보들로 새 Block 객체를 생성합니다. newIndex는 블록의 위치를, previousBlock.hash는 이전 블록과의 연결을, newTimestamp는 생성 시점을, data는 실제 저장할 정보를 담습니다.

Block 클래스의 constructor가 호출되면서 자동으로 이 블록의 해시도 계산됩니다. 마지막으로, this.chain.push(newBlock)으로 새 블록을 체인 배열의 끝에 추가합니다.

이 시점부터 새 블록은 블록체인의 일부가 되며, 다음 블록이 추가될 때는 이 블록이 previousBlock이 됩니다. 이렇게 블록들이 계속 연결되면서 체인이 성장합니다.

여러분이 이 코드를 사용하면 블록 추가 과정에서 발생할 수 있는 인적 오류를 제거할 수 있고, 항상 일관된 방식으로 블록체인이 성장하도록 보장할 수 있습니다. 실무에서는 addBlock 호출 전에 데이터 유효성 검사를 추가하거나, 블록 추가 후 이벤트를 발생시켜 다른 시스템에 알릴 수 있습니다.

실전 팁

💡 addBlock 함수에 블록 검증 로직을 추가하세요. 새 블록이 유효한지 확인한 후에만 체인에 추가하는 것이 안전합니다.

💡 대량의 블록을 추가할 때는 배치 처리를 고려하세요. 한 번에 여러 트랜잭션을 모아서 하나의 블록으로 만들면 효율적입니다.

💡 addBlock이 성공했는지 실패했는지 반환값으로 알려주세요. boolean이나 에러 객체를 반환하여 호출자가 적절히 대응할 수 있게 합니다.

💡 블록 추가 시 이벤트 시스템을 구현하세요. EventEmitter를 사용하여 'blockAdded' 이벤트를 발생시키면, 다른 컴포넌트가 블록 추가를 감지하고 반응할 수 있습니다.

💡 프로덕션에서는 addBlock을 비동기 함수로 만들어, 블록을 데이터베이스에 저장하거나 다른 노드에 브로드캐스트하는 작업을 수행하세요.


5. 블록 검증 로직

시작하며

여러분이 블록체인 네트워크를 운영하다가 악의적인 사용자가 과거의 블록을 몰래 수정하려고 시도한다면 어떻게 막을 수 있을까요? 또는 네트워크 오류로 블록 데이터가 손상되었을 때 이를 어떻게 감지할 수 있을까요?

실제 블록체인에서는 각 블록이 추가될 때마다 이전 블록들과의 연결이 올바른지, 해시가 정확히 계산되었는지 검증합니다. 만약 누군가 과거의 블록을 변조하면, 그 블록의 해시가 바뀌고, 다음 블록의 previousHash와 일치하지 않게 되어 즉시 감지됩니다.

이것이 블록체인이 "변조 불가능(Immutable)"하다고 불리는 이유입니다. 바로 이럴 때 필요한 것이 isValidChain 함수입니다.

전체 블록체인을 순회하면서 각 블록의 무결성과 블록 간의 연결을 검증합니다.

개요

간단히 말해서, isValidChain은 블록체인의 모든 블록을 검사하여 데이터가 변조되지 않았는지, 블록들이 올바르게 연결되어 있는지 확인하는 함수입니다. 블록체인의 핵심 가치는 데이터의 무결성과 투명성입니다.

하지만 이는 지속적인 검증이 있을 때만 보장됩니다. 실무에서는 새로운 노드가 네트워크에 참여할 때, 다른 노드로부터 받은 블록체인이 유효한지 검증하거나, 주기적으로 자신의 체인이 손상되지 않았는지 자가 진단할 때 이 함수를 사용합니다.

예를 들어, 분산 데이터베이스에서 여러 서버의 데이터 일관성을 확인하거나, 감사(audit) 목적으로 전체 거래 기록의 무결성을 검증할 때 필수적입니다. 기존에는 블록 하나하나를 수동으로 확인해야 했다면, 이제는 isValidChain 함수가 전체 체인을 자동으로 검증하여 결과를 boolean으로 반환합니다.

블록 검증의 핵심 특징은 포괄성(Comprehensiveness)과 엄격성(Strictness)입니다. 단 하나의 블록이라도 문제가 있으면 전체 체인이 무효로 판정되며, 해시 불일치, 인덱스 오류, 타임스탬프 이상 등 모든 가능한 문제를 체크합니다.

코드 예제

class Blockchain {
  public chain: Block[];

  constructor() {
    this.chain = [this.createGenesisBlock()];
  }

  // 블록체인 전체의 유효성 검증
  isValidChain(): boolean {
    // 제네시스 블록 검증 (첫 번째 블록이 올바른지)
    const genesisBlock = this.chain[0];
    if (genesisBlock.index !== 0 || genesisBlock.previousHash !== "0") {
      return false;  // 제네시스 블록이 잘못됨
    }

    // 두 번째 블록부터 순회하며 검증
    for (let i = 1; i < this.chain.length; i++) {
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      // 1. 현재 블록의 해시가 올바르게 계산되었는지 확인
      if (currentBlock.hash !== currentBlock.calculateHash()) {
        return false;  // 해시가 변조됨
      }

      // 2. 현재 블록의 previousHash가 이전 블록의 hash와 일치하는지
      if (currentBlock.previousHash !== previousBlock.hash) {
        return false;  // 체인이 끊어짐
      }

      // 3. 블록 인덱스가 순차적인지 확인
      if (currentBlock.index !== previousBlock.index + 1) {
        return false;  // 인덱스 순서가 잘못됨
      }
    }

    return true;  // 모든 검증 통과
  }
}

설명

이것이 하는 일: isValidChain 함수는 블록체인의 모든 블록을 순차적으로 검사하여, 데이터 변조나 체인 손상이 없는지 확인하고 결과를 boolean으로 반환합니다. 첫 번째로, 제네시스 블록을 검증합니다.

제네시스 블록은 블록체인의 시작점이므로 특별히 검증해야 합니다. index가 0이 아니거나 previousHash가 "0"이 아니면 잘못된 블록체인입니다.

만약 누군가 제네시스 블록을 바꿨다면, 이것은 완전히 다른 블록체인을 의미하므로 즉시 false를 반환합니다. 그 다음으로, 두 번째 블록(index 1)부터 체인의 끝까지 순회하며 각 블록을 검증합니다.

반복문에서 i는 현재 검사 중인 블록의 인덱스이고, i-1은 바로 이전 블록입니다. 이렇게 쌍으로 비교하면서 체인의 연결을 확인합니다.

세 번째로, 각 블록에 대해 세 가지 중요한 검증을 수행합니다. 첫째, currentBlock.hash가 실제로 블록 데이터로부터 계산된 해시와 일치하는지 확인합니다.

calculateHash()를 다시 호출하여 현재 블록의 데이터로 해시를 재계산하고, 저장된 해시와 비교합니다. 만약 다르다면 블록 데이터가 변조되었다는 의미입니다.

둘째, currentBlock.previousHash가 이전 블록의 실제 해시(previousBlock.hash)와 일치하는지 확인합니다. 이것이 블록체인의 "체인" 부분을 검증하는 핵심입니다.

만약 일치하지 않으면 체인이 끊어진 것입니다. 셋째, 블록의 index가 순차적으로 증가하는지 확인합니다.

이는 블록의 순서가 올바른지 보장합니다. 마지막으로, 모든 블록이 위의 검증을 통과하면 true를 반환합니다.

이는 블록체인이 완전히 유효하며 변조되지 않았음을 의미합니다. 하나라도 검증에 실패하면 즉시 false를 반환하여 문제가 있음을 알립니다.

여러분이 이 코드를 사용하면 블록체인의 무결성을 수학적으로 보장할 수 있고, 악의적인 변조 시도나 우발적인 데이터 손상을 즉시 감지할 수 있습니다. 실무에서는 이 함수를 정기적으로 실행하는 모니터링 시스템을 구축하거나, API 엔드포인트로 노출하여 외부에서 블록체인의 상태를 확인할 수 있게 합니다.

실전 팁

💡 블록 검증 시 타임스탬프도 확인하세요. 현재 블록의 timestamp가 이전 블록보다 크거나 같아야 합니다. 미래 시간은 허용 범위 내인지 체크하세요.

💡 검증 실패 시 어떤 블록이 문제인지, 어떤 검증에 실패했는지 로그를 남기세요. 디버깅할 때 매우 유용합니다.

💡 대규모 블록체인에서는 전체 검증이 오래 걸릴 수 있으므로, 마지막 N개 블록만 검증하는 옵션을 제공하세요.

💡 블록체인을 다른 노드로부터 받을 때는 반드시 isValidChain을 먼저 실행하세요. 유효하지 않은 체인을 받아들이면 전체 시스템이 손상됩니다.

💡 검증 로직을 별도의 Validator 클래스로 분리하세요. 검증 규칙이 복잡해지면 단일 책임 원칙(SRP)에 따라 코드를 구조화하는 것이 좋습니다.


6. Timestamp 관리

시작하며

여러분이 블록체인에 기록된 거래가 정말 그 시간에 발생했는지 어떻게 증명할 수 있을까요? 누군가 시간을 조작해서 과거의 거래를 나중에 만든 것처럼 위조할 수는 없을까요?

실제 블록체인에서 타임스탬프는 단순히 "언제 만들어졌는지" 기록하는 것 이상의 의미를 가집니다. 블록의 순서를 보장하고, 거래의 시간적 정당성을 입증하며, 이중 지불 같은 공격을 방지하는 데 핵심적인 역할을 합니다.

비트코인에서는 타임스탬프를 기반으로 난이도를 조정하고, 블록 생성 시간을 약 10분으로 유지합니다. 바로 이럴 때 필요한 것이 올바른 타임스탬프 관리입니다.

현재 시간을 정확히 기록하고, 타임스탬프의 유효성을 검증하여 시간 조작을 방지합니다.

개요

간단히 말해서, 타임스탬프 관리는 블록이 생성된 정확한 시간을 Unix timestamp로 기록하고, 이를 검증하여 시간 조작을 방지하는 메커니즘입니다. 블록체인에서 타임스탬프는 여러 중요한 목적으로 사용됩니다.

첫째, 블록의 생성 순서를 명확히 합니다. index만으로도 순서를 알 수 있지만, timestamp는 실제 시간 정보를 제공합니다.

둘째, 거래의 시간적 정당성을 입증합니다. 예를 들어, 금융 거래에서는 정확한 거래 시점이 법적으로 중요합니다.

셋째, 작업 증명(PoW)에서 난이도 조정의 기준이 됩니다. 예를 들어, 스마트 계약에서 시간 기반 조건을 검증하거나, 토큰 락업 기간을 확인할 때 블록의 타임스탬프를 사용합니다.

기존에는 서버의 시간을 그대로 믿어야 했다면, 이제는 타임스탬프 검증 로직을 추가하여 미래 시간이나 과거로 너무 거슬러 올라간 시간을 거부할 수 있습니다. 타임스탬프 관리의 핵심 특징은 정확성(Accuracy)과 검증 가능성(Verifiability)입니다.

밀리초 단위의 정밀한 시간을 기록하고, 허용 가능한 시간 범위 내에 있는지 검증합니다. 이를 통해 시간 조작 공격을 방지합니다.

코드 예제

class Block {
  public hash: string;

  constructor(
    public index: number,
    public previousHash: string,
    public timestamp: number,  // Unix timestamp (밀리초)
    public data: string,
  ) {
    this.hash = this.calculateHash();
  }

  calculateHash(): string {
    const blockData = this.index + this.previousHash + this.timestamp + this.data;
    return CryptoJS.SHA256(blockData).toString();
  }

  // 타임스탬프가 유효한지 검증
  isValidTimestamp(previousTimestamp: number): boolean {
    const now = Date.now();
    const FUTURE_TOLERANCE = 60 * 1000;  // 미래 시간 허용 범위 (1분)

    // 1. 미래 시간이 너무 많이 앞서있는지 확인
    if (this.timestamp > now + FUTURE_TOLERANCE) {
      return false;  // 미래 시간이 허용 범위를 초과
    }

    // 2. 이전 블록보다 이른 시간인지 확인
    if (this.timestamp < previousTimestamp) {
      return false;  // 시간이 역행함
    }

    return true;  // 타임스탬프 유효
  }
}

설명

이것이 하는 일: 타임스탬프 관리 로직은 블록 생성 시 정확한 현재 시간을 기록하고, 이 시간이 합리적인 범위 내에 있는지 검증합니다. 첫 번째로, 블록 생성 시 Date.now()를 사용하여 현재 시간을 Unix timestamp로 가져옵니다.

Unix timestamp는 1970년 1월 1일 00:00:00 UTC부터 경과한 밀리초를 나타내는 숫자입니다. 예를 들어, 1704067200000은 2024년 1월 1일을 의미합니다.

밀리초 단위를 사용하면 초 단위보다 1000배 더 정밀하게 시간을 기록할 수 있어, 같은 초 내에 여러 블록이 생성되어도 순서를 구분할 수 있습니다. 그 다음으로, isValidTimestamp 함수가 타임스탬프의 유효성을 검증합니다.

첫 번째 검증은 미래 시간 체크입니다. 블록의 timestamp가 현재 시간보다 너무 많이 앞서있으면 의심스럽습니다.

하지만 분산 시스템에서는 각 노드의 시계가 완벽히 동기화되지 않을 수 있으므로, 약간의 오차는 허용해야 합니다. 여기서는 1분(60,000 밀리초)의 여유를 두었습니다.

만약 블록의 timestamp가 현재 시간보다 1분 이상 미래라면 거부합니다. 세 번째로, 과거 시간 체크를 수행합니다.

블록의 timestamp는 반드시 이전 블록의 timestamp보다 크거나 같아야 합니다. 시간이 역행하는 것은 논리적으로 불가능하므로, 이런 블록은 명백히 조작되었거나 오류가 있는 것입니다.

이 검증을 통해 블록의 순서가 시간상으로도 올바른지 보장합니다. 마지막으로, 모든 검증을 통과하면 true를 반환합니다.

이는 타임스탬프가 합리적인 범위 내에 있으며, 블록의 시간적 정당성이 확인되었음을 의미합니다. 실제로 블록을 추가하기 전에 이 검증을 수행하여, 잘못된 타임스탬프를 가진 블록이 체인에 들어가는 것을 막습니다.

여러분이 이 코드를 사용하면 시간 조작 공격을 효과적으로 방지할 수 있고, 블록체인의 시간적 일관성을 보장할 수 있습니다. 실무에서는 NTP(Network Time Protocol) 서버와 동기화하여 노드의 시계를 정확히 유지하고, 타임스탬프 검증을 더욱 엄격하게 만들 수 있습니다.

실전 팁

💡 분산 환경에서는 각 노드의 시계가 다를 수 있으므로, 중앙값(median) 타임스탬프를 사용하세요. 최근 11개 블록의 타임스탬프 중앙값보다 크고, 현재 시간보다 작으면 유효하다고 판단합니다.

💡 타임스탬프를 사람이 읽을 수 있는 형식으로 변환하려면 new Date(timestamp).toISOString()을 사용하세요. 로그나 UI에 표시할 때 유용합니다.

💡 블록체인을 데이터베이스에 저장할 때 타임스탬프에 인덱스를 생성하세요. 시간 범위로 블록을 조회하는 쿼리가 빨라집니다.

💡 타임스탬프 검증 로직에 로깅을 추가하여, 거부된 블록의 시간 정보를 기록하세요. 시간 동기화 문제를 디버깅할 때 도움이 됩니다.

💡 블록체인 애플리케이션에서 시간 기반 로직을 구현할 때는 블록의 타임스탬프를 사용하세요. 서버의 현재 시간을 사용하면 블록체인과 일관성이 깨질 수 있습니다.


7. 블록체인 클래스

시작하며

여러분이 지금까지 만든 Block 클래스와 여러 함수들을 어떻게 하나로 통합할 수 있을까요? 블록을 생성하고, 추가하고, 검증하는 모든 기능을 어디에 모아야 할까요?

실제 블록체인 애플리케이션에서는 블록들을 관리하는 중앙 컨트롤러가 필요합니다. 단순히 블록 배열을 유지하는 것뿐만 아니라, 블록 추가 규칙을 강제하고, 체인의 무결성을 보장하며, 외부에서 블록체인 데이터를 조회할 수 있는 인터페이스를 제공해야 합니다.

비트코인에서는 이런 역할을 노드 소프트웨어가 담당합니다. 바로 이럴 때 필요한 것이 Blockchain 클래스입니다.

모든 블록체인 관련 로직을 하나의 클래스로 캡슐화하여 관리합니다.

개요

간단히 말해서, Blockchain 클래스는 블록들의 배열을 관리하고, 블록 추가, 검증, 조회 등 블록체인의 모든 핵심 기능을 제공하는 중앙 관리 클래스입니다. 객체 지향 프로그래밍에서 관련된 데이터와 기능을 하나의 클래스로 묶는 것은 매우 중요합니다.

Blockchain 클래스는 블록 배열(chain)을 private하게 관리하고, 외부에서는 제공된 메서드를 통해서만 블록체인을 조작할 수 있게 합니다. 이렇게 하면 잘못된 방식으로 블록이 추가되는 것을 방지할 수 있습니다.

예를 들어, DApp(분산 애플리케이션)을 개발할 때 Blockchain 클래스의 인스턴스 하나만 생성하여 전체 애플리케이션에서 공유하면, 모든 컴포넌트가 동일한 블록체인 상태를 보게 됩니다. 기존에는 블록 배열과 함수들이 흩어져 있어 관리가 어려웠다면, 이제는 Blockchain 클래스로 모든 것을 통합하여 코드의 응집도를 높이고 결합도를 낮출 수 있습니다.

Blockchain 클래스의 핵심 특징은 캡슐화(Encapsulation)와 상태 관리(State Management)입니다. 블록체인의 내부 상태를 보호하고, 오직 정의된 인터페이스를 통해서만 상태를 변경할 수 있게 하여 데이터 무결성을 보장합니다.

코드 예제

import * as CryptoJS from 'crypto-js';

class Block {
  public hash: string;

  constructor(
    public index: number,
    public previousHash: string,
    public timestamp: number,
    public data: string,
  ) {
    this.hash = this.calculateHash();
  }

  calculateHash(): string {
    return CryptoJS.SHA256(
      this.index + this.previousHash + this.timestamp + this.data
    ).toString();
  }
}

class Blockchain {
  public chain: Block[];

  constructor() {
    // 블록체인 초기화: 제네시스 블록으로 시작
    this.chain = [this.createGenesisBlock()];
  }

  private createGenesisBlock(): Block {
    return new Block(0, "0", Date.now(), "Genesis Block");
  }

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

  addBlock(data: string): void {
    const previousBlock = this.getLatestBlock();
    const newBlock = new Block(
      previousBlock.index + 1,
      previousBlock.hash,
      Date.now(),
      data
    );

    this.chain.push(newBlock);
  }

  isValidChain(): boolean {
    // 제네시스 블록 검증
    if (this.chain[0].hash !== this.chain[0].calculateHash()) {
      return false;
    }

    // 모든 블록 검증
    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()) {
        return false;
      }

      if (currentBlock.previousHash !== previousBlock.hash) {
        return false;
      }
    }

    return true;
  }
}

// 사용 예시
const myBlockchain = new Blockchain();
myBlockchain.addBlock("첫 번째 거래");
myBlockchain.addBlock("두 번째 거래");
console.log(myBlockchain.isValidChain());  // true

설명

이것이 하는 일: Blockchain 클래스는 블록체인의 전체 생명주기를 관리하는 중앙 컨트롤러로, 초기화부터 블록 추가, 검증까지 모든 작업을 조율합니다. 첫 번째로, constructor에서 블록체인을 초기화합니다.

this.chain = [this.createGenesisBlock()]는 블록 배열을 생성하고, 즉시 제네시스 블록을 첫 요소로 추가합니다. 이렇게 하면 블록체인은 항상 최소 하나의 블록(제네시스 블록)을 가지게 되어, getLatestBlock()을 안전하게 호출할 수 있습니다.

빈 배열에서 마지막 요소를 가져오려고 하면 undefined가 반환되어 오류가 발생할 수 있기 때문입니다. 그 다음으로, createGenesisBlock()을 private 메서드로 정의했습니다.

private 키워드는 이 메서드가 클래스 외부에서 호출될 수 없음을 의미합니다. 제네시스 블록은 오직 constructor에서 한 번만 생성되어야 하므로, 외부에서 임의로 호출하지 못하게 막는 것입니다.

이는 객체 지향의 캡슐화 원칙을 따른 것입니다. 세 번째로, addBlock() 메서드가 블록 추가의 유일한 진입점입니다.

외부에서는 this.chain.push()를 직접 호출할 수 없고(private로 만들 수도 있음), 반드시 addBlock()을 통해야 합니다. 이 메서드 내부에서 올바른 index, previousHash, timestamp를 자동으로 계산하므로, 잘못된 블록이 추가될 가능성이 없습니다.

이것이 바로 "불변 조건(invariant)"을 유지하는 방법입니다. 네 번째로, isValidChain() 메서드는 현재 블록체인의 무결성을 검증합니다.

이 메서드는 public이므로 외부에서 언제든 호출하여 체인의 상태를 확인할 수 있습니다. 예를 들어, 다른 노드로부터 블록체인을 받았을 때 isValidChain()을 호출하여 신뢰할 수 있는지 검증할 수 있습니다.

마지막으로, 사용 예시를 보면 매우 간단합니다. Blockchain 인스턴스를 생성하고, addBlock()으로 데이터를 추가하고, isValidChain()으로 검증하는 것이 전부입니다.

복잡한 내부 로직은 모두 클래스 안에 숨겨져 있어, 사용자는 깔끔한 인터페이스만 다루면 됩니다. 여러분이 이 코드를 사용하면 블록체인의 복잡성을 추상화하여 간단한 API로 제공할 수 있고, 코드의 재사용성과 유지보수성이 크게 향상됩니다.

실무에서는 이 클래스를 기반으로 더 많은 기능(블록 조회, 트랜잭션 풀, 네트워크 통신 등)을 추가하여 완전한 블록체인 시스템을 구축할 수 있습니다.

실전 팁

💡 chain 배열을 private으로 만들고, getChain() 메서드로 읽기 전용 복사본을 반환하세요. 외부에서 직접 배열을 수정하지 못하게 보호할 수 있습니다.

💡 Blockchain 클래스를 싱글톤 패턴으로 구현하세요. 애플리케이션 전체에서 하나의 블록체인 인스턴스만 존재하도록 보장할 수 있습니다.

💡 addBlock()이 성공하면 블록을 반환하고, 실패하면 null을 반환하도록 수정하세요. 호출자가 결과를 확인할 수 있습니다.

💡 블록체인 클래스에 이벤트 리스너를 추가하세요. 블록이 추가되거나 검증이 실패할 때 이벤트를 발생시켜, 다른 컴포넌트가 반응할 수 있게 합니다.

💡 블록체인을 JSON으로 직렬화하여 파일이나 데이터베이스에 저장하는 기능을 추가하세요. toJSON()과 fromJSON() 메서드를 구현하면 블록체인을 영속화할 수 있습니다.


#TypeScript#Blockchain#Block#Hash#CryptoJS#typescript

댓글 (0)

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