4. Transactions
Implementing very basic version of transactions into our blockchain.
시작
Coinbase
transaction.go
blockchain.go
예시 설명

Last updated
Implementing very basic version of transactions into our blockchain.

Last updated
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
}
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
}