즐코

지갑, 트랜잭션 구현 (4) - txIn, txOut, UTXO 본문

BlockChain

지갑, 트랜잭션 구현 (4) - txIn, txOut, UTXO

YJLEE_KR 2022. 6. 22. 07:05

트랜잭션을 코드로 나타내려면 트랜잭션이 만들어지는 과정에 대해서 먼저 이해를 해야한다.

 

트랜잭션의 과정

말로 표현해도 되지만, 엑셀로 표현하는 게 나중에 보기 편할 것 같아 정리해보았다

왼쪽 표는 트랜잭션 하나하나를 흐름에 따라 정리한 부분이고, 오른쪽은 각 트랜잭션에 따라 생기는 UTXO 목록이다. 

UTXO 가 txIn으로 쓰였으면 spent로 표시, 아직 쓰이기 전이면 unspent로 표시하였다.

오른쪽 하단 마지막 표는 트랙잰션 tx0004까지 남아있는 최종 UTXO 이다.

 

거래자 A,B,C 가 있다고 가정하고, A가 첫 블록 채굴에 성공해서 50BTC 를 받는 것부터 시작한다.

여기서 tx hash는 트랜잭션의 고유한 인덱스값을 가리킨다. 실제로는 해시값으로 찍히지만 여기선 흐름을 익히기 위해 간단하게 tx0000, tx0001 이렇게 구성하였다. 보통 txOuts의 배열에는 첫 트랜잭션 빼고는 2개의 txOut이 들어가게 된다.

반면, txIns의 배열에는 해당 코인을 보내는 사람의 utxo의 구성에 따라 한개가 될 수도 있고 2개보다 더 많은 txIns가 들어갈 수 있다.

자세한 예로, 아래 흐름처럼 마지막에 B가 A에게 35 BTC를 전송하는데, 이땐 B 앞으로 남은 utxo 는 우측 표 상에서 3번째 (30BTC) 와 7번째 (10BTC) 였다. (파란색바탕) 그러면 이걸 2개의 utxo를 전부 다 가져와서 txIns에 넣어서 spent해버리고 B는 5BTC의 txOut을 돌려받고 이게 utxo가 되는 것이다. 우측 utxo 목록에서 8번째에 해당한다. tx0004 트랜잭션을 보면 알 수 있다.

 

(* typo : 첫번째 utxo의 txOutIdx는 블록의 height가 아닌 0이다..)

 

트랜잭션과 utxo, 트랜잭션 내부 속성에 쓰이는 타입을 미리 지정해두면 아래와 같다.

// @types/transaction.d.ts

declare interface ITxOut {
  account: string;
  amount: number;
}

declare interface ITxIn {
  txOutId: string;
  txOutIndex: number;
  signature?: string | undefined;
}

declare interface ITransaction {
  hash: string;
  txOuts: ITxOut[];
  txIns: ITxIn[];
}

declare interface IUtxo {
  txOutId: string;
  txOutIndex: number;
  account: string;
  amount: number;
}

 

Transaction

트랜잭션은 아래와 같이 이루어졌다.

1- txIns : 해당 트랜잭션에 쓰인 txIn들 (배열)

2- txOuts : 해당 트랜잭션으로 생긴 txOut들 (배열) 

3- hash : 해당 트랜잭션의 고유 인덱스값 (txIns 배열 내 모든 txIns의 속성값 + txOuts 배열 내 모든 txOut의 속성값 => 해쉬한 값)

 

여기서 각각의 txIn과 txOut이 뭘까

TxIn 클래스

- txOutId : utxo에서 가져온 txout의 트랜잭션 해쉬/고유값 (상기표에 표시해둠)

- txOutIndex : 트랜잭션 상 txOuts 배열에서 가져온 txOut의 인덱스값 (상기표에 표시해둠)

- signature : 이 가져온 txOut의 지갑 주인의 서명

 

블록의 첫 거래, 트랜잭션은 채굴자가 채굴에 성공했을 때 받는 보상에 대한 트랜잭션이 들어간다. 이를 코인베이스라고 한다.

