이미지 로딩 중...

타입스크립트로 비트코인 클론하기 28편 - 잔액 조회 및 송금 기능 - 슬라이드 1/11
A

AI Generated

2025. 11. 11. · 2 Views

타입스크립트로 비트코인 클론하기 28편 - 잔액 조회 및 송금 기능

블록체인 네트워크에서 지갑의 잔액을 조회하고 안전하게 송금하는 기능을 구현합니다. UTXO 모델을 활용한 트랜잭션 생성과 디지털 서명을 통한 보안, 그리고 네트워크 브로드캐스팅까지 실무에서 사용되는 암호화폐 송금 시스템을 완성합니다.


목차

  1. UTXO 기반 잔액 조회 시스템 - 블록체인에서 사용 가능한 코인 찾기
  2. 트랜잭션 입력 선택 알고리즘 - 송금에 사용할 UTXO 고르기
  3. 트랜잭션 생성 및 서명 - 안전한 송금 데이터 만들기
  4. 거스름돈 처리 메커니즘 - UTXO 모델의 핵심 개념
  5. 트랜잭션 브로드캐스팅 - 네트워크에 전파하기
  6. 트랜잭션 수수료 계산 - 채굴자에게 인센티브 제공
  7. 멤풀 관리 - 미확정 트랜잭션 처리
  8. 트랜잭션 검증 시스템 - 블록체인의 보안 핵심
  9. HD 지갑과 주소 파생 - 프라이버시와 편의성
  10. 블록 생성과 트랜잭션 포함 - 채굴자의 역할

1. UTXO 기반 잔액 조회 시스템 - 블록체인에서 사용 가능한 코인 찾기

시작하며

여러분이 블록체인 지갑 앱을 만들 때 "내 지갑에 얼마가 있지?"라는 가장 기본적인 질문에 답해야 합니다. 일반 은행 앱처럼 단순히 잔액 필드를 조회하면 될 것 같지만, 블록체인은 완전히 다르게 동작합니다.

비트코인과 같은 UTXO(Unspent Transaction Output) 모델에서는 "잔액"이라는 개념이 직접적으로 존재하지 않습니다. 대신 블록체인 전체를 스캔하여 아직 사용되지 않은 나에게 온 트랜잭션 출력들을 모두 찾아서 합산해야 합니다.

마치 지갑에 흩어진 동전들을 세는 것과 비슷합니다. 바로 이럴 때 필요한 것이 UTXO 기반 잔액 조회 시스템입니다.

블록체인의 모든 블록을 순회하며 특정 주소로 온 미사용 출력을 찾아내고, 이를 집계하여 실제 사용 가능한 잔액을 계산합니다.

개요

간단히 말해서, UTXO 기반 잔액 조회는 블록체인 전체에서 특정 주소가 받았지만 아직 사용하지 않은 모든 트랜잭션 출력을 찾아 합산하는 방식입니다. 왜 이 방식이 필요한지 이해하려면 비트코인의 철학을 알아야 합니다.

중앙 서버에 "잔액: 10 BTC"라고 저장하는 대신, 모든 거래 기록을 투명하게 보관하고 누구나 검증할 수 있게 합니다. 예를 들어, 해커가 데이터베이스를 조작해서 잔액을 늘리는 것이 불가능해집니다.

모든 코인의 출처가 블록체인에 기록되어 있기 때문입니다. 기존 중앙화된 시스템에서는 데이터베이스의 balance 컬럼만 조회했다면, 이제는 전체 블록체인을 스캔하여 실시간으로 계산해야 합니다.

이 시스템의 핵심 특징은 첫째, 완전한 투명성입니다. 모든 코인의 출처를 추적할 수 있습니다.

둘째, 이중 지불 방지입니다. 사용된 UTXO는 다시 사용할 수 없습니다.

셋째, 분산 검증이 가능합니다. 누구나 독립적으로 잔액을 검증할 수 있습니다.

이러한 특징들이 블록체인의 신뢰성을 보장하는 핵심입니다.

코드 예제

// Wallet 클래스의 잔액 조회 메서드
public getBalance(): number {
  // 전체 블록체인에서 미사용 UTXO 조회
  const unspentTxOuts = this.blockchain.getUnspentTxOuts();

  // 내 주소로 온 UTXO만 필터링
  const myUnspentTxOuts = unspentTxOuts.filter(
    (uTxO: UnspentTxOut) => uTxO.address === this.address
  );

  // 모든 UTXO의 amount를 합산하여 잔액 계산
  return myUnspentTxOuts.reduce(
    (sum, uTxO) => sum + uTxO.amount,
    0
  );
}

설명

이것이 하는 일: 블록체인에 저장된 모든 블록과 트랜잭션을 순회하면서 특정 주소가 소유한 아직 사용되지 않은 트랜잭션 출력(UTXO)을 모두 찾아내고, 그 금액들을 합산하여 실제 사용 가능한 잔액을 계산합니다. 첫 번째로, this.blockchain.getUnspentTxOuts() 메서드가 실행되어 전체 블록체인에서 아직 사용되지 않은 모든 UTXO 목록을 가져옵니다.

이 과정에서 블록체인의 모든 블록을 순회하며 트랜잭션 출력 중 입력으로 참조되지 않은 것들만 선별합니다. 내부적으로는 각 블록의 트랜잭션을 검사하고, 이미 다른 트랜잭션의 입력으로 사용된 출력은 제외하는 복잡한 필터링 작업이 이루어집니다.

그 다음으로, filter 메서드를 사용하여 모든 UTXO 중에서 현재 지갑의 주소(this.address)와 일치하는 것들만 걸러냅니다. 블록체인에는 수많은 사용자들의 UTXO가 섞여 있기 때문에, 내 것만 정확히 식별하는 것이 중요합니다.

각 UTXO의 address 필드가 내 지갑 주소와 정확히 일치하는지 비교하여, 내가 실제로 사용할 수 있는 코인만 추립니다. 마지막으로, reduce 함수가 필터링된 UTXO 배열을 순회하면서 각 UTXO의 amount 값을 누적하여 합산합니다.

초기값 0에서 시작하여 각 UTXO의 금액을 더해나가며, 최종적으로 내가 사용할 수 있는 총 잔액을 반환합니다. 여러분이 이 코드를 사용하면 중앙 서버나 데이터베이스 없이도 완전히 분산된 방식으로 지갑 잔액을 계산할 수 있습니다.

모든 노드가 동일한 블록체인을 가지고 있다면 누구나 같은 결과를 얻을 수 있어 투명성과 검증 가능성이 보장됩니다. 또한 해킹이나 데이터 조작이 불가능하며, 모든 코인의 출처를 추적할 수 있어 보안성도 뛰어납니다.

실전 팁

💡 잔액 조회는 블록체인 크기에 비례하여 느려질 수 있으므로, 프로덕션 환경에서는 UTXO를 별도 인덱스에 캐싱하고 새 블록이 추가될 때만 업데이트하는 방식을 사용하세요.

💡 대규모 블록체인에서는 전체 스캔 대신 특정 주소의 UTXO만 추적하는 Bloom Filter나 주소 인덱스를 구축하여 조회 성능을 100배 이상 향상시킬 수 있습니다.

💡 잔액이 0인지 확인할 때는 getBalance() === 0보다 getBalance() < 최소_거래_금액을 사용하여 먼지(dust) 트랜잭션을 방지하세요. 실제 비트코인에서는 546 사토시 미만은 거래할 수 없습니다.

💡 멀티 스레드 환경에서는 잔액 조회 중 새 블록이 추가될 수 있으므로, 스냅샷 격리(Snapshot Isolation)를 구현하여 일관된 읽기를 보장하세요.

💡 테스트 시에는 모의 블록체인을 사용하여 다양한 UTXO 시나리오(여러 개의 작은 UTXO, 하나의 큰 UTXO 등)를 검증하고, 경계 조건(잔액 0, 소수점 처리 등)도 반드시 테스트하세요.


2. 트랜잭션 입력 선택 알고리즘 - 송금에 사용할 UTXO 고르기

시작하며

여러분이 친구에게 0.5 BTC를 보내려고 할 때, 지갑에는 0.3 BTC, 0.2 BTC, 0.4 BTC 짜리 UTXO가 여러 개 흩어져 있습니다. 이 중 어떤 것들을 선택해서 송금해야 할까요?

단순해 보이지만 이는 매우 중요한 결정입니다. 잘못된 UTXO 선택은 불필요하게 높은 수수료를 발생시키거나, 지갑의 UTXO를 비효율적으로 분할하여 미래의 거래 비용을 증가시킬 수 있습니다.

실제로 비트코인 지갑들은 이 문제를 최적화하기 위해 복잡한 알고리즘을 사용합니다. 바로 이럴 때 필요한 것이 트랜잭션 입력 선택 알고리즘입니다.

송금 금액을 만족하면서도 수수료를 최소화하고, 지갑의 UTXO 상태를 건강하게 유지하는 최적의 UTXO 조합을 선택합니다.

개요

간단히 말해서, 트랜잭션 입력 선택은 송금에 필요한 금액을 충족하는 UTXO들을 골라내는 최적화 문제입니다. 이는 배낭 문제(Knapsack Problem)와 유사한 NP-hard 문제입니다.

왜 이것이 중요한지 실무 관점에서 설명하면, 비트코인 수수료는 거래 금액이 아닌 트랜잭션의 바이트 크기에 비례합니다. 예를 들어, 10개의 작은 UTXO를 사용하는 것보다 1개의 큰 UTXO를 사용하는 것이 수수료가 훨씬 저렴합니다.

또한 잔돈(거스름돈)을 최소화하여 UTXO 분할을 줄이는 것도 장기적으로 수수료 절감에 도움이 됩니다. 기존 단순한 방식에서는 "금액이 충족될 때까지 순서대로 UTXO를 추가"했다면, 이제는 "가장 적은 개수로 금액을 맞추거나", "거스름돈이 최소가 되도록", 또는 "오래된 UTXO를 우선 사용"하는 등 다양한 전략을 사용할 수 있습니다.

