이미지 로딩 중...

타입스크립트로 비트코인 클론하기 18편 - 코인베이스 트랜잭션과 채굴 보상 - 슬라이드 1/11
A

AI Generated

2025. 11. 11. · 4 Views

타입스크립트로 비트코인 클론하기 18편 - 코인베이스 트랜잭션과 채굴 보상

비트코인의 핵심 메커니즘인 코인베이스 트랜잭션을 완벽하게 이해하고 구현해봅니다. 채굴자가 어떻게 보상을 받는지, 블록 생성 시 새로운 코인이 어떻게 발행되는지 실제 코드로 구현하면서 블록체인의 경제 모델을 배웁니다.


목차

  1. 코인베이스 트랜잭션 개념 - 블록체인에서 새로운 코인이 탄생하는 방법
  2. TxIn과 TxOut 구조 - 트랜잭션의 입출력 이해하기
  3. 트랜잭션 서명과 검증 - 소유권을 암호학적으로 증명하기
  4. 채굴 보상 계산과 반감기 - 비트코인의 경제 모델 구현하기
  5. 코인베이스 트랜잭션 검증 - 특별한 거래의 특별한 규칙
  6. 거래 수수료 계산 - 채굴자 인센티브와 네트워크 효율성
  7. UTXO 풀 관리 - 사용 가능한 출력 추적하기
  8. 트랜잭션 생성 - 사용자가 거래를 만드는 전체 과정
  9. 트랜잭션 검증 - 거래의 유효성을 철저히 확인하기
  10. 코인베이스 성숙도 - 채굴 보상 사용 제한하기

1. 코인베이스 트랜잭션 개념 - 블록체인에서 새로운 코인이 탄생하는 방법

시작하며

여러분이 블록체인을 공부하면서 이런 의문을 가져본 적 있나요? "비트코인은 처음에 어떻게 생성되는 거지?

모든 거래는 기존 코인을 전송하는 건데, 최초의 코인은 어디서 온 걸까?" 이 질문은 블록체인을 이해하는 핵심 중 하나입니다. 일반 트랜잭션은 기존 코인을 A에서 B로 보내는 것이지만, 새로운 코인을 생성하는 특별한 메커니즘이 없다면 시스템이 작동할 수 없습니다.

채굴자들은 왜 컴퓨팅 파워를 사용해가며 블록을 생성할까요? 바로 보상이 있기 때문입니다.

바로 이럴 때 필요한 것이 코인베이스 트랜잭션(Coinbase Transaction)입니다. 이것은 블록의 첫 번째 트랜잭션으로, 입력 없이 새로운 코인을 생성하여 채굴자에게 지급하는 특별한 거래입니다.

개요

간단히 말해서, 코인베이스 트랜잭션은 "무에서 유를 창조하는" 유일한 거래입니다. 일반 거래와 달리 이전 트랜잭션 출력(UTXO)을 참조하지 않고, 새로운 코인을 시스템에 발행합니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 블록체인의 경제 모델을 구축하는 핵심입니다. 채굴자들에게 인센티브를 제공하여 네트워크 보안을 유지하게 하고, 코인의 총 공급량을 제어합니다.

예를 들어, 비트코인은 21만 블록마다 보상이 절반으로 줄어드는 '반감기'를 통해 인플레이션을 조절합니다. 기존 일반 거래는 "A가 가진 10코인 중 5코인을 B에게 전송"이라면, 코인베이스는 "채굴자 C에게 50코인을 새롭게 생성하여 지급"입니다.

코인베이스 트랜잭션의 핵심 특징은 첫째, 항상 블록의 첫 번째 트랜잭션이어야 합니다. 둘째, 입력(input)이 없거나 특별한 더미 입력을 가집니다.

셋째, 출력은 블록 보상과 거래 수수료의 합계입니다. 이러한 특징들이 블록체인이 탈중앙화된 방식으로 화폐를 발행하고 채굴자들에게 보상하는 메커니즘을 가능하게 합니다.

코드 예제

// 코인베이스 트랜잭션 인터페이스 정의
interface CoinbaseTx {
  txId: string;           // 트랜잭션 고유 ID
  txIns: TxIn[];          // 입력 (코인베이스는 특별한 더미 입력)
  txOuts: TxOut[];        // 출력 (채굴 보상)
  timestamp: number;      // 생성 시간
}

// 코인베이스 트랜잭션 생성 함수
function createCoinbaseTx(
  minerAddress: string,   // 채굴자의 지갑 주소
  blockIndex: number      // 현재 블록 높이
): CoinbaseTx {
  // 더미 입력 생성 (이전 트랜잭션 참조 없음)
  const txIn = new TxIn('', blockIndex, '');

  // 블록 보상 계산 (50 BTC, 반감기 고려)
  const reward = 50 / Math.pow(2, Math.floor(blockIndex / 210000));

  // 출력 생성 (채굴자에게 보상 지급)
  const txOut = new TxOut(minerAddress, reward);

  return {
    txId: generateTxId([txIn], [txOut]),
    txIns: [txIn],
    txOuts: [txOut],
    timestamp: Date.now()
  };
}

설명

이것이 하는 일: 코인베이스 트랜잭션은 블록체인 네트워크에 새로운 코인을 발행하고, 블록을 생성한 채굴자에게 보상을 제공하는 특별한 거래입니다. 일반 거래와 달리 "무에서 유를 창조"하는 유일한 메커니즘입니다.

첫 번째로, 더미 입력(txIn) 생성 부분을 봅시다. 일반 트랜잭션은 이전 트랜잭션의 출력(UTXO)을 참조하지만, 코인베이스는 참조할 이전 출력이 없습니다.

따라서 빈 문자열('')과 현재 블록 높이(blockIndex)를 사용하여 특별한 입력을 만듭니다. 이는 "이 거래는 새로 생성된 것"이라는 표시입니다.

그 다음으로, 보상 계산 로직이 실행됩니다. 비트코인은 처음 50 BTC로 시작하여 21만 블록마다 절반으로 줄어듭니다.

Math.floor(blockIndex / 210000)으로 몇 번째 반감기인지 계산하고, Math.pow(2, n)으로 나누어 현재 보상액을 구합니다. 예를 들어 블록 높이가 0209,999면 50 BTC, 210,000419,999면 25 BTC입니다.

마지막으로, 계산된 보상을 담은 출력(txOut)을 생성하여 채굴자 주소로 지급합니다. 이 출력은 채굴자의 UTXO가 되어 나중에 다른 거래에서 사용할 수 있습니다.

트랜잭션 ID는 입력과 출력을 해시하여 생성되며, 블록체인에 기록됩니다. 여러분이 이 코드를 사용하면 블록체인의 통화 발행 메커니즘을 직접 구현할 수 있습니다.

채굴자에게 자동으로 보상이 지급되고, 반감기를 통해 인플레이션이 통제되며, 총 공급량이 약 2,100만 개로 제한되는 경제 모델을 완성할 수 있습니다.

실전 팁

💡 코인베이스 트랜잭션의 입력 서명 필드에 임의의 데이터를 넣을 수 있습니다. 비트코인 창시자 사토시 나카모토는 첫 블록의 코인베이스에 "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"라는 신문 헤드라인을 넣어 블록 생성 시점을 증명했습니다.

💡 코인베이스 트랜잭션을 검증할 때는 일반 거래와 다른 규칙을 적용해야 합니다. 입력 검증을 건너뛰고, 출력 금액이 (블록 보상 + 거래 수수료 합계)를 초과하지 않는지만 확인합니다. 이를 혼동하면 유효한 블록을 거부하는 버그가 발생합니다.

💡 코인베이스 보상으로 받은 코인은 일반적으로 100블록 이후에야 사용할 수 있습니다(coinbase maturity). 이는 체인 재구성(reorg) 시 블록이 무효화되면 보상도 사라지는 문제를 방지하기 위한 안전장치입니다.

💡 실제 구현에서는 블록 높이를 정확히 추적해야 합니다. 반감기 계산이 1블록이라도 틀리면 네트워크 전체가 해당 블록을 거부하므로, 블록 인덱스 관리가 매우 중요합니다.

💡 거래 수수료는 코인베이스 출력에 포함시킬 수 있습니다. 블록 내 모든 일반 거래의 (입력 합계 - 출력 합계)를 계산하여 코인베이스 보상에 더하면, 채굴자가 수수료도 함께 받을 수 있습니다.


2. TxIn과 TxOut 구조 - 트랜잭션의 입출력 이해하기

시작하며

여러분이 "A가 B에게 5 BTC를 보낸다"는 거래를 코드로 구현하려고 할 때, 어떻게 표현해야 할지 막막했던 경험이 있나요? 단순히 "보내는 사람, 받는 사람, 금액"만으로는 블록체인의 보안과 무결성을 보장할 수 없습니다.

실제로 비트코인은 계좌 잔액 개념이 없습니다. 대신 "미사용 트랜잭션 출력(UTXO)" 모델을 사용합니다.

이는 마치 현금을 다루는 것과 비슷합니다. 10달러짜리 지폐로 3달러 물건을 사면 7달러를 거스름돈으로 받는 것처럼, 트랜잭션도 이전 출력을 "소비"하고 새로운 출력을 "생성"합니다.

바로 이럴 때 필요한 것이 TxIn(Transaction Input)과 TxOut(Transaction Output) 구조입니다. 이 두 가지가 블록체인 거래의 핵심 데이터 구조를 형성합니다.

개요

간단히 말해서, TxOut은 "누구에게 얼마를 줄지"를 나타내고, TxIn은 "어떤 기존 출력을 사용할지"를 나타냅니다. 모든 거래는 하나 이상의 입력과 출력으로 구성됩니다.

실무 관점에서 보면, 이 구조는 이중 지불 방지와 잔액 추적에 필수적입니다. 각 출력은 한 번만 사용될 수 있으며, 사용된 출력은 "소비됨"으로 표시됩니다.

예를 들어, 앨리스가 10 BTC를 가진 UTXO로 7 BTC를 밥에게 보내려면, 입력으로 10 BTC UTXO를 참조하고, 출력으로 7 BTC(밥에게), 3 BTC(본인에게 거스름돈)를 생성합니다. 기존 계좌 기반 모델(Ethereum의 상태 모델)은 "앨리스 잔액 -= 7, 밥 잔액 += 7"이라면, UTXO 모델은 "앨리스의 10 BTC 출력을 소비하고, 밥의 7 BTC 출력과 앨리스의 3 BTC 출력을 생성"입니다.

