#7 UTXO management
UTXO들을 효율적으로 관리하기
Last updated
UTXO들을 효율적으로 관리하기
Last updated
UTXO들을 데이터베이스에 따로 저장해서 데이터베이스 전체를 탐색하지 않고 UTXO를 접근할 수 있도록 할 것입니다.
이전 파트까지 구현을 했다면, 뭔가 많이 비효율적이라는 생각이 들었을 것 입니다. Balance를 확인하거나 send 트랜잭션을 발생시킬 때마다 전체 블록을 찾아가면서 UTXO를 찾아야했습니다. 지금이야 블록체인이 아주 작기 때문에 괜찮지만 체인의 길이가 길어진다면 문제가 될 것입니다.
예를 들어, FindSpendableOutputs 함수나 FindUTXO함수가 이에 해당합니다.
이번 파트에서는 UTXO를 따로 저장하여 관리하는 방법으로 이를 최적화할 것입니다.
"utxo-"라는 prefix를 블록 해시에 붙힌 값을 key로 하여 UTXO들만 따로 저장합니다.
TxOutput struct의 집합인 TxOutputs 구조를 만들고, 이에 대한 Serialize, Deserialize 함수를 만든다.
// tx.go
// TxOutput 모음
type TxOutputs struct {
Outputs []TxOutput
}
// TxOutputs를 []byte로
func (outs TxOutputs) Serialize() []byte {
var buffer bytes.Buffer
encode := gob.NewEncoder(&buffer)
err := encode.Encode(outs)
Handle(err)
return buffer.Bytes()
}
// []byte를 TxOutputs로
func DeserializeOutputs(data []byte) TxOutputs {
var outputs TxOutputs
decode := gob.NewDecoder(bytes.NewReader(data))
err := decode.Decode(&outputs)
Handle(err)
return outputs
}
UTXO들을 관리하기 위한 코드들을 따로 정리하기 위해 utxo.go
파일을 만듭니다. BlockChain의 Database를 공유하고 "utxo-" prefix를 key앞에 붙혀서 UTXO들만 따로 관리합니다.
package blockchain
import (
"bytes"
"encoding/hex"
"log"
"github.com/dgraph-io/badger"
)
// utxo- 로 prefix된 key를 위한 상수 입니다.
var (
utxoPrefix = []byte("utxo-")
prefixLength = len(utxoPrefix)
)
// UTXO를 관리하는 구조입니다.
// BlockChain의 Database를 공유하므로 BlockChain에 대한 포인터를 가집니다.
type UTXOSet struct {
Blockchain *BlockChain
}
// blockchain.go 에서 옮겨온 함수.
// UTXOSet 중에 pubKeyHash 소유의 UTXO를 반환.
func (u UTXOSet) FindUnspentTransactions(pubKeyHash []byte) []TxOutput {
var UTXOs []TxOutput
db := u.Blockchain.Database
err := db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
it := txn.NewIterator(opts)
defer it.Close()
// "utxo-"로 prefix된 key를 찾아 value를 가져옵니다.
for it.Seek(utxoPrefix); it.ValidForPrefix(utxoPrefix); it.Next() {
item := it.Item()
v, err := item.ValueCopy(nil)
Handle(err)
outs := DeserializeOutputs(v)
// 결과값 중 pubKeyHash 소유의 UTXO만 저장합니다.
for _, out := range outs.Outputs {
if out.IsLockedWithKey(pubKeyHash) {
UTXOs = append(UTXOs, out)
}
}
}
return nil
})
Handle(err)
return UTXOs
}
// blockchain.go 에서 옮겨온 함수.
// amount를 집불하기위해 사용될 UTXO를 검색합니다.
// 사용할 수 있는 금액과 사용할 수 있는 TXO를 찾을 수 있는 매핑을 반환합니다.
func (u UTXOSet) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) {
// UTXO의 txID => outIdx(해당 트랜잭션에서 몇번째 TXO가 UTXO인지) 매핑
unspentOuts := make(map[string][]int)
accumulated := 0
db := u.Blockchain.Database
err := db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
it := txn.NewIterator(opts)
defer it.Close()
// "utxo-"로 prefix된 key 모두 탐색합니다.
for it.Seek(utxoPrefix); it.ValidForPrefix(utxoPrefix); it.Next() {
item := it.Item()
k := item.Key()
v, err := item.ValueCopy(nil)
Handle(err)
// "utxo-" prefix를 제거합니다.
k = bytes.TrimPrefix(k, utxoPrefix)
txID := hex.EncodeToString(k)
outs := DeserializeOutputs(v)
// 트랜잭션의 모든 Output(TXO)에 대해 for loop
for outIdx, out := range outs.Outputs {
// {pubKeyHash}소유이고 지금까지의 UTXO의 합이 amount보다 작다면 해당 UTXO를 추가
if out.IsLockedWithKey(pubKeyHash) && accumulated < amount {
accumulated += out.Value
unspentOuts[txID] = append(unspentOuts[txID], outIdx)
}
}
}
return nil
})
Handle(err)
return accumulated, unspentOuts
}
// "utxo-" prefix된 key를 지우고 새로 매핑한다.
func (u UTXOSet) Reindex() {
db := u.Blockchain.Database
// "utxo-" prefix된 데이터를 모두s 지운다.
u.DeleteByPrefix(utxoPrefix)
// UTXO를 모두 찾는다.
UTXO := u.Blockchain.FindUTXO()
err := db.Update(func(txn *badger.Txn) error {
for txId, outs := range UTXO {
key, err := hex.DecodeString(txId)
if err != nil {
return err
}
// 찾은 UTXO의 txId에 prefix를 더해 저장한다.
key = append(utxoPrefix, key...)
err = txn.Set(key, outs.Serialize())
Handle(err)
}
return nil
})
Handle(err)
}
// AddBlock시에 발생합니다.
// block의 transaction을 DB에 업데이트
func (u *UTXOSet) Update(block *Block) {
db := u.Blockchain.Database
err := db.Update(func(txn *badger.Txn) error {
for _, tx := range block.Transactions {
if !tx.IsCoinbase() {
// 트랜잭션의 모든 Input에 대하여
for _, in := range tx.Inputs {
updatedOuts := TxOutputs{}
inID := append(utxoPrefix, in.ID...)
// in.ID에 "utxo-" prefix를 붙혀 찾는다.
item, err := txn.Get(inID)
Handle(err)
v, err := item.ValueCopy(nil)
Handle(err)
// outs은 in.ID 트랜잭션의 모든 UTXO
outs := DeserializeOutputs(v)
// UTXO 중에
for outIdx, out := range outs.Outputs {
// 이번 트랜잭션에서 사용할 UTXO 제외하고 (XXX: 아래 if문이 맞나?)
if outIdx != in.Out {
// 새로운 UTXO 배열을 만든다.
updatedOuts.Outputs = append(updatedOuts.Outputs, out)
}
}
// UTXO가 남아있지 않다면 삭제.
if len(updatedOuts.Outputs) == 0 {
if err := txn.Delete(inID); err != nil {
log.Panic(err)
}
} else {
// UTXO가 남아있다면 다시 저장.
if err := txn.Set(inID, updatedOuts.Serialize()); err != nil {
log.Panic(err)
}
}
}
}
// 이번 트랜잭션으로 생기는 UTXO들
newOutputs := TxOutputs{}
for _, out := range tx.Outputs {
newOutputs.Outputs = append(newOutputs.Outputs, out)
}
// "utxo-" prefix하여 UTXO들을 저장한다.
txID := append(utxoPrefix, tx.ID...)
if err := txn.Set(txID, newOutputs.Serialize()); err != nil {
log.Panic(err)
}
}
return nil
})
Handle(err)
}
// "utxo-" prefix key를 가진 트랜잭션의 수를 반환한다.
func (u UTXOSet) CountTransactions() int {
db := u.Blockchain.Database
counter := 0
err := db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
it := txn.NewIterator(opts)
defer it.Close()
for it.Seek(utxoPrefix); it.ValidForPrefix(utxoPrefix); it.Next() {
counter++
}
return nil
})
Handle(err)
return counter
}
// {prefix}가 붙은 Key-Value를 제거한다.
func (u *UTXOSet) DeleteByPrefix(prefix []byte) {
// {KeysForDelete}에 속한 key-Value를 제거한다.
deleteKeys := func(KeysForDelete [][]byte) error {
if err := u.Blockchain.Database.Update(func(txn *badger.Txn) error {
for _, key := range KeysForDelete {
if err := txn.Delete(key); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
return nil
}
collectSize := 100000 // badger의 optimal batching size
u.Blockchain.Database.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false // value를 읽을 필요없으므로
it := txn.NewIterator(opts)
defer it.Close()
keysForDelete := make([][]byte, 0, collectSize)
keysCollected := 0
// prefix된 모든 Key-value에 대해
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
// key를 가져와 지울 키 리스트에 추가한다.
key := it.Item().KeyCopy(nil)
keysForDelete = append(keysForDelete, key)
keysCollected++
// batching size만큼 key를 모았으면
if keysCollected == collectSize {
// 해당 key값들을 지운다.
if err := deleteKeys(keysForDelete); err != nil {
log.Panic(err)
}
// 변수 초기화
keysForDelete = make([][]byte, 0, collectSize)
keysCollected = 0
}
}
// 처리되지않은 키들을 마저 처리한다.
if keysCollected > 0 {
if err := deleteKeys(keysForDelete); err != nil {
log.Panic(err)
}
}
return nil
})
}
FindUTXO 함수와 AddBlock 함수를 수정합니다.
// 새로운 블록을 만들어서 블록체인에 연결하는 함수
// 새로 추가된 블록을 리턴함.
func (chain *BlockChain) AddBlock(transactions []*Transaction) *Block {
var lastHash []byte
// Read만 하므로 View를 사용
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)
// lashHash를 토대로 다음 문제를 풀어 새로운 블록을 생성.
newBlock := CreateBlock(transactions, lastHash)
// 블록의 해시를 키값으로 새로운 블록을 저장하고
// lh의 값 또한 새로운 블록의 해시로 업데이트 해줍니다.
err = chain.Database.Update(func(txn *badger.Txn) error {
err := txn.Set(newBlock.Hash, newBlock.Serialize())
Handle(err)
err = txn.Set([]byte("lh"), newBlock.Hash)
chain.LastHash = newBlock.Hash
return err
})
Handle(err)
return newBlock
}
// block의 모든 UTXO (txID => TxOutputs mapping)을 찾아 반환합니다.
func (chain *BlockChain) FindUTXO() map[string]TxOutputs {
UTXOs := make(map[string]TxOutputs)
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 {
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {
if spentOut == outIdx {
continue Outputs
}
}
}
// outs에 UTXO추가
outs := UTXOs[txID]
outs.Outputs = append(outs.Outputs, out)
UTXOs[txID] = outs
}
if !tx.IsCoinbase() {
// Inputs에 참조된 모든 UTXO는 이번 트랜잭션에서 사용된 UTXO이다.
for _, in := range tx.Inputs {
inTxID := hex.EncodeToString(in.ID)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Out)
}
}
}
if len(block.PrevHash) == 0 {
break
}
}
return UTXOs
}
NewTransaction함수의 인자를 UTXOSet으로 바꾸고 이에 따라 코드도 약간 수정합니다.
// Transaction을 만드는 함수 입니다.
func NewTransaction(from, to string, amount int, UTXO *UTXOSet) *Transaction {
var inputs []TxInput
var outputs []TxOutput
// Wallets에서 {from}의 address갖는 wallet을 가져옵니다.
wallets, err := wallet.CreateWallets()
Handle(err)
w := wallets.GetWallet(from)
// Public key로 부터 publicKeyHash를 생성합니다.
pubKeyHash := wallet.PublicKeyHash(w.PublicKey)
// {from}이 {amount}를 지불하기 위해 필요한 {from}소유의 UTXO를 가지고 옵니다.
// {from}의 공개키 해시 {pubKeyHash}를 이용합니다.
acc, validOutputs := UTXO.FindSpendableOutputs(pubKeyHash, amount)
// UTXO를 다 모았는데 amount보다 작다면 잔액 부족입니다.
if acc < amount {
log.Panic("Error: not enough funds")
}
// 모아온 UTXOs에 대해 for loop
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
Handle(err)
for _, out := range outs {
// {txID}를 가지는 트랜잭션의 {out}번째 {from}소유의 아웃풋이 인풋이 됩니다.
// 4번째 인자로 {from}의 PublicKey가 추가됩니다.
input := TxInput{txID, out, nil, w.PublicKey}
inputs = append(inputs, input)
}
}
// 이제 트랜잭션의 인풋으로 사용될 UTXO를 모두 모았습니다.
// 그리고 그 가치의 합은 acc가 될 것입니다.
// 트랜잭션의 아웃풋은 전송될 TXO, 잔금(반환될) TXO
// 항상 두개로 이루어집니다.
// 전송될 TXO을 만듭니다.
outputs = append(outputs, *NewTXOutput(amount, to))
// 반환될 TXO
if acc > amount {
outputs = append(outputs, TxOutput{acc - amount, pubKeyHash})
}
// 인풋과 아웃풋을 바탕으로 Transaction이 생성됩니다.
tx := Transaction{nil, inputs, outputs}
tx.ID = tx.Hash()
UTXO.Blockchain.SignTransaction(&tx, w.PrivateKey)
return &tx
}
Cli 프로그램에 새로운 명령어 1개를 추가합니다. 커맨드 추가는 printchain과 같으므로 생략하겠습니다.
// UTXOSet을 rebuild합니다.
func (cli *CommandLine) reindexUTXO() {
chain := blockchain.ContinueBlockChain("")
defer chain.Database.Close()
UTXOset := blockchain.UTXOSet{chain}
UTXOset.Reindex()
count := UTXOset.CountTransactions()
fmt.Printf("Done! There are %d transactions in the UTXO set.\n", count)
}
UTXOSet을 이용하도록 하기 3개 함수를 수정합니다.
func (cli *CommandLine) createBlockChain(address string) {
if !wallet.ValidateAddress(address) {
log.Panic("Address is not Valid")
}
chain := blockchain.InitBlockChain(address)
defer chain.Database.Close()
UTXOset := blockchain.UTXOSet{chain}
UTXOset.Reindex()
fmt.Println("Finished!")
}
func (cli *CommandLine) getBalance(address string) {
if !wallet.ValidateAddress(address) {
log.Panic("Address is not Valid")
}
chain := blockchain.ContinueBlockChain("") // blockchain을 DB로 부터 받아온다.
UTXOset := blockchain.UTXOSet{chain}
defer chain.Database.Close()
balance := 0
// Human readable Address를 PubKeyHash로 다시 변환.
pubKeyHash := wallet.Base58Decode([]byte(address))
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
UTXOs := UTXOset.FindUnspentTransactions(pubKeyHash)
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of %s: %d\n", address, balance)
}
// {from}에서 {to}로 {amount}만큼 보냅니다.
func (cli *CommandLine) send(from, to string, amount int) {
if !wallet.ValidateAddress(from) {
log.Panic("Address is not Valid")
}
if !wallet.ValidateAddress(to) {
log.Panic("Address is not Valid")
}
chain := blockchain.ContinueBlockChain("") // blockchain을 DB로 부터 받아온다.
UTXOset := blockchain.UTXOSet{chain}
defer chain.Database.Close()
tx := blockchain.NewTransaction(from, to, amount, &UTXOset)
block := chain.AddBlock([]*blockchain.Transaction{tx})
UTXOset.Update(block)
fmt.Println("Success!")
}
Optimizing이기 때문에 새로 테스트 해 볼 것은 없습니다. 이전 테스트처럼 wallet을 생성하고 send를 호출한 후 getbalance나 printchain으로 정상 동작하는 지 살펴보세요.
혹시 getbalance가 제대로 동작하지 않는다면 reindexutxo 커맨드를 실행해보세요.
코드는 https://github.com/siisee11/golang-blockchain 의 step7 브랜치에 있습니다 .
Last updated: May 2, 2021