이 알고리즘의 핵심 특징은 첫째, 수수료 최적화입니다. 트랜잭션 크기를 최소화하여 비용을 절감합니다.

둘째, 프라이버시 보호입니다. UTXO 선택 패턴으로 사용자를 추적하는 것을 방지합니다.

셋째, UTXO 세트 관리입니다. 지갑의 UTXO가 너무 많이 분할되거나 통합되는 것을 방지합니다.

이러한 특징들이 효율적이고 안전한 암호화폐 거래를 가능하게 합니다.

코드 예제

// 송금에 사용할 UTXO 선택하기
private selectTxInputs(
  amount: number,
  unspentTxOuts: UnspentTxOut[]
): { selectedInputs: UnspentTxOut[], totalAmount: number } {
  // 금액이 큰 순서로 정렬 (큰 UTXO 우선 전략)
  const sorted = [...unspentTxOuts].sort((a, b) => b.amount - a.amount);

  let totalAmount = 0;
  const selectedInputs: UnspentTxOut[] = [];

  // 필요한 금액이 충족될 때까지 UTXO 선택
  for (const uTxO of sorted) {
    selectedInputs.push(uTxO);
    totalAmount += uTxO.amount;

    if (totalAmount >= amount) break; // 충분하면 중단
  }

  // 금액이 부족하면 에러
  if (totalAmount < amount) {
    throw new Error(`잔액 부족: 필요 ${amount}, 보유 ${totalAmount}`);
  }

  return { selectedInputs, totalAmount };
}

설명

이것이 하는 일: 사용자가 송금하려는 금액을 만족하면서도 트랜잭션 비용을 최소화할 수 있는 최적의 UTXO 조합을 선택합니다. 입력으로 사용할 UTXO 리스트와 전체 합산 금액을 반환합니다.

첫 번째로, 사용 가능한 모든 UTXO를 금액이 큰 순서대로 정렬합니다. [...unspentTxOuts]로 배열을 복사하는 이유는 원본 배열을 변경하지 않기 위함입니다.

sort((a, b) => b.amount - a.amount)는 내림차순 정렬을 수행하여 가장 큰 금액의 UTXO가 앞에 오도록 합니다. 큰 UTXO부터 사용하는 전략은 입력 개수를 최소화하여 트랜잭션 크기를 줄이는 효과가 있습니다.

그 다음으로, for...of 루프를 통해 정렬된 UTXO를 하나씩 순회하며 선택합니다. 각 UTXO를 selectedInputs 배열에 추가하고, totalAmount에 해당 UTXO의 금액을 누적합니다.

이 과정에서 if (totalAmount >= amount) break; 조건을 통해 필요한 금액을 충족하는 즉시 반복을 중단합니다. 불필요하게 많은 UTXO를 선택하지 않아 트랜잭션 크기를 최소화합니다.

마지막으로, 모든 UTXO를 다 선택했는데도 금액이 부족한 경우 Error를 던집니다. 이는 잔액 부족 상황을 명확히 알려주며, 사용자에게 송금이 불가능함을 즉시 피드백합니다.

에러 메시지에 필요한 금액과 실제 보유 금액을 포함하여 디버깅과 사용자 안내를 용이하게 합니다. 여러분이 이 코드를 사용하면 최소한의 UTXO로 송금을 처리하여 네트워크 수수료를 절감할 수 있습니다.

트랜잭션 입력이 적을수록 바이트 크기가 작아지고, 채굴자에게 지불하는 수수료도 줄어듭니다. 또한 명확한 에러 처리로 사용자 경험을 개선하고, 잔액 부족 상황을 사전에 방지할 수 있습니다.

실전 팁

💡 실제 비트코인 코어는 여러 전략을 혼합합니다: Branch and Bound 알고리즘으로 거스름돈이 0이 되는 조합을 먼저 찾고, 실패하면 큰 UTXO 우선 또는 작은 UTXO 우선 전략을 사용합니다.

💡 프라이버시를 위해 매번 다른 선택 전략을 랜덤하게 사용하거나, 의도적으로 불필요한 UTXO를 추가하여 거래 패턴 분석을 어렵게 만드는 기법도 있습니다.

💡 수수료가 높을 때는 UTXO 통합을 연기하고, 낮을 때 여러 작은 UTXO를 하나로 합치는 "UTXO 정리" 트랜잭션을 보내 장기적으로 비용을 절감하세요.

💡 SegWit 주소와 Legacy 주소를 섞어서 사용하지 마세요. SegWit UTXO만 사용하면 트랜잭션 크기를 약 40% 줄여 수수료를 크게 절감할 수 있습니다.

💡 선택 알고리즘의 성능을 테스트할 때는 다양한 UTXO 분포(많은 작은 UTXO, 적은 큰 UTXO, 혼합)와 금액 시나리오를 사용하여 최악의 경우도 검증하세요.


3. 트랜잭션 생성 및 서명 - 안전한 송금 데이터 만들기

시작하며

여러분이 온라인 뱅킹으로 송금할 때 비밀번호를 입력하는 것처럼, 블록체인에서도 송금을 승인하는 과정이 필요합니다. 하지만 중앙 서버가 없는 블록체인에서는 어떻게 "이 거래가 정말 지갑 소유자가 보낸 것"임을 증명할까요?

만약 서명 없이 트랜잭션을 보낸다면, 누구나 여러분의 UTXO를 사용하여 무단으로 송금할 수 있습니다. 블록체인의 모든 데이터는 공개되어 있기 때문에, 암호학적 서명이 없다면 완전히 무방비 상태가 됩니다.

바로 이럴 때 필요한 것이 디지털 서명이 포함된 트랜잭션 생성입니다. 개인키로 트랜잭션에 서명하여 소유권을 증명하고, 네트워크의 모든 노드가 공개키로 이를 검증할 수 있게 합니다.

개요

간단히 말해서, 트랜잭션 생성은 입력(사용할 UTXO), 출력(받는 주소와 금액), 그리고 서명(소유권 증명)을 포함하는 데이터 구조를 만드는 과정입니다. 왜 이것이 중요한지 실무 관점에서 설명하면, 블록체인의 보안은 전적으로 암호학적 서명에 의존합니다.

예를 들어, 해커가 네트워크 패킷을 가로채서 트랜잭션을 수정하려 해도, 서명이 유효하지 않게 되어 모든 노드가 거부합니다. 또한 서명은 부인 방지(Non-repudiation) 기능도 제공하여, 나중에 "내가 보낸 게 아니다"라고 주장할 수 없게 합니다.

기존 중앙화된 시스템에서는 서버가 사용자를 인증했다면, 이제는 수학적으로 증명 가능한 디지털 서명을 통해 분산된 네트워크의 모든 노드가 독립적으로 검증할 수 있습니다. 이 시스템의 핵심 특징은 첫째, 소유권 증명입니다.

개인키가 있어야만 UTXO를 사용할 수 있습니다. 둘째, 위변조 방지입니다.

트랜잭션의 어떤 부분이라도 변경되면 서명이 무효화됩니다. 셋째, 공개 검증 가능성입니다.

개인키 없이도 서명의 유효성을 누구나 확인할 수 있습니다. 이러한 특징들이 신뢰할 수 없는 네트워크에서도 안전한 거래를 가능하게 합니다.

코드 예제

// 송금 트랜잭션 생성 및 서명
public createTransaction(
  receiverAddress: string,
  amount: number
): Transaction {
  // 1. 사용 가능한 UTXO 조회
  const unspentTxOuts = this.blockchain
    .getUnspentTxOuts()
    .filter(uTxO => uTxO.address === this.address);

  // 2. 필요한 UTXO 선택
  const { selectedInputs, totalAmount } =
    this.selectTxInputs(amount, unspentTxOuts);

  // 3. 트랜잭션 입력 생성 (서명 포함)
  const txIns = selectedInputs.map(uTxO => {
    const signature = this.signTxIn(uTxO); // 개인키로 서명
    return new TxIn(uTxO.txOutId, uTxO.txOutIndex, signature);
  });

  // 4. 트랜잭션 출력 생성 (받는 사람 + 거스름돈)
  const txOuts = [
    new TxOut(receiverAddress, amount), // 받는 사람
  ];

  const change = totalAmount - amount;
  if (change > 0) {
    txOuts.push(new TxOut(this.address, change)); // 거스름돈
  }

  return new Transaction(txIns, txOuts);
}

설명

이것이 하는 일: 송금에 필요한 모든 정보를 담은 트랜잭션 객체를 생성하고, 개인키로 서명하여 블록체인 네트워크에 브로드캐스트할 수 있는 형태로 만듭니다. 입력, 출력, 서명이 모두 포함된 완전한 트랜잭션을 반환합니다.

첫 번째로, 사용 가능한 UTXO를 필터링하여 내 주소에 속한 것들만 추립니다. 그 다음 selectTxInputs 메서드를 호출하여 송금 금액을 충족하는 최적의 UTXO 조합을 선택합니다.

이 단계에서 잔액이 부족하면 에러가 발생하여 더 이상 진행되지 않습니다. totalAmount는 선택된 UTXO들의 총합으로, 나중에 거스름돈 계산에 사용됩니다.

그 다음으로, 선택된 각 UTXO를 트랜잭션 입력(TxIn)으로 변환합니다. 여기서 가장 중요한 부분은 this.signTxIn(uTxO)입니다.

이 메서드는 UTXO의 정보와 트랜잭션 해시를 개인키(private key)로 서명하여 소유권을 증명합니다. ECDSA(Elliptic Curve Digital Signature Algorithm) 같은 암호 알고리즘을 사용하며, 서명 없이는 어떤 노드도 이 트랜잭션을 유효하다고 인정하지 않습니다.

