使用golang从零开始搭建基于UTXO模型的区块链(四、存储运行)

孟振
2023-12-01

前言

在前面的章节中我们了解了交易信息与UTXO模型,这样就掌握了区块链系统的基本数据结构。你可能已经发现了我们在前几章对区块链系统进行调试时每次都需要重新创建区块链,区块链并没有得到保存,这与实际的区块链系统不符。

Badger键值对数据库

Badger是dgraph.io开发的一款基于 Log Structured Merge (LSM) Tree 的 key-value 本地数据库,其官网在这里https://github.com/dgraph-io/badger。使用Badger有几个好处,首先它是由go语言编写的,然后它功能单一专注于键值对的存储,最后是安装简单不占地。

想要使用Badger,我们只需要在项目路径下用终端输入

go get github.com/dgraph-io/badger

存储地址的设定

我们在项目目录下创建一个tmp文件夹,专门用来存储区块链系统中需要保存的文件。在tmp文件夹下再创建blocks文件夹用以存储区块链中的区块

设定一些全局变量

const (
   Difficulty          = 12
   InitCoin            = 1000
   TransactionPoolFile = "./tmp/transaction_pool.data"
   BCPath              = "./tmp/blocks"
   BCFile              = "./tmp/blocks/MANIFEST"
)

在util.go中增加一个检查文件地址下文件是否存在的函数

func FileExists(fileAddr string) bool {
	if _, err := os.Stat(fileAddr); os.IsNotExist(err) {
		return false
	}
	return true
}

区块改动

我们希望创世区块上的交易可以有一个地址输出

func GenesisBlock(address []byte) *Block {
   tx := transaction.BaseTx(address)
   genesis := CreateBlock([]byte("This is nothing"), []*transaction.Transaction{tx})
   genesis.SetHash()
   return genesis
}

由于badger只支持字符串存储,所以我们需要序列化和反序列化

func (b *Block) Serialize() []byte {
	var res bytes.Buffer
	encoder := gob.NewEncoder(&res)
	err := encoder.Encode(b)
	utils.Handle(err)
	return res.Bytes()
}

func DeSerializeBlock(data []byte) *Block {
	var block Block
	decoder := gob.NewDecoder(bytes.NewReader(data))
	err := decoder.Decode(&block)
	utils.Handle(err)
	return &block
}

区块链结构体重构

BlockChain的属性之一应该指向存储区块的数据库。重构我们的区块链结构体如下

type BlockChain struct {
	LastHash []byte
	Database *badger.DB
}

LastHash属性是指当前区块链最后一个区块的哈希值,它并不是必须的,但可以避免我们在后面的函数编写中每次都去数据库中查找LastHash
此前我们有一个CreateBlockChain函数,该函数可以创建一个区块链并返回该区块链的指针。现在我们既然要实现区块链的创建、存储与读取功能,就需要摒弃这个函数。我们注释掉该函数,然后创建两个新的函数,分别为InitBlockChain与LoadBlockChain。InitBlockChain可以初始化我们的区块链并创建一个数据库保存,而LoadBlockChain可以读取已有的数据库并加载区块链。

// // CreateBlockChain 初始化区块链
//
//	func CreateBlockChain() *BlockChain {
//		myBlockchain := BlockChain{}
//		myBlockchain.Blocks = append(myBlockchain.Blocks, GenesisBlock())
//		return &myBlockchain
//	}
func InitBlockChain(address []byte) *BlockChain {
	var lastHash []byte

	if utils.FileExists(constcoe.BCFile) {
		fmt.Println("blockchain already exists")
		runtime.Goexit()
	}

	opts := badger.DefaultOptions(constcoe.BCPath)
	opts.Logger = nil

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

	err = db.Update(func(txn *badger.Txn) error {
		genesis := GenesisBlock(address)
		fmt.Println("Genesis Created")
		err = txn.Set(genesis.Hash, genesis.Serialize())
		utils.Handle(err)
		err = txn.Set([]byte("lh"), genesis.Hash) //store the hash of the block in blockchain
		utils.Handle(err)
		err = txn.Set([]byte("ogprevhash"), genesis.PrevHash) //store the prevhash of genesis(original) block
		utils.Handle(err)
		lastHash = genesis.Hash
		return err
	})
	utils.Handle(err)
	blockchain := BlockChain{lastHash, db}
	return &blockchain
}

该函数用于初始化一个区块链,输入参数为一个字节数组address,用于创建区块链的创世块(genesis block)的coinbase交易(即第一个交易)的接收地址。

