使用Golang构建区块链Part3:持久化和命令行接口

介绍

到现在,我们已经构建了一个有工作量证明系统和可以挖矿的区块链。我们的实现离一个具有完整功能的区块链又进了一步,但是它仍然少一些重要的性质。今天,我们要将区块链存储到数据库中,在这之后,我们将做一个简单的命令行接口来支持区块链的操作。在它的一个重要的本质中,区块链是一个分布式数据库;我们现在要省略“分布式”的部分,而集中精力于“数据存储”部分。

数据库的选择

目前,在我们的实现中并没有数据库;反而我们在每一次运行程序创建区块时都存储在内存中。我们既不能重复使用区块链也不能与其他人共享,因此我们需要将它存储在磁盘中。

我们需要哪一个数据库呢?事实上,任何一个都可以。在比特币的原始论文中,没有说明使用哪一个特定的数据库。所以,使用什么数据库完全取决于开发者。 Bitcoin Core ,最初由中本聪发布的,现在是比特币的一个参考实现,它使用的是LevelDB(虽然它在2012年才发布客户端)。而我们将要使用的是…

BoltDB

原因:

  1. 它非常简单便捷。
  2. 它是由Go语言实现的。
  3. 它不需要运行任何一个服务器。
  4. 它允许我们想要的数据结构。

在__BoltDB__ Github上的README写道:

Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。

由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的 API 并不多,并且仅关注值的获取和设置。仅此而已。

听起来很完美的契合了我们的需求!让我们快速的回顾一下它:

__BoltDB__是一个键值存储结构,这就意味着它不像关系型数据库(MySQL、PostgreSQL等)有表,没有行、列。相反的,数据以一种key-value(键值对)的组合形式存储(就像Go语言中的__map__结构)。键值对存储在桶中,这是有意的给键值对分组(这有些像关系型数据库中的表)。因此,为了获得一个值,你需要知道一个桶(bucket)和一个键(key)。

关于__BoltDB__一个重要的点是它没有数据类型:键和值都是以字节数组的形式存储。由于我们要存储Go的数据结构(具体来说是__Block__),我们需要将它们进行序列化。也就是说,实现一个可以将Go结构体转化为字节数组并可以从字节数组恢复到Go结构体的机制。我们将使用encoding/gob来实现这个,但是JSON、XML、Protocol Buffers等也是可以的。我们使用encoding/gob是因为它简单而且是Go的标准库。

数据库结构

在开始实现持久化的逻辑之前,我们需要决定怎样将数据存储到数据库中。为此,我们参考比特币的做法。

简单来说,比特币使用了两个桶(bucket)来存储数据:

  1. __blocks__存储链上区块的元数据描述。
  2. __chainstate__存储链的状态,也就是目前所有的未花费交易输出以及一些数据。

同时,区块被存储为磁盘上不同的文件。出于对性能的考虑:加载单个的区块不需要从内存中加载所有(或者部分)文件。我们不需要实现这些。

blocks 中, key -> value 对应关系是这样的:

  1. ‘b’ + 32 字节的区块 hash -> 区块索引记录
  2. ‘f’ + 4 字节文件数字 -> 文件信息记录
  3. ‘l’ -> 4 字节文件数字: 最后一个使用过的区块文件数字
  4. ‘R’ -> 1 字节布尔值: 我们是否要去重新索引
  5. ‘F’ + 1 字节标志名长度 + 标志名 -> 1 字节布尔值: 开或关的多种标志
  6. ‘t’ + 32 字节交易 hash -> 交易索引记录

chainstate, key -> value 对应关系是这样的:

  1. ‘c’ + 32 字节交易 hash -> 未使用的交易出账记录
  2. ‘B’ -> 32 字节区块 hash: 数据库应该表示的未使用交易出账的区块哈希

(更详细的解释可以在这里找到)

因为我们现在还不需要交易,我们只需要有一个__blocks__桶就可以。同时,就像前面说的,我们将以单个文件的形式存储在数据库中,不需要在分开的文件中存储区块。所以我们也不需要任何与文件相关的数字编号。因此,我们只需要这些键值的对应关系:

  1. 32 字节区块 hash -> 区块数据(序列化后的)
  2. ‘l’ -> 链上最后一个区块的 hash

以上,是我们在实现持久化之前所需要知道的全部。

序列化

如前文所说,在__BoltDB__中数据只能是__[]byte__的形式,我们想存储__Block__结构在数据库中。我们将使用encoding/gob来对结构体进行序列化。

让我们实现区块的__Serialize__方法(为了简洁,我们暂时忽略错误处理):

func (b *Block) Serialize() []byte {
	var result bytes.Buffer
	encoder := gob.NewEncoder(&result)

	err := encoder.Encode(b)

	return result.Bytes()
}

这个模块非常直接了当:首先,我们为所要序列化的数据定义了一个buffer来存储;然后我们初始化了一个__gob encoder__并且对区块进行编码;结果以一个字节数组的形式返回。

