区块链第三版:持久化

本章源代码地址:[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来完成序列化。

区块链第三版:持久化 - 图1

这里需要注意,由于在go中,struct是一个值类型,所以创建实例时候直接按值类型的方式创建,而为了更好利用内存,通过引用(指针)来使用实例(无论是作为函数的左值还是作为函数的参数)。

undefined持久化

持久化在blockchain.go之中实现,从NewBlockchain改造开始:

  • 打开一个数据库文件
  • 检查文件里面是否已经存储了一个区块链
  • 如果已经存储了一个区块链:
    • 创建一个新的Blockchain实例
    • 设置Blockchain实例的 tip 为数据库中存储的最后一个块的哈希
  • 如果没有区块链:
    • 创建创世块
    • 存储到数据库
    • 将创世块哈希保存为最后一个块的哈希
    • 创建一个新的Blockchain实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)

区块链第三版:持久化 - 图2

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

undefined区块链结构

区块链第三版:持久化 - 图3

序列化后,新的区块链结构不再存储所有的区块,而只存储区块链最后一个区块的哈希(tip),另外,存储了一个数据库连接,这样方便后面随时使用。

undefinedAddBlock

将普通区块存入数据库比较简单:

  • 从数据库读取最后一个区块的哈希;
  • 挖出符合要求的区块;
  • 将新区块写入数据库表;
  • 修改数据库表中的tip;
  • 更新当前区块实例的tip。

区块链第三版:持久化 - 图4

undefined区块链迭代器

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

区块链第三版:持久化 - 图5

实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。

undefinedCLI命令行

到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在main函数中简单执行了NewBlockchainbc.AddBlock。是时候改变了!现在我们想要拥有这些命令:

  1. blockchain_go addblock "Pay 0.031337 for a coffee"
  2. blockchain_go printchain

CLI的具体实现在cli.go中,见github。

undefinedmain.go

是时候检验我们的目标是否实现了:

区块链第三版:持久化 - 图6

注意,struct实例化的写法:

(1)以key:value的方式构建struct实例是更严谨的写法

(2)如果是在包内使用,直接写value是可以的

(3)如果在包外使用,直接写value会出错:composite literal uses unkeyed fields

注意,在命令行执行前,区块链已经创建好了,当然创始区块也挖好了。

运行结果:

区块链第三版:持久化 - 图7