区块链第三版:持久化
本章源代码地址:[https://github.com/daleboy/blockchain3]
undefined前言
前面两个版本的区块链都是在内存中运行,本质上区块链是一个分布式数据库,我们在本版中只关注存储,即将区块链存储到数据库中,而分布式在后面的版本中实现。
undefined数据选择
比特币选择的数据库是LevelDB,我们将选择BoltDB:
- 非常简单和简约
- 用 Go 实现
- 不需要运行一个服务器
能够允许我们构造想要的数据结构
BoltDB使用键值存储,其API仅限于值的获取或存储。
BoltDB没有数据类型,键和值都是字节数组(byte array),为了存储区块(Block),需要将struct序列化为byte array,取出时候,还需要将byte array反序列化为struct。
undefined数据库结构
数据库结构决定了我们存储什么到数据库,以及存储的形式。我们参考比特币的数据库结构:
Bitcoin Core 使用两个 “bucket” 来存储数据:
- 其中一个 bucket 是blocks,它存储了描述一条链中所有块的元数据
- 另一个 bucket 是chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。
因为目前还没有交易,所以我们只需要blocksbucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
- 32 字节的 block-hash -> block 的结构,目的性不言而喻
- 链中最后一块的hash,目的是为了挖新区块时候需要用到
undefined序列化
我们用encoding/gob来完成序列化。

这里需要注意,由于在go中,struct是一个值类型,所以创建实例时候直接按值类型的方式创建,而为了更好利用内存,通过引用(指针)来使用实例(无论是作为函数的左值还是作为函数的参数)。
undefined持久化
持久化在blockchain.go之中实现,从NewBlockchain改造开始:
- 打开一个数据库文件
- 检查文件里面是否已经存储了一个区块链
- 如果已经存储了一个区块链:
- 创建一个新的
Blockchain实例 - 设置
Blockchain实例的 tip 为数据库中存储的最后一个块的哈希
- 创建一个新的
- 如果没有区块链:
- 创建创世块
- 存储到数据库
- 将创世块哈希保存为最后一个块的哈希
- 创建一个新的
Blockchain实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)

注意的是,存储到数据库的最后一个区块的哈希键值对:键为字符串"1"的二进制数组,值为哈希值的二进制数组。
undefined区块链结构

序列化后,新的区块链结构不再存储所有的区块,而只存储区块链最后一个区块的哈希(tip),另外,存储了一个数据库连接,这样方便后面随时使用。
undefinedAddBlock
将普通区块存入数据库比较简单:
- 从数据库读取最后一个区块的哈希;
- 挖出符合要求的区块;
- 将新区块写入数据库表;
- 修改数据库表中的tip;
- 更新当前区块实例的tip。

undefined区块链迭代器
使用区块链迭代器的好处是,我们在读取区块链中的区块时候,不必将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),而是一个一个地读取它们。

实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。
undefinedCLI命令行
到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在main函数中简单执行了NewBlockchain和bc.AddBlock。是时候改变了!现在我们想要拥有这些命令:
blockchain_go addblock "Pay 0.031337 for a coffee"blockchain_go printchain
CLI的具体实现在cli.go中,见github。
undefinedmain.go
是时候检验我们的目标是否实现了:

注意,struct实例化的写法:
(1)以key:value的方式构建struct实例是更严谨的写法
(2)如果是在包内使用,直接写value是可以的
(3)如果在包外使用,直接写value会出错:composite literal uses unkeyed fields
注意,在命令行执行前,区块链已经创建好了,当然创始区块也挖好了。
运行结果:

