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标准库的一部分。

数据库结构

实现持久化逻辑前,需要先确定在数据库中如何存储数据。对此,我们计划参考比特币的实现。
简单来说,比特币使用下面两个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中.

我在阅读和实践中, 为本文做了一些注解以便更好的理解.

有任何问题欢迎在下方评论讨论.

立即登录, 发表评论.
没有帐号? 立即注册