3 持久化和命令行
引言
目前为止,我们已经构建了一个具备工作量证明机制的blockchain。当前实现的blockchain仍然缺乏一些重要的特性。今天,我们会先将blockchain存储到数据库,然后实现一个命令行接口来操作blockchain。本质上blockchain是一个分布式数据库。现在我们先忽略“分布式”聚焦到“数据库”上。
数据库选型
到目前为止,我们实现的blockchain在每次程序运行时存储在内存中,不能重用或者与其他人共享该blockchain,因此需要使用一个数据来存储blockchain。
那么,使用哪种数据库呢?实际上,任何一种数据库都可以。在比特币的论文中,对数据库的使用没有任何要求,使用哪种数据库完全取决于开发者的意愿。比特币发布之初使用的是LevelDB,那么我们选用BoltDB。
BoltDB
为什么选用BoltDB?理由如下:
- 足够简洁
- 基于Go实现
- 不需要服务端
BoltDB在Github上的说明:
- Bolt是基于纯Go语言开发的KV存储,灵感来自于Howard Chu的LMDB项目。该项目目标是开发一个简单、快速、可靠的无服务端的数据库。API非常小巧和简洁,仅仅关注如何获取或设置数据,这就是全部。
听起来和我们的需求非常匹配!让我们花几分钟研究一下。
BoltDB是K-V存储,没有关系型数据库中类似表、行、列结构,数据以key-value对存储(类似GO语言中的map)。相似的key-value对存储在同一bucket中,类似于关系型数据库中的Table。因此,为了获取一个value,需要知道其所在的bucket以及对应的key。
BoltDB中没有数据类,key和value都是字节数组。我们的blockchain数据均为struct,因此为了在BoltDB中存取数据,需要对struct进行序列化和反序列化。虽然JSON, XML, Protocol Buffers均可以到达相同的目的,我们还是决定采用encoding/gob库来完成此工作,一方面是因为使用简单,另一方面是因为该模块是Go标准库的一部分。
- 注: 关于BoltDB与levelDB的对比, 请查看 http://www.opscoder.info/boltdb_intro.html
- 安装BoltDB:
`go get github.com/boltdb/bolt`
数据库结构
实现持久化逻辑前,需要先确定在数据库中如何存储数据。对此,我们计划参考比特币的实现。
简单来说,比特币使用下面两个bucket来存储数据:
- blocks存储用于描述所有block的元数据信息。
- chainstate用于存储blockchain的状态,包括所有交易元数据和部分元数据。
同时,出于性能考虑,每个block存放在独立的文件,这样获取单个block时不需要加载所有block。但为了简化,我们还是把所有数据存储在一个文件中。
在blocks bucket中,存储如下k-v对:
- 'b' + 32-byte block hash -> block index record
- 'f' + 4-byte file number -> file information record
- 'l' -> 4-byte file number: the last block file number used
- 'R' -> 1-byte boolean: whether we're in the process of reindexing
- 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
- 't' + 32-byte transaction hash -> transaction index record
在chainstate bucket中,存储如下k-v对:
- 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction. These records are only present for transactions that have at least one unspent output left. Each record stores:
- The version of the transaction.
- Whether the transaction was a coinbase or not.
- Which height block contains the transaction.
- Which outputs of that transaction are unspent.
- The scriptPubKey and amount for those unspent outputs.
- 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
(设计原理请参考这里)
由于目前实现还没有设计交易,因此我们仅需要实现blocks bucket。同时,就如上面所说,所有数据存储在一个单一的文件中,因此不需要任何和文件号相关的信心。所以,我们仅需要一下k-v数据:
- 32-byte block-hash -> Block structure (serialized)
- 'l' -> the hash of the last block in a chain 最近一个block的hash
这就是我们实现持久化所需的相关信息。
序列化
若前所述,BoltDB仅仅存储字节数组,因此我们实现Seriable方式(基于encoding/gob库)用于序列化Block结构:
- func (b *Block) Serialize() []byte {
- var result bytes.Buffer
- encoder := gob.NewEncoder(&result)
- err := encoder.Encode(b)
- return result.Bytes()
- }
过程很简单:首先声明一个buffer;然后初始化一个gob编码block;最后返回编码后的字节数组。
接下来,我们需要一个反序列化函数,用于将一个字节数组解码为一个block:
- func DeserializeBlock(d []byte) *Block {
- var block Block
- decoder := gob.NewDecoder(bytes.NewReader(d))
- err := decoder.Decode(&block)
- return &block
- }
持久化
让我们来实现 NewBlockchain函数,该函数创建一个blockchain实例同时添加genesis block,步骤如下:
- 打开数据库文件
- 检查是否有blockchain
- 如果有blockchain
- 创建一个blockchain实例
- 将blockchain的tip指向最新的block
- 如果没有blockchain
- 创建genesis block
- 存储在数据库中
- 将genesis block的hash值保存为最新的block hash值
- 创建一个新的blockchain实例,同时tip指向最新的block
代码如下:
- func NewBlockchain() *Blockchain {
- var tip []byte
- db, err := bolt.Open(dbFile, 0600, nil)
- err = db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(blocksBucket))
- if b == nil {
- genesis := NewGenesisBlock()
- b, err := tx.CreateBucket([]byte(blocksBucket))
- err = b.Put(genesis.Hash, genesis.Serialize())
- err = b.Put([]byte("l"), genesis.Hash)
- tip = genesis.Hash
- } else {
- tip = b.Get([]byte("l"))
- }
- return nil
- })
- bc := Blockchain{tip, db}
- return &bc
- }
让我们一段一段来看实现。
- db, err := bolt.Open(dbFile, 0600, nil)
上面代码是打开BoltDB文件的标准方式,如果文件不存在不会反馈错误。
- err = db.Update(func(tx *bolt.Tx) error {
- ...
- })
在BoltDB中,数据库操作都在一个事务中运行。BoltDB有两种事务:只读事务和读写事务。在这里,由于可能会存在添加genesis block的操作,因此我们使用读写事务(db.Update(...))。
- b := tx.Bucket([]byte(blocksBucket))
- if b == nil {
- genesis := NewGenesisBlock()
- b, err := tx.CreateBucket([]byte(blocksBucket))
- err = b.Put(genesis.Hash, genesis.Serialize())
- err = b.Put([]byte("l"), genesis.Hash)
- tip = genesis.Hash
- } else {
- tip = b.Get([]byte("l"))
- }
上述代码是核心逻辑。首先,我们尝试获取一个bucket,如果bucket存在,则将tip设置为key(l)的值;如果bucket不存在,则创建genesis block、创建bucket、存储genesis block、将tip设置为genesis block的hash值。
此外,注意创建blockchain的方法有了变化:
- bc := Blockchain{tip, db}
现在不再存储任何block,而仅仅存储blockchain的tip,同时为了避免程序运行过程中数据库反复被打开,因此blockchain中会存储已打开的数据库链接,blockchain的结构修改如下:
- type Blockchain struct {
- tip []byte
- db *bolt.DB
- }
接下来,修改AddBlock方法:新增一个block会变得复杂一些:
- func (bc *Blockchain) AddBlock(data string) {
- var lastHash []byte
- err := bc.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(blocksBucket))
- lastHash = b.Get([]byte("l"))
- return nil
- })
- newBlock := NewBlock(data, lastHash)
- err = bc.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(blocksBucket))
- err := b.Put(newBlock.Hash, newBlock.Serialize())
- err = b.Put([]byte("l"), newBlock.Hash)
- bc.tip = newBlock.Hash
- return nil
- })
- }
我们来一段一段来看:
- err := bc.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(blocksBucket))
- lastHash = b.Get([]byte("l"))
- return nil
- })
首先使用只读事务获取当前数据库中最新block的hash值。
- newBlock := NewBlock(data, lastHash)
- b := tx.Bucket([]byte(blocksBucket))
- err := b.Put(newBlock.Hash, newBlock.Serialize())
- err = b.Put([]byte("l"), newBlock.Hash)
- bc.tip = newBlock.Hash
在挖到一个新block后,我们将序列化后的结果存储到数据库文件中同时将key(l)值更新为最新block的hash值。
完成!一切简单明了。
查看区块链内容
现在所有block都保存在数据库文件中,因此我们可以重新打开一个blockchain并且新增一个block。但我们现在失去一个很好的特性:我们不能输出blockchain的内容。让我们修改这个缺陷!
BoltDB允许遍历bucket中的所有key,但是key按照字节序的顺序来存储的。我们想按照block被加入的顺序来打印blockchain内容。此外,我们不想将所有block全部加载到内存,因此将实现一个迭代器来逐个遍历block:
- type BlockchainIterator struct {
- currentHash []byte
- db *bolt.DB
- }
当想遍历blockchain时,我们会创建一个迭代器。迭代器包括当前遍历到的block的hash值和数据库链接。
- func (bc *Blockchain) Iterator() *BlockchainIterator {
- bci := &BlockchainIterator{bc.tip, bc.db}
- return bci
- }
一开始迭代器指向blockchain的tip,意味着将从顶到底、从新到旧的遍历blockchain。实际上,选择tip就好比blockchain投票。Blockchain可能有多个分支,最长的那个分支被当做主分支。获得tip后,就可以重建整个blockchain并且得到blockchain的长度。因此,某种程度上可以说tip是blockchain的标识。
BlockchainIterator仅仅做一件事:返回前一个block。
- func (i *BlockchainIterator) Next() *Block {
- var block *Block
- err := i.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(blocksBucket))
- encodedBlock := b.Get(i.currentHash)
- block = DeserializeBlock(encodedBlock)
- return nil
- })
- i.currentHash = block.PrevBlockHash
- return block
- }
数据部分到此为止!
命令行接口
目前为止,程序还没有提供任何交互接口,仅仅在main函数中调用NewBlockchain, bc.AddBlock等函数。现在让我们实现以下命令:
- blockchain_go addblock "Pay 0.031337 for a coffee"
- blockchain_go printchain
所有命令行命令通过CLI结构处理:
- type CLI struct {
- bc *Blockchain
- }
CLI的入口是Run函数:
- func (cli *CLI) Run() {
- cli.validateArgs()
- addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
- printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
- addBlockData := addBlockCmd.String("data", "", "Block data")
- switch os.Args[1] {
- case "addblock":
- err := addBlockCmd.Parse(os.Args[2:])
- case "printchain":
- err := printChainCmd.Parse(os.Args[2:])
- default:
- cli.printUsage()
- os.Exit(1)
- }
- if addBlockCmd.Parsed() {
- if *addBlockData == "" {
- addBlockCmd.Usage()
- os.Exit(1)
- }
- cli.addBlock(*addBlockData)
- }
- if printChainCmd.Parsed() {
- cli.printChain()
- }
- }
我们使用flag标准库处理命令行参数:
- addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
- printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
- addBlockData := addBlockCmd.String("data", "", "Block data")
首先,创建两个子命令:addblock、printchain,addblock具有一个命令参数-data,printchain没有任何命令参数。
- switch os.Args[1] {case "addblock":
- err := addBlockCmd.Parse(os.Args[2:])case "printchain":
- err := printChainCmd.Parse(os.Args[2:])default:
- cli.printUsage()
- os.Exit(1)
- }
接下来,我们检查用户输入的命令以及与之相关的命令参数:
- f addBlockCmd.Parsed() {
- if *addBlockData == "" {
- addBlockCmd.Usage()
- os.Exit(1)
- }
- cli.addBlock(*addBlockData)
- }
- if printChainCmd.Parsed() {
- cli.printChain()
- }
对于解析成功的应命令执行对应的函数。
- func (cli *CLI) addBlock(data string) {
- cli.bc.AddBlock(data)
- fmt.Println("Success!")
- }
- func (cli *CLI) printChain() {
- bci := cli.bc.Iterator()
- for {
- block := bci.Next()
- fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
- fmt.Printf("Data: %s\n", block.Data)
- fmt.Printf("Hash: %x\n", block.Hash)
- pow := NewProofOfWork(block)
- fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
- fmt.Println()
- if len(block.PrevBlockHash) == 0 {
- break
- }
- }
- }
整个过程与之前非常相似,唯一区别在于现在使用BlockchainIterator遍历blockchain。
最后,修改一下main函数:
- func main() {
- bc := NewBlockchain()
- defer bc.db.Close()
- cli := CLI{bc}
- cli.Run()
- }
需要注意的是无论是否有命令行参数,程序一运行就会创建一个blockchain。
一切OK,尝试运行下吧:
- $ blockchain_go printchain
- No existing blockchain found. Creating a new one...
- Mining the block containing "Genesis Block"
- 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
- Prev. hash:
- Data: Genesis Block
- Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
- PoW: true
- $ blockchain_go addblock -data "Send 1 BTC to Ivan"
- Mining the block containing "Send 1 BTC to Ivan"
- 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
- Success!
- $ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
- Mining the block containing "Pay 0.31337 BTC for a coffee"
- 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
- Success!
- $ blockchain_go printchain
- Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
- Data: Pay 0.31337 BTC for a coffee
- Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
- PoW: true
- Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
- Data: Send 1 BTC to Ivan
- Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
- PoW: true
- Prev. hash:
- Data: Genesis Block
- Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
- PoW: true
总结
接下来,我们将实现地址、钱包、交易等概念。
本文来自 gitbooks zhangli1的书一章, 他翻译自Ivan Kuznetsov的系列文章《Building Blockchain in Go》,原文地址: https://jeiwan.cc . 程序代码都在 https://github.com/Jeiwan/blockchain_go中.
我在阅读和实践中, 为本文做了一些注解以便更好的理解.
有任何问题欢迎在下方评论讨论.
没有帐号? 立即注册