위의 예시에서 A가 채굴에 성공해서 받은 50BTC 보상에 대한 거래내역이 코인베이스인것이다. 

따지고보면 이 첫 거래는 UTXO 에서 가져온 게 아니라 블록체인 네트워크 자체에서 제공하는 것이므로 txIn 이 없다. 

따라서, 이 코인베이스는 txIn을 만들 때 txOutId 는 빈값을 넣고, txOutIndex는 해당 블록의 높이를 넣어주기로 한다. 그리고 서명은 생략한다. 

 

txOutIndex도 0을 넣어주면 되지 않나 싶지만, 이렇게 하면 트랜잭션의 hash값을 만들때, 같은 채굴자가 채굴을 여러 번했다고 가정하면 그 코인베이스의 트랜잭션 해시값이 항상 같게 나오기 때문에 이를 방지하기 위해 해당 거래가 들어가는 블록 높이로 넣어준 것이다. 

// src/core/transaction/txin.ts

export class TxIn implements ITxIn {
  public txOutId: string;
  public txOutIndex: number;
  public signature?: string;
  // wallet에서 블체서버로 들어올때 16진수로 들어오니까 string으로 처리

  constructor(_txOutId: string, _txOutIndex: number, _signature: string | undefined = undefined) {
    this.txOutId = _txOutId;
    this.txOutIndex = _txOutIndex;
    this.signature = _signature;
  }
}

 

 

TxOut 클래스

- account : 트랜잭션으로 인해 나온 txOut amount의 주인 지갑계정

- amount : 코인의 양

// src/core/transaction/txout.ts

export class TxOut implements ITxOut {
  public account: string;
  public amount: number;

  constructor(_account: string, _amount: number) {
    this.account = _account;
    this.amount = _amount;
  }
}

 

본격적으로 위의 트랜잭션에 대해 코드를 짜보자..

 

Transaction 클래스

트랜잭션 고유의 해쉬값은 위에서 설명한대로 txOuts 배열 내 모든 txOut과 txIns 배열 내 모든 txIn의 속성값을 더해서 SHA256으로 해시한 값이다. 

 

createUTXO() 

트랜잭션이 발생함과 동시에 txOut들이 만들어지므로 이것들은 동시에 UTXO인 것이다.

따라서 트랜잭션 생성과 동시에 UTXO도 생성해준다. 

// src/core/transaction/transaction.ts

import { SHA256 } from "crypto-js";
import { TxIn } from "./txin";
import { TxOut } from "./txout";
import { Utxo } from "./utxo";

export class Transaction {
  public hash: string;
  public txIns: TxIn[];
  public txOuts: TxOut[];

  constructor(_txIns: TxIn[], _txOuts: TxOut[]) {
    this.txIns = _txIns;
    this.txOuts = _txOuts;
    this.hash = this.createTransactionHash();
  }

  createTransactionHash(): string {
    const txoutContent: string = this.txOuts.map((v) => Object.values(v).join("")).join("");
    const txinContent: string = this.txIns.map((v) => Object.values(v).join("")).join("");

    return SHA256(txoutContent + txinContent).toString();
  }

  createUTXO(): Utxo[] {
    return this.txOuts.map((txout: TxOut, k) => {
      return new Utxo(this.hash, k, txout.account, txout.amount);
    });
  }
}

 

chain 클래스 수정 

miningBlock(), appendUTXO(), getUTXO() 추가

 

원래는 블록 채굴 시 바로 블록을 블록 체인에 add했다. ( const newBlock = ws.addBlock(data))

이젠 채굴 보상을 주는 코드를 추가할 거고, 그게 블록 내 첫 거래가 되게끔 코드를 추가해야한다. 그걸 위한 함수인 miningBlock() 을 chain 클래스 내에서 추가로 만들어서 addBlock 대신 넣어주었다. 그리고 이 '/mineBlock' POST 요청에선 요청 바디를 받을 때 클라이언트 쪽에서 무의미한 데이터가 아닌 account 를 전달할 수 있게끔 해야한다. 왜냐하면 채굴 보상은 해당 블록을 채굴한 채굴자한테 떨어져야하기 때문이다. 