핵심 특징으로는 첫째, TxOut은 수신자 주소(address)와 금액(amount)을 포함합니다. 둘째, TxIn은 이전 트랜잭션 ID(txOutId), 출력 인덱스(txOutIndex), 서명(signature)을 포함합니다.

셋째, 모든 입력의 합은 모든 출력의 합보다 크거나 같아야 합니다(차액은 수수료). 이러한 구조가 블록체인의 투명성과 보안성을 보장합니다.

코드 예제

// 트랜잭션 출력 클래스
class TxOut {
  public address: string;   // 수신자의 공개키 주소
  public amount: number;    // 전송할 코인 양

  constructor(address: string, amount: number) {
    this.address = address;
    this.amount = amount;
  }
}

// 트랜잭션 입력 클래스
class TxIn {
  public txOutId: string;      // 참조하는 이전 트랜잭션 ID
  public txOutIndex: number;   // 이전 트랜잭션의 출력 인덱스
  public signature: string;    // 소유권 증명 서명

  constructor(txOutId: string, txOutIndex: number, signature: string) {
    this.txOutId = txOutId;
    this.txOutIndex = txOutIndex;
    this.signature = signature;
  }
}

// UTXO (미사용 트랜잭션 출력) 클래스
class UnspentTxOut {
  public readonly txOutId: string;
  public readonly txOutIndex: number;
  public readonly address: string;
  public readonly amount: number;

  constructor(txOutId: string, txOutIndex: number, address: string, amount: number) {
    this.txOutId = txOutId;
    this.txOutIndex = txOutIndex;
    this.address = address;
    this.amount = amount;
  }
}

설명

이것이 하는 일: TxIn과 TxOut은 블록체인 트랜잭션의 핵심 구성 요소입니다. TxOut은 "이 코인은 누구 것이다"라는 소유권을 정의하고, TxIn은 "나는 이 코인을 사용할 권리가 있다"는 것을 증명합니다.

먼저 TxOut 클래스를 살펴봅시다. address 필드는 코인을 받을 사람의 공개키 해시이며, amount는 전송할 코인의 양입니다.

이 출력이 블록체인에 기록되면, 해당 주소의 소유자만이 나중에 이 출력을 입력으로 사용할 수 있습니다. 마치 수표에 수취인 이름과 금액을 적는 것과 같습니다.

다음으로 TxIn 클래스입니다. txOutId는 어떤 트랜잭션의 출력을 사용할지 지정하고, txOutIndex는 그 트랜잭션의 여러 출력 중 몇 번째 것인지 가리킵니다.

signature는 개인키로 서명한 값으로, "나는 이 출력의 정당한 소유자"임을 암호학적으로 증명합니다. 서명 없이는 아무도 다른 사람의 코인을 사용할 수 없습니다.

UnspentTxOut 클래스는 현재 사용되지 않은(아직 다른 거래의 입력으로 소비되지 않은) 출력을 추적합니다. 블록체인 전체를 스캔하여 생성된 출력 중 아직 입력으로 참조되지 않은 것들을 모으면 "현재 사용 가능한 모든 코인"의 목록이 됩니다.

이를 통해 특정 주소의 "잔액"을 계산할 수 있습니다(해당 주소의 모든 UTXO 금액 합계). 트랜잭션을 검증할 때는 모든 TxIn이 유효한 UnspentTxOut을 참조하는지, 서명이 올바른지, 입력 합계가 출력 합계 이상인지 확인합니다.

이러한 과정을 통해 이중 지불이 방지되고 네트워크의 무결성이 유지됩니다. 여러분이 이 구조를 사용하면 계좌 잔액 없이도 모든 거래를 추적하고 검증할 수 있습니다.

병렬 처리가 쉽고, 프라이버시가 더 강화되며, 거래 내역이 완전히 투명하게 기록되는 장점이 있습니다.

실전 팁

💡 UTXO 풀(pool)은 메모리에 캐싱하여 빠르게 접근할 수 있도록 하세요. 매번 전체 블록체인을 스캔하면 성능이 급격히 떨어집니다. Map<string, UnspentTxOut> 형태로 txOutId:txOutIndex를 키로 사용하면 O(1) 조회가 가능합니다.

💡 거스름돈 출력을 잊지 마세요. 10 BTC 출력으로 3 BTC를 보낼 때, 출력을 3 BTC 하나만 만들면 나머지 7 BTC는 수수료로 채굴자에게 갑니다! 반드시 자신에게 돌아오는 거스름돈 출력(change output)을 추가해야 합니다.

💡 트랜잭션 서명은 개인키로 하지만, 검증은 공개키로 합니다. TxIn의 signature를 검증할 때 해당 UTXO의 address(공개키 해시)와 매칭되는 공개키로 검증해야 합니다. ECDSA 알고리즘을 사용하면 안전하게 구현할 수 있습니다.

💡 출력 인덱스(txOutIndex)는 0부터 시작합니다. 트랜잭션의 출력 배열에서 몇 번째인지를 나타내므로, 첫 번째 출력은 0, 두 번째는 1입니다. Off-by-one 에러를 주의하세요.

💡 코인베이스 트랜잭션의 입력은 특별합니다. txOutId가 빈 문자열이고 txOutIndex가 블록 높이인 더미 입력을 사용하므로, 입력 검증 로직에서 코인베이스를 예외 처리해야 합니다.


3. 트랜잭션 서명과 검증 - 소유권을 암호학적으로 증명하기

시작하며

여러분이 블록체인에서 거래를 보낼 때 이런 의문이 들지 않나요? "내가 10 BTC를 가지고 있다고 주장하면, 시스템은 그걸 어떻게 믿지?

누군가 내 이름으로 거짓 거래를 만들면 어떻게 막을까?" 이것은 블록체인 보안의 핵심 질문입니다. 중앙 서버가 없는 분산 시스템에서는 신뢰할 수 있는 제3자가 없습니다.

은행처럼 "이 계좌는 본인 확인이 되었습니다"라고 보증해주는 기관이 없는 상황에서, 어떻게 거래의 정당성을 보장할 수 있을까요? 바로 이럴 때 필요한 것이 디지털 서명(Digital Signature)입니다.

공개키 암호화를 사용하여 "나만이 이 거래를 만들 수 있었다"는 것을 수학적으로 증명합니다.

개요

간단히 말해서, 트랜잭션 서명은 개인키로 거래 데이터를 암호화한 것이며, 누구나 공개키로 이 서명을 검증하여 진짜 소유자가 만든 거래임을 확인할 수 있습니다. 실무 관점에서 이는 블록체인의 신뢰성을 담보하는 가장 중요한 메커니즘입니다.

서명 없이는 누구든 다른 사람의 코인을 마음대로 사용할 수 있습니다. 예를 들어, 앨리스의 10 BTC UTXO를 밥이 사용하려 한다면, 앨리스의 개인키로 서명해야 하는데 밥은 그 키를 모르므로 불가능합니다.

반대로 앨리스가 서명하면 누구나 앨리스의 공개키로 검증할 수 있습니다. 기존 중앙화 시스템은 "서버가 세션을 확인하여 본인 인증"이라면, 블록체인은 "개인키 서명을 공개키로 검증하여 본인 인증"입니다.

서버 없이도 완벽한 보안이 가능합니다. 핵심 특징으로는 첫째, 비대칭 암호화(ECDSA)를 사용하여 개인키는 비밀로 유지하고 공개키만 공개합니다.

둘째, 서명은 거래 내용에 대한 해시를 포함하여 변조 불가능합니다. 셋째, 한 번 사용된 서명은 다른 거래에 재사용할 수 없습니다(재생 공격 방지).

이러한 특징들이 탈중앙화된 환경에서도 완벽한 보안을 제공합니다.

코드 예제

import * as ecdsa from 'elliptic';
import * as crypto from 'crypto';

const ec = new ecdsa.ec('secp256k1');  // 비트코인과 동일한 타원곡선

// 트랜잭션 서명 생성
function signTxIn(
  transaction: Transaction,
  txInIndex: number,
  privateKey: string,
  unspentTxOuts: UnspentTxOut[]
): string {
  const txIn = transaction.txIns[txInIndex];

  // 참조하는 UTXO 찾기
  const referencedUtxo = unspentTxOuts.find(
    utxo => utxo.txOutId === txIn.txOutId && utxo.txOutIndex === txIn.txOutIndex
  );

  if (!referencedUtxo) {
    throw new Error('참조된 UTXO를 찾을 수 없습니다');
  }

  // 서명할 데이터 생성 (트랜잭션 ID + 출력 주소)
  const dataToSign = transaction.id + referencedUtxo.address;

  // 개인키로 서명
  const key = ec.keyFromPrivate(privateKey, 'hex');
  const signature = key.sign(dataToSign).toDER('hex');

  return signature;
}

// 서명 검증
function validateTxIn(
  txIn: TxIn,
  transaction: Transaction,
  unspentTxOuts: UnspentTxOut[]
): boolean {
  const referencedUtxo = unspentTxOuts.find(
    utxo => utxo.txOutId === txIn.txOutId && utxo.txOutIndex === txIn.txOutIndex
  );

  if (!referencedUtxo) return false;

  const dataToVerify = transaction.id + referencedUtxo.address;

  // 공개키로 서명 검증
  const key = ec.keyFromPublic(referencedUtxo.address, 'hex');
  return key.verify(dataToVerify, txIn.signature);
}

설명

이것이 하는 일: 트랜잭션 서명과 검증은 블록체인의 보안 핵심입니다. 개인키를 가진 사람만이 자신의 코인을 사용할 수 있고, 모든 노드가 독립적으로 거래의 진위를 검증할 수 있습니다.

먼저 서명 생성 과정을 봅시다. signTxIn 함수는 트랜잭션의 특정 입력에 서명합니다.

첫 번째 단계는 이 입력이 참조하는 UTXO를 찾는 것입니다. UTXO 풀에서 txOutIdtxOutIndex가 일치하는 항목을 찾아, 이것이 실제로 존재하고 아직 사용되지 않았는지 확인합니다.

찾지 못하면 이미 사용되었거나 존재하지 않는 출력을 쓰려는 것이므로 에러를 발생시킵니다. 다음으로 서명할 데이터를 준비합니다.

