블록체인/Mainnet

Go 언어로 블록체인 메인넷 만들기 - 트랜잭션(Transaction)

이번에는 블록체인의 구성요소 중 또 하나의 요소인 트랜잭션을 만들어보자. 트랜잭션을 이해하기 제법 시간이 걸렸는데, 포스트를 적어가면서 하나씩 풀어나갈 생각이다. 현재 이 포스트는 트랜잭션의 초입부분이며 이후에 거래, 공개키와 개인키 등 다뤄야 할 내용이 또 있다.

type Transaction

트랜잭션은 기본적으로 Block 에 포함된다. 우리가 Data 라는 필드를 블록에 포함시켰었는데 그것 대신에 거래들이 포함될 예정이다. 트랜잭션은 하나의 거래다. 현실세계에서 일어나는 거래를 어떻게 표현할 수 있을까? 비트코인에서 제시하는 거래의 아이디어는 제법 흥미롭다. 먼저 거래는 입력 값(Input Transaction)출력 값(Output Transaction)이 있다. 하나의 트랜잭션은 다수의 입력과 출력을 가질 수 있기 때문에 배열로 표현한다. 또한 블록의 해시값이 있듯이 거래도 ID 로 불리는 해시값이 있다. 이제 우리는 Transaction 타입을 정의할 수 있다.

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

여기서 우리는 두 가지 타입이 더 있다는 것을 알 수 있다. 입력 값을 의미하는 TXInput, 출력 값을 의미하는 TXOutput 이다. 거래 자체의 구성요소는 알아봤지만, 입력과 출력을 구성하는 요소들도 알아봐야 한다. 위에서 트랜잭션의 입력과 출력은 여러 개가 있다고 했기때문에 배열로 구성된 것을 볼 수 있다.

func NewTransaction([]TXInput, []TXOutput) *Transaction

새로운 트랜잭션을 만든다. .ID 의 경우 별도로 지정해준다는 것을 지목하자.

func NewTransaction(vin []TXInput, vout []TXOutput) *Transaction {
	tx := Transaction{nil, vin, vout}
	tx.SetID()

	return &tx
}

func .SetID()

트랜잭션의 아이디를 설정한다. 대체 무엇으로 아이디를 설정해야 할까? 그건 트랜잭션을 직렬화하고 해시하여 .ID 로 지정하는 방식을 사용하게 된다. 직렬화를 위해 encoding/gob 을 사용한다. 트랜잭션의 아이디는 추후 트랜잭션의 출력값에서 중요한 역할을 하게 될 것이다.

func (tx *Transaction) SetID() {
	buf := new(bytes.Buffer)

	encoder := gob.NewEncoder(buf)
	err := encoder.Encode(tx)
	if err != nil {
		log.Panic(err)
	}

	hash := sha256.Sum256(buf.Bytes())
	tx.ID = hash[:]
}

func Block.HashTransaction() []byte

트랜잭션의 ID 를 묶어서 해싱하는 메서드다. 이 메서드의 필요성은 작업증명에서 필요하게 된다. 작업증명을 위해 데이터를 준비해야 할 때 기존에는 Block.Data 를 사용했지만 이제는 Block.Transactions 를 이용해야하기 때문이다. 비트코인에서는 머클트리라는 자료구조를 사용하여 블록헤더에 블록에 포함된 트랜잭션에 대한 것들을 표현하는 하나의 해시가 포함되어 있는데, 그것을 재현하기 위한 것이다.

func (b *Block) HashTransaction() []byte {
	var txHashes [][]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.ID)
	}

	txHash := sha256.Sum256(bytes.Join(txHashes, []byte{}))

	return txHash[:]
}

type TXOutput

입력보다 출력을 먼저하는 이유는, 실제로 트랜잭션의 경우 입력보다 출력이 우선시되기 때문이다. 트랜잭션의 출력이란 자금을 얼마나 어디로 송금할 것인가에 대한 이야기다. 내가 가진 돈을 얼마만큼 누군가에게 지불하는 것을 말한다. 이러한 출력이 여러 개라는 이야기는 입력으로 들어온 것을 여러 지불처에게로 배분하는 것을 이야기한다. 회사에서 급여를 지불할 때 자금이 여러사람에게 배분되는 것처럼 말이다.

 

실제로 트랜잭션의 출력은 코인(Value) 을 의미한다. 어디로(ScriptPubKey)에 대해서는 주소를 지칭하는 것으로 생각될 수도 있겠지만, 일단 일반적으로는 받는 사람의 공개키(Public Key)잠근다. 잠근다는 것의 의미는 거래를 구현하면서 알아보도록 하겠다.

type TXOutput struct {
	Value        uint64
	ScriptPubKey string
}

