4. Transactions

Implementing very basic version of transactions into our blockchain.

시작

  • 간단한 Transaction을 구현해보자.

  • Transaction은 블록에 저장될 기본 거래의 기능을 가지는 데이터이다.

Coinbase

  • Miner로부터만 생성되는 비트코인 tx의 tx타입이다.

  • 새로운 블록의 첫 tx이다.

  • 인풋이 없고 아웃풋만 있다.

  • 아웃풋은 miner가 받는 리워드이다.

아래 코드에서는 coinbase tx가 블록마다 생성되어 있지 않고 블록체인의 첫 블록에서만 존재한다.

1개의 블록체인, 블록마다 1개의 tx만 존재. (앞으로 수정될 것으로 보인다. )

transaction.go

  • tx의 인풋과 아웃풋을 정의하고 tx의 생성과 검증에 대한 함수들을 구현한다.

  • TxInput

    • ID : input으로 사용할 tx의 id

    • Out : 해당 tx에서 output의 index

    • Sig : 소유자의 주소 (코인을 보내는 사람, 인풋의 소유자)

  • TxOutput

    • Value : 코인의 값, 크기

    • PubKey : 받을 사람의 주소

package blockchain

import (
	"bytes"
	"crypto/sha256"
	"encoding/gob"
	"encoding/hex"
	"fmt"
	"log"
)

// Transaction : 블록에 쓰여질 데이터.
type Transaction struct {
	// hash
	ID []byte
	// Array of input
	Inputs []TxInput
	// Array of output
	Outputs []TxOutput
}

// TxOutput : Transaction Output
// Indivisible - 나눌 수 없는, 불가분의
// Output은 쪼갤 수 없다.
// ex) 500원짜리 물건을 사는데 1000원을 내면 1000원을 반으로 쪼개는 것이 아니라 500원(새로운 아웃풋)을 돌려준다.
// Value만큼의 값을 PubKey가 받는다.
type TxOutput struct {
	// locked value in token
	Value int
	// 공개키 : token을 언락하기 위해 필요하다. (value의 안쪽을 보기 위해)
	// Bitcoin에서는 Pubkey가 복잡한 스크립트 언어로 되어 있다.
	// User's account, address
	PubKey string
}

// TxInput : Transaction Input
// ID에 해당하는 Transaction의 아웃풋에서 Out값 위치에 있는 UTXO를 Sig가 보낸다.
type TxInput struct {
	// Input의 ID
	// 사용할 Transaction의 ID.
	ID []byte
	// Output 의 인덱스.
	// 해당 transaction에서 몇 번째 위치한 output과 연결되어 있는지 알려줌.
	Out int
	// Signature : TxOutput의 PubKey와 비슷한 역할.
	// User's account, address
	Sig string
}

// SetID : transaction의 ID를 만들어 넣어준다.
func (tx *Transaction) SetID() {
	var encoded bytes.Buffer
	var hash [32]byte

	encode := gob.NewEncoder(&encoded)
	err := encode.Encode(tx)
	Handle(err)

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

// CoinbaseTx : 하나의 인풋과 하나의 아웃풋이 있음. 채굴자가 아웃풋을 받는다.
// to : data 받을 사람의 address
func CoinbaseTx(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Coins to %s", to)
	}

	// 참조하는 TxOutput이 없다.
	txin := TxInput{[]byte{}, -1, data}
	// 100 coin을 to에게 보낸다.
	txout := TxOutput{100, to}

	// Transaction Init
	tx := Transaction{nil, []TxInput{txin}, []TxOutput{txout}}

	return &tx
}

