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