세 번째로, 트랜잭션 출력(TxOut)을 생성합니다. 첫 번째 출력은 받는 사람의 주소와 송금 금액입니다.

그리고 totalAmount - amount로 거스름돈을 계산하여, 0보다 크면 내 주소로 돌려받는 두 번째 출력을 추가합니다. 예를 들어, 1 BTC를 가진 UTXO로 0.3 BTC를 보내면, 0.7 BTC가 거스름돈으로 내 지갑에 새로운 UTXO로 반환됩니다.

마지막으로, 모든 입력과 출력을 포함하는 Transaction 객체를 생성하여 반환합니다. 이 트랜잭션은 블록체인 네트워크에 브로드캐스트되어 채굴자에 의해 블록에 포함될 준비가 완료된 상태입니다.

여러분이 이 코드를 사용하면 암호학적으로 안전한 송금을 수행할 수 있습니다. 개인키를 가진 사람만이 UTXO를 사용할 수 있으며, 네트워크의 모든 노드가 서명을 검증하여 이중 지불이나 무단 사용을 방지합니다.

또한 거스름돈 처리가 자동으로 이루어져 사용자가 신경 쓸 필요가 없고, UTXO 모델의 복잡성이 추상화됩니다.

실전 팁

💡 서명 생성은 CPU 집약적이므로, 대량의 트랜잭션을 처리할 때는 Worker Thread나 Web Worker를 활용하여 메인 스레드를 블로킹하지 않도록 하세요.

💡 개인키는 절대 네트워크로 전송하지 마세요. 서명은 로컬에서만 생성하고, 완성된 서명만 트랜잭션에 포함하여 브로드캐스트해야 합니다. 이는 하드웨어 월렛이 사용하는 방식입니다.

💡 거스름돈을 원래 주소가 아닌 새로운 주소로 보내면 프라이버시가 향상됩니다. HD(Hierarchical Deterministic) 월렛을 사용하여 매번 새 주소를 생성하는 것이 좋습니다.

💡 트랜잭션 생성 전에 "Dry Run" 모드로 예상 수수료를 계산하여 사용자에게 미리 보여주고 확인받으세요. 예상치 못한 높은 수수료로 인한 사용자 불만을 예방할 수 있습니다.

💡 서명 과정에서 nonce(k 값)를 재사용하면 개인키가 유출될 수 있습니다. RFC 6979(Deterministic ECDSA)를 구현한 검증된 라이브러리를 사용하여 이 위험을 제거하세요.


4. 거스름돈 처리 메커니즘 - UTXO 모델의 핵심 개념

시작하며

여러분이 편의점에서 1,500원짜리 물건을 사면서 5,000원을 내면 3,500원을 거스름돈으로 받습니다. 블록체인도 놀랍게도 정확히 같은 방식으로 동작합니다.

하지만 많은 개발자들이 이 개념을 이해하지 못해 혼란을 겪습니다. 일반 은행 계좌처럼 "잔액에서 차감"하는 개념이 없기 때문에, UTXO를 사용할 때는 반드시 전체 금액을 "소비"하고 거스름돈을 "새로운 UTXO"로 받아야 합니다.

부분적으로 UTXO를 사용할 수 없습니다. 바로 이럴 때 필요한 것이 거스름돈 처리 메커니즘입니다.

선택한 UTXO의 총합과 실제 송금 금액의 차이를 계산하여, 남은 금액을 내 주소로 돌려받는 새로운 UTXO를 자동으로 생성합니다.

개요

간단히 말해서, 거스름돈 처리는 트랜잭션 출력에 송금 금액뿐만 아니라 남은 금액을 내 주소로 돌려보내는 추가 출력을 만드는 과정입니다. 왜 이 메커니즘이 필수적인지 이해하려면 UTXO의 작동 방식을 알아야 합니다.

UTXO는 "일회용 수표"와 같아서, 한 번 사용하면 완전히 소비되고 더 이상 사용할 수 없습니다. 예를 들어, 1 BTC를 가진 UTXO로 0.3 BTC를 보낼 때 거스름돈을 처리하지 않으면, 나머지 0.7 BTC는 채굴자의 수수료로 가버립니다!

실제로 초기 비트코인 사용자들이 이런 실수로 많은 돈을 잃었습니다. 기존에 "잔액 = 잔액 - 송금액" 같은 단순한 차감 연산을 했다면, 이제는 "입력으로 선택한 모든 UTXO의 합 - 송금액 = 거스름돈"을 계산하여 명시적으로 새 UTXO를 생성해야 합니다.

이 메커니즘의 핵심 특징은 첫째, 자금 보호입니다. 남은 금액을 명시적으로 처리하지 않으면 손실됩니다.

둘째, UTXO 재생성입니다. 송금 후에도 사용 가능한 잔액이 새로운 UTXO로 유지됩니다.

셋째, 투명성입니다. 모든 자금의 흐름이 블록체인에 명확히 기록됩니다.

이러한 특징들이 블록체인의 회계 투명성을 보장합니다.

코드 예제

// 거스름돈을 포함한 트랜잭션 출력 생성
private createTxOutputs(
  receiverAddress: string,
  amount: number,
  totalInputAmount: number
): TxOut[] {
  const txOuts: TxOut[] = [];

  // 1. 받는 사람에게 보낼 출력
  txOuts.push(new TxOut(receiverAddress, amount));

  // 2. 거스름돈 계산
  const change = totalInputAmount - amount;

  // 3. 거스름돈이 있으면 내 주소로 반환하는 출력 추가
  if (change > 0) {
    txOuts.push(new TxOut(this.address, change));
  } else if (change < 0) {
    // 이 경우는 발생하면 안 됨 (입력 선택 오류)
    throw new Error('입력 금액이 출력 금액보다 적습니다');
  }

  // 참고: change === 0이면 모든 금액이 수수료로 사용됨

  return txOuts;
}

설명

이것이 하는 일: 트랜잭션 출력 배열을 생성하되, 받는 사람의 출력과 거스름돈 출력을 모두 포함하여 입력으로 사용한 UTXO의 모든 금액이 적절히 분배되도록 합니다. 첫 번째로, 빈 출력 배열을 생성하고 받는 사람의 주소와 송금 금액을 담은 첫 번째 TxOut 객체를 추가합니다.

이것이 실제로 송금되는 부분입니다. new TxOut(receiverAddress, amount)는 블록체인에 "이 주소에 이만큼의 코인이 새로 생성되었다"고 기록하는 것과 같습니다.

이 출력은 받는 사람의 UTXO 세트에 추가되어 그들이 사용할 수 있게 됩니다. 그 다음으로, 거스름돈을 계산합니다.

totalInputAmount는 트랜잭션 입력으로 선택한 모든 UTXO의 합계이고, amount는 실제 송금액입니다. 차이가 바로 거스름돈(change)이 됩니다.

예를 들어, 1.5 BTC 짜리 UTXO를 사용하여 0.4 BTC를 보낸다면, change는 1.1 BTC가 됩니다. 세 번째로, 거스름돈이 양수인 경우 내 주소(this.address)로 거스름돈을 보내는 두 번째 출력을 추가합니다.

이렇게 하면 송금 후에도 남은 금액이 내 새로운 UTXO로 생성되어 나중에 다시 사용할 수 있습니다. 만약 거스름돈이 음수라면 입력 선택 로직에 버그가 있다는 의미이므로 에러를 던집니다.

change === 0인 경우는 선택한 UTXO가 정확히 송금액과 일치하는 드문 경우로, 거스름돈 출력이 필요 없습니다. 실무에서 주의할 점은, 거스름돈이 너무 작으면(예: 0.00000001 BTC, 즉 1 사토시) "더스트(dust)"로 간주되어 네트워크가 거부할 수 있습니다.

비트코인에서는 보통 546 사토시 미만의 출력은 표준 거래로 인정되지 않습니다. 여러분이 이 코드를 사용하면 UTXO를 안전하게 사용하고 남은 금액을 보호할 수 있습니다.

거스름돈 처리를 빠뜨리면 의도치 않게 채굴자에게 막대한 수수료를 지불하는 재앙적인 결과를 초래할 수 있으므로, 자동화된 거스름돈 처리는 필수적입니다. 또한 명확한 에러 처리로 논리적 오류를 조기에 발견할 수 있습니다.

실전 팁

💡 거스름돈이 더스트 금액(예: 546 사토시 미만)이면 아예 출력을 생성하지 말고 채굴자 수수료에 포함시키세요. 더스트 UTXO는 나중에 사용하는 비용이 더 비쌉니다.

💡 프라이버시를 위해 거스름돈을 원래 주소가 아닌 HD 월렛의 새로운 파생 주소로 보내세요. 이렇게 하면 블록체인 분석으로 지갑을 추적하기 어려워집니다.

💡 거스름돈 출력의 순서를 랜덤화하세요. 항상 마지막에 거스름돈을 놓으면 패턴이 노출되어 "어느 것이 거스름돈인지" 쉽게 파악됩니다.

💡 수수료를 별도로 계산하는 경우 change = totalInputAmount - amount - fee로 수정하세요. 수수료를 빠뜨리면 채굴자가 트랜잭션을 처리하지 않을 수 있습니다.

💡 거스름돈 계산을 단위 테스트로 철저히 검증하세요. 실제 돈이 걸린 문제이므로 경계 조건(정확히 맞는 금액, 1 사토시 차이 등)을 모두 테스트해야 합니다.


5. 트랜잭션 브로드캐스팅 - 네트워크에 전파하기

시작하며

여러분이 완벽한 트랜잭션을 만들었어도, 그것이 내 컴퓨터에만 있다면 아무 의미가 없습니다. 블록체인 네트워크의 수천 개 노드들이 이 트랜잭션을 받아서 검증하고 블록에 포함시켜야만 실제로 송금이 완료됩니다.