이 포스팅의 다음까지는 ScriptPubKey 에 대해 기존 구현과는 다른 방식을 취할 것이다. 그것을 이해하기 위해서는 P2PKH(Pay-To-Public-Key-Hash), P2SH(Pay-To-Script-Hash)에 대한 이해가 필요하기 때문이다. 따라서 조금 더 이해하기 쉬운 방식으로 표현하고 이후 트랜잭션 포스트에서는 이를 다뤄볼 예정이다. 이 부분은 나도 이해가 더 필요한 부분이다.

type TXInput

트랜잭션의 입력을 이해해보자. 현실세계의 거래와 연결시켜 생각해보면 좋다. 거래에서 입력이라는 것은 무엇일까? 그건 돈이 어디에서 왔는지 그 원천에 대한 이야기다. 자본의 흐름을 이어주는 역할을 하기 때문에 입력에서는 얼마가 들어왔는지에 대한 내용은 실제로 없다.

 

입력에는 자금이 어디에서 왔는지를 설명하기 위해 TXOutput 을 참조하는 필드를 가지고 있다. '얼마' 에 대한 내용은 TXOutput 이 가지고 있으니까 말이다. 입력이 여러 개라는 것은 거래로 사용할 자금이 부족하거나 하여 코인 뭉치를 조합하여 지불해야 하거나 또는 다수의 거래처로부터 자금을 받았을 때를 말한다.

 

따라서 입력의 경우 과거에 내게 들어온 자본의 흐름, 즉 이전 트랜잭션의 출력값을 참조하기 위한 필드인 TxidVout 필드가 있다. Txid 는 참조한 트랜잭션의 ID 이며 Vout 은 해당 트랜잭션이 가진 출력값의 인덱스다. 하나의 트랜잭션은 다수의 출력을 가질 수 있기 때문에 지목을 해줄 필요가 있다.

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

ScriptSig 는 일반적으로 디지털 서명이 들어가야 할 자리다. 서명의 경우 메시지를 해싱하고 개인키(Private Key)로 사인하면 만들어낼 수 있으나, ScriptPubKey 와 마찬가지로 단순하게 표현할 예정이다.

Coinbase Transaction

코인베이스 트랜잭션은 블록을 채굴하면 채굴자에게 보상을 주기위한 제일 첫 번째 트랜잭션이다. 이 트랜잭션은 입력이 없고 채굴자에게 보상을 지급하기 위한 출력 값만이 존재할 뿐이다. 코인베이스 트랜잭션의 입력 값이 특별한 형태를 가지고 있다는 것을 주목하자.

const (
	subsidy = 10 // BTC
)

func NewCoinbaseTX(data, to string) *Transaction {
	txin := TXInput{[]byte{}, -1, data}
	txout := TXOutput{subsidy, to}

	return NewTransaction([]TXInput{txin}, []TXOutput{txout})
}

subsidy 를 편의를 위해 BTC 단위로 취급했지만, 실제 비트코인에서는 최소 단위인 사토시(Satoshi)를 사용하여 표현하게 된다. 이는 위에서도 적었듯이 블록채굴자를 위한 보상으로 주어질 것이다.

변경 사항

func CreateBlockchain(string) *Blockchain

이 함수에는 파라매터가 추가되었다. 왜냐하면 블록체인을 생성하고 제네시스 블록을 채굴한 사람에게 보상을 지급해야 하기 때문이다.

func CreateBlockchain(address string) *Blockchain {
	// ...
	err = db.Update(func(tx *bolt.Tx) error {
		// ...
        
		genesis := NewBlock([]*Transaction{NewCoinbaseTX("", address)}, []byte{})
		// ...
	})

	return &Blockchain{db, l}
}

func Blockchain.AddBlock([]*Transaction)

NewBlock() 의 구현이 변경되었으므로 그것을 사용하는 Blockchain.AddBlock() 의 변경이 일부 필요하게 되었다.

func (bc *Blockchain) AddBlock(transactions []*Transaction) {
	block := NewBlock(transactions, bc.l)
	// ...
}

type Block

이제 .Data 필드를 지우고 .Transactions 으로 변경해야 한다.

type Block struct {
	// Data []byte
	Transactions  []*Transaction
}

func NewBlock([]*Transaction, []byte) *Block

기존에는 블록 데이터를 받았지만, 이제는 []*Transaction 으로 변경하고 블록을 생성할 때도 바꿔주어야 한다.

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{prevBlockHash, []byte{}, time.Now().Unix(), transactions, 0}
	// ...
}

func ProofOfWork.prepareData(int64) []byte

작업증명을 위한 데이터를 준비할 때 Block.Data 를 사용했는데, 이제는 그렇게할 필요없이 Block.HashTransaction() 을 사용해서 트랜잭션을 해싱하자.

func (pow *ProofOfWork) prepareData(nonce int64) []byte {
	data := bytes.Join([][]byte{
		// ...
		pow.block.HashTransaction(),
		// ...
	}, []byte{})

	return data
}