函数首先定义一个变量lastHash,表示区块链中最后一个区块的hash值,初始值为空字节数组。

然后,函数判断是否已经存在了区块链文件,如果已经存在,则输出"blockchain already exists",并调用runtime.Goexit()函数终止程序的执行。

接下来,函数使用badger库提供的DefaultOptions()函数设置badger数据库的默认选项,使用badger.Open()函数打开或创建一个badger数据库,并将返回的db赋值给db变量。

接下来,函数使用db.Update()函数来更新数据库。在该函数中,函数首先使用GenesisBlock()函数创建创世块genesis,并将其序列化后存储到数据库中。然后,函数使用txn.Set()函数将创世块的hash值存储到键名为"lh"的键值对中,表示该hash值为区块链中最后一个区块的hash值。接着,函数使用txn.Set()函数将创世块的PrevHash存储到键名为"ogprevhash"的键值对中,表示创世块的PrevHash为原始创世块的hash值(即空字节数组)。最后,函数将创世块的hash值赋值给lastHash变量,并返回err。

最后,函数使用lastHash和db变量创建一个新的区块链结构体BlockChain,并将其地址返回。

func LoadBlockChain() *BlockChain {
	if utils.FileExists(constcoe.BCFile) == false {
		fmt.Println("No blockchain found, please create one first")
		runtime.Goexit()
	}

	var lastHash []byte

	opts := badger.DefaultOptions(constcoe.BCPath)
	opts.Logger = nil
	db, err := badger.Open(opts)
	utils.Handle(err)

	err = db.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)

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

该代码逻辑与init类似,大家可以参考比较学习
现在我们如果要将一个区块加入到区块链中,就需要通过数据库来完成。修改AddBlock函数。

func (bc *BlockChain) AddBlock(newBlock *Block) {
	var lastHash []byte

	err := bc.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return nil
		})
		utils.Handle(err)

		return err
	})
	utils.Handle(err)
	if !bytes.Equal(newBlock.PrevHash, lastHash) {
		fmt.Println("This block is out of age")
		runtime.Goexit()
	}

	err = bc.Database.Update(func(transaction *badger.Txn) error {
		err := transaction.Set(newBlock.Hash, newBlock.Serialize())
		utils.Handle(err)
		err = transaction.Set([]byte("lh"), newBlock.Hash)
		bc.LastHash = newBlock.Hash
		return err
	})
	utils.Handle(err)
}

把区块加到数据库中去

区块链查找

要实现寻找未花费的交易,我们要实现区块链的遍历
这里我们创建一个基于区块的迭代器来实现区块链的遍历。在blockchain.go中创建结构体

type BlockChainIterator struct {
	CurrentHash []byte
	Database    *badger.DB
}

这里我们的迭代器初始化并且遍历

func (chain *BlockChain) Iterator() *BlockChainIterator {
	iterator := BlockChainIterator{chain.LastHash, chain.Database}
	return &iterator
}

// Next 开始迭代
func (iterator *BlockChainIterator) Next() *Block {
	var block *Block

	err := iterator.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get(iterator.CurrentHash)
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			block = DeSerializeBlock(val)
			return nil
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)

	iterator.CurrentHash = block.PrevHash

	return block
}

首先定义一个指向区块的指针 block。然后使用当前迭代器指向的区块哈希值,从数据库中查找对应的区块数据。如果查询成功,将返回的区块数据进行反序列化,得到一个完整的区块对象。然后将迭代器的当前哈希值更新为上一个区块的哈希值,以便下一次迭代使用。最后返回获取到的完整区块对象。

通过比较迭代器的CurrentHash与数据库存储的OgPrevHash是否相等就能够判断迭代器是否已经迭代到创始区块。

func (chain *BlockChain) BackOgPrevHash() []byte {
	var ogprevhash []byte
	err := chain.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("ogprevhash"))
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			ogprevhash = val
			return nil
		})

		utils.Handle(err)
		return err
	})
	utils.Handle(err)

	return ogprevhash
}

现在我们可以修改FindUnspentTransactions函数了。通过迭代器,我们实现区块链由后到前的遍历