// index.ts (블록체인 서버)

app.post("/mineBlock", (req, res) => {
  const { data } = req.body;
  // data에 이제 transaction의 배열을 넣어야 한다. 
  // 이때, miningBlock 함수 인자에는 첫 마이닝 보상이 떨어지는 account를 넣어줄것
  const newBlock = ws.miningBlock(data); //원래는 const newBlock = ws.addBlock(data)였음
  const msg: Message = {
    type: MessageType.latest_block,
    payload: [],
  };
  ws.broadcast(msg);
  if (newBlock.isError) return res.status(500).json(newBlock.error);
  console.log(newBlock);
  res.json(newBlock.value);
});

miningBlock()

이전에 썼던 저 addBlock메소드에는 data가 스트링값을 가진 배열 이었지만, 이젠 그 데이터에 transaction[] 을 넣어줘야한다.

여기서 addBlock 함수 자체는 건드릴 필요가 없다. 왜냐하면 이미 data만 넣으면 블록을 생성해서 검증하고 블록을 추가해주는 코드이기 때문에 굳이 잘 돌아가는 함수를 건드릴 필요가 없고, 하나의 함수는 하나의 역할만 하는 게 맞기 때문이다. 그래서 이 트랜잭션 데이터를 가공해서 addBlock()의 인자로 던져줄 수 있는 새로운 함수(miningBlock())만 하나 더 만들어주는 게 효율적이다. 

그리고 transaction이 추가됨과 동시에 utxo도 만들어야하므로 트랜잭션 클래스의 createUTXO()를 사용했다.

// src/core/blockchain/chain.ts - chain 클래스

import { Transaction } from "@core/transaction/transaction";
import { TxIn } from "@core/transaction/txin";
import { TxOut } from "@core/transaction/txout";

export class Chain {
    private blockchain: Block[];
    private utxo: IUtxo[];
    
    constructor(){
    	this.blockchain = [Block.createGENESIS(GENESIS)];
        this.utxo = [];
    }

// ... 클래스 내 다른 메소드들 생략

public getUTXO: IUTXO[] {
	return this.utxo;
}

public miningBlock(_account: string): Failable<Block, string>{
    // 우선은 채굴보상을 넣어주는 함수로 만들것
    // 1. todo : transaction의 데이터 가공해주기
    const txin: ITxIn = new TxIn("", this.getLatestBlock().height+1);
    const txout: ITxOut = new TxOut(_account, 50); // 첫 채굴에 대한 보상
    const transactio: Transaction = new Transaction([txin], [txout]);
    console.log("트랜잭션", transaction);
    /*
     트랜잭션 Transaction {
      txIns: [ TxIn { txOutId: '', txOutIndex: 6, signature: undefined } ],
      txOuts: [
        TxOut {
          account: '203d1de5a622fb91a0b073c8bb729f85f16053a4',
          amount: 50
        }
      ],
      hash: 'bc7332b97f6aaf8bc2197b92ef6cf0a75c247f00106b1e4506c3b6c2af5a8e78'
    }
    */
    const utxo = transaction.createUTXO();
    console.log("UTXO", utxo);
    /*
    [
      Utxo {
        txOutId: 'bc7332b97f6aaf8bc2197b92ef6cf0a75c247f00106b1e4506c3b6c2af5a8e78',
        txOutIndex: 0,
        account: '203d1de5a622fb91a0b073c8bb729f85f16053a4',
        amount: 50
      }
    ]
    */
    this.appendUTXO(utxo) // utxo를 utxo를 모아두는 배열에 추가해줌
    // 2. todo: addBlock 실행
    return this.addBlock([transaction]) 
    // 이전에 넣었던 string[] 대신 transaction이 담긴 배열을 넣어준것
}

 public appendUTXO(_utxo: IUtxo[]): void {
 	this.utxo.push(..._utxo);
 }
 
}

 

Comments