Blockchain

Go로 만드는 블록체인 part 6 - UTXO 집합

hou27 2022. 2. 1. 13:26

지금까지는 BoltDB에 저장된 블록의 수가 그리 많지 않았다.

UTXO를 계산할 때 모든 블록을 순회하며 확인하고 있는데,

지금은 문제가 없지만 만약 블록이 실제 비트코인만큼 존재한다면 엄청난 과부하를 피할 수 없을 것이다.

 

그래서 이번 포스트에서는 UTXO들만 따로 저장하여 계산하는 방식으로 개선해보도록 하겠다

UTXO란?

Part 4에서도 다뤘었지만 한번 더 짚고 넘어가도록 하겠다.

 

Unspent Transaction Output의 약자로, 소비되지 않은 Transaction의 출력이란 뜻이다.

이 UTXO를 새로운 chainstate라는 이름의 bucket을 생성하고 따로 저장해 줄 것이다.

UTXO의 소유권 이동 원리

vin

  • ScriptSig   []byte // Unlock script 다른 사람으로부터 받은 UTXO를 사용하기 위해 잠금 해제하는 스크립트

vout

  • ScriptPubKey []byte // Lock script 수신자 이외에 그 누구도 열지 못하도록 잠그는 스크립트

💡 우선 vout을 살펴 본인 소유가 맞으면 UTXO(**unspent transaction output)**에 추가하고, vin을 살펴 ScriptSig를 이용하여 해당 tx를 잠금해제하여 사용하게 된다.

 

그렇다면 갑자기 튀어나온 chainstate는 무엇인가?

 

Understanding the data behind Bitcoin Core

 

Understanding the data behind Bitcoin Core

In this tutorial, we will be taking a closer look at the data directory and files behind the Bitcoin core reference client.

bitcoindev.network

 

Chainstate

  • The blocks directory contains the actual blocks. The chainstate directory contains the state as of the latest block (in simplified terms, it stores every spendable coin, who owns it, and how much it's worth)

위 문서를 보면 비트코인 코어에선 chainstate라고 명명한 곳에 블록의 상태를 저장한다고 한다.

간단히 말하면, 소비할 수 있는 모든 코인을 누가 소유하고, 얼마나 가치가 있는지의 정보와 함께 저장한다.

 

Type UTXOSet

type UTXOSet struct {
	Blockchain *Blockchain
}

기본적으로 UTXOSet은 Blockchain과 연결되어있다.

 

Build UTXOSet

func (u UTXOSet) init(db *bolt.DB, bucketName []byte) error {
	err := db.Update(func(tx *bolt.Tx) error {
		
		b := tx.Bucket(bucketName)

		if b != nil {
			err := tx.DeleteBucket(bucketName)
		}
		_, err := tx.CreateBucket(bucketName)

		return nil
	})
	return err
}

// Builds the UTXO set
func (u UTXOSet) Build() {
	db := u.Blockchain.db

	bucketName := []byte(utxoBucket)
	err := u.init(db, bucketName)

	UTXO := u.Blockchain.FindAllUTXOs()

	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket(bucketName)

		for txID, outs := range UTXO {
			key, err := hex.DecodeString(txID)
			err = b.Put(key, SerializeTxs(outs))
		}

		return nil
	})
}

UTXOSet.Build()는 가장 초기에 실행되는 메서드이다.

db := u.Blockchain.db

bucketName := []byte(utxoBucket)
err := u.init(db, bucketName)

블록체인과 연결한 후, init 메서드를 통해

b := tx.Bucket(bucketName)

if b != nil {
    err := tx.DeleteBucket(bucketName)
}
_, err := tx.CreateBucket(bucketName)

기존에 UTXO 버킷이 존재하는지 확인한 후, 존재한다면 삭제 후 새로 생성한다.

func (cli *Cli) createBlockchain(address string) {
	if !IsValidWallet(address) {
		fmt.Println("Use correct wallet")
		os.Exit(1)
	}
	newBc := CreateBlockchain(address)
	defer newBc.db.Close()

	UTXOSet := UTXOSet{newBc}
	UTXOSet.Build()
	fmt.Println("Successfully done with create blockchain!")
}

초기에 블록체인을 생성한 후 Build를 통해 chainstate를 생성한다.

Function FindUTXOs

// Finds UTXO in chainstate
func (u UTXOSet) FindUTXOs(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	db := u.Blockchain.db

	err := db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))

		b.ForEach(func(k, v []byte) error {
			outs := DeserializeTxs(v)

			for _, out := range outs {
				if out.IsLockedWithKey(pubKeyHash) {
					UTXOs = append(UTXOs, out)
				}
			}
			return nil
		})

		return nil
	})

	return UTXOs
}

생성한 UTXO Set에서 

잔고를 확인하기 위한 함수이다.

func (cli *Cli) getBalance(address string) {
	bc := GetBlockchain()
	UTXOSet := UTXOSet{bc}
	defer bc.db.Close()
	
	balance := 0
	
	publicKeyHash, _, err := base58.CheckDecode(address)
	if err != nil {
		log.Panic(err)
	}
	utxos := UTXOSet.FindUTXOs(publicKeyHash)

	for _, out := range utxos {
		balance += out.Value
	}

	fmt.Printf("Balance of '%s': %d\n", address, balance)
}