트랜잭션 ID와 UTXO의 주소(공개키)를 결합한 문자열을 만듭니다. 이렇게 하면 이 서명은 이 특정 거래에만 유효하고, 다른 거래에 복사해서 사용할 수 없습니다(재생 공격 방지).

그런 다음 elliptic 라이브러리의 secp256k1 곡선(비트코인 표준)을 사용하여 개인키로 데이터에 서명합니다. 결과는 DER 형식의 16진수 문자열입니다.

검증 과정은 반대입니다. validateTxIn 함수는 동일하게 UTXO를 찾고 동일한 데이터 문자열을 만듭니다.

하지만 서명하는 대신, UTXO의 주소(공개키)를 사용하여 TxIn의 서명을 검증합니다. key.verify()는 수학적으로 "이 서명은 이 공개키에 대응하는 개인키로만 만들 수 있다"는 것을 확인합니다.

서명이 유효하면 true, 아니면 false를 반환합니다. 여러분이 이 코드를 사용하면 중앙 서버 없이도 완벽한 인증 시스템을 구축할 수 있습니다.

개인키는 절대 네트워크에 노출되지 않고, 모든 노드가 독립적으로 검증하며, 수학적으로 위조가 불가능한 보안을 달성합니다. 또한 서명은 거래 내용의 무결성도 보장하여, 서명 후 거래 데이터가 조금이라도 변경되면 검증이 실패합니다.

실전 팁

💡 개인키는 절대 코드에 하드코딩하거나 로그에 출력하지 마세요. 환경 변수나 암호화된 키스토어 파일에 보관하고, 메모리에서도 사용 후 즉시 삭제해야 합니다. 개인키가 유출되면 모든 코인을 잃습니다.

💡 서명 전에 트랜잭션 ID가 올바르게 계산되었는지 확인하세요. ID는 입력과 출력의 해시이므로, 서명 전에 입력의 signature 필드는 빈 문자열이어야 합니다. 서명이 포함된 상태로 ID를 계산하면 순환 참조가 발생합니다.

💡 모든 입력에 개별적으로 서명해야 합니다. 트랜잭션에 입력이 3개면 각각 다른 UTXO를 참조하므로, 3번 서명 과정을 거쳐야 합니다. 하나라도 서명이 없거나 잘못되면 전체 거래가 무효입니다.

💡 타원곡선 암호화(ECDSA)는 RSA보다 짧은 키로 동일한 보안을 제공합니다. secp256k1 곡선은 256비트 키로 3072비트 RSA와 동일한 보안 수준을 제공하며, 블록체인의 데이터 크기를 줄이는 데 중요합니다.

💡 서명 검증 실패 시 상세한 에러 메시지를 남기세요. "서명이 유효하지 않습니다" 외에도 "참조된 UTXO 없음", "공개키 형식 오류", "서명 형식 오류" 등을 구분하면 디버깅이 훨씬 쉽습니다.


4. 채굴 보상 계산과 반감기 - 비트코인의 경제 모델 구현하기

시작하며

여러분이 "비트코인은 총 2,100만 개만 발행된다"는 말을 들어보셨나요? 그런데 어떻게 중앙 기관 없이 이 발행량이 자동으로 제어될까요?

누가 "이제 그만 발행하자"고 결정하는 걸까요? 이것은 블록체인의 놀라운 점 중 하나입니다.

코드에 경제 정책이 내장되어 있어, 인간의 개입 없이 자동으로 실행됩니다. 중앙은행이 금리를 조정하듯이 통화량을 관리하는 것이 아니라, 수학적 알고리즘이 모든 것을 결정합니다.

바로 이럴 때 필요한 것이 채굴 보상 계산 로직과 반감기(Halving) 메커니즘입니다. 블록이 생성될 때마다 보상이 지급되고, 일정 주기마다 보상이 절반으로 줄어들어 결국 발행이 멈춥니다.

개요

간단히 말해서, 채굴 보상은 처음 50 BTC로 시작하여 210,000블록(약 4년)마다 절반씩 줄어듭니다. 이 과정이 약 32번 반복되면 보상이 0에 수렴하여 총 발행량이 약 2,100만 BTC가 됩니다.

실무 관점에서 이는 인플레이션을 통제하고 희소성을 만드는 핵심 메커니즘입니다. 초기에는 많은 코인을 발행하여 네트워크를 빠르게 성장시키고, 시간이 지나면서 발행을 줄여 가치를 보존합니다.

예를 들어, 2009년 0블록은 50 BTC, 2012년 210,000블록부터는 25 BTC, 2016년 420,000블록부터는 12.5 BTC, 2020년 630,000블록부터는 6.25 BTC가 지급됩니다. 기존 법정화폐는 "중앙은행이 재량적으로 발행량 결정"이라면, 비트코인은 "미리 정해진 알고리즘이 자동으로 발행량 감소"입니다.

투명하고 예측 가능한 통화 정책입니다. 핵심 특징으로는 첫째, 반감기는 블록 높이로만 결정되며 시간과 무관합니다(채굴 속도가 느려져도 블록 수만 중요).

둘째, 보상이 정수로 나누어떨어지지 않으면 소수점 이하를 사용합니다(6.25 BTC 등). 셋째, 보상이 0이 되면 채굴자는 거래 수수료만으로 생존해야 합니다.

이러한 메커니즘이 비트코인의 장기적인 경제 모델을 정의합니다.

코드 예제

// 채굴 보상 상수
const INITIAL_REWARD = 50;           // 초기 블록 보상 (BTC)
const HALVING_INTERVAL = 210000;     // 반감기 주기 (블록 수)

// 특정 블록 높이의 채굴 보상 계산
function getBlockReward(blockIndex: number): number {
  // 몇 번째 반감기인지 계산 (0부터 시작)
  const halvings = Math.floor(blockIndex / HALVING_INTERVAL);

  // 64번 이상 반감되면 보상은 0 (2^64로 나누면 사실상 0)
  if (halvings >= 64) {
    return 0;
  }

  // 보상 = 초기 보상 / (2^반감기 횟수)
  const reward = INITIAL_REWARD / Math.pow(2, halvings);

  return reward;
}

// 코인베이스 트랜잭션에 거래 수수료 포함
function getCoinbaseReward(
  blockIndex: number,
  transactions: Transaction[]
): number {
  // 블록 보상 계산
  const blockReward = getBlockReward(blockIndex);

  // 거래 수수료 합계 계산 (입력 - 출력)
  const totalFees = transactions
    .filter(tx => !isCoinbaseTx(tx))  // 코인베이스 제외
    .reduce((acc, tx) => {
      const inputSum = getTxInputSum(tx);
      const outputSum = getTxOutputSum(tx);
      return acc + (inputSum - outputSum);
    }, 0);

  // 블록 보상 + 거래 수수료
  return blockReward + totalFees;
}

// 총 발행량 계산 (모든 반감기 보상 합계)
function calculateTotalSupply(): number {
  let total = 0;
  let halving = 0;

  while (halving < 64) {
    const reward = INITIAL_REWARD / Math.pow(2, halving);
    // 각 반감기 기간 동안 발행량 = 보상 * 블록 수
    total += reward * HALVING_INTERVAL;
    halving++;
  }

  return total;  // 약 20,999,999.9769 BTC ≈ 21,000,000 BTC
}

설명

이것이 하는 일: 채굴 보상 계산 로직은 비트코인의 통화 정책을 코드로 구현한 것입니다. 블록 높이만으로 현재 보상액을 정확히 계산하고, 미래의 발행량까지 예측할 수 있습니다.

먼저 getBlockReward 함수의 핵심을 봅시다. 블록 인덱스를 210,000으로 나눈 몫이 반감기 횟수입니다.

예를 들어 블록 0209,999는 Math.floor(0~209999 / 210000) = 0번째 반감기, 블록 210,000419,999는 1번째 반감기입니다. 그런 다음 50 / 2^0 = 50, 50 / 2^1 = 25처럼 계산하여 현재 보상을 구합니다.

64번 이상 반감되면 보상이 너무 작아져 사실상 0이므로 조기 종료합니다. 다음으로 getCoinbaseReward 함수는 실제 코인베이스 출력에 넣을 금액을 계산합니다.

블록 보상에 더해 거래 수수료도 포함시킵니다. 블록 내 모든 일반 거래(코인베이스 제외)의 입력 합계에서 출력 합계를 빼면, 그 차액이 수수료입니다.

예를 들어 입력 10 BTC, 출력 9.9 BTC인 거래는 0.1 BTC를 수수료로 지불한 것입니다. 모든 거래의 수수료를 합산하여 블록 보상에 더하면, 채굴자가 받을 총 금액이 됩니다.

calculateTotalSupply 함수는 비트코인의 총 발행량을 이론적으로 계산합니다. 각 반감기 동안 210,000블록이 생성되고, 각 블록마다 해당 보상이 지급됩니다.

첫 번째 반감기: 50 * 210,000 = 10,500,000 BTC, 두 번째: 25 * 210,000 = 5,250,000 BTC, 이런 식으로 계속 더하면 등비급수의 합이 되어 약 2,100만 BTC에 수렴합니다. 이는 수학적으로 증명 가능한 상한선입니다.

여러분이 이 코드를 사용하면 투명하고 예측 가능한 통화 시스템을 만들 수 있습니다. 누구든 현재와 미래의 발행량을 계산할 수 있고, 중앙 기관의 자의적인 정책 변경이 불가능하며, 인플레이션이 자동으로 통제되는 경제 모델을 구현할 수 있습니다.

실전 팁

💡 블록 높이 추적이 매우 중요합니다. 블록 인덱스를 잘못 관리하면 보상 계산이 틀어져 네트워크 전체가 해당 블록을 거부합니다. 제네시스 블록을 0으로 시작하는지 1로 시작하는지 명확히 정의하세요.

💡 부동소수점 정밀도 문제를 주의하세요. JavaScript의 number는 64비트 부동소수점이므로, 매우 작은 금액(10^-8 BTC, 즉 1 satoshi)을 다룰 때 오차가 생길 수 있습니다. 실제 구현에서는 satoshi(10^-8 BTC) 단위의 정수로 계산하는 것이 안전합니다.

💡 코인베이스 검증 시 출력 금액이 (블록 보상 + 수수료 합계)를 초과하지 않는지 확인해야 합니다. 채굴자가 임의로 더 많은 코인을 발행하려 하면 블록이 무효화됩니다.

💡 반감기는 시간이 아닌 블록 수로 결정됩니다. 비트코인은 평균 10분마다 1블록을 목표로 하지만, 실제로는 변동이 있습니다. 따라서 "4년마다 반감기"는 근사치이고, 정확히는 "210,000블록마다"입니다.

💡 먼 미래의 보상을 계산할 때 캐싱을 사용하세요. 블록 1,000,000의 보상은 한 번 계산하면 영원히 같은 값이므로, Map에 저장해두면 반복 계산을 피할 수 있습니다.


5. 코인베이스 트랜잭션 검증 - 특별한 거래의 특별한 규칙

시작하며

여러분이 블록체인 검증 로직을 작성하다가 이런 오류를 만난 적 있나요? "코인베이스 트랜잭션이 일반 거래 검증 로직에서 계속 실패합니다.

입력에 서명이 없다고 에러가 나는데, 어떻게 해야 할까요?" 이것은 많은 블록체인 초보자가 겪는 문제입니다. 코인베이스 트랜잭션은 일반 거래와 구조는 비슷하지만, 검증 규칙이 완전히 다릅니다.

입력이 이전 UTXO를 참조하지 않고, 서명도 필요 없으며, 새로운 코인을 "무에서" 생성하기 때문입니다. 바로 이럴 때 필요한 것이 코인베이스 전용 검증 로직입니다.

일반 거래와 분리하여 다른 규칙을 적용해야 블록 검증이 올바르게 동작합니다.

개요

간단히 말해서, 코인베이스 트랜잭션 검증은 일반 거래 검증과 다릅니다. 입력 검증을 건너뛰고, 출력 금액만 확인하며, 블록의 첫 번째 거래여야 한다는 추가 조건이 있습니다.

실무 관점에서 이는 블록 전체의 무결성을 보장하는 핵심입니다. 악의적인 채굴자가 과도한 보상을 발행하거나, 일반 거래를 코인베이스로 위장하는 것을 방지합니다.

예를 들어, 블록 높이 300,000일 때 보상은 25 BTC인데, 채굴자가 코인베이스에 50 BTC를 넣으면 블록 전체가 거부됩니다. 기존 일반 거래 검증은 "모든 입력의 서명 확인, UTXO 존재 확인, 금액 합계 확인"이라면, 코인베이스 검증은 "블록 위치 확인, 출력 금액 상한 확인, 입력 형식 확인"입니다.

핵심 특징으로는 첫째, 코인베이스는 블록당 정확히 1개만 존재하며 항상 첫 번째 거래입니다. 둘째, 입력의 txOutId는 빈 문자열이고 txOutIndex는 블록 높이입니다.

셋째, 출력 금액은 블록 보상 + 거래 수수료 합계 이하여야 합니다. 이러한 규칙들이 네트워크의 통화 정책을 강제합니다.

코드 예제

// 코인베이스 트랜잭션 여부 확인
function isCoinbaseTx(tx: Transaction): boolean {
  // 입력이 정확히 1개이고
  if (tx.txIns.length !== 1) {
    return false;
  }

  const txIn = tx.txIns[0];

  // txOutId가 빈 문자열이고 txOutIndex가 0 이상이면 코인베이스
  return txIn.txOutId === '' && txIn.txOutIndex >= 0;
}

// 코인베이스 트랜잭션 검증
function validateCoinbaseTx(
  tx: Transaction,
  blockIndex: number,
  transactions: Transaction[]
): boolean {
  // 1. 코인베이스 구조 확인
  if (!isCoinbaseTx(tx)) {
    console.error('코인베이스 구조가 올바르지 않습니다');
    return false;
  }

  // 2. 입력의 txOutIndex가 블록 높이와 일치하는지 확인
  if (tx.txIns[0].txOutIndex !== blockIndex) {
    console.error(`블록 높이 불일치: 예상 ${blockIndex}, 실제 ${tx.txIns[0].txOutIndex}`);
    return false;
  }

  // 3. 출력 금액 계산
  const totalOutput = tx.txOuts.reduce((sum, txOut) => sum + txOut.amount, 0);

  // 4. 허용된 최대 보상 계산 (블록 보상 + 수수료)
  const maxReward = getCoinbaseReward(blockIndex, transactions);

  // 5. 출력이 최대 보상을 초과하지 않는지 확인
  if (totalOutput > maxReward) {
    console.error(`과도한 보상: 최대 ${maxReward}, 실제 ${totalOutput}`);
    return false;
  }

  return true;
}

// 블록 검증 시 코인베이스 처리
function validateBlockTransactions(
  transactions: Transaction[],
  blockIndex: number
): boolean {
  // 첫 번째 거래가 코인베이스인지 확인
  if (transactions.length === 0 || !isCoinbaseTx(transactions[0])) {
    console.error('첫 번째 거래가 코인베이스가 아닙니다');
    return false;
  }

  // 코인베이스 검증
  if (!validateCoinbaseTx(transactions[0], blockIndex, transactions)) {
    return false;
  }

  // 나머지 거래가 코인베이스가 아닌지 확인
  for (let i = 1; i < transactions.length; i++) {
    if (isCoinbaseTx(transactions[i])) {
      console.error(`거래 ${i}가 코인베이스입니다. 코인베이스는 첫 번째만 가능합니다`);
      return false;
    }

    // 일반 거래 검증
    if (!validateTransaction(transactions[i])) {
      return false;
    }
  }

  return true;
}

설명

이것이 하는 일: 코인베이스 트랜잭션 검증은 채굴자가 정당한 보상만 받고 임의로 코인을 발행하지 못하도록 막는 핵심 보안 메커니즘입니다. 먼저 isCoinbaseTx 함수로 거래가 코인베이스인지 판단합니다.

코인베이스의 특징은 입력이 정확히 1개이고, 그 입력의 txOutId가 빈 문자열('')이라는 점입니다. 일반 거래는 항상 실제 트랜잭션 ID(64자리 16진수 해시)를 참조하므로, 빈 문자열이면 "이전 출력을 참조하지 않는다 = 코인베이스"입니다.

txOutIndex는 음수가 아닌 정수면 됩니다(보통 블록 높이). validateCoinbaseTx 함수는 여러 단계로 검증합니다.

첫째, 구조가 코인베이스인지 확인합니다. 둘째, 입력의 txOutIndex가 현재 블록 높이와 일치하는지 확인하여 거래가 이 블록을 위해 만들어졌음을 보장합니다(다른 블록의 코인베이스를 재사용하는 것 방지).

셋째, 모든 출력의 금액을 합산합니다. 넷째, 이 블록에서 허용되는 최대 보상(블록 보상 + 모든 거래 수수료)을 계산합니다.

마지막으로 출력 합계가 최대 보상을 초과하지 않는지 확인합니다. validateBlockTransactions 함수는 블록 전체의 거래를 검증합니다.

가장 먼저 첫 번째 거래가 코인베이스인지 확인합니다. 블록에 거래가 하나도 없거나 첫 번째가 일반 거래면 무효입니다.

그런 다음 코인베이스를 전용 검증 함수로 검증합니다. 이후 나머지 거래들(인덱스 1부터)을 순회하며, 코인베이스가 또 나오면 에러 처리하고, 일반 거래는 일반 검증 로직을 적용합니다.

여러분이 이 코드를 사용하면 채굴자의 부정행위를 완벽히 차단할 수 있습니다. 과도한 보상 발행, 코인베이스 위조, 블록 구조 오류 등을 자동으로 감지하여 네트워크 합의를 유지하고, 통화 정책이 코드에 명시된 대로 정확히 실행되도록 보장합니다.

실전 팁

💡 코인베이스 검증은 블록 검증의 가장 첫 단계여야 합니다. 코인베이스가 유효하지 않으면 나머지 거래를 검증할 필요 없이 블록 전체를 즉시 거부하여 CPU 시간을 절약할 수 있습니다.

💡 채굴자가 최대 보상보다 적게 받는 것은 허용됩니다. 예를 들어 보상이 50 BTC인데 채굴자가 코인베이스에 40 BTC만 넣으면, 10 BTC는 영원히 발행되지 않습니다. 실제로는 거의 없지만 프로토콜상 유효합니다.

💡 거래 수수료 합계를 계산할 때 코인베이스를 제외해야 합니다. 코인베이스는 입력이 없으므로 "입력 합계 - 출력 합계"를 계산하면 음수가 나옵니다. 반드시 transactions.filter(tx => !isCoinbaseTx(tx))로 필터링하세요.

💡 코인베이스의 서명 필드는 무시하거나, 임의의 데이터를 넣을 수 있습니다. 검증 시 서명을 확인하지 않으므로, 채굴자가 메시지를 남기는 용도로 사용할 수 있습니다(예: 비트코인 제네시스 블록의 신문 헤드라인).

💡 블록 높이가 txOutIndex와 일치하는지 확인하는 것은 선택적이지만 권장됩니다. 이를 통해 각 블록의 코인베이스가 고유하게 되어, 트랜잭션 ID 충돌을 방지할 수 있습니다.


6. 거래 수수료 계산 - 채굴자 인센티브와 네트워크 효율성

시작하며

여러분이 블록체인 거래를 보낼 때 "왜 수수료를 내야 하지? 이건 탈중앙화 네트워크인데?"라는 의문을 가져본 적 있나요?

은행 송금 수수료는 은행의 운영비와 이윤을 위한 것인데, 블록체인에서는 누구에게 가는 걸까요? 이것은 블록체인 경제학의 핵심입니다.

수수료는 채굴자에게 가며, 두 가지 중요한 역할을 합니다. 첫째, 블록 보상이 줄어들거나 없어진 후에도 채굴자가 네트워크를 유지하도록 동기부여합니다.

둘째, 블록 크기가 제한되어 있을 때 어떤 거래를 우선 처리할지 결정하는 메커니즘입니다. 바로 이럴 때 필요한 것이 거래 수수료 계산 로직입니다.

입력과 출력의 차액을 자동으로 계산하여 채굴자에게 인센티브를 제공하고, 네트워크 자원을 효율적으로 배분합니다.

개요

간단히 말해서, 거래 수수료는 트랜잭션의 입력 금액 합계에서 출력 금액 합계를 뺀 값입니다. 명시적으로 "수수료" 필드가 있는 것이 아니라, 암묵적으로 계산됩니다.

실무 관점에서 이는 블록체인의 지속 가능성을 보장합니다. 비트코인은 2140년경 모든 코인이 발행되면 블록 보상이 0이 되는데, 그때도 채굴자가 거래를 처리하도록 하려면 수수료가 필수입니다.

예를 들어, 10 BTC 입력, 9.99 BTC 출력인 거래는 0.01 BTC를 수수료로 지불하며, 이 블록을 채굴한 사람이 가져갑니다. 기존 중앙화 시스템은 "고정 수수료를 명시적으로 부과"라면, 비트코인은 "사용자가 임의로 정하되 시장 원리로 결정"입니다.

급한 거래는 높은 수수료, 여유 있는 거래는 낮은 수수료를 내는 자율적 시장이 형성됩니다. 핵심 특징으로는 첫째, 수수료는 선택적이지만 실제로는 필수입니다(수수료 없는 거래는 채굴자가 무시).

둘째, 수수료율은 사용자가 결정하며, 높을수록 빨리 처리됩니다. 셋째, 모든 수수료는 블록 채굴자에게 갑니다.

이러한 메커니즘이 탈중앙화된 인센티브 시스템을 만듭니다.

코드 예제

// 트랜잭션 입력 금액 합계 계산
function getTxInputSum(tx: Transaction, unspentTxOuts: UnspentTxOut[]): number {
  return tx.txIns.reduce((sum, txIn) => {
    // 각 입력이 참조하는 UTXO 찾기
    const utxo = unspentTxOuts.find(
      u => u.txOutId === txIn.txOutId && u.txOutIndex === txIn.txOutIndex
    );

    if (!utxo) {
      throw new Error(`UTXO를 찾을 수 없습니다: ${txIn.txOutId}:${txIn.txOutIndex}`);
    }

    return sum + utxo.amount;
  }, 0);
}

// 트랜잭션 출력 금액 합계 계산
function getTxOutputSum(tx: Transaction): number {
  return tx.txOuts.reduce((sum, txOut) => sum + txOut.amount, 0);
}

// 단일 트랜잭션의 수수료 계산
function calculateTxFee(tx: Transaction, unspentTxOuts: UnspentTxOut[]): number {
  // 코인베이스는 수수료 개념이 없음 (새로운 코인 생성)
  if (isCoinbaseTx(tx)) {
    return 0;
  }

  const inputSum = getTxInputSum(tx, unspentTxOuts);
  const outputSum = getTxOutputSum(tx);

  // 입력 - 출력 = 수수료
  return inputSum - outputSum;
}

// 블록 내 모든 거래 수수료 합계
function getTotalFees(transactions: Transaction[], unspentTxOuts: UnspentTxOut[]): number {
  return transactions
    .filter(tx => !isCoinbaseTx(tx))  // 코인베이스 제외
    .reduce((total, tx) => {
      return total + calculateTxFee(tx, unspentTxOuts);
    }, 0);
}

// 수수료율 계산 (바이트당 수수료)
function calculateFeeRate(tx: Transaction, unspentTxOuts: UnspentTxOut[]): number {
  const fee = calculateTxFee(tx, unspentTxOuts);

  // 트랜잭션 크기 (JSON 직렬화 후 바이트 수)
  const txSize = JSON.stringify(tx).length;

  // satoshi per byte
  return fee / txSize;
}

// 거래 우선순위 정렬 (수수료율 높은 순)
function prioritizeTransactions(
  transactions: Transaction[],
  unspentTxOuts: UnspentTxOut[]
): Transaction[] {
  return transactions
    .filter(tx => !isCoinbaseTx(tx))
    .sort((a, b) => {
      const feeRateA = calculateFeeRate(a, unspentTxOuts);
      const feeRateB = calculateFeeRate(b, unspentTxOuts);
      return feeRateB - feeRateA;  // 내림차순 (높은 수수료 우선)
    });
}

설명

이것이 하는 일: 거래 수수료 계산은 블록체인의 경제적 인센티브 구조를 만듭니다. 사용자는 네트워크 자원 사용에 대가를 지불하고, 채굴자는 거래 처리에 대한 보상을 받습니다.

먼저 입력 금액 합계 계산을 봅시다. getTxInputSum 함수는 트랜잭션의 모든 입력을 순회하며, 각 입력이 참조하는 UTXO를 찾습니다.

UTXO 풀에서 txOutIdtxOutIndex가 일치하는 항목을 찾아 그 금액을 합산합니다. 만약 UTXO를 찾지 못하면 이미 사용된 출력이거나 존재하지 않는 것이므로 에러를 발생시킵니다.

예를 들어 입력이 3개이고 각각 5, 3, 2 BTC를 참조하면 합계는 10 BTC입니다. 출력 금액 합계는 더 간단합니다.

getTxOutputSum 함수는 트랜잭션의 txOuts 배열을 순회하며 모든 amount를 더합니다. 출력이 2개이고 각각 7, 2.99 BTC면 합계는 9.99 BTC입니다.

calculateTxFee 함수는 이 두 값의 차이를 계산합니다. 10 BTC 입력 - 9.99 BTC 출력 = 0.01 BTC 수수료입니다.

사용자는 명시적으로 "수수료 0.01 BTC"라고 쓰지 않지만, 출력을 적게 만들어서 암묵적으로 수수료를 지불합니다. 코인베이스 트랜잭션은 입력이 없으므로(새로운 코인 생성) 수수료 개념이 없어 0을 반환합니다.

calculateFeeRate는 바이트당 수수료를 계산합니다. 절대 수수료보다 수수료율이 중요한 이유는, 큰 거래(입력/출력이 많아 데이터가 큼)는 블록 공간을 많이 차지하기 때문입니다.

1 MB 블록에 1 KB 거래 1,000개를 넣을 수 있는데, 100 KB 거래는 10개밖에 못 넣습니다. 따라서 채굴자는 "총 수수료"가 아닌 "바이트당 수수료"로 거래를 선택합니다.

prioritizeTransactions는 채굴자가 블록에 포함할 거래를 선택할 때 사용합니다. 수수료율이 높은 거래부터 정렬하여, 블록이 가득 찰 때까지 위에서부터 선택하면 채굴자 수익을 최대화할 수 있습니다.

여러분이 이 코드를 사용하면 지속 가능한 블록체인 경제를 구축할 수 있습니다. 사용자는 거래 속도와 비용을 스스로 선택하고, 채굴자는 시장 원리에 따라 보상을 받으며, 네트워크는 자원을 효율적으로 배분할 수 있습니다.

실전 팁

💡 수수료가 음수가 되면 안 됩니다. 입력보다 출력이 많으면 무언가 잘못된 것이므로, 거래 생성 시점에 inputSum >= outputSum 검증을 반드시 수행하세요. 음수 수수료는 프로토콜 위반입니다.

💡 실제 비트코인에서는 satoshi(10^-8 BTC) 단위로 계산합니다. JavaScript의 부동소수점 연산은 정밀도 문제가 있으므로, 모든 금액을 정수(satoshi)로 변환하여 계산하고 마지막에 BTC로 표시하는 것이 안전합니다.

💡 수수료 추정 로직을 제공하면 사용자 경험이 향상됩니다. 최근 블록들의 평균 수수료율을 계산하여 "빠름(상위 10%), 보통(중간값), 느림(하위 10%)" 같은 옵션을 제시할 수 있습니다.

💡 블록 크기 제한이 있는 경우, 채굴자는 수수료율 순으로 거래를 선택해야 수익을 최대화할 수 있습니다. 단순히 먼저 들어온 순서(FIFO)로 선택하면 수수료를 놓치게 됩니다.

💡 거래 풀(mempool)을 관리할 때, 오랫동안 확인되지 않는 저수수료 거래는 주기적으로 제거하세요. 그렇지 않으면 메모리가 무한정 증가합니다. 보통 24-72시간 후 제거하는 것이 일반적입니다.


7. UTXO 풀 관리 - 사용 가능한 출력 추적하기

시작하며

여러분이 "내 지갑에 5 BTC가 있습니다"라고 할 때, 블록체인은 실제로 어떻게 이 정보를 알고 있을까요? 계좌 잔액 테이블이 있는 걸까요?

아니면 매번 전체 거래 내역을 스캔해서 계산하는 걸까요? 이것은 UTXO(Unspent Transaction Output) 모델의 핵심 질문입니다.

비트코인은 "계좌"라는 개념이 없고, 대신 "아직 사용되지 않은 출력들의 집합"을 관리합니다. 마치 지갑 속의 동전과 지폐처럼, 각 UTXO는 독립적인 "조각"입니다.

바로 이럴 때 필요한 것이 UTXO 풀(Pool) 관리입니다. 모든 사용 가능한 출력을 효율적으로 추적하고, 거래가 발생할 때마다 업데이트하여 빠른 조회와 검증을 가능하게 합니다.

개요

간단히 말해서, UTXO 풀은 현재 블록체인에 존재하는 모든 미사용 트랜잭션 출력의 목록입니다. 새로운 블록이 추가될 때마다 이 풀을 업데이트하여 최신 상태를 유지합니다.

실무 관점에서 이는 블록체인의 상태를 효율적으로 관리하는 핵심 자료구조입니다. 전체 블록체인을 매번 스캔하지 않고도 특정 주소의 잔액을 즉시 계산하고, 거래가 유효한지 빠르게 검증할 수 있습니다.

예를 들어, 앨리스가 10 BTC를 가지고 있다는 것은 "앨리스 주소의 UTXO들의 합계가 10 BTC"라는 의미입니다. 기존 계좌 모델(Ethereum)은 "각 주소의 현재 잔액을 상태 변수로 저장"이라면, UTXO 모델은 "사용 가능한 모든 출력의 목록을 유지"입니다.

병렬 처리와 프라이버시 측면에서 장점이 있습니다. 핵심 특징으로는 첫째, UTXO 풀은 블록체인의 현재 "상태"를 나타냅니다.

둘째, 거래가 처리되면 입력으로 사용된 UTXO는 제거되고 새로운 출력이 추가됩니다. 셋째, UTXO 풀만 있으면 과거 블록 없이도 현재 상태를 알 수 있습니다.