중앙 서버가 있는 시스템이라면 간단히 API 요청을 보내면 되지만, P2P(Peer-to-Peer) 네트워크에서는 연결된 모든 피어에게 트랜잭션을 전파하는 복잡한 메커니즘이 필요합니다. 바로 이럴 때 필요한 것이 트랜잭션 브로드캐스팅입니다.

내가 생성한 트랜잭션을 네트워크의 모든 노드에 빠르고 효율적으로 전달하여, 채굴자가 블록에 포함시킬 수 있도록 합니다.

개요

간단히 말해서, 트랜잭션 브로드캐스팅은 생성한 트랜잭션을 직접 연결된 피어들에게 전송하고, 그 피어들이 다시 자신의 피어들에게 전송하는 방식으로 네트워크 전체에 확산시키는 과정입니다. 왜 이것이 중요한지 실무 관점에서 설명하면, 블록체인은 분산 시스템이므로 단일 장애 지점이 없습니다.

예를 들어, 일부 노드가 오프라인이거나 악의적이어도, 충분한 수의 정직한 노드에 도달하기만 하면 트랜잭션이 결국 블록에 포함됩니다. 또한 빠른 전파는 이중 지불 공격을 더 어렵게 만듭니다.

기존 중앙화된 시스템에서는 단일 서버에 HTTP POST 요청을 보냈다면, 이제는 WebSocket이나 TCP 연결로 여러 피어에 동시에 메시지를 전송하고, 중복 전송을 방지하며, 네트워크 분할에 대응해야 합니다. 이 메커니즘의 핵심 특징은 첫째, 고시프(Gossip) 프로토콜입니다.

노드들이 서로 소문을 퍼뜨리듯 트랜잭션을 전파합니다. 둘째, 중복 제거입니다.

같은 트랜잭션을 여러 번 받아도 한 번만 처리하고 전파합니다. 셋째, 탄력성입니다.

일부 경로가 막혀도 다른 경로로 전달됩니다. 이러한 특징들이 검열 저항성과 네트워크 안정성을 제공합니다.

코드 예제

// 트랜잭션을 P2P 네트워크에 브로드캐스트
public async broadcastTransaction(tx: Transaction): Promise<void> {
  // 1. 트랜잭션 유효성 검증 (전파 전 필수)
  if (!this.validateTransaction(tx)) {
    throw new Error('유효하지 않은 트랜잭션입니다');
  }

  // 2. 멤풀(Mempool)에 추가
  this.addToMempool(tx);

  // 3. 연결된 모든 피어에게 전송
  const peers = this.p2pServer.getPeers();
  const message = {
    type: 'NEW_TRANSACTION',
    data: tx.serialize(), // 트랜잭션을 JSON으로 직렬화
  };

  // 병렬로 모든 피어에 전송
  await Promise.all(
    peers.map(peer => peer.send(JSON.stringify(message)))
  );

  console.log(`트랜잭션 ${tx.id}${peers.length}개 피어에 전파했습니다`);
}

설명

이것이 하는 일: 로컬에서 생성한 트랜잭션을 블록체인 네트워크의 모든 노드에 전달하여, 채굴자가 발견하고 블록에 포함시킬 수 있도록 합니다. 첫 번째로, 브로드캐스트 전에 반드시 트랜잭션의 유효성을 검증합니다.

validateTransaction 메서드는 서명이 올바른지, 입력으로 사용하는 UTXO가 실제로 존재하고 아직 사용되지 않았는지, 입력과 출력의 합이 일치하는지 등을 확인합니다. 유효하지 않은 트랜잭션을 전파하면 네트워크에서 즉시 거부되고, 내 노드의 평판이 나빠질 수 있으므로 이 단계는 필수입니다.

그 다음으로, 유효한 트랜잭션을 멤풀(Mempool, Memory Pool)에 추가합니다. 멤풀은 아직 블록에 포함되지 않은 트랜잭션들을 임시로 보관하는 메모리 영역입니다.

채굴자는 멤풀에서 트랜잭션을 꺼내 블록에 포함시키며, 보통 수수료가 높은 트랜잭션을 우선적으로 선택합니다. addToMempool 메서드는 중복 트랜잭션을 필터링하고, 이중 지불을 시도하는 충돌 트랜잭션도 감지합니다.

세 번째로, P2P 서버에서 현재 연결된 모든 피어 목록을 가져옵니다. getPeers() 메서드는 활성 WebSocket 연결이나 TCP 연결을 유지하고 있는 노드들의 리스트를 반환합니다.

그런 다음 표준화된 메시지 형식으로 트랜잭션을 패키징합니다. type: 'NEW_TRANSACTION'은 메시지 유형을 식별하는 태그이고, data에는 트랜잭션의 직렬화된 JSON 문자열이 들어갑니다.

마지막으로, Promise.allmap을 사용하여 모든 피어에 동시에 메시지를 전송합니다. 순차적으로 보내는 것보다 병렬로 보내는 것이 훨씬 빠르며, 한 피어가 느려도 다른 피어들의 전송에 영향을 주지 않습니다.

각 피어는 이 트랜잭션을 받으면 자신도 검증하고 멤풀에 추가한 후, 다시 자신의 피어들에게 전파합니다. 이렇게 고시프 방식으로 수초 내에 전체 네트워크에 도달합니다.

여러분이 이 코드를 사용하면 중앙 서버 없이도 트랜잭션을 네트워크에 제출할 수 있습니다. 분산된 방식으로 작동하므로 검열이나 단일 장애 지점에 대한 걱정이 없으며, 네트워크가 클수록 더 안정적이고 빠르게 전파됩니다.

또한 병렬 전송으로 지연 시간을 최소화하여 사용자 경험을 향상시킵니다.

실전 팁

💡 같은 트랜잭션을 여러 번 전파하지 않도록 "이미 전파한 트랜잭션 ID" 캐시를 유지하세요. Bloom Filter를 사용하면 메모리 효율적으로 중복을 감지할 수 있습니다.

💡 네트워크가 분할되었을 때를 대비해 여러 피어 그룹에 연결하세요. 지리적으로 분산된 노드들(미국, 유럽, 아시아)에 연결하면 글로벌 전파 속도가 향상됩니다.

💡 브로드캐스트 실패 시 재시도 로직을 구현하되, 지수 백오프(exponential backoff)를 사용하여 네트워크에 부하를 주지 않도록 하세요. 예: 1초, 2초, 4초, 8초 간격으로 재시도.

💡 트랜잭션 크기가 클 때는 먼저 트랜잭션 ID만 전송하고, 피어가 요청하면 전체 데이터를 보내는 "Inventory" 방식을 사용하여 대역폭을 절약하세요. 비트코인이 사용하는 방식입니다.

💡 악의적인 피어가 유효하지 않은 트랜잭션을 계속 보내면 연결을 끊고 블랙리스트에 추가하세요. DoS 공격으로부터 노드를 보호하는 필수 메커니즘입니다.


6. 트랜잭션 수수료 계산 - 채굴자에게 인센티브 제공

시작하며

여러분이 트랜잭션을 보냈는데 몇 시간, 심지어 며칠이 지나도 확정되지 않는 경험을 해본 적 있나요? 이는 수수료가 너무 낮아서 채굴자들이 내 트랜잭션을 선택하지 않기 때문입니다.

블록체인에서 수수료는 선택사항이 아닙니다. 채굴자는 자발적으로 트랜잭션을 처리해주는 것이 아니라, 수수료라는 경제적 인센티브에 의해 움직입니다.

네트워크가 혼잡할 때는 수수료 경쟁이 치열해져서, 높은 수수료를 제시한 트랜잭션만 블록에 포함됩니다. 바로 이럴 때 필요한 것이 적절한 수수료 계산 메커니즘입니다.

트랜잭ション 크기와 네트워크 상황을 고려하여 충분히 빠르게 확정되면서도 과도하게 지불하지 않는 최적의 수수료를 산정합니다.

개요

간단히 말해서, 트랜잭션 수수료는 입력 UTXO의 총합에서 출력 UTXO의 총합을 뺀 차이로 암묵적으로 결정됩니다. 명시적인 "수수료" 필드가 없고, 남은 금액이 자동으로 채굴자에게 갑니다.

왜 이 방식이 사용되는지 이해하려면 비트코인의 설계 철학을 알아야 합니다. 수수료를 별도 필드로 만들면 검증이 복잡해지고 버그가 생길 수 있습니다.

예를 들어, "수수료: 0.1 BTC"라고 적어놓고 실제로는 0.01 BTC만 남겼다면 어떻게 될까요? 입력-출력의 차이로 수수료를 계산하면 이런 불일치가 원천적으로 불가능합니다.

기존에 "송금액 + 수수료"를 별도로 관리했다면, 이제는 "입력 총합 - 송금액 - 거스름돈 = 수수료"로 계산됩니다. 이 메커니즘의 핵심 특징은 첫째, 시장 기반 가격 결정입니다.

공급(블록 공간)과 수요(트랜잭션 수)에 따라 수수료가 자동 조절됩니다. 둘째, 채굴자 인센티브입니다.

높은 수수료는 더 빠른 확정을 보장합니다. 셋째, 네트워크 스팸 방지입니다.

수수료가 있어야 무의미한 트랜잭션 공격이 비용이 듭니다. 이러한 특징들이 블록체인 경제 모델의 지속 가능성을 만듭니다.

코드 예제

// 트랜잭션 수수료 계산 및 적용
private calculateFee(txIns: TxIn[], txOuts: TxOut[]): number {
  // 입력의 총 금액 (UTXO 총합)
  const inputAmount = txIns.reduce((sum, txIn) => {
    const uTxO = this.findUnspentTxOut(txIn);
    return sum + uTxO.amount;
  }, 0);

  // 출력의 총 금액 (송금액 + 거스름돈)
  const outputAmount = txOuts.reduce(
    (sum, txOut) => sum + txOut.amount,
    0
  );

  // 수수료 = 입력 - 출력 (암묵적 계산)
  const fee = inputAmount - outputAmount;

  if (fee < 0) {
    throw new Error('출력이 입력보다 큽니다 (불가능한 트랜잭션)');
  }

  return fee;
}