接下来,我们需要一个反序列化的函数接受输入的字节数组并返回一个__Block__区块。这不是一个方法而是一个独立的函数:

func DeserializeBlock(d []byte) *Block {
	var block Block

	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)

	return &block
}

这就是反序列化了!

持久化

让我们开始__NewBlockchain__函数。目前,它创建一个新的__Blockchain__并且添加一个创世纪块进去。我们希望它可以做:

  1. 打开一个数据库文件。
  2. 检查里面是否以及存在一个区块链在里面。
  3. 如果这里已经有了一个区块链:
    1. 创建一个新的区块链实例。
    2. 设置这个区块链实例的__tip__为数据库中最后一个区块的哈希。

代码中,它看起来像:

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__中,数据库通过一个事务进行操作。这里有两种事务:只读(read-only)和读写(read-write)。这里我们开启一个读写事务(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去存储我们的区块:如果它存在,我们从里面读取键值"l"(字母L小写,不是1);如果不存在,我们就生成一个创世纪块,创建一个桶,将区块保存进去,之后更新"l"键,使其存储区块链的最后一个区块的哈希。

同时,注意一下创建__Blockchain__的新方法:

bc := Blockchain{tip, db}

我们不再存储所有的区块,取而代之的是仅仅存储区块链的__tip__。同时我们也保存一个数据库连接,因为我们想只打开它一次,并且让它在程序运行的过程中一直保持着连接。所以,__Blockchain__结构看起来就像这样:

type Blockchain struct {
	tip []byte
	db  *bolt.DB
}

接下来我们想做的就是更新__AddBlock__方法:现在添加区块到区块链上不是简单的向数组中添加一个元素了。从现在开始我们将把区块存储到数据库中:

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
})

这是__BoltDB__数据库的另一种事务:只读(read-only)。这里我们从数据库中得到了最后一个区块的哈希,用来挖新的区块哈希。

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

在挖出新区块之后,我们将其序列化特征值存储到数据库中,并且更新"l"键,让它保存最新区块的哈希。

完成了!并不是很难,对吗!

检查区块链

所有的新区块现在保存在数据库中,所以我们现在可以重新打开这条链并且向其中添加新的区块。但是在实现了这些后,我们失去了一个非常好的特性:我们不能打印区块链中的区块了,因为我们不再像以前那样存储区块了。让我们修复这个瑕疵!

__BoltDB__数据库允许对桶里的所有key进行迭代,但是所有的key都以字节顺序存储,我们又想以区块在区块链中的顺序进行打印。而且,因为我们不想加载内存中所有的区块(我们的区块链存储数据可能非常庞大!或者假装是这样),我们将把它们一个一个读出来。为此,我们需要一个区块链迭代器:

type BlockchainIterator struct {
	currentHash []byte
	db          *bolt.DB
}

每一个迭代器将在区块链中的区块需要迭代时创建,并且它将会保存当前迭代区块的哈希和一个数据库连接。因为后面的,一个迭代器附属于一个区块(这里的区块链是指存储了一个数据库连接的__Blockchain__实例),因此我们需要通过__Blockchain__方法进行创建:

func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip, bc.db}

	return bci
}

注意的是,最初一个迭代器初始指向区块链的tip,所以区块将会被从顶到底获取,从最近创建的到最久之前创建的获取(我们这里把区块链想象成一个桶,最早创建的落在桶底,最晚(新)创建的在上面)。实际上,选择一个tip就意味着给一条链投票。一条区块链可以有多个分支,最长的那条被认为是主分支。在得到tip之后(它可以是区块链中的任意一个区块)我们就可以复现整条链,找到它的长度和构建它所需要的工作。这也同样意味着,一个tip也就是区块链的一种标识符。

__BlockchainIterator__将只做一件事情:从一条区块链中返回下一个区块。

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
}

这就是数据库部分!

命令行接口(CLI)

截止到目前,我们的实现没有提供任何程序交互接口:我们只是很简单的在__main__函数中执行__NewBlockchain__和__bc.AddBlock__。是时候改进它了!我们想要这样的命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有的与命令行相关的操作将交给__CLI__结构体进行处理:

type CLI struct {
	bc *Blockchain
}

它的入口在__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__,然后我们添加__-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)
}

然后,我们检查用户提供的命令并且解析相关的__flag__子命令。

if 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__去迭代区块链中的区块。

当然也不要忘了__main__函数中相应的修改:

func main() {
	bc := NewBlockchain()
	defer bc.db.Close()

	cli := CLI{bc}
	cli.Run()
}

注意,无论提供哪一个命令行参数都会创建一个新的区块链。

完事儿了!让我们检查一切的运行是否如我们所愿:

$ 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

Github的译者版本附加的测试:

(看起来可以开酒(🍷or🍺)了)

总结

接下来我们将实现地址、钱包以及交易(或许也有)。尽情期待!

Links:

  1. Full source codes
  2. Bitcoin Core Data Storage
  3. boltdb
  4. encoding/gob
  5. flag
自认为是幻象波普星的来客
Built with Hugo
主题 StackJimmy 设计