이러한 특징이 블록체인 노드의 성능을 크게 향상시킵니다.

코드 예제

// 초기 UTXO 풀 생성 (제네시스 블록부터 모든 블록 처리)
function createUtxoPool(blockchain: Block[]): UnspentTxOut[] {
  let utxoPool: UnspentTxOut[] = [];

  blockchain.forEach(block => {
    block.data.forEach(tx => {
      // 1. 이 거래의 입력들이 소비하는 UTXO 제거
      tx.txIns.forEach(txIn => {
        utxoPool = utxoPool.filter(
          utxo => !(utxo.txOutId === txIn.txOutId && utxo.txOutIndex === txIn.txOutIndex)
        );
      });

      // 2. 이 거래의 출력들을 새 UTXO로 추가
      tx.txOuts.forEach((txOut, index) => {
        const newUtxo = new UnspentTxOut(tx.id, index, txOut.address, txOut.amount);
        utxoPool.push(newUtxo);
      });
    });
  });

  return utxoPool;
}

// 새 블록 추가 시 UTXO 풀 업데이트
function updateUtxoPool(
  currentPool: UnspentTxOut[],
  transactions: Transaction[]
): UnspentTxOut[] {
  // 현재 풀 복사 (불변성 유지)
  let newPool = [...currentPool];

  transactions.forEach(tx => {
    // 코인베이스가 아닌 경우에만 입력 처리
    if (!isCoinbaseTx(tx)) {
      // 소비된 UTXO 제거
      tx.txIns.forEach(txIn => {
        newPool = newPool.filter(
          utxo => !(utxo.txOutId === txIn.txOutId && utxo.txOutIndex === txIn.txOutIndex)
        );
      });
    }

    // 새로운 UTXO 추가
    tx.txOuts.forEach((txOut, index) => {
      const newUtxo = new UnspentTxOut(tx.id, index, txOut.address, txOut.amount);
      newPool.push(newUtxo);
    });
  });

  return newPool;
}

// 특정 주소의 잔액 계산
function getBalance(address: string, utxoPool: UnspentTxOut[]): number {
  return utxoPool
    .filter(utxo => utxo.address === address)
    .reduce((sum, utxo) => sum + utxo.amount, 0);
}

// 특정 주소의 UTXO 목록 조회
function findUtxosForAddress(address: string, utxoPool: UnspentTxOut[]): UnspentTxOut[] {
  return utxoPool.filter(utxo => utxo.address === address);
}

// UTXO 선택 (거래 생성 시 사용)
function selectUtxos(
  address: string,
  amount: number,
  utxoPool: UnspentTxOut[]
): { selectedUtxos: UnspentTxOut[]; change: number } {
  const myUtxos = findUtxosForAddress(address, utxoPool);

  // 금액이 큰 것부터 정렬 (입력 개수 최소화)
  myUtxos.sort((a, b) => b.amount - a.amount);

  let accumulated = 0;
  const selected: UnspentTxOut[] = [];

  for (const utxo of myUtxos) {
    selected.push(utxo);
    accumulated += utxo.amount;

    if (accumulated >= amount) {
      const change = accumulated - amount;
      return { selectedUtxos: selected, change };
    }
  }

  throw new Error(`잔액 부족: 필요 ${amount}, 보유 ${accumulated}`);
}

설명

이것이 하는 일: UTXO 풀 관리는 블록체인의 현재 상태를 효율적으로 추적하는 핵심 시스템입니다. 이를 통해 빠른 잔액 조회, 거래 검증, 새 거래 생성이 가능합니다.

먼저 createUtxoPool 함수는 전체 블록체인을 처음부터 스캔하여 UTXO 풀을 구축합니다. 제네시스 블록부터 시작하여 모든 블록의 모든 거래를 순회합니다.

각 거래마다 두 단계를 수행합니다. 첫째, 이 거래의 입력들이 참조하는 UTXO들을 풀에서 제거합니다(소비되었으므로).

둘째, 이 거래의 출력들을 새로운 UTXO로 풀에 추가합니다. 이 과정을 모든 거래에 적용하면, 최종적으로 "현재 사용 가능한 모든 출력"의 목록이 완성됩니다.

updateUtxoPool 함수는 새 블록이 추가될 때 풀을 증분 업데이트합니다. 전체를 다시 계산하지 않고, 새 블록의 거래들만 처리하여 효율성을 높입니다.

현재 풀을 복사하여 불변성을 유지하고(함수형 프로그래밍), 각 거래의 입력들을 풀에서 제거하고 출력들을 추가합니다. 코인베이스는 입력이 없으므로 제거 과정을 건너뜁니다.

getBalance 함수는 특정 주소의 잔액을 계산합니다. UTXO 풀에서 해당 주소의 모든 UTXO를 찾아 금액을 합산합니다.

예를 들어 앨리스 주소의 UTXO가 5 BTC, 3 BTC, 2 BTC 세 개 있으면 잔액은 10 BTC입니다. 이 연산은 O(n) 시간이 걸리지만, 전체 블록체인을 스캔하는 것(O(블록 수 × 거래 수))보다 훨씬 빠릅니다.

selectUtxos 함수는 거래를 생성할 때 어떤 UTXO들을 입력으로 사용할지 선택합니다. 지갑에 동전이 여러 개 있을 때 어떤 것을 사용할지 고르는 것과 같습니다.

큰 금액의 UTXO부터 선택하여 입력 개수를 최소화합니다(거래 크기 감소 = 수수료 절감). 필요한 금액이 모이면 거스름돈(change)을 계산하여 반환합니다.

예를 들어 7 BTC를 보내야 하는데 5 BTC, 3 BTC UTXO를 선택하면 총 8 BTC이므로 1 BTC가 거스름돈입니다. 여러분이 이 코드를 사용하면 블록체인 노드를 효율적으로 운영할 수 있습니다.

메모리에 UTXO 풀을 캐싱하여 빠른 조회를 제공하고, 거래 검증 시 O(1) 시간에 UTXO 존재 여부를 확인하며, 지갑 기능을 구현하여 사용자에게 실시간 잔액을 보여줄 수 있습니다.

실전 팁

💡 UTXO 풀은 Map 자료구조로 저장하면 조회 성능이 향상됩니다. Map<'txOutId:txOutIndex', UnspentTxOut> 형태로 키를 만들면, 특정 UTXO를 찾을 때 O(n) 대신 O(1) 시간에 접근할 수 있습니다.

💡 대규모 블록체인에서는 UTXO 풀을 디스크에 영속화해야 합니다. LevelDB나 RocksDB 같은 키-값 데이터베이스를 사용하면, 메모리 부족 없이 수백만 개의 UTXO를 관리할 수 있습니다.

💡 블록 재구성(reorg) 시 UTXO 풀도 롤백해야 합니다. 체인이 분기되어 다른 브랜치가 메인이 되면, 무효화된 블록의 변경사항을 되돌리고 새 블록들을 적용해야 합니다. 각 블록마다 UTXO 풀 스냅샷을 저장하면 쉽게 롤백할 수 있습니다.

💡 UTXO 선택 알고리즘은 여러 전략이 있습니다. "가장 큰 것부터"는 입력 수를 줄이지만 UTXO 파편화를 유발할 수 있습니다. "가장 작은 것부터"는 작은 UTXO를 정리하지만 입력이 많아져 수수료가 높습니다. 상황에 맞게 선택하세요.

💡 주기적으로 UTXO 풀의 무결성을 검증하세요. 예를 들어 풀의 총 금액이 "총 발행량 - 소각된 양"과 일치하는지 확인하면, 버그로 인한 코인 생성/소멸을 감지할 수 있습니다.


8. 트랜잭션 생성 - 사용자가 거래를 만드는 전체 과정

시작하며

여러분이 친구에게 3 BTC를 보내려고 할 때, "그냥 '밥에게 3 BTC 전송' 버튼만 누르면 되는 거 아닌가?"라고 생각할 수 있습니다. 하지만 그 버튼 뒤에서는 복잡한 과정이 일어납니다.

실제로 거래를 생성하려면 여러 단계를 거쳐야 합니다. 지갑에서 사용 가능한 UTXO를 찾고, 필요한 만큼 선택하고, 거스름돈 계산하고, 각 입력에 서명하고, 트랜잭션 ID를 계산하고...

이 모든 과정이 자동으로 이루어져야 사용자는 간단히 버튼만 누르면 됩니다. 바로 이럴 때 필요한 것이 트랜잭션 생성 로직입니다.

사용자의 의도("A에게 X 코인 보내기")를 프로토콜이 요구하는 형식(입력, 출력, 서명)으로 변환하는 전체 과정을 구현합니다.

개요

간단히 말해서, 트랜잭션 생성은 UTXO 선택, 입출력 구성, 서명, ID 계산의 4단계로 이루어집니다. 이 과정이 자동화되어야 사용자 친화적인 지갑을 만들 수 있습니다.

실무 관점에서 이는 블록체인 애플리케이션의 핵심 기능입니다. 복잡한 UTXO 모델을 추상화하여 사용자에게 간단한 인터페이스를 제공합니다.

예를 들어, "밥에게 3 BTC, 앨리스에게 2 BTC"라는 요청을 받으면, 자동으로 적절한 UTXO를 선택하고, 필요하면 거스름돈 출력을 추가하고, 모든 입력에 서명하여 유효한 거래를 만듭니다. 기존 중앙화 시스템은 "계좌 잔액에서 차감, 수신자 계좌에 추가"라는 단순한 로직이라면, UTXO 기반 블록체인은 "여러 조각(UTXO)을 모아 사용하고 새 조각 생성"하는 복잡한 과정입니다.

핵심 특징으로는 첫째, 모든 입력에 개별적으로 서명해야 합니다. 둘째, 출력 합계가 입력 합계보다 적어야 합니다(차액은 수수료).

셋째, 트랜잭션 ID는 모든 입출력의 해시이므로 거래 내용이 변조 불가능합니다. 이러한 과정이 보안과 무결성을 보장합니다.

코드 예제