이제 자신의 잔고를 확인할 때 FindUTXOs를 통해 확인하게 된다.

Function FindMyUTXOs

// Finds unspend transaction outputs for the address
func (u UTXOSet) FindMyUTXOs(publicKeyHash []byte, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	db := u.Blockchain.db
	accumulated := 0

	err := db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))
		
		b.ForEach(func(k, v []byte) error {
			txID := hex.EncodeToString(k)
			outs := DeserializeTxs(v)
		Work:
			for index, txout := range outs {
				if txout.IsLockedWithKey(publicKeyHash) && accumulated < amount {
					accumulated += txout.Value
					unspentOutputs[txID] = append(unspentOutputs[txID], index)
				}
				if accumulated >= amount {
					break Work
				}
			}
			return nil
		})

		return nil
	})
	
	return accumulated, unspentOutputs
}

 

 

그리고 이 친구는 원하는 만큼만 UTXO를 찾기 위한 함수이다.

// Creates a new transaction
func NewUTXOTransaction(from, to string, amount int, UTXOSet *UTXOSet) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	wallets, err := NewWallets()
	
	wallet := wallets.GetWallet(from)
	publicKeyHash := HashPublicKey(wallet.PublicKey)
	balance, validOutputs := UTXOSet.FindMyUTXOs(publicKeyHash, amount)

	...
}

거래를 생성할 때 FindMyUTXOs를 통해 코인을 보낸다.

UTXOSet.Blockchain.SignTransaction(&tx, wallet.PrivateKey)

서명 라인은 위와 같이 변경하였다.

Function Update

// Updates the UTXO set(Add new UTXO && Remove used UTXO)
func (u UTXOSet) Update(block *Block) {
	db := u.Blockchain.db

	db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))

		for _, tx := range block.Transactions {
			if !tx.IsCoinbase() {
				// Remove used UTXO
				for _, vin := range tx.Vin {
					var newOuts []TXOutput
					data := b.Get(vin.Txid)
					outs := DeserializeTxs(data)

					for outIdx, out := range outs {
						if outIdx != vin.TxoutIdx {
							newOuts = append(newOuts, out)
						}
					}

					if len(newOuts) == 0 {
						err := b.Delete(vin.Txid)
					} else {
						// Save other UTXOs that still available
						err := b.Put(vin.Txid, SerializeTxs(newOuts))
					}
				}
			}

			// Add new UTXO
			var newOuts []TXOutput
			newOuts = append(newOuts, tx.Vout...)

			err := b.Put(tx.ID, SerializeTxs(newOuts))
		}

		return nil
	})
}

이 친구는 새로운 블록이 채굴되었을 때 UTXO 집합을 업데이트해주는 기능을 담당한다.

// Remove used UTXO
for _, vin := range tx.Vin {
    var newOuts []TXOutput
    data := b.Get(vin.Txid)
    outs := DeserializeTxs(data)

    for outIdx, out := range outs {
        if outIdx != vin.TxoutIdx {
            newOuts = append(newOuts, out)
        }
    }

    if len(newOuts) == 0 {
        err := b.Delete(vin.Txid)
    } else {
        // Save other UTXOs that still available
        err := b.Put(vin.Txid, SerializeTxs(newOuts))
    }
}

사용된 출력은 삭제하고,

// Add new UTXO
var newOuts []TXOutput
newOuts = append(newOuts, tx.Vout...)

err := b.Put(tx.ID, SerializeTxs(newOuts))

새로운 출력을 추가한다.

func (cli *Cli) send(from, to string, amount int) {
	bc := GetBlockchain()
	defer bc.db.Close()

	UTXOSet := UTXOSet{bc}
	tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
	rewardTx := NewCoinbaseTX(from, "Mining reward")
	newBlock := bc.AddBlock([]*Transaction{rewardTx, tx})
	UTXOSet.Update(newBlock) // 요기 ^^
	fmt.Println("Send Complete!!")
}

거래가 발생했을 때 블록 채굴 후 Update를 진행한다.

 

실행 시 모습 :

$ go run . createblockchain -address 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U
330ab61f791b76d83ba9fab13867e0e7f4dd5e87ade89b1ca9ec730efffc384b
Successfully done with create blockchain!

$ go run . getbalance -address 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U
Balance of '1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U': 10

$ go run . send -from 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U -to 1159YQDpHy97WrXacYjFg3ET84A8qi1B3b -amount 3
0346ce7bf9ec0c8b828dc1962230e1ddf12dc8220fa8ffcd53c68ba30e7655e6
Successfully Added
Send Complete!!

$ go run . getbalance -address 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U
Balance of '1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U': 17

정상적으로 잘 작동하는 것을 확인할 수 있었다.

 

상세코드 변경 내역은 아래에서 확인할 수 있다.

https://github.com/hou27/blockchain_go/tree/part6

 

GitHub - hou27/blockchain_go: Making a Cryptocurrency with GO.

Making a Cryptocurrency with GO. . Contribute to hou27/blockchain_go development by creating an account on GitHub.

github.com


참고자료

 

Bitcoin Developer Network - Understanding the data behind Bitcoin Core