// 최적 수수료를 고려한 출력 생성
private createTxOutputsWithFee(
  receiverAddress: string,
  amount: number,
  totalInputAmount: number,
  feePerByte: number,
  estimatedTxSize: number
): TxOut[] {
  // 예상 수수료 계산
  const estimatedFee = feePerByte * estimatedTxSize;

  // 거스름돈 = 입력 - 송금액 - 수수료
  const change = totalInputAmount - amount - estimatedFee;

  if (change < 0) {
    throw new Error('잔액이 부족합니다 (수수료 포함)');
  }

  const txOuts = [new TxOut(receiverAddress, amount)];

  // 거스름돈이 더스트보다 크면 추가
  if (change > 546) { // 546 사토시 = 비트코인 더스트 한계
    txOuts.push(new TxOut(this.address, change));
  }
  // 그렇지 않으면 거스름돈도 수수료에 포함

  return txOuts;
}

설명

이것이 하는 일: 트랜잭션의 입력과 출력을 분석하여 채굴자에게 지불될 수수료를 계산하고, 네트워크 상황에 맞는 최적의 수수료를 적용하여 빠른 확정을 보장합니다. 첫 번째로, calculateFee 메서드는 모든 입력 UTXO의 금액을 합산합니다.

reduce 함수로 각 TxIn에 대해 findUnspentTxOut을 호출하여 해당하는 UTXO를 찾고, 그 금액들을 누적합니다. 이것이 트랜잭션에서 사용하는 총 자금입니다.

예를 들어, 0.5 BTC와 0.3 BTC 두 개의 UTXO를 입력으로 사용하면 inputAmount는 0.8 BTC가 됩니다. 그 다음으로, 모든 출력의 금액을 합산합니다.

이는 받는 사람에게 가는 금액과 거스름돈의 합입니다. 예를 들어, 0.4 BTC를 송금하고 0.39 BTC를 거스름돈으로 받는다면 outputAmount는 0.79 BTC입니다.

입력 0.8 BTC에서 출력 0.79 BTC를 빼면 수수료 0.01 BTC가 암묵적으로 계산됩니다. 이 금액은 트랜잭션 어디에도 명시되지 않지만, 채굴자가 블록을 생성할 때 자동으로 가져갑니다.

세 번째로, createTxOutputsWithFee 메서드는 사용자가 예상 수수료를 미리 계산하여 출력을 생성합니다. feePerByte는 바이트당 수수료 비율(예: 10 사토시/바이트)이고, estimatedTxSize는 트랜잭션의 예상 크기입니다.

트랜잭션 크기는 입력과 출력의 개수, 서명 크기 등에 의해 결정되며, 보통 입력당 약 148바이트, 출력당 약 34바이트로 추정할 수 있습니다. 마지막으로, 거스름돈에서 예상 수수료를 미리 차감하여 실제로 사용자에게 돌아갈 금액을 계산합니다.

change = totalInputAmount - amount - estimatedFee 공식으로 계산하며, 이 금액이 더스트 한계(546 사토시) 이상일 때만 거스름돈 출력을 생성합니다. 만약 더스트 미만이라면 거스름돈 출력을 만들지 않고, 그 작은 금액도 수수료에 포함시킵니다.

더스트 UTXO는 나중에 사용하는 비용이 더 비싸므로 만들지 않는 것이 합리적입니다. 여러분이 이 코드를 사용하면 적절한 수수료를 자동으로 계산하여 트랜잭션이 합리적인 시간 내에 확정되도록 보장할 수 있습니다.

너무 낮은 수수료로 몇 시간씩 기다리는 불편함을 방지하고, 과도한 수수료로 돈을 낭비하는 것도 막을 수 있습니다. 또한 네트워크 혼잡도에 따라 동적으로 수수료를 조절할 수 있어 사용자 경험이 향상됩니다.

실전 팁

💡 실시간 수수료 예측을 위해 최근 블록들의 평균 수수료를 분석하세요. 비트코인의 경우 mempool.space 같은 서비스에서 "다음 블록", "30분 이내", "1시간 이내" 수수료 추천을 제공합니다.

💡 RBF(Replace-By-Fee)를 활성화하면 수수료가 너무 낮았을 때 나중에 더 높은 수수료로 트랜잭션을 교체할 수 있습니다. 트랜잭션 입력의 시퀀스 번호를 0xfffffffd 이하로 설정하면 RBF가 활성화됩니다.

💡 긴급하지 않은 트랜잭션은 네트워크가 한산한 주말이나 밤 시간대에 보내면 수수료를 최대 50% 절감할 수 있습니다. 과거 데이터를 분석하여 저렴한 시간대를 찾으세요.

💡 SegWit(Segregated Witness) 트랜잭션을 사용하면 서명 데이터가 별도로 계산되어 실질적인 크기가 약 40% 감소하므로 수수료도 그만큼 줄어듭니다.

💡 수수료 계산 시 항상 안전 마진(예: 10%)을 추가하세요. 예상 크기보다 실제 크기가 조금 더 클 수 있고, 네트워크 상황이 급변할 수 있습니다.


7. 멤풀 관리 - 미확정 트랜잭션 처리

시작하며

여러분이 송금 버튼을 누른 후 "확정 중..." 상태가 몇 분씩 지속되는 것을 본 적 있나요? 이 시간 동안 트랜잭션은 멤풀(Mempool)이라는 대기실에 머물며 채굴자가 블록에 포함시켜주기를 기다립니다.

멤풀은 단순한 대기열이 아닙니다. 수천 개의 트랜잭션이 경쟁하는 시장이며, 이중 지불 시도를 감지하고, 수수료 기반으로 우선순위를 매기며, 메모리 한계 내에서 효율적으로 관리되어야 합니다.

바로 이럴 때 필요한 것이 멤풀 관리 시스템입니다. 트랜잭션을 검증하고 저장하며, 충돌을 감지하고, 채굴자가 블록을 생성할 때 최적의 트랜잭션을 선택할 수 있도록 합니다.

개요

간단히 말해서, 멤풀은 블록에 아직 포함되지 않은 유효한 트랜잭션들을 메모리에 임시 보관하는 자료구조입니다. 각 노드는 독립적인 멤풀을 유지합니다.

왜 멤풀이 필요한지 실무 관점에서 설명하면, 블록은 평균 10분(비트코인 기준)마다 생성되므로 그 사이에 발생한 트랜잭션들을 어딘가에 보관해야 합니다. 예를 들어, 초당 수백 건의 트랜잭션이 들어오는데 블록은 10분마다 하나씩만 생성된다면, 멤풀 없이는 트랜잭션을 처리할 수 없습니다.

또한 멤풀은 이중 지불을 조기에 감지하는 첫 번째 방어선 역할도 합니다. 기존 중앙 서버에서는 데이터베이스의 "pending_transactions" 테이블이었다면, 이제는 각 노드가 독립적으로 관리하는 메모리 기반 우선순위 큐가 됩니다.

멤풀의 핵심 특징은 첫째, 휘발성입니다. 노드가 재시작되면 멤풀은 비워지고 다시 채워집니다.

둘째, 분산성입니다. 각 노드의 멤풀은 약간씩 다를 수 있습니다.

셋째, 동적 크기입니다. 네트워크 혼잡도에 따라 멤풀 크기가 변동합니다.

이러한 특징들이 블록체인의 유연성과 확장성을 제공합니다.

코드 예제

// 멤풀 관리 클래스
class Mempool {
  private transactions: Map<string, Transaction> = new Map();
  private readonly MAX_MEMPOOL_SIZE = 1000; // 최대 트랜잭션 수

  // 멤풀에 트랜잭션 추가
  public addTransaction(tx: Transaction): boolean {
    // 1. 이미 존재하는지 확인 (중복 방지)
    if (this.transactions.has(tx.id)) {
      return false; // 이미 있음
    }

    // 2. 이중 지불 검사 (같은 UTXO를 사용하는 다른 트랜잭션이 있는지)
    if (this.hasConflictingTransaction(tx)) {
      console.log(`이중 지불 시도 감지: ${tx.id}`);
      return false;
    }

    // 3. 멤풀이 가득 찼으면 가장 낮은 수수료 트랜잭션 제거
    if (this.transactions.size >= this.MAX_MEMPOOL_SIZE) {
      this.evictLowestFeeTx();
    }

    // 4. 멤풀에 추가
    this.transactions.set(tx.id, tx);
    return true;
  }

  // 충돌하는 트랜잭션 검사 (같은 UTXO 사용)
  private hasConflictingTransaction(newTx: Transaction): boolean {
    for (const existingTx of this.transactions.values()) {
      // 두 트랜잭션이 같은 UTXO를 입력으로 사용하면 충돌
      const conflict = newTx.txIns.some(newIn =>
        existingTx.txIns.some(existingIn =>
          newIn.txOutId === existingIn.txOutId &&
          newIn.txOutIndex === existingIn.txOutIndex
        )
      );
      if (conflict) return true;
    }
    return false;
  }

  // 수수료가 가장 낮은 트랜잭션 제거
  private evictLowestFeeTx(): void {
    let lowestFeeTx: Transaction | null = null;
    let lowestFee = Infinity;

    for (const tx of this.transactions.values()) {
      const fee = this.calculateFee(tx);
      if (fee < lowestFee) {
        lowestFee = fee;
        lowestFeeTx = tx;
      }
    }

    if (lowestFeeTx) {
      this.transactions.delete(lowestFeeTx.id);
      console.log(`낮은 수수료로 인해 제거됨: ${lowestFeeTx.id}`);
    }
  }

  // 채굴을 위해 높은 수수료 순으로 트랜잭션 가져오기
  public getTopTransactions(count: number): Transaction[] {
    return Array.from(this.transactions.values())
      .sort((a, b) => this.calculateFee(b) - this.calculateFee(a))
      .slice(0, count);
  }

  // 블록이 확정되면 해당 트랜잭션들을 멤풀에서 제거
  public removeConfirmedTransactions(txIds: string[]): void {
    txIds.forEach(id => this.transactions.delete(id));
  }
}

설명

이것이 하는 일: 블록에 포함되기를 기다리는 트랜잭션들을 효율적으로 관리하고, 유효성을 검증하며, 이중 지불을 조기에 감지하고, 채굴자가 수익을 최대화할 수 있도록 수수료 순으로 정렬합니다. 첫 번째로, addTransaction 메서드는 새로운 트랜잭션을 멤풀에 추가하기 전에 여러 검증을 수행합니다.

Map 자료구조를 사용하여 트랜잭션 ID를 키로 하는 빠른 조회를 가능하게 합니다. 먼저 has 메서드로 같은 ID의 트랜잭션이 이미 있는지 확인하여 중복을 방지합니다.

같은 트랜잭션을 여러 번 받는 것은 P2P 네트워크에서 흔한 일이므로 이 필터링은 필수적입니다. 그 다음으로, hasConflictingTransaction 메서드를 호출하여 이중 지불 시도를 감지합니다.

이 메서드는 멤풀의 모든 기존 트랜잭션과 새 트랜잭션의 입력을 비교합니다. 두 트랜잭션이 같은 UTXO(같은 txOutIdtxOutIndex)를 입력으로 사용하면 충돌입니다.

이는 누군가 같은 코인을 두 번 쓰려는 시도이므로, 나중에 온 트랜잭션을 거부합니다. 실제로는 RBF(Replace-By-Fee) 같은 예외 케이스도 있어 더 복잡하지만, 기본 원칙은 동일합니다.

세 번째로, 멤풀의 크기 제한을 관리합니다. 무제한으로 트랜잭션을 받으면 메모리가 고갈되거나 DoS 공격을 당할 수 있습니다.

MAX_MEMPOOL_SIZE를 초과하면 evictLowestFeeTx 메서드를 호출하여 가장 낮은 수수료의 트랜잭션을 제거합니다. 이는 시장 메커니즘과 같아서, 수수료가 낮은 트랜잭션은 혼잡할 때 밀려납니다.

제거된 트랜잭션은 나중에 다시 브로드캐스트하거나 더 높은 수수료로 재전송해야 합니다. 마지막으로, getTopTransactions 메서드는 채굴자가 블록을 생성할 때 사용합니다.

멤풀의 모든 트랜잭션을 수수료 내림차순으로 정렬하여 상위 N개를 반환합니다. 채굴자는 수익을 최대화하기 위해 높은 수수료를 지불한 트랜잭션을 우선적으로 블록에 포함시킵니다.

블록 크기 제한(비트코인은 약 1MB)이 있으므로 모든 트랜잭션을 포함할 수 없을 때 이 정렬이 중요합니다. 여러분이 이 코드를 사용하면 노드가 안정적으로 트랜잭션을 처리할 수 있습니다.

이중 지불 시도를 조기에 차단하여 네트워크 보안을 강화하고, 메모리 사용을 제한하여 노드의 안정성을 보장하며, 수수료 기반 우선순위로 공정한 시장 메커니즘을 구현합니다. 또한 채굴자에게 수익 최적화 도구를 제공하여 네트워크 인센티브 구조를 유지합니다.

실전 팁

💡 프로덕션 환경에서는 멤풀 크기를 트랜잭션 개수가 아닌 총 바이트 크기로 제한하세요. 비트코인 코어는 기본 300MB 제한을 사용합니다.

💡 멤풀이 가득 찰 때 "수수료/바이트" 비율로 정렬하여 제거하세요. 단순히 총 수수료로 비교하면 큰 트랜잭션이 유리해져 공정하지 않습니다.

💡 CPFP(Child Pays For Parent) 지원을 위해 트랜잭션 간 의존성 그래프를 유지하세요. 자식 트랜잭션의 높은 수수료가 부모 트랜잭션을 함께 끌어올리는 효과를 구현합니다.

💡 오래된 트랜잭션(예: 2주 이상)은 자동으로 제거하세요. 그만큼 오래 확정되지 않았다면 수수료가 너무 낮거나 문제가 있을 가능성이 높습니다.

💡 멤풀 상태를 모니터링하여 평균 수수료, 크기, 대기 시간 등의 메트릭을 수집하세요. 이 데이터는 사용자에게 최적 수수료를 추천하는 데 매우 유용합니다.


8. 트랜잭션 검증 시스템 - 블록체인의 보안 핵심

시작하며

여러분이 은행에서 송금을 받을 때, 은행이 "이 돈이 진짜인지, 발신자가 충분한 잔액이 있는지"를 검증합니다. 블록체인에서는 중앙 은행이 없으므로, 모든 노드가 독립적으로 이 검증을 수행해야 합니다.

만약 검증 없이 트랜잭션을 받아들인다면, 누군가 없는 코인을 만들어내거나 다른 사람의 코인을 훔칠 수 있습니다. 실제로 많은 암호화폐 프로젝트가 검증 로직의 버그로 인해 해킹을 당했습니다.

바로 이럴 때 필요한 것이 포괄적인 트랜잭션 검증 시스템입니다. 서명의 암호학적 유효성, UTXO의 존재 여부, 금액의 일관성, 이중 지불 여부 등을 다각도로 검증하여 악의적인 트랜잭션을 차단합니다.

개요

간단히 말해서, 트랜잭션 검증은 서명, 입력, 출력, 금액 등 트랜잭션의 모든 측면을 수학적이고 논리적으로 검사하여 블록체인 규칙을 위반하지 않았는지 확인하는 프로세스입니다. 왜 이것이 블록체인의 가장 중요한 부분인지 설명하면, 분산 시스템에서는 신뢰할 수 없는 참여자들을 가정합니다.

예를 들어, 악의적인 노드가 "100 BTC를 자기 계좌에 입금"하는 트랜잭션을 만들어 보낸다면, 모든 정직한 노드가 이를 검증하고 거부해야 합니다. 검증이 없다면 블록체인은 몇 초 만에 무너집니다.

기존 중앙 시스템에서는 서버가 권한과 데이터베이스 무결성 제약을 통해 검증했다면, 이제는 암호학적 증명과 합의 알고리즘을 통해 분산된 방식으로 검증합니다. 검증 시스템의 핵심 특징은 첫째, 결정론적입니다.

같은 트랜잭션과 블록체인 상태에 대해 모든 노드가 같은 결과를 얻습니다. 둘째, 포괄적입니다.

여러 계층의 검증을 통과해야 합니다. 셋째, 효율적이어야 합니다.

초당 수천 건을 검증할 수 있어야 네트워크가 확장됩니다. 이러한 특징들이 블록체인의 신뢰 없는 보안을 가능하게 합니다.

코드 예제