// NewTransaction : from account, to account
// amount : 보내고 싶은 코인의 양
// from이 to에게 amount만큼의 코인을 보낸다.
func NewTransaction(from, to string, amount int, chain *BlockChain) *Transaction {
	var inputs []TxInput
	var outputs []TxOutput

	acc, validOutputs := chain.FindSpendableOutputs(from, amount)

	// 보내고 싶은 만큼의 코인이 없다.
	if acc < amount {
		// 시간과 에러 문자열을 출력한 뒤 패닉을 발생시킴.
		// 패닉 : 프로그램을 종료시킨다. (런타임 에러)
		// recover 함수를 사용하면 panic 후에 복구할 수 있다. (프로그램이 종료되지 않음.)
		log.Panic("Error: not enough funds")
	}

	// 사용할 수 있는 아웃풋의 인덱스들.
	for txid, outs := range validOutputs {
		txID, err := hex.DecodeString(txid)
		Handle(err)

		for _, out := range outs {
			input := TxInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}

	// to address로 amount만큼의 코인을 보낸다.
	outputs = append(outputs, TxOutput{amount, to})

	// 모은 UTXO의 양이 보낼 양보다 크면 거스름돈을 받는다.
	if acc > amount {
		outputs = append(outputs, TxOutput{acc - amount, from})
	}

	tx := Transaction{nil, inputs, outputs}
	tx.SetID()

	return &tx
}

// IsCoinbase : Coinbase 인지 판별한다.
func (tx *Transaction) IsCoinbase() bool {
	return len(tx.Inputs) == 1 && len(tx.Inputs[0].ID) == 0 && tx.Inputs[0].Out == -1
}

// CanUnlock : input의 Sig(address)를 알고 있는 사람만 unlock할 수 있다.
func (in *TxInput) CanUnlock(data string) bool {
	return in.Sig == data
}

// CanBeUnlocked : output의 PubKey(address)를 알고 있는 사람만 unlock할 수 있다.
func (out *TxOutput) CanBeUnlocked(data string) bool {
	return out.PubKey == data
}

blockchain.go

  • 기존에 Data로 표현되었던 블록 내부의 값을 Transactions로 바꿨다.

  • 사용 가능한 Tx, UTXO 찾는 등의 함수를 추가했다.

package blockchain

import (
	"encoding/hex"
	"fmt"
	"os"
	"runtime"

	"github.com/dgraph-io/badger/v3"
)

const (
	// database Path.
	dbPath = "./tmp/blocks"
	// DB가 존재하는지 체크하기 위한 MANIFEST file.
	dbFile      = "./tmp/blocks/MANIFEST"
	genesisData = "First Transaction from Genesis"
)

// BlockChain structure
// 마지막 해쉬를 저장하고 DB 포인터를 저장해서 블록을 관리.
type BlockChain struct {
	LastHash []byte
	Database *badger.DB
}

// BlockChainIterator : DB에 저장된 블록체인을 순회하기 위해 생성.
type BlockChainIterator struct {
	// 현재 가리키고 있는 hash
	CurrentHash []byte
	Database    *badger.DB
}

// DBexists : DB가 존재하는지 체크.
func DBexists() bool {
	if _, err := os.Stat(dbFile); os.IsNotExist(err) {
		return false
	}
	return true
}

// InitBlockChain : Genesis 블록을 시작으로 하는 블록체인을 생성한다.
func InitBlockChain(address string) *BlockChain {
	var lastHash []byte

	// DB가 이미 존재하면 종료.
	if DBexists() {
		fmt.Println("Blockchain already exists")
		runtime.Goexit()
	}

	opts := badger.DefaultOptions(dbPath)
	// store key and metadata
	opts.Dir = dbPath
	// store all values
	opts.ValueDir = dbPath

	db, err := badger.Open(opts)
	Handle(err)

	// Update : read and write transactions on our databases.
	// Txn : Transaction, which can be read-only or read-write.
	err = db.Update(func(txn *badger.Txn) error {
		// 첫 블록을 Coinbase로 한다.
		cbtx := CoinbaseTx(address, genesisData)
		genesis := Genesis(cbtx)
		fmt.Println("Genesis created")
		err = txn.Set(genesis.Hash, genesis.Serialize())
		Handle(err)
		err = txn.Set([]byte("lh"), genesis.Hash)

		lastHash = genesis.Hash

		return err
	})

	Handle(err)

	blockchain := BlockChain{lastHash, db}
	return &blockchain
}

// ContinueBlockChain : Blockchain이 이미 존재하고 있을 경우 그 블록체인을 리턴.
// address를 인자로 받고 있지만 이 함수 어디에서도 쓰이지 않는다.
// 현재는 블록체인이 하나만 생성되도록 되어 있어서 address에 아무 값이나 넣어도 상관이 없다.
// DB에 하나의 블록체인만 존재.
func ContinueBlockChain(address string) *BlockChain {
	// DB가 존재하지 않으면 종료.
	if DBexists() == false {
		fmt.Println("No existing blockchain found, create one!")
		runtime.Goexit()
	}

	var lastHash []byte

	opts := badger.DefaultOptions(dbPath)
	// store key and metadata
	opts.Dir = dbPath
	// store all values
	opts.ValueDir = dbPath

	db, err := badger.Open(opts)
	Handle(err)

	// Update : read and write transactions on our databases.
	// Txn : Transaction, which can be read-only or read-write.
	err = db.Update(func(txn *badger.Txn) error {
		// 마지막 해쉬를 찾는다.
		item, err := txn.Get([]byte("lh"))
		Handle(err)
		lastHash, err = item.ValueCopy(nil)

		return err
	})
	Handle(err)

	blockchain := BlockChain{lastHash, db}
	return &blockchain
}

// AddBlock : transactions를 가지고 있는 블록을 추가한다.
func (chain *BlockChain) AddBlock(transactions []*Transaction) {
	var lastHash []byte

	// View : read-only type of transaction
	err := chain.Database.View(func(txn *badger.Txn) error {
		// 마지막 해쉬를 찾는다.
		item, err := txn.Get([]byte("lh"))
		Handle(err)
		lastHash, err = item.ValueCopy(nil)

		return err
	})
	Handle(err)

	// 이전 해쉬값과 데이터로 새로운 블록을 생성.
	newBlock := CreateBlock(transactions, lastHash)

	err = chain.Database.Update(func(txn *badger.Txn) error {
		// 새로운 블록의 해쉬값을 저장한다.
		err := txn.Set(newBlock.Hash, newBlock.Serialize())
		Handle(err)
		// lh 키값을 새로운 블록의 해쉬값으로 바꾼다.
		err = txn.Set([]byte("lh"), newBlock.Hash)

		// BlockChain의 마지막 해쉬를 새로운 블록의 해쉬값으로 지정한다.
		chain.LastHash = newBlock.Hash

		return err
	})
	Handle(err)
}

// Iterator : 블록체인 이터레이터. LastHash부터 이전 해쉬로 가며 순회할 수 있다.
func (chain *BlockChain) Iterator() *BlockChainIterator {
	iter := &BlockChainIterator{chain.LastHash, chain.Database}

	return iter
}

// Next : BlockChain의 다음 블록을 반환한다.
func (iter *BlockChainIterator) Next() *Block {
	var block *Block

	err := iter.Database.View(func(txn *badger.Txn) error {
		// 현재 가리키고 있는 hash를 deserialize해서 Block을 복원한다.
		item, err := txn.Get(iter.CurrentHash)
		Handle(err)
		encodedBlock, err := item.ValueCopy(nil)
		block = Deserialize(encodedBlock)

		return err
	})
	Handle(err)

	// iter가 이전 해시를 가리키도록 한다.
	iter.CurrentHash = block.PrevHash

	return block
}

// FindUnspentTransactions : 사용하지 않은 Transaction들을 찾는다.
// Transaction에 사용하지 않은 output이 한개라도 있으면 추가해서 반환한다.
func (chain *BlockChain) FindUnspentTransactions(address string) []Transaction {
	var unspentTxs []Transaction

	// key : string
	// value : []int
	spentTXOs := make(map[string][]int)

	iter := chain.Iterator()

	for {
		block := iter.Next()

		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)

		Outputs:
			for outIdx, out := range tx.Outputs {
				// 사용한 TXO인지 체크.
				// 사용했다면 (spentTXOs에 들어있다면) continue
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}
				// 사용하지 않았고 unlock할 수 있다면 unspentTxs에 추가.
				if out.CanBeUnlocked(address) {
					unspentTxs = append(unspentTxs, *tx)
					// 강의에서는 break문이 없다.
					// tx를 unspentTxs에 추가했다면 다음 tx로 넘어가야하지 않을까?
					// 같은 tx가 여러개 추가되는 현상이 발생할 것 같음.
					break
				}
			}
			// Coinbase가 아닐 때
			if tx.IsCoinbase() == false {
				// Output을 찾기 위해 Input을 돈다.
				// inTxID를 ID로 가지는 Tx 의 in.Out 번째 output은 사용한 output이다.
				for _, in := range tx.Inputs {
					if in.CanUnlock(address) {
						inTxID := hex.EncodeToString(in.ID)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Out)
					}
				}
			}
		}

		// Genesis block 까지 오면 break.
		if len(block.PrevHash) == 0 {
			break
		}
	}
	return unspentTxs
}