func (bc *BlockChain) FindUnspentTransactions(address []byte) []transaction.Transaction {
	var unSpentTxs []transaction.Transaction
	spentTxs := make(map[string][]int) // can't use type []byte as key value

	iter := bc.Iterator()

all:
	for {
		block := iter.Next()

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

		IterOutputs:
			for outIdx, out := range tx.Outputs {
				if spentTxs[txID] != nil {
					for _, spentOut := range spentTxs[txID] {
						if spentOut == outIdx {
							continue IterOutputs
						}
					}
				}

				if out.ToAddressRight(address) {
					unSpentTxs = append(unSpentTxs, *tx)
				}
			}
			if !tx.IsBase() {
				for _, in := range tx.Inputs {
					if in.FromAddressRight(address) {
						inTxID := hex.EncodeToString(in.TxID)
						spentTxs[inTxID] = append(spentTxs[inTxID], in.OutIdx)
					}
				}
			}
		}
		if bytes.Equal(block.PrevHash, bc.BackOgPrevHash()) {
			break all
		}
	}
	return unSpentTxs
}

交易池

区块链的交易信息池(Transaction Pool)是指一个临时存储未被打包进区块的交易的区块链节点内存池。当一个节点收到新的交易时,它会将其加入到自己的交易信息池中,并将其广播到网络中的其他节点。这些节点也会将其加入到自己的交易信息池中,以便进一步验证和广播。
创建交易池结构体,并简单的把交易加到交易池里

type TransactionPool struct {
	Txs []*transaction.Transaction
}

func (tp *TransactionPool) AddTransaction(tx *transaction.Transaction) {
	tp.Txs = append(tp.Txs, tx)
}

我们要能保存和加载交易池信息

func (tp *TransactionPool) SaveFile() {
	var content bytes.Buffer
	encoder := gob.NewEncoder(&content)
	err := encoder.Encode(tp)
	utils.Handle(err)
	err = ioutil.WriteFile(constcoe.TransactionPoolFile, content.Bytes(), 0644)
	utils.Handle(err)
}

func (tp *TransactionPool) LoadFile() error {
	if !utils.FileExists(constcoe.TransactionPoolFile) {
		return nil
	}

	var transactionPool TransactionPool

	fileContent, err := ioutil.ReadFile(constcoe.TransactionPoolFile)
	if err != nil {
		return err
	}

	decoder := gob.NewDecoder(bytes.NewBuffer(fileContent))
	err = decoder.Decode(&transactionPool)

	if err != nil {
		return err
	}

	tp.Txs = transactionPool.Txs
	return nil
}

上面两个函数实现在磁盘上保存和加载交易池信息。
SaveFile() 函数将交易池的信息编码为 gob 格式,然后写入磁盘文件 constcoe.TransactionPoolFile。具体过程是:先创建一个 bytes.Buffer 缓存,然后用 gob.NewEncoder() 得到一个编码器,将交易池信息编码后写入缓存。最后将缓存中的内容写入磁盘文件即可。
LoadFile() 函数从磁盘文件中读取交易池信息。如果该文件不存在,则直接返回 nil。如果文件存在,则读取文件内容,并用 gob.NewDecoder() 得到一个解码器,将文件内容解码为一个 TransactionPool 结构体。最后将解码出的 Txs 赋值给 tp.Txs,表示将解码出的交易添加到交易池中。如果解码过程中出错,函数将返回相应的错误信息。

我们还需要能够创建交易池

func CreateTransactionPool() *TransactionPool {
	transactionPool := TransactionPool{}
	err := transactionPool.LoadFile()
	utils.Handle(err)
	return &transactionPool
}

最后考虑当节点在mine后需要清空交易信息池

func RemoveTransactionPoolFile() error {
	err := os.Remove(constcoe.TransactionPoolFile)
	return err
}

MINE

我们将原blockchain.go下的Mine函数删除,在blockchain文件夹下创建mine.go文件,并创建RunMine函数,如下

package blockchain

import (
	"fmt"
	"lighteningchain/utils"
)

func (bc *BlockChain) RunMine() {
	transactionPool := CreateTransactionPool()
	//In the near future, we'll have to validate the transactions first here.
	candidateBlock := CreateBlock(bc.LastHash, transactionPool.PubTx) //PoW has been done here.
	if candidateBlock.ValidatePoW() {
		bc.AddBlock(candidateBlock)
		err := RemoveTransactionPoolFile()
		utils.Handle(err)
		return
	} else {
		fmt.Println("Block has invalid nonce.")
		return
	}
}

总结

本章我们讲解并实现了区块链的存储与读取,同时更加深入地了解了交易信息池与挖矿过程。下一章我们将构建命令行来管理此区块链,并进行调试。

 类似资料: