🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

타입스크립트로 비트코인 클론하기 13편 - 트랜잭션 데이터 구조 설계 - 슬라이드 1/11
A

AI Generated

2025. 11. 10. · 18 Views

타입스크립트로 비트코인 클론하기 13편 - 트랜잭션 데이터 구조 설계

비트코인의 핵심인 트랜잭션 데이터 구조를 타입스크립트로 구현하는 방법을 배웁니다. UTXO 모델의 Input과 Output 구조부터 서명 검증까지, 실제 블록체인 트랜잭션의 모든 것을 단계별로 알아봅니다.


목차

  1. TxIn 트랜잭션 입력 구조 - 이전 출력을 참조하는 핵심 설계
  2. TxOut 트랜잭션 출력 구조 - 금액과 수신자를 정의하는 설계
  3. Transaction 트랜잭션 클래스 - 입력과 출력을 통합하는 완전한 거래
  4. getTxId 트랜잭션 ID 생성 - 해시 기반 고유 식별자
  5. signTxIn 입력 서명 생성 - 개인키로 소유권 증명하기
  6. validateTxIn 입력 서명 검증 - 위조 방지를 위한 필수 단계
  7. UnspentTxOut UTXO 구조 - 사용 가능한 출력 추적하기
  8. updateUnspentTxOuts UTXO 집합 업데이트 - 상태 전이 로직
  9. findUnspentTxOut UTXO 검색 - 효율적인 조회 유틸리티
  10. 코인베이스 트랜잭션 - 블록 보상과 새로운 비트코인 발행

1. TxIn 트랜잭션 입력 구조 - 이전 출력을 참조하는 핵심 설계

시작하며

여러분이 친구에게 돈을 송금할 때, 은행 시스템은 여러분의 계좌 잔액을 확인하고 차감합니다. 그런데 비트코인은 이와 완전히 다른 방식으로 작동한다는 사실, 알고 계셨나요?

비트코인에는 "잔액"이라는 개념이 없습니다. 대신 "아직 사용하지 않은 이전 거래의 출력(UTXO)"들을 모아서 새로운 거래의 입력으로 사용합니다.

마치 지갑에 있는 지폐들을 꺼내서 물건값을 지불하는 것과 비슷하죠. 바로 이럴 때 필요한 것이 TxIn(Transaction Input) 구조입니다.

이 구조는 "어떤 이전 거래의 몇 번째 출력을 사용할 것인가"를 정확히 가리키고, 그것을 사용할 권한이 있음을 증명하는 역할을 합니다.

개요

간단히 말해서, TxIn은 이전 트랜잭션의 특정 출력(UTXO)을 가리키는 포인터이자, 그것을 사용할 권한을 증명하는 서명을 담는 컨테이너입니다. 왜 이런 구조가 필요할까요?

실무 관점에서 보면, 블록체인의 모든 거래는 추적 가능해야 합니다. 새로운 거래가 생성될 때마다 "이 돈은 어디서 왔는가?"를 명확히 알 수 있어야 이중 지불을 막을 수 있죠.

예를 들어, Alice가 Bob에게 1 BTC를 보낼 때, Alice가 정말로 그 1 BTC를 소유하고 있는지 확인해야 합니다. 전통적인 은행 시스템에서는 중앙 데이터베이스가 잔액을 관리했다면, 비트코인에서는 각 트랜잭션이 이전 트랜잭션을 명시적으로 참조함으로써 전체 거래 체인을 만들어냅니다.

TxIn의 핵심 특징은 세 가지입니다. 첫째, txOutId로 이전 트랜잭션을 식별하고, 둘째, txOutIndex로 그 트랜잭션의 몇 번째 출력인지 지정하며, 셋째, signature로 소유권을 증명합니다.

이러한 특징들이 분산 환경에서 신뢰 없이도 안전한 거래를 가능하게 만듭니다.

코드 예제

// TxIn: 트랜잭션 입력 - 이전 거래의 출력을 참조
class TxIn {
  // 참조하는 이전 트랜잭션의 ID (해시값)
  public txOutId: string;

  // 이전 트랜잭션의 출력 배열에서 몇 번째 출력인지
  public txOutIndex: number;

  // 소유권을 증명하는 디지털 서명
  public signature: string;

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

설명

이것이 하는 일: TxIn 클래스는 비트코인 트랜잭션에서 "입력"을 표현하는 데이터 구조입니다. 새로운 거래를 만들 때 기존에 받았던 비트코인(UTXO)을 어떻게 찾아서 사용할지 정의합니다.

첫 번째로, txOutId는 이전 트랜잭션의 고유 식별자입니다. 블록체인에 기록된 수많은 트랜잭션 중에서 정확히 어떤 트랜잭션의 출력을 사용할 것인지 지정합니다.

이 값은 SHA256 해시로 생성되어 64자의 16진수 문자열 형태를 가집니다. 마치 도서관에서 책의 ISBN 번호로 특정 책을 찾는 것과 같습니다.

두 번째로, txOutIndex가 중요한 이유는 하나의 트랜잭션이 여러 개의 출력을 가질 수 있기 때문입니다. 예를 들어, Alice가 Bob에게 1 BTC를 보내고 자신에게 0.5 BTC를 거스름돈으로 돌려받는다면, 이 트랜잭션은 2개의 출력을 갖습니다.

txOutIndex는 0부터 시작하는 인덱스로 정확히 몇 번째 출력을 사용할지 지정합니다. 세 번째로, signature는 가장 중요한 보안 요소입니다.

이전 출력의 소유자만이 자신의 개인키로 유효한 서명을 만들 수 있습니다. 네트워크의 모든 노드는 이 서명을 검증하여 트랜잭션 생성자가 정당한 소유자인지 확인합니다.

서명이 없다면 누구나 다른 사람의 비트코인을 마음대로 사용할 수 있겠죠. 여러분이 이 코드를 사용하면 UTXO 기반의 트랜잭션 시스템을 구현할 수 있습니다.

각 입력이 명확한 출처를 가지므로 전체 거래 흐름을 추적할 수 있고, 서명 검증을 통해 권한 없는 지출을 막을 수 있으며, 이중 지불 같은 공격을 효과적으로 방어할 수 있습니다.

실전 팁

💡 txOutId는 반드시 존재하는 트랜잭션의 ID여야 하며, 해당 트랜잭션이 블록체인에 실제로 기록되어 있는지 검증하세요. 존재하지 않는 ID를 참조하면 invalid transaction으로 거부됩니다.

💡 txOutIndex가 배열 범위를 벗어나지 않는지 체크하세요. 이전 트랜잭션의 출력 개수를 확인하고, index가 그보다 작은지 검증하는 로직이 필요합니다.

💡 signature는 트랜잭션 데이터와 개인키로 생성되므로, 트랜잭션 내용이 조금이라도 변경되면 서명이 무효화됩니다. 이를 활용하여 데이터 무결성을 보장할 수 있습니다.

💡 개발 단계에서는 signature를 빈 문자열로 초기화하고, 나중에 트랜잭션 서명 단계에서 실제 값을 채우는 패턴을 사용하면 코드 구조가 깔끔해집니다.

💡 메모리 풀(mempool)에서 대기 중인 트랜잭션의 입력들을 추적하여, 같은 UTXO를 참조하는 중복 트랜잭션이 생성되지 않도록 관리하세요.


2. TxOut 트랜잭션 출력 구조 - 금액과 수신자를 정의하는 설계

시작하며

여러분이 친구에게 현금을 줄 때를 생각해보세요. 얼마를 주는지 금액이 중요하고, 누구에게 주는지 수신자가 명확해야 합니다.

그런데 디지털 화폐에서는 이 정보를 어떻게 안전하게 표현할 수 있을까요? 비트코인 트랜잭션의 출력은 단순히 "Bob에게 1 BTC"라고 기록하는 게 아닙니다.

대신 암호학적 퍼즐을 만들어서 "이 퍼즐을 풀 수 있는 사람이 1 BTC를 가져갈 수 있다"는 방식으로 작동합니다. Bob의 개인키만이 이 퍼즐을 풀 수 있죠.

바로 이럴 때 필요한 것이 TxOut(Transaction Output) 구조입니다. 이 구조는 송금할 금액과 수신자의 주소를 담아, 미래에 누군가가 이 출력을 입력으로 사용할 때의 조건을 정의합니다.

개요

간단히 말해서, TxOut은 특정 주소로 보낼 비트코인의 양을 정의하는 데이터 구조이며, 이 비트코인을 나중에 사용하기 위한 "자물쇠"를 만드는 역할을 합니다. 왜 이런 구조가 필요한가?

실무에서 블록체인의 모든 거래는 투명하게 공개되지만, 동시에 소유권은 보호되어야 합니다. TxOut은 금액(amount)과 주소(address)를 함께 저장함으로써, 누구나 "얼마가 어디로 갔는지"는 볼 수 있지만, 오직 개인키를 가진 사람만 그것을 사용할 수 있게 만듭니다.

예를 들어, 거래소에서 사용자에게 출금할 때 정확한 금액과 사용자 주소를 TxOut으로 만들어 전송합니다. 기존에는 중앙 서버가 "Alice 계좌에서 1 BTC 차감, Bob 계좌에 1 BTC 추가"라고 기록했다면, 비트코인에서는 "Bob의 주소로 잠긴 1 BTC 출력을 생성"하는 방식으로 변경됩니다.

TxOut의 핵심 특징은 두 가지입니다. 첫째, amount는 사토시(satoshi) 단위로 정확한 금액을 저장하여 소수점 오류를 방지하고, 둘째, address는 공개키의 해시값으로 수신자를 익명화하면서도 검증 가능하게 만듭니다.

이러한 특징들이 투명성과 프라이버시의 균형을 이룹니다.

코드 예제

// TxOut: 트랜잭션 출력 - 누구에게 얼마를 보낼지 정의
class TxOut {
  // 수신자의 비트코인 주소 (공개키 해시)
  public address: string;