// FindUTXO : 사용하지 않은 모든 output을 리턴한다.
// UTXOs의 Value를 모두 더하면 총잔고가 된다.
func (chain *BlockChain) FindUTXO(address string) []TxOutput {
	var UTXOs []TxOutput
	unspentTransactions := chain.FindUnspentTransactions(address)

	// UTXO중에 unlock할 수 있는(해당 address 소유자) 것만 모아서 리턴.
	for _, tx := range unspentTransactions {
		for _, out := range tx.Outputs {
			if out.CanBeUnlocked(address) {
				UTXOs = append(UTXOs, out)
			}

		}
	}
	return UTXOs
}

// FindSpendableOutputs : coin based transaction이 아닌 보통의 transaction을 생성한다.
// address로 amount만큼의 코인을 보낸다.
func (chain *BlockChain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOuts := make(map[string][]int)
	unspentTxs := chain.FindUnspentTransactions(address)
	accumulated := 0

Work:
	// UTX 를 순회
	for _, tx := range unspentTxs {
		txID := hex.EncodeToString(tx.ID)

		// 한 transaction의 output을 순회.
		for outIdx, out := range tx.Outputs {
			// unlock할 수 있어야 하고 보내고 싶은 코인의 수가 UTXO로부터 모든 금액보다 클 때
			// 보낼 금액이 부족할 때 (accumulated를 더 증가시켜야 함.)
			if out.CanBeUnlocked(address) && accumulated < amount {
				accumulated += out.Value
				unspentOuts[txID] = append(unspentOuts[txID], outIdx)

				// 금액이 다 모임.
				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOuts
}

나머지 코드는 기존의 코드의 Data부분을 Trasaction으로 바꾼 것뿐이다. github을 참고하자.

예시 설명

  • 마지막 블록에 대한 설명

    • Taeha 가 JY에게 50코인을 보내는 것에 대한 설명이다.

    • ID : 1a8dd71… 인 인풋은 바로 아래 블록의 ID : 1a8dd71…로 같은 Transaction의 Out 0, 0번째 인덱스의 아웃풋을 의미.

    • ID : 710d7bd… 인 인풋은 두칸 아래 블록의 ID : 710d7bd…로 같은 Transaction의 Out 1, 1번째 인덱스의 아웃풋을 의미.

    • 이 2개의 아웃풋을 사용해서 JY에게 보낼 50 UTXO와 Taeha에게 거슬러줄 20 UTXO를 생성한다는 의미이다.

Last updated