// 트랜잭션 생성 헬퍼 함수
function createTransaction(
  senderAddress: string,
  senderPrivateKey: string,
  recipientAddress: string,
  amount: number,
  utxoPool: UnspentTxOut[],
  feePerByte: number = 0.0001
): Transaction {
  // 1. 필요한 UTXO 선택
  const { selectedUtxos, change } = selectUtxos(senderAddress, amount, utxoPool);

  // 2. 입력 생성 (서명 전에는 빈 서명)
  const txIns: TxIn[] = selectedUtxos.map(utxo =>
    new TxIn(utxo.txOutId, utxo.txOutIndex, '')
  );

  // 3. 출력 생성
  const txOuts: TxOut[] = [
    new TxOut(recipientAddress, amount)  // 수신자 출력
  ];

  // 거스름돈이 있으면 자신에게 출력 추가
  if (change > 0) {
    txOuts.push(new TxOut(senderAddress, change));
  }

  // 4. 임시 트랜잭션 ID 계산 (서명 전)
  const tempTx: Transaction = {
    id: '',
    txIns,
    txOuts,
    timestamp: Date.now()
  };

  tempTx.id = calculateTransactionId(tempTx);

  // 5. 각 입력에 서명
  txIns.forEach((txIn, index) => {
    txIn.signature = signTxIn(tempTx, index, senderPrivateKey, utxoPool);
  });

  // 6. 최종 트랜잭션 ID 재계산 (서명 포함 안 함)
  const finalTx: Transaction = {
    id: tempTx.id,  // ID는 서명 전 상태로 계산됨
    txIns,
    txOuts,
    timestamp: tempTx.timestamp
  };

  return finalTx;
}

// 트랜잭션 ID 계산
function calculateTransactionId(tx: Transaction): string {
  // 입력: txOutId + txOutIndex 결합
  const txInContent = tx.txIns
    .map(txIn => txIn.txOutId + txIn.txOutIndex)
    .join('');

  // 출력: address + amount 결합
  const txOutContent = tx.txOuts
    .map(txOut => txOut.address + txOut.amount)
    .join('');

  // 전체 해시
  return crypto
    .createHash('sha256')
    .update(txInContent + txOutContent + tx.timestamp)
    .digest('hex');
}

// 다중 수신자 거래 생성
function createMultiOutputTransaction(
  senderAddress: string,
  senderPrivateKey: string,
  recipients: Array<{ address: string; amount: number }>,
  utxoPool: UnspentTxOut[]
): Transaction {
  // 총 전송 금액 계산
  const totalAmount = recipients.reduce((sum, r) => sum + r.amount, 0);

  // UTXO 선택
  const { selectedUtxos, change } = selectUtxos(senderAddress, totalAmount, utxoPool);

  // 입력 생성
  const txIns = selectedUtxos.map(utxo => new TxIn(utxo.txOutId, utxo.txOutIndex, ''));

  // 출력 생성 (모든 수신자)
  const txOuts = recipients.map(r => new TxOut(r.address, r.amount));

  // 거스름돈 출력 추가
  if (change > 0) {
    txOuts.push(new TxOut(senderAddress, change));
  }

  // 트랜잭션 완성
  const tx: Transaction = {
    id: '',
    txIns,
    txOuts,
    timestamp: Date.now()
  };

  tx.id = calculateTransactionId(tx);

  // 서명
  txIns.forEach((txIn, index) => {
    txIn.signature = signTxIn(tx, index, senderPrivateKey, utxoPool);
  });

  return tx;
}

설명

이것이 하는 일: 트랜잭션 생성은 사용자의 간단한 요청을 블록체인이 이해할 수 있는 암호학적으로 안전한 데이터 구조로 변환하는 과정입니다. 첫 번째 단계는 UTXO 선택입니다.

selectUtxos 함수를 호출하여 발신자 주소의 UTXO 중에서 필요한 금액을 충족하는 것들을 선택합니다. 예를 들어 3 BTC를 보내야 하는데 지갑에 5 BTC, 2 BTC UTXO가 있다면 5 BTC 하나만 선택하고 2 BTC를 거스름돈으로 받습니다.

이 과정에서 잔액이 부족하면 에러가 발생합니다. 두 번째 단계는 입력 배열 생성입니다.

선택된 각 UTXO를 TxIn 객체로 변환합니다. 이때 서명 필드는 빈 문자열로 남겨둡니다.

트랜잭션 ID를 계산할 때는 서명이 없는 상태여야 하기 때문입니다(서명은 ID에 의존하므로 순환 참조 방지). 세 번째 단계는 출력 배열 생성입니다.

먼저 수신자 주소와 전송 금액으로 TxOut을 만듭니다. 그런 다음 거스름돈이 있으면(입력 합계 > 전송 금액) 발신자 자신에게 돌아오는 출력을 추가합니다.

거스름돈을 빼먹으면 그 금액이 전부 수수료로 가므로 주의해야 합니다! 네 번째 단계는 트랜잭션 ID 계산입니다.

calculateTransactionId 함수는 모든 입력의 txOutId + txOutIndex와 모든 출력의 address + amount, 그리고 타임스탬프를 결합하여 SHA-256 해시를 계산합니다. 이 ID는 거래의 고유 식별자이자 무결성 증명입니다.

입출력 중 하나라도 변경되면 ID가 완전히 달라집니다. 다섯 번째 단계는 서명입니다.

모든 입력에 대해 signTxIn을 호출하여 발신자의 개인키로 서명합니다. 각 입력은 다른 UTXO를 참조할 수 있으므로(다른 주소 소유일 수도), 개별적으로 서명해야 합니다.

서명은 "나는 이 UTXO의 정당한 소유자이며, 이 거래를 승인한다"는 증거입니다. createMultiOutputTransaction은 여러 수신자에게 동시에 전송하는 경우입니다.

예를 들어 밥에게 2 BTC, 앨리스에게 1 BTC를 한 번에 보낼 수 있습니다. 출력을 여러 개 만들어 각 수신자의 출력을 추가하고, 마찬가지로 거스름돈 출력도 포함시킵니다.

여러분이 이 코드를 사용하면 사용자는 "주소와 금액"만 입력하면 되고, 나머지 복잡한 과정은 자동으로 처리됩니다. 지갑 애플리케이션, 거래소, DApp 등 모든 블록체인 애플리케이션의 기반이 되는 기능입니다.

실전 팁

💡 거스름돈 주소를 매번 새로 생성하면 프라이버시가 향상됩니다. 항상 발신자 주소로 거스름돈을 받으면 거래 패턴이 노출되지만, HD 지갑(Hierarchical Deterministic Wallet)으로 매번 새 주소를 생성하면 추적이 어려워집니다.

💡 수수료를 명시적으로 계산하여 사용자에게 보여주세요. (입력 합계) - (출력 합계) = 수수료이므로, 거래 생성 전에 예상 수수료를 표시하면 사용자가 실수로 높은 수수료를 지불하는 것을 방지할 수 있습니다.

💡 트랜잭션 ID 계산 시 정규화된 형식을 사용하세요. 숫자를 문자열로 변환할 때 부동소수점 표현이 달라지면 같은 거래인데도 다른 ID가 나올 수 있습니다. JSON.stringify() 대신 명확히 정의된 직렬화 방법을 사용하세요.

💡 서명 전에 트랜잭션을 검증하면 불필요한 작업을 줄일 수 있습니다. 예를 들어 출력 합계가 입력 합계보다 크면 어차피 무효이므로, 서명 단계 전에 미리 확인하여 CPU를 절약하세요.

💡 대용량 거래(입력/출력이 많음)는 수수료가 높아집니다. 가능하면 UTXO를 통합(여러 작은 UTXO를 하나로 합치는 거래)하여 미래의 수수료를 줄이는 전략을 고려하세요. 네트워크가 한가할 때 저수수료로 통합 거래를 보내면 효율적입니다.


9. 트랜잭션 검증 - 거래의 유효성을 철저히 확인하기

시작하며

여러분이 블록체인 네트워크 노드를 운영하고 있다고 상상해보세요. 다른 노드로부터 새로운 거래를 받았을 때, "이 거래를 믿어도 될까?

악의적인 사용자가 만든 가짜 거래는 아닐까?"라는 의문이 듭니다. 이것은 탈중앙화 시스템의 핵심 도전 과제입니다.

중앙 서버가 모든 것을 검증해주는 것이 아니라, 각 노드가 독립적으로 모든 거래를 검증해야 합니다. 하나라도 잘못된 거래가 블록에 포함되면 전체 블록이 무효가 되고, 네트워크 합의가 깨집니다.

바로 이럴 때 필요한 것이 포괄적인 트랜잭션 검증 로직입니다. 구조, 서명, UTXO 존재, 금액, 이중 지불 등 모든 측면을 체크하여 악의적이거나 잘못된 거래를 걸러냅니다.

개요

간단히 말해서, 트랜잭션 검증은 구조 검증, 입력 검증(UTXO 존재, 서명 확인), 금액 검증(입력 ≥ 출력)의 다층 방어 체계입니다. 실무 관점에서 이는 블록체인 보안의 최전선입니다.

잘못된 거래가 블록에 포함되면 체인이 분기되고, 노드 간 합의가 깨지며, 이중 지불 같은 공격이 성공할 수 있습니다. 예를 들어, 존재하지 않는 UTXO를 사용하는 거래, 서명이 잘못된 거래, 출력이 입력보다 많은 거래 등은 즉시 거부되어야 합니다.

기존 중앙화 시스템은 "서버가 권한과 잔액만 확인"이라면, 블록체인은 "모든 노드가 암호학적 증명과 전체 이력을 확인"합니다. 훨씬 철저하고 투명합니다.

핵심 특징으로는 첫째, 모든 검증은 로컬에서 수행 가능합니다(다른 노드에 의존 안 함). 둘째, 검증 실패 시 구체적인 이유를 로깅하여 디버깅을 돕습니다.

셋째, 코인베이스는 별도 규칙으로 검증합니다. 이러한 엄격한 검증이 네트워크의 무결성을 지킵니다.

코드 예제