  // 전송할 금액 (사토시 단위, 1 BTC = 100,000,000 satoshi)
  public amount: number;

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

// 실제 사용 예시
const output1 = new TxOut("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 50000000); // Bob에게 0.5 BTC
const output2 = new TxOut("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", 49000000); // 자신에게 거스름돈 0.49 BTC

설명

이것이 하는 일: TxOut 클래스는 비트코인 트랜잭션의 "출력"을 표현합니다. 송금의 최종 목적지와 금액을 명시하여, 블록체인에 새로운 UTXO를 생성하는 역할을 합니다.

첫 번째로, address 필드는 수신자를 식별합니다. 비트코인 주소는 공개키를 여러 번 해싱하여 생성된 문자열로, "1"로 시작하는 Base58 인코딩 형태를 가집니다.

실제 이름이나 신원 정보는 포함되지 않지만, 이 주소에 대응하는 개인키를 가진 사람만이 나중에 이 출력을 사용할 수 있습니다. 마치 사물함 번호처럼, 번호는 공개되어 있지만 열쇠는 소유자만 갖고 있는 구조입니다.

두 번째로, amount는 정수형으로 저장됩니다. 비트코인에서는 부동소수점 연산의 정밀도 문제를 피하기 위해 가장 작은 단위인 사토시로 표현합니다.

1 BTC = 100,000,000 satoshi이므로, 0.5 BTC는 50,000,000으로 저장됩니다. 이렇게 하면 0.1 + 0.2 = 0.30000000004 같은 부동소수점 오류를 완전히 제거할 수 있습니다.

생성된 TxOut은 블록체인에 기록되어 "미사용 트랜잭션 출력(UTXO)"이 됩니다. 다른 사람이나 자기 자신이 나중에 새로운 트랜잭션을 만들 때, 이 UTXO를 TxIn으로 참조하여 비트코인을 사용할 수 있습니다.

사용되면 UTXO 집합에서 제거되고, 사용되지 않으면 계속 보유 상태로 남아있습니다. 여러분이 이 코드를 사용하면 정확한 금액의 송금을 구현할 수 있습니다.

소수점 오류 없이 정밀한 계산이 가능하고, 주소 기반의 익명성을 제공하면서도 소유권 증명은 확실하게 할 수 있으며, UTXO 모델의 기반이 되어 전체 시스템의 상태를 효율적으로 관리할 수 있습니다.

실전 팁

💡 amount는 항상 양수여야 하며, 최소값(dust limit) 이하의 너무 작은 금액은 네트워크에서 거부될 수 있습니다. 실제 비트코인은 546 satoshi를 최소 금액으로 권장합니다.

💡 address 형식을 검증하는 로직을 추가하세요. Base58 디코딩 후 체크섬을 확인하여 잘못된 주소로의 송금을 방지할 수 있습니다.

💡 트랜잭션의 모든 입력 금액의 합과 모든 출력 금액의 합을 비교하여, 차액이 적절한 수수료 범위 내에 있는지 검증하세요. 차액이 너무 크면 실수로 과도한 수수료를 지불할 수 있습니다.

💡 거스름돈 출력을 만들 때는 새로운 주소를 생성하여 프라이버시를 강화하세요. 같은 주소를 재사용하면 거래 패턴이 노출됩니다.

💡 출력을 생성할 때 BTC 단위가 아닌 satoshi 단위로 직접 작업하여 변환 과정의 오류를 줄이세요. 사용자 인터페이스에서만 BTC로 표시하고, 내부 로직은 항상 satoshi를 사용하는 것이 안전합니다.


3. Transaction 트랜잭션 클래스 - 입력과 출력을 통합하는 완전한 거래

시작하며

여러분이 쇼핑몰에서 물건을 살 때를 상상해보세요. 계산대에서 지갑의 지폐 여러 장을 내고, 물건값과 거스름돈을 받습니다.

비트코인 트랜잭션도 정확히 이와 같은 방식으로 작동합니다. 하지만 디지털 환경에서는 물리적인 지갑과 지폐가 없습니다.

여러 개의 입력(TxIn)을 모아서 충분한 금액을 만들고, 여러 개의 출력(TxOut)으로 수신자와 거스름돈을 처리해야 합니다. 이 모든 것을 하나의 원자적(atomic) 단위로 묶어야 전체 거래가 성공하거나 실패하죠.

바로 이럴 때 필요한 것이 Transaction 클래스입니다. 이 클래스는 여러 입력과 출력을 하나로 묶고, 고유한 ID를 부여하며, 전체 거래의 유효성을 보장하는 컨테이너 역할을 합니다.

개요

간단히 말해서, Transaction은 하나 이상의 입력(TxIn)과 출력(TxOut)을 포함하는 완전한 거래 단위이며, 블록체인에 기록되는 최소 단위입니다. 왜 이 구조가 필요할까요?

실무 관점에서 보면, 블록체인은 개별 입력이나 출력이 아닌 완전한 트랜잭션 단위로 검증하고 기록합니다. 하나의 트랜잭션 안에서 모든 입력의 서명이 유효해야 하고, 입력 금액의 합이 출력 금액의 합보다 크거나 같아야 합니다.

예를 들어, Alice가 3 BTC를 보내려는데 2 BTC짜리 UTXO 하나와 1.5 BTC짜리 UTXO 하나를 가지고 있다면, 두 개의 입력을 하나의 트랜잭션으로 묶어야 합니다. 기존의 데이터베이스 트랜잭션에서는 BEGIN-COMMIT으로 원자성을 보장했다면, 비트코인에서는 Transaction 객체 자체가 원자적 단위가 되어 전체가 성공하거나 전체가 실패합니다.

Transaction의 핵심 특징은 네 가지입니다. 첫째, id는 트랜잭션 내용의 해시로 생성되어 고유성과 무결성을 보장하고, 둘째, txIns 배열로 여러 입력을 통합하며, 셋째, txOuts 배열로 복수의 수신자를 지원하고, 넷째, 전체 구조가 직렬화 가능하여 네트워크 전송과 블록 저장이 용이합니다.

이러한 특징들이 분산 시스템에서 일관성 있는 거래를 가능하게 합니다.

코드 예제

// Transaction: 완전한 트랜잭션 - 여러 입력과 출력을 포함
class Transaction {
  // 트랜잭션 고유 ID (전체 내용의 해시)
  public id: string;

  // 입력 배열: 사용할 UTXO들
  public txIns: TxIn[];

  // 출력 배열: 생성할 새로운 UTXO들
  public txOuts: TxOut[];

  constructor(id: string, txIns: TxIn[], txOuts: TxOut[]) {
    this.id = id;
    this.txIns = txIns;
    this.txOuts = txOuts;
  }
}

// 실제 사용 예시: Alice가 Bob에게 1.5 BTC 송금
const inputs = [new TxIn("abc123...", 0, "signature1")]; // 2 BTC UTXO 사용
const outputs = [
  new TxOut("bob_address", 150000000),    // Bob에게 1.5 BTC
  new TxOut("alice_address", 49000000)    // 자신에게 0.49 BTC 거스름돈 (0.01 BTC는 수수료)
];
const tx = new Transaction("tx_id_hash", inputs, outputs);

설명

이것이 하는 일: Transaction 클래스는 비트코인 거래의 전체 구조를 표현합니다. 누가 어떤 비트코인을 사용하고(txIns), 누구에게 얼마를 보내는지(txOuts)를 하나의 패키지로 묶어 네트워크에 전파하고 블록체인에 기록합니다.

첫 번째로, id는 이 트랜잭션의 유일한 식별자입니다. txIns와 txOuts의 모든 데이터를 직렬화한 후 SHA256 해시를 두 번 적용하여 생성됩니다.

내용이 1비트라도 변경되면 완전히 다른 ID가 생성되므로, 트랜잭션 위조나 변조를 즉시 탐지할 수 있습니다. 이 ID는 나중에 다른 트랜잭션이 이 트랜잭션의 출력을 참조할 때 사용됩니다.

두 번째로, txIns 배열이 실행되면서 각 입력이 가리키는 UTXO를 찾고 서명을 검증합니다. 모든 입력의 서명이 유효해야만 트랜잭션이 승인됩니다.

예를 들어, txIns에 3개의 입력이 있다면 3개의 서로 다른 UTXO를 소비하는 것이며, 각각에 대한 소유권 증명이 필요합니다. 하나라도 실패하면 전체 트랜잭션이 거부됩니다.

세 번째로, txOuts 배열은 새로운 UTXO들을 생성합니다. 일반적으로 첫 번째 출력은 실제 수신자에게 가는 금액이고, 두 번째 출력은 자신에게 돌아오는 거스름돈입니다.

출력은 2개 이상일 수도 있어서, 한 번의 트랜잭션으로 여러 사람에게 동시에 송금하는 것도 가능합니다. 각 출력은 독립적인 UTXO가 되어 나중에 따로 사용될 수 있습니다.

마지막으로, 입력 금액의 총합에서 출력 금액의 총합을 뺀 차액이 채굴자에게 가는 수수료가 됩니다. 위 예시에서는 2 BTC 입력 - (1.5 BTC + 0.49 BTC) 출력 = 0.01 BTC 수수료입니다.

여러분이 이 코드를 사용하면 복잡한 송금 시나리오를 처리할 수 있습니다. 여러 입력을 모아 큰 금액을 만들 수 있고, 여러 출력으로 동시에 다수에게 송금할 수 있으며, 수수료를 유연하게 조절하여 처리 속도를 조정할 수 있고, 전체 거래 히스토리를 추적 가능한 형태로 블록체인에 영구 기록할 수 있습니다.

실전 팁

💡 트랜잭션 ID를 생성할 때는 반드시 서명 필드를 제외하고 해싱하세요. 서명은 트랜잭션 ID에 의존하므로, ID에 서명을 포함하면 순환 참조가 발생합니다.

💡 입력 금액의 합을 계산할 때는 각 TxIn이 참조하는 이전 TxOut을 실제로 찾아서 금액을 더해야 합니다. TxIn 자체에는 금액 정보가 없습니다.

💡 수수료를 0으로 설정하면 채굴자가 처리하지 않을 수 있으니, 네트워크 상황에 따라 적절한 수수료를 계산하는 로직을 구현하세요. 일반적으로 바이트당 수수료를 기준으로 합니다.

💡 대용량 트랜잭션(입력이나 출력이 매우 많은 경우)은 블록 크기 제한에 걸릴 수 있으니, 필요하다면 여러 개의 작은 트랜잭션으로 분할하세요.

💡 트랜잭션을 직렬화할 때는 결정론적(deterministic) 순서를 유지하여, 같은 데이터가 항상 같은 ID를 생성하도록 하세요. JSON.stringify는 객체 키 순서를 보장하지 않으므로 커스텀 직렬화가 필요합니다.


4. getTxId 트랜잭션 ID 생성 - 해시 기반 고유 식별자

시작하며

여러분이 택배를 보낼 때 송장번호가 부여되는 것처럼, 블록체인의 모든 트랜잭션에도 고유한 식별자가 필요합니다. 그런데 중앙 서버가 없는 분산 시스템에서 어떻게 중복 없는 ID를 생성할 수 있을까요?

비트코인은 이 문제를 해시 함수로 해결합니다. 트랜잭션의 모든 내용을 입력으로 받아 SHA256 해시를 계산하면, 그 결과가 곧 고유한 ID가 됩니다.

같은 내용은 항상 같은 ID를 만들고, 내용이 조금이라도 다르면 완전히 다른 ID가 나오죠. 바로 이럴 때 필요한 것이 getTxId 함수입니다.

이 함수는 트랜잭션의 입력과 출력 데이터를 결정론적으로 문자열화하고 해싱하여, 전 세계 어디서 계산해도 동일한 결과를 보장하는 ID를 생성합니다.

개요

간단히 말해서, getTxId는 트랜잭션의 내용을 암호학적 해시 함수로 변환하여 64자의 16진수 문자열 형태로 고유 ID를 만드는 함수입니다. 왜 이런 함수가 필요할까요?

실무에서 분산 시스템의 각 노드는 독립적으로 트랜잭션을 검증합니다. 이때 모든 노드가 같은 ID를 계산할 수 있어야 합니다.

예를 들어, Alice가 서울에서 트랜잭션을 생성하고, Bob이 뉴욕에서 검증할 때, 둘 다 정확히 같은 ID를 얻어야 동일한 트랜잭션임을 확인할 수 있습니다. 전통적인 시스템에서는 데이터베이스의 AUTO_INCREMENT나 UUID를 사용했다면, 블록체인에서는 내용 기반 해싱(content-addressed)으로 변경됩니다.

이는 데이터 무결성 검증과 ID 생성을 동시에 해결하는 우아한 방법입니다. getTxId의 핵심 특징은 세 가지입니다.

첫째, 결정론적(deterministic)이어서 같은 입력은 항상 같은 출력을 만들고, 둘째, 충돌 저항성(collision resistance)이 있어 서로 다른 트랜잭션이 같은 ID를 가질 확률이 사실상 0이며, 셋째, 단방향성(one-way)이 있어 ID로부터 원본 데이터를 역산할 수 없습니다. 이러한 특징들이 보안성과 신뢰성을 동시에 제공합니다.

코드 예제

import * as CryptoJS from "crypto-js";

// getTxId: 트랜잭션 내용을 해싱하여 고유 ID 생성
const getTxId = (transaction: Transaction): string => {
  // 모든 입력의 내용을 문자열로 결합
  const txInContent: string = transaction.txIns
    .map((txIn: TxIn) => txIn.txOutId + txIn.txOutIndex)
    .reduce((a, b) => a + b, '');

  // 모든 출력의 내용을 문자열로 결합
  const txOutContent: string = transaction.txOuts
    .map((txOut: TxOut) => txOut.address + txOut.amount)
    .reduce((a, b) => a + b, '');

  // 전체 내용을 SHA256으로 해싱
  return CryptoJS.SHA256(txInContent + txOutContent).toString();
};

설명

이것이 하는 일: getTxId 함수는 Transaction 객체를 받아서 그 내용을 기반으로 암호학적 해시값을 계산하고, 이를 트랜잭션의 고유 식별자로 반환합니다. 첫 번째로, txInContent를 생성하는 부분에서 모든 입력의 txOutId와 txOutIndex를 추출합니다.

map 함수로 각 TxIn을 순회하면서 "txOutId + txOutIndex" 형태의 문자열을 만들고, reduce로 이들을 모두 이어붙입니다. 예를 들어, 입력이 2개라면 "abc123...0def456...1" 같은 긴 문자열이 됩니다.

여기서 중요한 점은 서명(signature) 필드를 포함하지 않는다는 것입니다. 서명은 트랜잭션 ID를 알아야 생성할 수 있으므로, ID 계산에 서명을 포함하면 순환 참조가 발생합니다.

두 번째로, txOutContent를 생성하는 부분에서는 모든 출력의 address와 amount를 결합합니다. 마찬가지로 map과 reduce를 사용하여 "address1amount1address2amount2" 형태로 만듭니다.

주소와 금액이 모두 포함되므로, 수신자나 금액이 조금이라도 변경되면 완전히 다른 해시가 나옵니다. 세 번째로, 입력 내용과 출력 내용을 결합한 전체 문자열에 SHA256 해시를 적용합니다.

CryptoJS 라이브러리의 SHA256 함수는 256비트(32바이트)의 해시값을 생성하고, toString()으로 64자의 16진수 문자열로 변환합니다. 이 해시값은 트랜잭션의 "지문"과 같아서, 내용의 무결성을 검증하는 데도 사용됩니다.

여러분이 이 함수를 사용하면 중앙 서버 없이도 고유 ID를 생성할 수 있습니다. 모든 노드가 독립적으로 같은 ID를 계산하므로 합의가 쉽고, 트랜잭션 내용이 변조되었는지 즉시 확인할 수 있으며, ID만 저장해도 전체 데이터의 무결성을 보장할 수 있고, 중복 트랜잭션을 효율적으로 걸러낼 수 있습니다.

실전 팁

💡 직렬화 순서가 중요합니다. txIns와 txOuts의 배열 순서를 항상 유지하세요. 순서가 바뀌면 다른 ID가 나오므로, 정렬 로직을 신중하게 설계해야 합니다.

💡 빈 배열 처리를 고려하세요. reduce의 초기값을 빈 문자열('')로 설정하여 입력이나 출력이 0개인 경우에도 에러가 발생하지 않도록 합니다.

💡 코인베이스 트랜잭션(블록 보상)은 입력이 없으므로 특별 처리가 필요합니다. 일반적으로 블록 높이나 임의의 데이터를 입력 대신 사용합니다.

💡 성능 최적화를 위해 한 번 계산한 ID는 캐싱하세요. 같은 트랜잭션의 ID를 여러 번 계산할 필요가 없으므로, Transaction 객체에 _cachedId 같은 private 필드를 두는 것이 좋습니다.

💡 타입스크립트에서는 CryptoJS 대신 Node.js의 네이티브 crypto 모듈을 사용하는 것이 더 빠릅니다. createHash('sha256').update(data).digest('hex') 패턴을 고려하세요.


5. signTxIn 입력 서명 생성 - 개인키로 소유권 증명하기

시작하며

여러분이 은행에서 돈을 인출할 때 신분증을 보여주거나 비밀번호를 입력합니다. 비트코인에서는 어떻게 "내가 이 비트코인의 진짜 주인이다"라고 증명할 수 있을까요?

비트코인은 공개키 암호화를 사용합니다. 각 사용자는 개인키와 공개키 쌍을 가지고 있는데, 개인키로 데이터에 서명하면 누구나 공개키로 그 서명을 검증할 수 있습니다.

하지만 서명을 만들 수는 없죠. 마치 봉인과 같아서, 누구나 봉인이 진짜인지 확인할 수 있지만 만들 수는 없습니다.

바로 이럴 때 필요한 것이 signTxIn 함수입니다. 이 함수는 트랜잭션 ID와 개인키를 사용하여 디지털 서명을 생성하고, 이를 TxIn 객체에 저장하여 UTXO를 사용할 권한이 있음을 증명합니다.

개요

간단히 말해서, signTxIn은 개인키로 트랜잭션 데이터에 서명하여 해당 입력을 사용할 권한이 있음을 암호학적으로 증명하는 함수입니다. 왜 이런 함수가 필요할까요?

블록체인은 신뢰가 필요 없는(trustless) 시스템입니다. 중앙 권한 없이도 각 노드가 독립적으로 "이 사람이 정말 이 비트코인을 쓸 권리가 있나?"를 검증할 수 있어야 합니다.

예를 들어, Alice가 Bob으로부터 받은 1 BTC를 Charlie에게 보낼 때, Alice의 개인키로 서명함으로써 네트워크 전체에 "나는 Bob이 준 그 1 BTC의 합법적 소유자다"라고 증명합니다. 전통적인 시스템에서는 서버가 세션이나 토큰으로 인증을 관리했다면, 비트코인에서는 각 트랜잭션마다 암호학적 서명으로 인증이 이루어집니다.

서버를 해킹해도 소용없고, 오직 개인키를 탈취해야만 비트코인을 훔칠 수 있습니다. signTxIn의 핵심 특징은 세 가지입니다.

첫째, ECDSA(타원곡선 디지털 서명 알고리즘)를 사용하여 짧은 서명으로 강력한 보안을 제공하고, 둘째, 트랜잭션 ID와 개인키를 함께 사용하여 각 트랜잭션마다 고유한 서명을 만들며, 셋째, 서명은 공개키로 검증 가능하지만 역으로 개인키를 추론할 수 없습니다. 이러한 특징들이 안전하면서도 효율적인 인증을 가능하게 합니다.

코드 예제

import * as ecdsa from "elliptic";
const ec = new ecdsa.ec("secp256k1"); // 비트코인이 사용하는 타원곡선

// signTxIn: 개인키로 트랜잭션 입력에 서명
const signTxIn = (
  transaction: Transaction,
  txInIndex: number,
  privateKey: string,
  aUnspentTxOuts: UnspentTxOut[]
): string => {
  const txIn: TxIn = transaction.txIns[txInIndex];
  const dataToSign: string = transaction.id; // 트랜잭션 ID를 서명 대상으로 사용

  // 참조하는 이전 출력을 찾아서 주소 검증
  const referencedUnspentTxOut: UnspentTxOut = findUnspentTxOut(
    txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts
  );
  const referencedAddress = referencedUnspentTxOut.address;

  // 개인키에서 공개키 추출
  const key = ec.keyFromPrivate(privateKey, 'hex');

  // 서명 생성 (DER 형식)
  const signature: string = toHexString(key.sign(dataToSign).toDER());
  return signature;
};

설명

이것이 하는 일: signTxIn 함수는 특정 TxIn에 대해 개인키를 사용하여 디지털 서명을 생성하고, 이를 통해 해당 입력이 참조하는 UTXO를 사용할 정당한 권한이 있음을 증명합니다. 첫 번째로, 서명 대상 데이터를 결정합니다.

여기서는 transaction.id, 즉 트랜잭션의 고유 ID를 서명합니다. 트랜잭션 ID는 이미 모든 입력과 출력의 내용을 해싱한 값이므로, ID에 서명하는 것은 곧 전체 트랜잭션 내용에 서명하는 것과 같습니다.

이렇게 하면 트랜잭션의 어떤 부분이라도 변조되면 서명 검증이 실패합니다. 두 번째로, findUnspentTxOut 함수로 이 입력이 참조하는 실제 UTXO를 찾습니다.

이 단계에서 두 가지를 확인합니다: 1) 해당 UTXO가 정말 존재하는가, 2) 그 UTXO의 주소가 현재 개인키와 매칭되는가. 만약 다른 사람의 UTXO를 사용하려고 하면 주소가 일치하지 않아 서명이 무효가 됩니다.

세 번째로, elliptic 라이브러리로 개인키 객체를 생성합니다. secp256k1은 비트코인이 사용하는 타원곡선 파라미터로, 256비트의 보안 강도를 제공하면서도 서명이 작고 검증이 빠릅니다.

keyFromPrivate로 16진수 문자열 형태의 개인키를 파싱하여 키 객체를 만듭니다. 네 번째로, key.sign(dataToSign)으로 실제 서명을 생성합니다.

ECDSA 알고리즘은 개인키와 메시지를 입력받아 (r, s) 형태의 서명 쌍을 출력합니다. toDER()로 이를 표준 DER 인코딩 형식으로 변환하고, toHexString으로 16진수 문자열로 만들어 반환합니다.

이 서명은 나중에 TxIn.signature 필드에 저장됩니다. 여러분이 이 함수를 사용하면 안전한 트랜잭션 인증을 구현할 수 있습니다.

개인키 없이는 유효한 서명을 만들 수 없으므로 도용이 불가능하고, 각 트랜잭션마다 새로운 서명을 생성하므로 재사용 공격을 막을 수 있으며, 공개키만으로 검증 가능하므로 개인키 노출 없이 인증할 수 있고, 수학적으로 증명된 ECDSA 알고리즘으로 장기적인 보안성을 보장합니다.

실전 팁

💡 개인키는 절대 네트워크로 전송하지 마세요. 서명 생성은 항상 로컬에서만 수행하고, 생성된 서명만 네트워크로 보냅니다.

💡 서명 전에 반드시 UTXO 존재 여부와 주소 일치 여부를 확인하세요. 이 검증을 생략하면 이미 사용된 UTXO나 다른 사람의 UTXO에 서명할 수 있습니다.

💡 nonce 재사용을 방지하세요. ECDSA에서 같은 개인키와 nonce로 두 번 서명하면 개인키가 노출됩니다. elliptic 라이브러리는 자동으로 랜덤 nonce를 생성하지만, 커스텀 구현 시 주의가 필요합니다.

💡 서명 생성이 실패하면 명확한 에러 메시지를 제공하세요. "UTXO not found", "Address mismatch", "Invalid private key" 등 구체적인 이유를 알려주면 디버깅이 쉬워집니다.

💡 하드웨어 지갑 통합을 고려한다면, 서명 로직을 인터페이스로 추상화하여 소프트웨어 서명과 하드웨어 서명을 교체 가능하게 설계하세요.


6. validateTxIn 입력 서명 검증 - 위조 방지를 위한 필수 단계

시작하며

여러분이 온라인 쇼핑몰을 운영한다고 상상해보세요. 누군가 "나는 100만원을 송금했다"고 주장할 때, 그 말을 그냥 믿을 수 있을까요?

당연히 실제 송금 내역을 확인해야 합니다. 비트코인 네트워크도 마찬가지입니다.

누군가 트랜잭션을 만들어 네트워크에 전파하면, 수천 개의 노드가 독립적으로 "이 서명이 정말 유효한가?"를 검증합니다. 단 하나의 노드라도 서명이 잘못되었다고 판단하면 그 트랜잭션은 거부됩니다.

바로 이럴 때 필요한 것이 validateTxIn 함수입니다. 이 함수는 TxIn의 서명을 공개키로 검증하여, 트랜잭션 생성자가 정말로 해당 UTXO의 주인인지 확인하는 핵심 보안 메커니즘입니다.

개요

간단히 말해서, validateTxIn은 트랜잭션 입력의 디지털 서명을 검증하여 해당 입력이 정당하게 생성되었고 위조되지 않았음을 확인하는 함수입니다. 왜 이런 검증이 필수일까요?

블록체인은 누구나 데이터를 볼 수 있는 공개 장부입니다. 악의적인 사용자가 다른 사람의 UTXO를 가리키는 TxIn을 만들고 가짜 서명을 붙일 수 있습니다.

하지만 개인키 없이는 유효한 서명을 만들 수 없으므로, 검증 단계에서 반드시 걸러집니다. 예를 들어, 해커가 유명인의 지갑을 해킹하려고 할 때, UTXO 정보는 공개되어 있지만 개인키가 없으면 검증을 통과할 수 없어 공격이 실패합니다.

전통적인 시스템에서는 서버가 세션 토큰을 검증했다면, 비트코인에서는 각 노드가 암호학적 서명을 독립적으로 검증합니다. 중앙 권한이 없어도 수학적 증명으로 보안을 달성하는 것이죠.

validateTxIn의 핵심 특징은 네 가지입니다. 첫째, 공개키 추출로 UTXO 주소에서 실제 공개키를 복원하고, 둘째, 서명-메시지 쌍 검증으로 트랜잭션 ID와 서명이 일치하는지 확인하며, 셋째, UTXO 존재 확인으로 이중 지불을 방지하고, 넷째, 모든 검증이 통과해야만 true를 반환하여 "실패 안전(fail-safe)" 원칙을 따릅니다.

이러한 특징들이 네트워크 전체의 무결성을 보장합니다.

코드 예제

import * as ecdsa from "elliptic";
const ec = new ecdsa.ec("secp256k1");

// validateTxIn: 트랜잭션 입력의 서명 검증
const validateTxIn = (
  txIn: TxIn,
  transaction: Transaction,
  aUnspentTxOuts: UnspentTxOut[]
): boolean => {
  // 참조하는 UTXO 찾기
  const referencedUTxOut: UnspentTxOut = aUnspentTxOuts.find(
    (uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex
  );

  if (!referencedUTxOut) {
    console.log('Referenced UTXO not found');
    return false;
  }

  const address: string = referencedUTxOut.address;

  // 주소에서 공개키 복원
  const key = ec.keyFromPublic(address, 'hex');

  // 서명 검증: 트랜잭션 ID와 서명이 공개키와 일치하는가?
  return key.verify(transaction.id, txIn.signature);
};

설명

이것이 하는 일: validateTxIn 함수는 TxIn 객체가 참조하는 UTXO를 찾고, 해당 UTXO의 주소로부터 공개키를 추출한 뒤, TxIn의 서명이 그 공개키로 검증되는지 확인하여 입력의 정당성을 판단합니다. 첫 번째로, aUnspentTxOuts 배열에서 txIn.txOutId와 txIn.txOutIndex가 일치하는 UTXO를 찾습니다.

find 메서드는 조건을 만족하는 첫 번째 요소를 반환하는데, 만약 찾지 못하면 undefined를 반환합니다. 이 경우 "Referenced UTXO not found" 메시지를 로깅하고 false를 반환하여 검증 실패를 알립니다.

이는 이중 지불 시도를 막는 첫 번째 방어선입니다. 이미 사용된 UTXO는 배열에서 제거되었을 것이므로 찾을 수 없습니다.

두 번째로, 찾은 UTXO에서 address 필드를 추출합니다. 비트코인 주소는 실제로는 공개키의 해시값이지만, 여기서는 단순화를 위해 주소를 공개키로 직접 사용하는 모델을 가정합니다.

실제 비트코인에서는 주소를 Base58 디코딩하고 여러 단계의 해싱을 역으로 처리해야 하지만, 교육 목적의 구현에서는 address 자체를 16진수 공개키 문자열로 취급합니다. 세 번째로, ec.keyFromPublic으로 공개키 객체를 생성합니다.

이 객체는 서명 검증에 필요한 수학적 연산을 수행할 수 있습니다. secp256k1 곡선 상의 점으로 공개키를 해석하여, 타원곡선 암호화의 검증 알고리즘을 적용할 준비를 합니다.

네 번째로, key.verify(transaction.id, txIn.signature)로 실제 검증을 수행합니다. 이 메서드는 내부적으로 다음을 계산합니다: 서명에서 (r, s) 값을 추출하고, transaction.id를 해싱한 뒤, ECDSA 검증 공식을 적용하여 서명이 이 공개키로 만들어졌는지 확인합니다.

수학적으로 일치하면 true, 아니면 false를 반환합니다. 여러분이 이 함수를 사용하면 신뢰 없는 환경에서도 안전하게 거래할 수 있습니다.

각 노드가 독립적으로 검증하므로 중앙 권한이 필요 없고, 위조된 트랜잭션은 자동으로 거부되며, 이중 지불 시도를 효과적으로 탐지할 수 있고, 전체 네트워크의 합의가 수학적 증명에 기반하여 이루어집니다.

실전 팁

💡 UTXO를 찾을 때 O(n) 선형 탐색보다 Map이나 Set을 사용한 O(1) 조회가 훨씬 효율적입니다. "txOutId:txOutIndex"를 키로 하는 해시맵을 유지하세요.

💡 서명 검증 실패 시 구체적인 이유를 로깅하세요. "UTXO not found", "Invalid signature format", "Verification failed" 등을 구분하면 디버깅과 보안 모니터링에 유용합니다.

💡 공개키 형식 검증을 추가하세요. keyFromPublic이 실패하면 예외가 발생할 수 있으므로, try-catch로 감싸고 false를 반환하는 것이 안전합니다.

💡 성능 최적화를 위해 검증 결과를 캐싱할 수 있지만, 캐시 무효화 로직이 복잡해질 수 있으니 신중하게 판단하세요. 일반적으로 검증은 충분히 빠르므로 매번 수행하는 것이 안전합니다.

💡 병렬 처리를 고려하세요. 하나의 트랜잭션에 여러 입력이 있을 때, 각 입력의 검증은 독립적이므로 Promise.all로 동시에 검증하면 속도를 높일 수 있습니다.


7. UnspentTxOut UTXO 구조 - 사용 가능한 출력 추적하기

시작하며

여러분의 지갑에 1만원짜리 지폐 3장과 5천원짜리 지폐 2장이 있다고 상상해보세요. 총 잔액은 4만원이지만, 실제로는 5개의 개별 지폐로 구성되어 있습니다.

무언가를 살 때 이 지폐들을 조합해서 사용하죠. 비트코인의 잔액도 정확히 이렇게 작동합니다.

"Alice는 3 BTC를 가지고 있다"는 것은 실제로는 "Alice 주소로 잠긴 여러 개의 미사용 출력들의 합이 3 BTC다"를 의미합니다. 각 출력은 개별적으로 추적되고 사용됩니다.

바로 이럴 때 필요한 것이 UnspentTxOut(UTXO) 구조입니다. 이 구조는 아직 사용되지 않은 트랜잭션 출력을 표현하며, 전체 네트워크의 "상태"를 나타냅니다.

모든 UTXO의 집합이 곧 누가 얼마를 가지고 있는지를 보여주는 것이죠.

개요

간단히 말해서, UnspentTxOut은 아직 다른 트랜잭션의 입력으로 사용되지 않은 TxOut을 표현하는 데이터 구조이며, 블록체인의 현재 상태를 구성하는 기본 단위입니다. 왜 별도의 UTXO 구조가 필요할까요?

블록체인에는 수백만 개의 트랜잭션이 있고, 그 중 대부분의 출력은 이미 사용되었습니다. 새로운 트랜잭션을 검증할 때마다 전체 블록체인을 스캔하여 "이 출력이 아직 사용되지 않았는가?"를 확인하는 것은 비효율적입니다.

대신 현재 사용 가능한 출력들만 따로 모아놓으면 O(1)에 가까운 빠른 조회가 가능합니다. 예를 들어, 비트코인 노드는 약 8천만 개의 UTXO를 메모리나 데이터베이스에 유지하여 빠른 검증을 수행합니다.

기존의 계좌 잔액 모델에서는 "Alice: 3 BTC, Bob: 2 BTC" 같은 단순한 맵을 유지했다면, UTXO 모델에서는 각 출력의 출처(어느 트랜잭션의 몇 번째 출력인지)까지 추적합니다. 이는 투명성과 감사 가능성을 크게 향상시킵니다.

UnspentTxOut의 핵심 특징은 네 가지입니다. 첫째, txOutId와 txOutIndex로 원본 트랜잭션을 추적할 수 있고, 둘째, address와 amount를 중복 저장하여 빠른 조회를 가능하게 하며, 셋째, 트랜잭션이 승인되면 새로운 UTXO가 추가되고 사용된 UTXO가 제거되는 동적 집합을 형성하고, 넷째, 전체 UTXO 집합이 블록체인의 "현재 상태"를 완전히 표현합니다.

이러한 특징들이 효율적이고 검증 가능한 상태 관리를 가능하게 합니다.

코드 예제

// UnspentTxOut: 미사용 트랜잭션 출력 - 사용 가능한 비트코인
class UnspentTxOut {
  // 원본 트랜잭션의 ID
  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;
  }
}

// 실제 사용: 특정 주소의 잔액 계산
const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => {
  return unspentTxOuts
    .filter(uTxO => uTxO.address === address)
    .map(uTxO => uTxO.amount)
    .reduce((a, b) => a + b, 0);
};

설명

이것이 하는 일: UnspentTxOut 클래스는 블록체인에 기록된 트랜잭션 출력 중 아직 다른 트랜잭션의 입력으로 소비되지 않은 것들을 표현하고, 이들의 집합이 현재 네트워크의 전체 상태를 나타냅니다. 첫 번째로, txOutId와 txOutIndex는 이 UTXO가 어디서 왔는지를 정확히 가리킵니다.

예를 들어, 트랜잭션 "abc123..."의 두 번째 출력(인덱스 1)이라면, 나중에 이 UTXO를 사용할 때 TxIn에 정확히 같은 값을 넣어 참조합니다. 이 두 필드의 조합이 UTXO의 고유 식별자가 되어, 같은 출력을 두 번 사용하는 이중 지불을 방지합니다.

두 번째로, address와 amount를 "중복 저장"하는 것에 주목하세요. 이 정보는 원본 트랜잭션의 TxOut에도 있지만, UTXO에 다시 저장함으로써 빠른 조회가 가능합니다.

특정 주소의 잔액을 계산하려면 전체 블록체인을 스캔할 필요 없이 UTXO 집합만 필터링하면 됩니다. 메모리 사용량은 증가하지만 성능은 수천 배 향상됩니다.

세 번째로, readonly 키워드가 중요합니다. 일단 생성된 UTXO는 절대 수정되지 않습니다.

트랜잭션이 확정되면 입력으로 사용된 UTXO는 집합에서 완전히 제거되고, 새로운 출력은 새로운 UTXO로 추가됩니다. 수정이 아닌 추가/삭제만 일어나므로, 불변성(immutability)이 보장되어 동시성 제어가 쉬워집니다.

네 번째로, getBalance 함수 예시에서 볼 수 있듯이, 잔액은 실시간으로 계산됩니다. 별도의 "잔액" 필드가 없고, UTXO 집합을 필터링하고 합산하여 얻습니다.

이는 투명성을 제공하며, 각 비트코인의 출처를 추적할 수 있게 합니다. 여러분이 이 구조를 사용하면 효율적인 블록체인 상태 관리가 가능합니다.

트랜잭션 검증 시 빠르게 UTXO 존재를 확인할 수 있고, 특정 주소의 잔액을 즉시 계산할 수 있으며, 이중 지불을 O(1) 시간에 탐지할 수 있고, 전체 시스템 상태를 명확하게 파악할 수 있습니다.

실전 팁

💡 UTXO 집합은 Map 구조로 관리하세요. "txOutId:txOutIndex"를 키로 사용하면 조회, 추가, 삭제가 모두 O(1)입니다. 배열을 사용하면 매번 선형 탐색이 필요합니다.

💡 주소별 UTXO 인덱스를 추가로 유지하세요. Map<address, Set<UTXO>> 구조로 특정 주소의 모든 UTXO를 빠르게 찾을 수 있습니다. 잔액 계산과 트랜잭션 생성 시 유용합니다.

💡 UTXO 집합의 체크포인트를 주기적으로 저장하세요. 노드 재시작 시 전체 블록체인을 다시 스캔하지 않고 최근 체크포인트부터 재구성할 수 있습니다.

💡 메모리 부족 시 "UTXO 압축"을 고려하세요. 같은 주소의 작은 UTXO들을 합치는 consolidation 트랜잭션을 만들어 UTXO 개수를 줄일 수 있습니다.

💡 UTXO 업데이트는 트랜잭션 단위로 원자적으로 수행하세요. 입력 제거와 출력 추가를 하나의 데이터베이스 트랜잭션으로 묶어 일관성을 보장합니다.


8. updateUnspentTxOuts UTXO 집합 업데이트 - 상태 전이 로직

시작하며

여러분이 레고 블록으로 집을 짓는다고 상상해보세요. 새로운 층을 추가할 때마다 사용한 블록은 더미에서 빼고, 새로 만든 구조물은 완성품 영역에 추가합니다.

블록체인의 상태 관리도 이와 같습니다. 새로운 블록이 추가될 때마다 그 안의 트랜잭션들이 UTXO 집합을 변경합니다.

트랜잭션의 입력들은 기존 UTXO를 "소비"하여 제거하고, 출력들은 새로운 UTXO를 "생성"하여 추가합니다. 이 과정을 정확하게 수행해야 네트워크의 모든 노드가 같은 상태를 유지할 수 있죠.

바로 이럴 때 필요한 것이 updateUnspentTxOuts 함수입니다. 이 함수는 새로운 트랜잭션들을 받아 현재 UTXO 집합을 업데이트하고, 블록체인의 상태를 한 단계 진행시키는 핵심 로직입니다.

개요

간단히 말해서, updateUnspentTxOuts는 새로운 트랜잭션들을 처리하여 사용된 UTXO를 제거하고 생성된 UTXO를 추가함으로써, 블록체인의 현재 상태를 업데이트하는 함수입니다. 왜 이 로직이 중요할까요?

블록체인은 상태 기계(state machine)입니다. 각 블록은 상태 전이를 일으키는데, 이 전이가 모든 노드에서 동일하게 일어나야 합니다.

하나의 노드라도 다르게 업데이트하면 네트워크가 분기(fork)됩니다. 예를 들어, 블록 #100000이 10개의 트랜잭션을 포함한다면, 전 세계 모든 노드가 정확히 같은 순서로 같은 UTXO를 제거하고 추가해야 합니다.

하나라도 틀리면 블록 #100001부터 검증이 실패합니다. 전통적인 데이터베이스에서는 UPDATE나 INSERT 쿼리로 상태를 변경했다면, 블록체인에서는 결정론적 함수로 상태 전이를 수행합니다.

같은 입력은 항상 같은 출력을 보장해야 하죠. updateUnspentTxOuts의 핵심 특징은 네 가지입니다.

첫째, 새로 생성된 UTXO들을 기존 집합에 추가하고, 둘째, 트랜잭션 입력으로 소비된 UTXO들을 제거하며, 셋째, 순서가 보장되어 결정론적 결과를 만들고, 넷째, 불변성을 유지하여 원본 배열을 수정하지 않고 새 배열을 반환합니다. 이러한 특징들이 분산 합의와 일관성을 보장합니다.

코드 예제

// updateUnspentTxOuts: 트랜잭션들로 UTXO 집합 업데이트
const updateUnspentTxOuts = (
  newTransactions: Transaction[],
  aUnspentTxOuts: UnspentTxOut[]
): UnspentTxOut[] => {
  // 1단계: 새로운 트랜잭션들에서 생성된 모든 UTXO 추출
  const newUnspentTxOuts: UnspentTxOut[] = newTransactions
    .map((t) => {
      return t.txOuts.map((txOut, index) =>
        new UnspentTxOut(t.id, index, txOut.address, txOut.amount)
      );
    })
    .reduce((a, b) => a.concat(b), []); // 2차원 배열을 1차원으로 평탄화

  // 2단계: 소비된(사용된) UTXO들의 목록 추출
  const consumedTxOuts: UnspentTxOut[] = newTransactions
    .map((t) => t.txIns)
    .reduce((a, b) => a.concat(b), [])
    .map((txIn) => new UnspentTxOut(txIn.txOutId, txIn.txOutIndex, '', 0));

  // 3단계: 기존 UTXO에서 소비된 것 제거하고 새로운 것 추가
  const resultingUnspentTxOuts = aUnspentTxOuts
    .filter((uTxO) => !findUnspentTxOut(uTxO.txOutId, uTxO.txOutIndex, consumedTxOuts))
    .concat(newUnspentTxOuts);

  return resultingUnspentTxOuts;
};

설명

이것이 하는 일: updateUnspentTxOuts 함수는 새로운 블록의 트랜잭션들을 받아, 각 트랜잭션이 생성한 출력들을 UTXO 집합에 추가하고 소비한 입력들을 제거하여, 블록체인의 다음 상태를 계산합니다. 첫 번째 단계에서는 newUnspentTxOuts를 생성합니다.

newTransactions 배열의 각 트랜잭션을 순회하면서, 그 안의 모든 txOuts를 UnspentTxOut 객체로 변환합니다. 중요한 점은 트랜잭션 ID와 출력 인덱스를 함께 저장한다는 것입니다.

예를 들어, 트랜잭션 "abc123"이 3개의 출력을 가지면, 인덱스 0, 1, 2로 세 개의 UTXO가 생성됩니다. map이 2차원 배열을 만들기 때문에 reduce로 평탄화하여 1차원 배열로 만듭니다.

두 번째 단계에서는 consumedTxOuts를 추출합니다. 모든 트랜잭션의 모든 입력을 모아서, 각 입력이 참조하는 txOutId와 txOutIndex로 "소비된 UTXO"의 식별자를 만듭니다.

실제로는 address와 amount가 필요 없으므로 빈 문자열과 0으로 설정하는데, 이는 나중에 식별만 할 수 있으면 되기 때문입니다. 이 목록이 "제거해야 할 UTXO"의 명단이 됩니다.

세 번째 단계는 실제 업데이트입니다. aUnspentTxOuts에서 filter를 사용하여 consumedTxOuts에 포함된 것들을 제거합니다.

findUnspentTxOut 헬퍼 함수로 txOutId와 txOutIndex가 일치하는지 확인하고, 일치하지 않는 것만 남깁니다. 그런 다음 concat으로 새로 생성된 UTXO들을 추가합니다.

이렇게 하면 원본 배열은 변경되지 않고 새로운 배열이 반환되어 불변성이 유지됩니다. 여러분이 이 함수를 사용하면 블록체인의 상태를 정확하게 관리할 수 있습니다.

모든 노드가 동일한 순서로 동일한 업데이트를 수행하여 일관성을 유지하고, 이중 지불된 UTXO는 자동으로 제거되며, 새로운 출력은 즉시 사용 가능해지고, 전체 과정이 추적 가능하고 검증 가능합니다.

실전 팁

💡 대량의 트랜잭션 처리 시 filter와 concat보다 Set 연산이 훨씬 빠릅니다. consumedTxOuts를 Set으로 변환하여 O(1) 조회를 활용하세요.

💡 업데이트 전에 유효성 검증을 먼저 수행하세요. 모든 트랜잭션의 입력이 실제로 UTXO 집합에 존재하는지 확인한 후에 업데이트를 진행해야 합니다.

💡 원자성(atomicity)을 보장하세요. 블록 전체의 트랜잭션을 한 번에 처리하고, 하나라도 실패하면 전체를 롤백해야 합니다. 부분 업데이트는 상태를 망칩니다.

💡 UTXO 집합의 스냅샷을 정기적으로 저장하여, 블록 재구성 시 처음부터 다시 계산할 필요가 없도록 하세요. 예를 들어, 1000블록마다 스냅샷을 저장합니다.

💡 성능 모니터링을 추가하세요. UTXO 집합 크기와 업데이트 시간을 로깅하여, 네트워크 성장에 따른 성능 저하를 미리 감지할 수 있습니다.


9. findUnspentTxOut UTXO 검색 - 효율적인 조회 유틸리티

시작하며

여러분이 도서관에서 특정 책을 찾는다고 상상해보세요. 제목만 알고 있다면 모든 책장을 뒤져야 하지만, ISBN 번호를 안다면 정확한 위치를 바로 찾을 수 있습니다.

UTXO 집합도 마찬가지입니다. 수백만 개의 UTXO 중에서 특정 트랜잭션의 특정 출력을 찾아야 할 때가 많습니다.

서명 검증, 잔액 확인, 트랜잭션 생성 등 거의 모든 작업에서 UTXO 조회가 필요하죠. 바로 이럴 때 필요한 것이 findUnspentTxOut 함수입니다.

이 함수는 트랜잭션 ID와 출력 인덱스를 받아 UTXO 집합에서 정확히 일치하는 항목을 찾아 반환하는, 간단하지만 매우 자주 사용되는 유틸리티입니다.

개요

간단히 말해서, findUnspentTxOut은 트랜잭션 ID와 출력 인덱스로 UTXO 배열을 검색하여 일치하는 항목을 반환하거나, 없으면 undefined를 반환하는 조회 함수입니다. 왜 별도의 검색 함수가 필요할까요?

코드 재사용과 일관성을 위해서입니다. UTXO 검색 로직은 서명 검증, 트랜잭션 검증, 잔액 계산 등 여러 곳에서 반복됩니다.

매번 find 로직을 복붙하면 오타나 로직 차이로 버그가 발생할 수 있습니다. 예를 들어, 한 곳에서는 txOutId와 txOutIndex를 모두 확인하는데 다른 곳에서는 txOutId만 확인한다면 심각한 보안 취약점이 생깁니다.

전통적인 데이터베이스에서는 SELECT * WHERE id=? 같은 쿼리를 사용했다면, 인메모리 배열에서는 find나 필터 같은 메서드를 사용합니다.

하지만 추상화 함수로 감싸면 나중에 Map이나 데이터베이스로 변경하기 쉬워집니다. findUnspentTxOut의 핵심 특징은 세 가지입니다.

첫째, 복합 키 검색으로 txOutId와 txOutIndex를 모두 확인하여 정확성을 보장하고, 둘째, null 안전성으로 undefined를 명시적으로 반환하여 호출자가 처리하게 하며, 셋째, 단순한 인터페이스로 복잡한 검색 로직을 캡슐화합니다. 이러한 특징들이 코드 품질과 유지보수성을 향상시킵니다.

코드 예제

// findUnspentTxOut: UTXO 배열에서 특정 항목 검색
const findUnspentTxOut = (
  transactionId: string,
  index: number,
  aUnspentTxOuts: UnspentTxOut[]
): UnspentTxOut | undefined => {
  return aUnspentTxOuts.find(
    (uTxO) => uTxO.txOutId === transactionId && uTxO.txOutIndex === index
  );
};

// 실제 사용 예시
const utxo = findUnspentTxOut("abc123...", 0, unspentTxOuts);
if (utxo) {
  console.log(`Found UTXO: ${utxo.amount} satoshi to ${utxo.address}`);
} else {
  console.log("UTXO not found - possibly already spent");
}

설명

이것이 하는 일: findUnspentTxOut 함수는 UTXO 배열에서 특정 트랜잭션의 특정 출력을 찾는 헬퍼 함수로, 두 개의 식별자가 모두 일치하는 첫 번째 항목을 반환합니다. 첫 번째로, transactionId 매개변수는 찾고자 하는 UTXO가 생성된 트랜잭션의 ID입니다.

이는 일반적으로 64자의 16진수 문자열 형태를 가지며, 블록체인에 기록된 트랜잭션의 고유 식별자입니다. 예를 들어, "abc123def456..."처럼 SHA256 해시의 결과입니다.

두 번째로, index 매개변수는 해당 트랜잭션 내에서 몇 번째 출력인지를 나타냅니다. 하나의 트랜잭션이 여러 개의 출력을 가질 수 있으므로, 트랜잭션 ID만으로는 특정 출력을 식별할 수 없습니다.

0부터 시작하는 인덱스와 함께 사용해야 정확히 하나의 UTXO를 지정할 수 있습니다. 세 번째로, find 메서드를 사용하여 배열을 순회합니다.

find는 조건을 만족하는 첫 번째 요소를 찾으면 즉시 반환하고 순회를 중단하므로, filter보다 효율적입니다. 조건식 uTxO.txOutId === transactionId && uTxO.txOutIndex === index에서 두 조건이 모두 true여야 일치로 판단합니다.

하나라도 틀리면 다른 UTXO입니다. 네 번째로, 반환 타입이 UnspentTxOut | undefined인 점에 주목하세요.

일치하는 UTXO가 없으면 undefined를 반환하므로, 호출하는 쪽에서 반드시 null 체크를 해야 합니다. 타입스크립트의 strict null check가 활성화되어 있다면, if (utxo) 같은 검사를 강제하여 런타임 에러를 방지합니다.

여러분이 이 함수를 사용하면 일관된 UTXO 검색 로직을 유지할 수 있습니다. 검색 조건을 한 곳에서 관리하므로 버그 가능성이 줄고, 나중에 인덱스 구조로 변경할 때 이 함수만 수정하면 되며, 타입 안전성으로 undefined 처리를 강제하고, 코드 가독성이 향상됩니다.

실전 팁

💡 성능 개선을 위해 Map<string, UnspentTxOut> 구조로 전환하세요. "txOutId:txOutIndex"를 키로 사용하면 O(n)에서 O(1)로 향상됩니다.

💡 찾지 못한 경우의 로깅을 추가하세요. 디버깅 모드에서 "UTXO not found: txId=${transactionId}, index=${index}"를 출력하면 문제 파악이 쉽습니다.

💡 캐싱을 고려하세요. 같은 UTXO를 반복 조회하는 패턴이 있다면, 최근 조회 결과를 LRU 캐시에 저장하여 성능을 높일 수 있습니다.

💡 타입 가드 함수를 제공하세요. isUnspentTxOutExists(txId, index, utxos): boolean 같은 함수로 존재 여부만 빠르게 확인할 수 있습니다.

💡 배열이 정렬되어 있다면 이진 탐색을 사용하세요. O(log n)으로 개선되지만, 삽입/삭제 시 정렬 유지 비용을 고려해야 합니다.


10. 코인베이스 트랜잭션 - 블록 보상과 새로운 비트코인 발행

시작하며

여러분이 금광에서 금을 캐는 광부라고 상상해보세요. 땅을 파서 금을 찾으면 그것은 여러분의 것입니다.

아무도 그 금을 준 게 아니라, 여러분이 노력해서 얻은 것이죠. 비트코인도 마찬가지입니다.

채굴자들은 복잡한 수학 문제를 풀어 새로운 블록을 생성하고, 그 보상으로 새로운 비트코인을 받습니다. 이 비트코인은 어디서 온 것이 아니라, 완전히 새롭게 "발행"되는 것입니다.

이전 트랜잭션에서 온 것이 아니니 입력이 없는 특별한 트랜잭션이 필요합니다. 바로 이럴 때 필요한 것이 코인베이스(Coinbase) 트랜잭션입니다.

이 트랜잭션은 입력 없이 출력만 가지며, 채굴자에게 블록 보상을 지급하고 새로운 비트코인을 시스템에 공급하는 특별한 역할을 합니다.

개요

간단히 말해서, 코인베이스 트랜잭션은 블록의 첫 번째 트랜잭션으로, 입력이 없고 채굴 보상을 채굴자 주소로 보내는 출력만 가지는 특별한 형태의 트랜잭ション입니다. 왜 이런 특별한 트랜잭션이 필요할까요?

비트코인의 경제 모델을 이해해야 합니다. 전체 공급량은 2100만 BTC로 제한되어 있고, 새로운 비트코인은 오직 블록 채굴을 통해서만 발행됩니다.

은행처럼 중앙 기관이 화폐를 찍어내는 게 아니라, 채굴이라는 작업 증명을 통해 탈중앙화된 방식으로 발행됩니다. 예를 들어, 2024년 현재 블록당 보상은 6.25 BTC이며, 약 4년마다 반감기를 거쳐 절반으로 줄어듭니다.

전통적인 금융 시스템에서는 중앙은행이 통화를 발행했다면, 비트코인에서는 코인베이스 트랜잭션이 그 역할을 대신합니다. 하지만 발행량이 코드로 고정되어 있어 인플레이션을 예측할 수 있습니다.

코인베이스 트랜잭션의 핵심 특징은 다섯 가지입니다. 첫째, 입력이 없는 대신 블록 높이 정보를 포함하고, 둘째, 출력은 블록 보상과 트랜잭션 수수료의 합이며, 셋째, 반드시 블록의 첫 번째 트랜잭션이어야 하고, 넷째, 100블록이 지나야 사용할 수 있는 성숙도(maturity) 규칙이 있으며, 다섯째, 시간에 따라 보상이 감소하는 반감기 메커니즘을 따릅니다.

이러한 특징들이 비트코인의 공급 정책을 구현합니다.

코드 예제

// 코인베이스 트랜잭션: 블록 보상 지급
const COINBASE_AMOUNT: number = 50; // 초기 블록 보상 (실제로는 반감기마다 변경)

const getCoinbaseTransaction = (
  address: string,
  blockIndex: number
): Transaction => {
  const txIn: TxIn = new TxIn();
  txIn.signature = '';
  txIn.txOutId = '';
  txIn.txOutIndex = blockIndex; // 블록 높이를 인덱스로 사용

  const txOut: TxOut = new TxOut(address, COINBASE_AMOUNT);

  const tx: Transaction = new Transaction();
  tx.txIns = [txIn];
  tx.txOuts = [txOut];
  tx.id = getTxId(tx);

  return tx;
};

// 실제 사용: 새 블록 생성 시
const minerAddress = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"; // 채굴자 주소
const newBlockIndex = 123456;
const coinbaseTx = getCoinbaseTransaction(minerAddress, newBlockIndex);

설명

이것이 하는 일: getCoinbaseTransaction 함수는 채굴자의 주소와 블록 높이를 받아, 블록 보상을 채굴자에게 지급하는 특별한 형태의 트랜잭션을 생성합니다. 첫 번째로, 코인베이스 트랜잭션의 입력(TxIn)은 일반 트랜잭션과 완전히 다릅니다.

txOutId는 빈 문자열이고 signature도 빈 문자열입니다. 이전 출력을 참조하지 않으므로 서명도 필요 없습니다.

대신 txOutIndex 필드에 blockIndex를 저장하여 몇 번째 블록인지 기록합니다. 이는 각 블록의 코인베이스 트랜잭션을 고유하게 만들어, 같은 내용의 코인베이스가 생성되어도 블록마다 다른 ID를 갖게 합니다.

두 번째로, 출력(TxOut)은 매우 간단합니다. 채굴자의 주소와 COINBASE_AMOUNT만 지정합니다.

실제 비트코인에서는 이 금액이 블록 높이에 따라 계산됩니다: 처음 210,000블록까지는 50 BTC, 그 다음 210,000블록은 25 BTC, 이런 식으로 반감기마다 절반이 됩니다. 또한 블록에 포함된 모든 일반 트랜잭션의 수수료(입력 합 - 출력 합)를 더해야 합니다.

세 번째로, 트랜잭션 ID를 getTxId로 생성합니다. 입력의 txOutId와 txOutIndex, 출력의 address와 amount를 모두 해싱하는데, 블록 인덱스가 달라지면 다른 ID가 나옵니다.

이는 중요한 보안 기능으로, 두 개의 블록이 똑같은 코인베이스 트랜잭션을 가질 수 없게 만들어 블록 복제 공격을 방지합니다. 네 번째로, 성숙도 규칙을 고려해야 합니다.

코인베이스 트랜잭션의 출력은 즉시 사용할 수 없고, 100블록이 추가로 채굴된 후에야 사용 가능합니다. 이는 포크(fork)가 발생했을 때 무효화될 수 있는 블록의 보상이 이미 사용되는 것을 방지합니다.

예를 들어, 블록 #1000의 코인베이스는 블록 #1100 이후에야 송금할 수 있습니다. 여러분이 이 코드를 사용하면 비트코인의 화폐 발행 메커니즘을 구현할 수 있습니다.

채굴 인센티브를 제공하여 네트워크 보안을 유지하고, 예측 가능한 공급 일정으로 인플레이션을 제어하며, 트랜잭션 수수료를 채굴자에게 분배하고, 탈중앙화된 방식으로 새로운 화폐를 발행할 수 있습니다.

실전 팁

💡 반감기 로직을 구현하세요. Math.floor(blockIndex / 210000)로 몇 번째 반감기인지 계산하고, INITIAL_REWARD / Math.pow(2, halvings) 공식으로 현재 보상을 구합니다.

💡 트랜잭션 수수료를 계산하여 코인베이스 출력에 추가하세요. 블록의 모든 일반 트랜잭션을 순회하며 (총 입력 - 총 출력)을 합산합니다.

💡 코인베이스 검증 로직을 추가하세요. 블록의 첫 번째 트랜잭션인지, 입력이 정확히 하나인지, 출력 금액이 허용 범위 내인지 확인합니다.

💡 성숙도 체크를 UTXO 관리에 통합하세요. UnspentTxOut에 blockIndex 필드를 추가하고, 현재 블록 높이와 비교하여 100블록 이상 지났는지 확인합니다.

💡 코인베이스 텍스트 필드를 활용하세요. 실제 비트코인은 입력의 scriptSig에 임의의 데이터를 넣을 수 있어, 채굴 풀 정보나 메시지를 기록할 수 있습니다. 제네시스 블록의 "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"가 유명한 예입니다.


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

댓글 (0)

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

함께 보면 좋은 카드 뉴스

VSCode Extension으로 만드는 나만의 AI 코딩 도우미

VSCode Extension API를 활용하여 인라인 코드 제안, 코드 액션, 단축키 등 실무에서 바로 쓸 수 있는 AI 통합 기능을 개발하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 구성했습니다.

Few-Shot으로 프로젝트 패턴 학습 완벽 가이드

프로젝트의 기존 코드를 활용해 AI가 우리 팀의 코딩 스타일을 학습하도록 만드는 방법을 배웁니다. Few-Shot Learning과 Chain-of-Thought를 결합하여 일관된 코드 패턴을 유지하는 실전 테크닉을 소개합니다.

코드베이스 컨텍스트 주입하기 완벽 가이드

AI에게 프로젝트 정보를 제대로 전달하는 방법을 배웁니다. 정적/동적 컨텍스트 수집부터 자동화 시스템 구축까지, 실무에서 바로 쓸 수 있는 컨텍스트 주입 기법을 소개합니다.

마이크로서비스 배포 완벽 가이드

Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.

Application Load Balancer 완벽 가이드

AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.