// 트랜잭션 검증 메서드
public validateTransaction(
  tx: Transaction,
  unspentTxOuts: UnspentTxOut[]
): boolean {
  // 1. 트랜잭션 ID 검증 (해시가 올바른지)
  if (tx.id !== this.calculateTxId(tx)) {
    console.log('트랜잭션 ID가 일치하지 않습니다');
    return false;
  }

  // 2. 모든 입력의 서명 검증
  for (const txIn of tx.txIns) {
    if (!this.validateTxIn(txIn, tx, unspentTxOuts)) {
      console.log(`입력 검증 실패: ${txIn.txOutId}:${txIn.txOutIndex}`);
      return false;
    }
  }

  // 3. 입력과 출력의 금액 일치 검증
  const totalInputAmount = tx.txIns.reduce((sum, txIn) => {
    const uTxO = this.findUnspentTxOut(txIn, unspentTxOuts);
    return sum + (uTxO ? uTxO.amount : 0);
  }, 0);

  const totalOutputAmount = tx.txOuts.reduce(
    (sum, txOut) => sum + txOut.amount,
    0
  );

  // 출력이 입력보다 크면 유효하지 않음 (코인 생성 불가)
  if (totalOutputAmount > totalInputAmount) {
    console.log('출력이 입력을 초과합니다');
    return false;
  }

  // 4. 출력 금액이 모두 양수인지 확인
  for (const txOut of tx.txOuts) {
    if (txOut.amount <= 0) {
      console.log('출력 금액이 0 이하입니다');
      return false;
    }
  }

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

// 개별 입력 검증 (서명 확인)
private validateTxIn(
  txIn: TxIn,
  tx: Transaction,
  unspentTxOuts: UnspentTxOut[]
): boolean {
  // 참조하는 UTXO 찾기
  const referencedUTxO = this.findUnspentTxOut(txIn, unspentTxOuts);

  if (!referencedUTxO) {
    console.log('참조된 UTXO를 찾을 수 없습니다 (이미 사용되었거나 존재하지 않음)');
    return false;
  }

  // 공개키(주소)로 서명 검증
  const address = referencedUTxO.address;
  const message = tx.id; // 트랜잭션 ID를 메시지로 서명

  return this.verifySignature(address, txIn.signature, message);
}

설명

이것이 하는 일: 네트워크에서 받은 트랜잭션이 블록체인의 모든 규칙을 준수하는지 철저히 검사하여, 위조, 이중 지불, 불법적인 코인 생성 등을 방지하고 블록체인의 무결성을 유지합니다. 첫 번째로, 트랜잭션 ID의 무결성을 검증합니다.

트랜잭션 ID는 입력과 출력의 내용을 해싱한 값이므로, 트랜잭션이 전송 중에 변조되었다면 ID가 일치하지 않습니다. calculateTxId 메서드는 동일한 해싱 알고리즘(예: SHA-256)을 사용하여 ID를 재계산하고, 트랜잭션에 포함된 ID와 비교합니다.

이는 데이터 무결성의 첫 번째 방어선입니다. 그 다음으로, 가장 중요한 서명 검증을 수행합니다.

validateTxIn 메서드는 각 입력에 대해 참조하는 UTXO를 찾고, 그 UTXO의 소유자 주소(공개키)를 사용하여 입력의 서명을 검증합니다. verifySignature 함수는 ECDSA 같은 암호 알고리즘을 사용하여 "이 서명이 정말 해당 공개키에 대응하는 개인키로 만들어졌는지"를 수학적으로 증명합니다.

서명이 유효하지 않으면 해당 UTXO의 소유자가 아니라는 뜻이므로 트랜잭션을 거부합니다. 세 번째로, 금액의 일관성을 검증합니다.

모든 입력 UTXO의 금액을 합산하고(totalInputAmount), 모든 출력의 금액을 합산합니다(totalOutputAmount). 블록체인에서는 코인을 무에서 창조할 수 없으므로(코인베이스 트랜잭션 제외), 출력이 입력을 초과하면 명백히 불법입니다.

이 검증이 실패하면 누군가 없는 돈을 만들어내려는 시도이므로 즉시 거부됩니다. 입력이 출력보다 큰 것은 허용되며, 그 차이가 채굴자 수수료가 됩니다.

마지막으로, 각 출력의 금액이 양수인지 확인합니다. 음수 금액이나 0은 논리적으로 의미가 없으며, 일부 구현에서 정수 오버플로우 같은 버그를 유발할 수 있습니다.

과거 비트코인에서도 오버플로우 버그로 184억 BTC가 생성된 사건이 있었습니다. 모든 금액을 양수로 제한하여 이런 취약점을 예방합니다.

여러분이 이 코드를 사용하면 블록체인 네트워크가 공격에 강해집니다. 모든 노드가 독립적으로 같은 검증을 수행하므로, 악의적인 노드가 일부 있어도 다수의 정직한 노드가 잘못된 트랜잭션을 거부합니다.

또한 암호학적 서명으로 소유권을 수학적으로 증명하여, 물리적 보안이나 신뢰가 필요 없는 진정한 탈중앙화를 실현합니다.

실전 팁

💡 검증 실패 시 구체적인 에러 메시지와 함께 로깅하세요. 디버깅과 보안 모니터링에 필수적입니다. 하지만 외부에 노출할 때는 일반화된 메시지를 사용하여 공격 힌트를 주지 마세요.

💡 서명 검증은 CPU 집약적이므로, 대량의 트랜잭션을 처리할 때는 멀티코어를 활용한 병렬 검증을 구현하세요. 비트코인 코어는 스크립트 검증 스레드를 사용합니다.

💡 같은 트랜잭션을 여러 번 검증하지 않도록 검증 결과를 캐싱하세요. 트랜잭션 ID를 키로 하는 LRU 캐시를 사용하면 효율적입니다.

💡 "타임락(timelock)"이나 "시퀀스 넘버" 같은 고급 기능을 지원한다면 별도의 검증 로직을 추가하세요. 이는 결제 채널이나 원자적 스왑 같은 응용에 필수적입니다.

💡 검증 로직을 별도 모듈로 분리하고 포괄적인 단위 테스트를 작성하세요. 유효한 트랜잭션을 거부하거나 무효한 트랜잭션을 승인하는 버그는 재앙적일 수 있습니다.


9. HD 지갑과 주소 파생 - 프라이버시와 편의성

시작하며

여러분이 비트코인을 매번 받을 때마다 같은 주소를 사용한다면, 누구나 블록체인 탐색기에서 여러분의 전체 거래 내역과 잔액을 볼 수 있습니다. 이는 은행 계좌번호를 모든 사람에게 공개하는 것과 같습니다.

프라이버시를 위해 매번 새로운 주소를 사용하는 것이 권장되지만, 각 주소마다 개인키를 따로 관리하면 백업이 악몽이 됩니다. 100개의 주소를 사용했다면 100개의 개인키를 모두 백업해야 할까요?

바로 이럴 때 필요한 것이 HD(Hierarchical Deterministic) 지갑입니다. 단 하나의 시드(seed)에서 수십억 개의 주소와 개인키를 결정론적으로 생성하여, 백업은 한 번만 하고 프라이버시는 최대한 보호합니다.

개요

간단히 말해서, HD 지갑은 마스터 시드에서 BIP32/BIP44 같은 표준 알고리즘을 사용하여 계층적으로 무한한 키 쌍을 파생하는 지갑 시스템입니다. 왜 이것이 현대 암호화폐 지갑의 표준이 되었는지 설명하면, 사용자 경험과 보안을 모두 개선하기 때문입니다.

예를 들어, 12~24단어의 니모닉 문구만 안전하게 보관하면 지갑을 완전히 복원할 수 있습니다. 새 주소를 받고 싶을 때마다 백업할 필요가 없습니다.

또한 거래마다 다른 주소를 사용하여 블록체인 분석을 어렵게 만들어 프라이버시가 향상됩니다. 기존 "랜덤 주소" 방식에서는 각 개인키를 독립적으로 생성하고 관리했다면, 이제는 하나의 시드에서 파생 경로(derivation path)를 사용하여 수학적으로 계산합니다.

HD 지갑의 핵심 특징은 첫째, 결정론적 파생입니다. 같은 시드와 경로는 항상 같은 키를 생성합니다.

둘째, 계층적 구조입니다. m/44'/0'/0'/0/0 같은 경로로 계정, 체인, 인덱스를 구분합니다.

셋째, 하드닝(hardening)입니다. 일부 키는 확장 공개키로부터 파생할 수 없게 보호합니다.

이러한 특징들이 보안과 유연성을 동시에 제공합니다.

코드 예제

// HD 지갑 구현 (BIP32/BIP44 기반)
import { BIP32Factory } from 'bip32';
import * as bip39 from 'bip39';

class HDWallet {
  private masterNode: any;
  private currentIndex: number = 0;

  // 니모닉 문구로 HD 지갑 생성
  constructor(mnemonic?: string) {
    // 니모닉이 없으면 새로 생성
    if (!mnemonic) {
      mnemonic = bip39.generateMnemonic(256); // 24단어
      console.log('🔐 니모닉 문구를 안전하게 보관하세요:', mnemonic);
    }

    // 니모닉 → 시드 → 마스터 키
    const seed = bip39.mnemonicToSeedSync(mnemonic);
    this.masterNode = BIP32Factory().fromSeed(seed);
  }

  // BIP44 경로로 주소 파생: m/44'/0'/0'/0/index
  public deriveAddress(index: number): {
    address: string;
    privateKey: string;
  } {
    // 44' = BIP44, 0' = Bitcoin, 0' = Account 0, 0 = External chain
    const path = `m/44'/0'/0'/0/${index}`;
    const child = this.masterNode.derivePath(path);

    // 공개키에서 주소 생성 (실제로는 해싱 필요)
    const address = this.publicKeyToAddress(child.publicKey);
    const privateKey = child.privateKey.toString('hex');

    return { address, privateKey };
  }

  // 새로운 받기 주소 생성 (프라이버시를 위해 매번 다른 주소)
  public getNewReceivingAddress(): string {
    const { address } = this.deriveAddress(this.currentIndex);
    this.currentIndex++; // 다음 번에는 새 주소 사용
    return address;
  }

  // 거스름돈 주소 생성 (Change chain: m/44'/0'/0'/1/index)
  public getChangeAddress(index: number): string {
    const path = `m/44'/0'/0'/1/${index}`; // 1 = Change chain
    const child = this.masterNode.derivePath(path);
    return this.publicKeyToAddress(child.publicKey);
  }

  // 공개키를 주소로 변환 (간소화)
  private publicKeyToAddress(publicKey: Buffer): string {
    // 실제로는 SHA-256 → RIPEMD-160 → Base58Check
    return publicKey.toString('hex').substring(0, 40);
  }
}

설명

이것이 하는 일: 단일 마스터 시드를 기반으로 계층적이고 결정론적인 방식으로 수십억 개의 주소와 개인키를 생성하여, 사용자는 니모닉 문구만 보관하면 되고 프라이버시는 최대한 보호합니다. 첫 번째로, 생성자에서 니모닉 문구(mnemonic phrase)를 처리합니다.

니모닉이 제공되지 않으면 bip39.generateMnemonic(256)으로 새로운 24단어 문구를 생성합니다. 이 문구는 2048개의 단어 목록에서 선택되며, 256비트의 엔트로피를 제공하여 사실상 추측 불가능합니다.

그런 다음 mnemonicToSeedSync로 니모닉을 512비트 시드로 변환하고, 이 시드에서 BIP32 마스터 키를 생성합니다. 이 마스터 키가 모든 자식 키의 뿌리가 됩니다.

그 다음으로, deriveAddress 메서드는 BIP44 표준 경로를 사용하여 특정 인덱스의 주소를 파생합니다. 경로 m/44'/0'/0'/0/index에서 각 요소는 의미가 있습니다: 44'는 BIP44 표준, 0'는 비트코인(코인 타입), 첫 번째 0'는 계정 번호, 두 번째 0은 외부 체인(받기 주소), index는 주소 인덱스입니다.

따옴표(')는 하드닝을 의미하여, 확장 공개키가 노출되어도 하드닝된 키는 파생할 수 없게 보호합니다. 세 번째로, getNewReceivingAddress 메서드는 매번 호출될 때마다 currentIndex를 증가시켜 새로운 주소를 반환합니다.

이렇게 하면 사용자가 비트코인을 받을 때마다 다른 주소를 사용하게 되어, 블록체인 분석자가 "이 주소들이 모두 같은 지갑에 속한다"는 것을 쉽게 파악할 수 없습니다. 이는 재사용 주소 공격(address reuse attack)과 클러스터링 분석을 방어하는 핵심 기법입니다.

마지막으로, getChangeAddress 메서드는 거스름돈을 받을 별도의 주소를 생성합니다. BIP44는 두 개의 체인을 정의합니다: 외부 체인(0)은 다른 사람으로부터 받는 주소, 내부 체인(1)은 거스름돈을 받는 주소입니다.

이렇게 분리하면 지갑 소프트웨어가 "아직 사용하지 않은 받기 주소"를 더 효율적으로 추적할 수 있습니다. 여러분이 이 코드를 사용하면 사용자에게 최고의 보안과 편의성을 제공할 수 있습니다.

니모닉 문구 24단어만 종이에 적어 금고에 보관하면 되고, 모바일 지갑을 잃어버려도 언제든 복원할 수 있습니다. 또한 거래마다 새 주소를 자동으로 사용하여 프라이버시 모범 사례를 자연스럽게 따르게 됩니다.

기업용 지갑에서는 여러 계정을 생성하여 부서별로 관리할 수도 있습니다.

실전 팁

💡 니모닉 문구를 절대 평문으로 저장하지 마세요. 사용자에게 종이에 적어 오프라인으로 보관하도록 안내하고, 앱에는 암호화된 시드만 저장하세요. 하드웨어 월렛(Ledger, Trezor)은 시드를 장치 밖으로 절대 내보내지 않습니다.

💡 BIP39 니모닉에 추가로 패스프레이즈(25번째 단어)를 지원하면 보안이 더 강화됩니다. 같은 니모닉이라도 다른 패스프레이즈는 완전히 다른 지갑을 생성하므로, 강제로 지갑을 열라는 상황에서 "미끼 지갑"을 보여줄 수 있습니다.

💡 주소 파생 시 "갭 리밋(gap limit)" 개념을 구현하세요. BIP44는 연속으로 20개의 사용되지 않은 주소를 발견하면 스캔을 중단하도록 권장합니다. 그렇지 않으면 잔액 복원 시 무한히 스캔해야 합니다.

💡 멀티코인 지갑을 만들 때는 코인 타입 인덱스를 올바르게 사용하세요: 비트코인=0, 이더리움=60, 리플=144 등. SLIP-44에 정의된 표준을 따라야 다른 지갑과 호환됩니다.

💡 확장 공개키(xpub)를 서버에 저장하면 개인키 없이도 받기 주소를 생성할 수 있어, 온라인 쇼핑몰 같은 곳에서 유용합니다. 하지만 xpub가 유출되면 모든 거래 내역이 노출되므로 주의하세요.


10. 블록 생성과 트랜잭션 포함 - 채굴자의 역할

시작하며

여러분이 만든 완벽한 트랜잭션이 멤풀에서 몇 시간씩 대기하다가 마침내 블록에 포함되는 순간, 비로소 "확정"됩니다. 이 마법 같은 일을 누가, 어떻게 하는 걸까요?

채굴자(miner)는 블록체인의 회계사이자 보안 담당자입니다. 수천 개의 트랜잭션 중에서 유효한 것들을 선택하여 블록으로 묶고, 작업 증명(Proof of Work)을 수행하여 블록체인에 추가합니다.

이 과정 없이는 트랜잭션이 영원히 대기 상태로 남습니다. 바로 이럴 때 필요한 것이 블록 생성 메커니즘입니다.

멤풀에서 수익이 높은 트랜잭션을 선택하고, 코인베이스 트랜잭션을 추가하며, 이전 블록과 연결하여 체인의 무결성을 유지합니다.

개요

간단히 말해서, 블록 생성은 멤풀의 트랜잭션들을 모아 새로운 블록을 만들고, 이전 블록의 해시를 참조하여 체인에 연결하며, 작업 증명을 통해 네트워크가 승인하도록 하는 프로세스입니다. 왜 이것이 블록체인의 핵심인지 설명하면, 블록 없이는 트랜잭션이 영구적으로 기록될 수 없습니다.

예를 들어, 채굴자가 없다면 여러분의 송금은 영원히 "pending" 상태로 남습니다. 채굴자는 경제적 인센티브(블록 보상 + 수수료)를 받고 이 일을 하므로, 블록체인 경제 모델의 핵심입니다.

기존 중앙 서버에서는 데이터베이스 트랜잭션을 커밋하는 것으로 충분했다면, 이제는 분산된 노드들이 합의를 이루고 경쟁적으로 블록을 생성하는 복잡한 프로세스가 필요합니다. 블록 생성의 핵심 특징은 첫째, 경쟁적입니다.

여러 채굴자가 동시에 블록을 생성하려 하며, 먼저 작업 증명을 완료한 자가 보상을 받습니다. 둘째, 인센티브 기반입니다.

채굴자는 블록 보상과 수수료로 동기부여됩니다. 셋째, 검증 가능합니다.

생성된 블록은 모든 노드가 독립적으로 검증할 수 있습니다. 이러한 특징들이 탈중앙화된 합의를 가능하게 합니다.

코드 예제

// 새 블록 생성 (채굴)
public generateBlock(minerAddress: string): Block {
  // 1. 최신 블록 가져오기
  const previousBlock = this.blockchain.getLatestBlock();
  const nextIndex = previousBlock.index + 1;
  const timestamp = Math.floor(Date.now() / 1000);

  // 2. 코인베이스 트랜잭션 생성 (채굴 보상)
  const BLOCK_REWARD = 50; // 블록 보상 (비트코인은 현재 6.25 BTC)
  const coinbaseTx = this.createCoinbaseTransaction(
    minerAddress,
    BLOCK_REWARD,
    nextIndex
  );

  // 3. 멤풀에서 수수료가 높은 트랜잭션 선택
  const MAX_BLOCK_SIZE = 1000; // 블록당 최대 트랜잭션 수
  const selectedTxs = this.mempool.getTopTransactions(MAX_BLOCK_SIZE - 1);

  // 4. 총 수수료 계산
  const totalFees = selectedTxs.reduce((sum, tx) => {
    return sum + this.calculateFee(tx);
  }, 0);

  // 5. 코인베이스 트랜잭션에 수수료 추가
  coinbaseTx.txOuts[0].amount = BLOCK_REWARD + totalFees;

  // 6. 모든 트랜잭션 결합 (코인베이스가 첫 번째)
  const transactions = [coinbaseTx, ...selectedTxs];

  // 7. 블록 생성
  const newBlock = new Block(
    nextIndex,
    previousBlock.hash,
    timestamp,
    transactions,
    0 // nonce는 작업 증명에서 증가
  );

  // 8. 작업 증명 수행 (난이도 조건을 만족하는 해시 찾기)
  newBlock.hash = this.mineBlock(newBlock);

  console.log(`✅ 블록 #${nextIndex} 생성 완료! 보상: ${BLOCK_REWARD + totalFees} BTC`);

  // 9. 블록체인에 추가
  this.blockchain.addBlock(newBlock);

  // 10. 포함된 트랜잭션을 멤풀에서 제거
  const txIds = transactions.map(tx => tx.id);
  this.mempool.removeConfirmedTransactions(txIds);

  return newBlock;
}

// 코인베이스 트랜잭션 생성 (보상 받기)
private createCoinbaseTransaction(
  minerAddress: string,
  reward: number,
  blockIndex: number
): Transaction {
  const txIn = new TxIn('', blockIndex, ''); // 입력 없음 (코인 생성)
  const txOut = new TxOut(minerAddress, reward);
  return new Transaction([txIn], [txOut]);
}

설명

이것이 하는 일: 멤풀에 대기 중인 트랜잭션들을 선별하여 새로운 블록으로 묶고, 채굴자에게 보상을 주는 특별한 코인베이스 트랜잭션을 포함시키며, 작업 증명을 수행하여 블록체인에 추가합니다. 첫 번째로, 블록의 기본 정보를 설정합니다.

getLatestBlock()으로 현재 블록체인의 가장 최근 블록을 가져와서 그 인덱스에 1을 더해 다음 블록의 인덱스를 결정합니다. 타임스탬프는 현재 Unix 시간(초 단위)으로 설정하여 블록의 생성 시점을 기록합니다.

이전 블록의 해시를 참조하여 블록들이 연결된 체인을 형성하며, 이것이 "블록체인"이라는 이름의 유래입니다. 그 다음으로, 매우 특별한 코인베이스 트랜잭션을 생성합니다.

일반 트랜잭션과 달리 코인베이스는 입력이 없습니다(txIntxOutId가 빈 문자열). 이는 새로운 코인이 무에서 창조되는 유일한 합법적인 방법입니다.

비트코인 초기에는 블록 보상이 50 BTC였지만, 21만 블록(약 4년)마다 절반으로 줄어들어 현재는 6.25 BTC입니다. 이것이 비트코인의 인플레이션을 통제하는 메커니즘입니다.

세 번째로, 멤풀에서 수익을 최대화할 트랜잭션을 선택합니다. getTopTransactions는 수수료가 높은 순서로 정렬된 트랜잭션을 반환하며, 블록 크기 제한(실제 비트코인은 약 1MB)을 고려하여 개수를 제한합니다.

코인베이스 트랜잭션을 위해 한 자리를 남겨두므로 MAX_BLOCK_SIZE - 1개를 선택합니다. 그 다음 선택된 모든 트랜잭션의 수수료를 합산하여 totalFees를 계산합니다.

네 번째로, 코인베이스 트랜잭션의 출력 금액을 블록 보상과 총 수수료의 합으로 설정합니다. 이것이 채굴자의 실제 수익이 되며, 채굴자가 높은 수수료의 트랜잭션을 우선 선택하도록 동기부여합니다.

트랜잭션들을 배열로 결합할 때 코인베이스가 항상 첫 번째에 오는 것이 규칙입니다. 마지막으로, mineBlock 메서드를 호출하여 작업 증명을 수행합니다.


#TypeScript#Blockchain#Bitcoin#UTXO#Transaction#typescript

댓글 (0)

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