// 트랜잭션 구조 검증
function validateTransactionStructure(tx: Transaction): boolean {
  // 기본 필드 존재 확인
  if (typeof tx.id !== 'string' || tx.id.length !== 64) {
    console.error('유효하지 않은 트랜잭션 ID');
    return false;
  }

  if (!Array.isArray(tx.txIns) || !Array.isArray(tx.txOuts)) {
    console.error('입력 또는 출력이 배열이 아닙니다');
    return false;
  }

  if (tx.txIns.length === 0) {
    console.error('입력이 없습니다');
    return false;
  }

  if (tx.txOuts.length === 0) {
    console.error('출력이 없습니다');
    return false;
  }

  // 각 입력의 구조 확인
  const validIns = tx.txIns.every(txIn =>
    typeof txIn.txOutId === 'string' &&
    typeof txIn.txOutIndex === 'number' &&
    typeof txIn.signature === 'string'
  );

  if (!validIns) {
    console.error('입력 구조가 올바르지 않습니다');
    return false;
  }

  // 각 출력의 구조 및 금액 확인
  const validOuts = tx.txOuts.every(txOut =>
    typeof txOut.address === 'string' &&
    typeof txOut.amount === 'number' &&
    txOut.amount > 0  // 음수 또는 0 금액 방지
  );

  if (!validOuts) {
    console.error('출력 구조가 올바르지 않거나 금액이 유효하지 않습니다');
    return false;
  }

  return true;
}

// 완전한 트랜잭션 검증
function validateTransaction(
  tx: Transaction,
  utxoPool: UnspentTxOut[]
): boolean {
  // 1. 구조 검증
  if (!validateTransactionStructure(tx)) {
    return false;
  }

  // 2. 트랜잭션 ID 재계산 및 확인
  const calculatedId = calculateTransactionId(tx);
  if (tx.id !== calculatedId) {
    console.error(`ID 불일치: 예상 ${calculatedId}, 실제 ${tx.id}`);
    return false;
  }

  // 3. 각 입력 검증
  for (let i = 0; i < tx.txIns.length; i++) {
    const txIn = tx.txIns[i];

    // UTXO 존재 확인
    const referencedUtxo = utxoPool.find(
      utxo => utxo.txOutId === txIn.txOutId && utxo.txOutIndex === txIn.txOutIndex
    );

    if (!referencedUtxo) {
      console.error(`UTXO를 찾을 수 없음: ${txIn.txOutId}:${txIn.txOutIndex}`);
      return false;
    }

    // 서명 검증
    if (!validateTxIn(txIn, tx, utxoPool)) {
      console.error(`입력 ${i}의 서명이 유효하지 않습니다`);
      return false;
    }
  }

  // 4. 금액 검증 (입력 합계 >= 출력 합계)
  const inputSum = getTxInputSum(tx, utxoPool);
  const outputSum = getTxOutputSum(tx);

  if (inputSum < outputSum) {
    console.error(`금액 초과: 입력 ${inputSum}, 출력 ${outputSum}`);
    return false;
  }

  return true;
}

// 이중 지불 검증 (같은 UTXO를 여러 번 사용하는지 확인)
function hasDoubleSpending(transactions: Transaction[]): boolean {
  const usedUtxos = new Set<string>();

  for (const tx of transactions) {
    if (isCoinbaseTx(tx)) continue;

    for (const txIn of tx.txIns) {
      const utxoKey = `${txIn.txOutId}:${txIn.txOutIndex}`;

      if (usedUtxos.has(utxoKey)) {
        console.error(`이중 지불 감지: ${utxoKey}`);
        return true;
      }

      usedUtxos.add(utxoKey);
    }
  }

  return false;
}

// 블록 내 모든 거래 검증
function validateBlockTransactions(
  transactions: Transaction[],
  utxoPool: UnspentTxOut[],
  blockIndex: number
): boolean {
  // 코인베이스 검증
  if (!validateCoinbaseTx(transactions[0], blockIndex, transactions)) {
    return false;
  }

  // 일반 거래 검증
  for (let i = 1; i < transactions.length; i++) {
    if (!validateTransaction(transactions[i], utxoPool)) {
      console.error(`거래 ${i} 검증 실패`);
      return false;
    }
  }

  // 블록 내 이중 지불 확인
  if (hasDoubleSpending(transactions)) {
    return false;
  }

  return true;
}

설명

이것이 하는 일: 트랜잭션 검증은 블록체인 네트워크의 면역 체계입니다. 각 노드가 독립적으로 모든 거래를 검증하여 악의적인 행위를 차단하고 네트워크 합의를 유지합니다.

먼저 validateTransactionStructure 함수는 기본적인 데이터 구조를 확인합니다. 트랜잭션 ID가 64자리 16진수 문자열인지(SHA-256 해시 형식), 입력과 출력이 배열인지, 최소한 하나 이상씩 존재하는지 검사합니다.

각 입력의 필드(txOutId, txOutIndex, signature)와 출력의 필드(address, amount)가 올바른 타입인지 확인합니다. 특히 출력 금액이 양수인지 체크하여 음수나 0 금액 출력을 방지합니다.

validateTransaction 함수는 더 깊은 검증을 수행합니다. 첫 번째로 구조 검증을 통과했는지 확인합니다.

두 번째로 트랜잭션 ID를 재계산하여 거래 내용이 변조되지 않았는지 검증합니다. ID는 입출력의 해시이므로, 조금이라도 변경되면 ID가 달라집니다.

세 번째로 모든 입력이 유효한 UTXO를 참조하는지, 그리고 서명이 올바른지 확인합니다. UTXO 풀에서 각 입력이 참조하는 출력을 찾고, 찾지 못하면 이미 사용되었거나 존재하지 않는 것이므로 거부합니다.

네 번째로 입력 합계가 출력 합계 이상인지 확인하여 "무에서 유를 창조"하는 것을 방지합니다. hasDoubleSpending 함수는 블록 내에서 이중 지불을 감지합니다.

같은 UTXO를 여러 거래의 입력으로 사용하면 이중 지불입니다. Set 자료구조를 사용하여 이미 사용된 UTXO를 추적하고, 중복이 발견되면 즉시 true를 반환합니다.

예를 들어 거래 A와 거래 B가 모두 UTXO "abc123:0"을 입력으로 사용하면, 한 거래만 유효하고 다른 것은 무효입니다. validateBlockTransactions는 블록 전체를 검증합니다.

첫 번째 거래는 코인베이스이므로 전용 검증 함수를 사용하고, 나머지는 일반 검증을 적용합니다. 마지막으로 블록 내 이중 지불이 없는지 확인합니다.

블록 내에서는 이중 지불이 없어야 하지만, 다른 블록(아직 확인되지 않은)의 거래와 충돌할 수 있으므로 멤풀 관리도 중요합니다. 여러분이 이 코드를 사용하면 노드가 자동으로 잘못된 거래를 걸러냅니다.

네트워크에 악의적인 노드가 있어도, 각 노드가 독립적으로 검증하므로 잘못된 블록은 합의에 포함되지 않습니다. 이것이 블록체인의 탈중앙화된 보안 모델의 핵심입니다.

실전 팁

💡 검증 실패 시 상세한 로그를 남기세요. "거래 무효"보다 "UTXO abc123:0을 찾을 수 없음, 이미 사용되었거나 존재하지 않음"이 훨씬 유용합니다. 디버깅 시간을 크게 줄일 수 있습니다.

💡 검증 순서를 최적화하여 빠른 거부(fast rejection)를 구현하세요. 구조 검증 → ID 검증 → UTXO 존재 → 서명 순으로 진행하면, 간단한 오류는 빠르게 걸러져 CPU를 절약할 수 있습니다. 서명 검증은 비용이 높으므로 마지막에 수행합니다.

💡 UTXO 풀 조회를 최적화하세요. 배열 대신 Map을 사용하면 find() 연산이 O(n)에서 O(1)로 개선됩니다. 수천 개의 UTXO가 있을 때 성능 차이가 큽니다.

💡 멤풀(mempool, 아직 블록에 포함되지 않은 거래 풀)에서도 이중 지불을 확인하세요. 블록 내뿐 아니라 멤풀 내에서도 같은 UTXO를 사용하는 거래가 여러 개 있으면 하나만 선택해야 합니다(보통 높은 수수료).

💡 타임스탬프 검증을 추가하면 더 안전합니다. 미래 시간의 거래나 너무 오래된 거래를 거부하여 타임스탬프 조작 공격을 방지할 수 있습니다. 예를 들어 현재 시간 ± 2시간 범위 밖의 거래는 무효로 처리합니다.


10. 코인베이스 성숙도 - 채굴 보상 사용 제한하기

시작하며

여러분이 블록을 채굴하여 50 BTC를 받았다고 상상해보세요. "좋아, 바로 친구에게 10 BTC를 보내자!"라고 생각할 수 있습니다.

하지만 실제 비트코인에서는 방금 채굴한 보상을 즉시 사용할 수 없습니다. 이것은 블록체인의 안전장치 중 하나입니다.

만약 채굴한 블록이 나중에 체인 재구성(reorg)으로 무효화되면 어떻게 될까요? 이미 그 보상으로 거래를 했다면 그 거래들도 모두 무효가 됩니다.

이는 체인 전체에 혼란을 일으킵니다. 바로 이럴 때 필요한 것이 코인베이스 성숙도(Coinbase Maturity) 규칙입니다.

채굴 보상은 일정 블록(보통 100블록) 후에야 사용할 수 있도록 제한하여 체인 안정성을 보장합니다.

개요

간단히 말해서, 코인베이스 성숙도는 채굴 보상으로 받은 코인이 특정 블록 수 이후에야 거래에 사용될 수 있도록 하는 규칙입니다. 비트코인은 100블록(약 16시간) 이후에 사용 가능합니다.

실무 관점에서 이는 체인 재구성 시 발생할 수 있는 복잡한 문제를 예방합니다. 블록체인은 때때로 분기되어 더 긴 체인이 메인 체인이 됩니다.

이때 무효화된 블록의 채굴 보상도 사라집니다. 예를 들어, 블록 1000의 보상을 블록 1001에서 사용했는데, 블록 1000이 무효화되면 블록 1001의 거래도 무효가 되어 연쇄적인 문제가 발생합니다.

기존 일반 UTXO는 "즉시 사용 가능"이라면, 코인베이스 UTXO는 "성숙 기간 후 사용 가능"입니다. 이는 채굴자에게는 약간의 불편이지만 네트워크 전체의 안정성에 기여합니다.

핵심 특징으로는 첫째, 성숙도는 블록 수로 측정됩니다(시간이 아님). 둘째, 일반 거래 출력에는 적용되지 않고 코인베이스에만 적용됩니다.

셋째, 성숙되지 않은 코인베이스


#TypeScript#Blockchain#Bitcoin#Coinbase#Mining#typescript

댓글 (0)

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