多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
本章源代码地址:[https://github.com/daleboy/blockchain3] # 前言 前面两个版本的区块链都是在内存中运行,本质上区块链是一个分布式数据库,我们在本版中只关注存储,即将区块链存储到数据库中,而分布式在后面的版本中实现。 # 数据选择 比特币选择的数据库是LevelDB,我们将选择BoltDB: 1. 非常简单和简约 2. 用 Go 实现 3. 不需要运行一个服务器 4. 能够允许我们构造想要的数据结构 BoltDB使用键值存储,其API仅限于值的获取或存储。 BoltDB没有数据类型,键和值都是字节数组(byte array),为了存储区块(Block),需要将struct序列化为byte array,取出时候,还需要将byte array反序列化为struct。 # 数据库结构 数据库结构决定了我们存储什么到数据库,以及存储的形式。我们参考比特币的数据库结构: Bitcoin Core 使用两个 “bucket” 来存储数据: 1. 其中一个 bucket 是**blocks**,它存储了描述一条链中所有块的元数据 2. 另一个 bucket 是**chainstate**,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据 此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。 因为目前还没有交易,所以我们只需要**blocks**bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有: 1. 32 字节的 block-hash -> block 的结构,目的性不言而喻 2. 链中最后一块的hash,目的是为了挖新区块时候需要用到 # 序列化 我们用[encoding/gob](https://link.jianshu.com/?t=https://golang.org/pkg/encoding/gob/)来完成序列化。 ![](https://img.kancloud.cn/63/f2/63f29d0d8e5e933e635084b30c034096_1147x932.png) 这里需要注意,由于在go中,struct是一个值类型,所以创建实例时候直接按值类型的方式创建,而为了更好利用内存,通过引用(指针)来使用实例(无论是作为函数的左值还是作为函数的参数)。 # 持久化 持久化在blockchain.go之中实现,从NewBlockchain改造开始: 1. 打开一个数据库文件 2. 检查文件里面是否已经存储了一个区块链 3. 如果已经存储了一个区块链: 1. 创建一个新的`Blockchain`实例 2. 设置`Blockchain`实例的 tip 为数据库中存储的最后一个块的哈希 4. 如果没有区块链: 1. 创建创世块 2. 存储到数据库 3. 将创世块哈希保存为最后一个块的哈希 4. 创建一个新的`Blockchain`实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希) ![](https://img.kancloud.cn/dc/b7/dcb7cb8dfb5fdce751f5f28a73f492d9_2003x1508.png) 注意的是,存储到数据库的最后一个区块的哈希键值对:键为字符串"1"的二进制数组,值为哈希值的二进制数组。 ## 区块链结构 ![](https://img.kancloud.cn/c6/6a/c66a8ccdc1f199eefe31492522e8fe25_1224x259.png) 序列化后,新的区块链结构不再存储所有的区块,而只存储区块链最后一个区块的哈希(tip),另外,存储了一个数据库连接,这样方便后面随时使用。 ## AddBlock 将普通区块存入数据库比较简单: 1. 从数据库读取最后一个区块的哈希; 2. 挖出符合要求的区块; 3. 将新区块写入数据库表; 4. 修改数据库表中的tip; 5. 更新当前区块实例的tip。 ![](https://img.kancloud.cn/71/36/713652cdcfdee3537ae4bd10bf8db7d5_1438x1174.png) # 区块链迭代器 使用区块链迭代器的好处是,我们在读取区块链中的区块时候,不必将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),而是一个一个地读取它们。 ![](https://img.kancloud.cn/bd/a4/bda46cbe26086d1a5ea7feae59644a7e_1148x1147.png) 实际上,**选择一个 tip 就是意味着给一条链“投票”**。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。 # CLI命令行 到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在`main`函数中简单执行了`NewBlockchain`和`bc.AddBlock`。是时候改变了!现在我们想要拥有这些命令: ~~~ blockchain_go addblock "Pay 0.031337 for a coffee" blockchain_go printchain ~~~ CLI的具体实现在cli.go中,见github。 # main.go 是时候检验我们的目标是否实现了: ![](https://img.kancloud.cn/df/c3/dfc3381b0475c8302d30eea221dae351_1049x258.png) 注意,struct实例化的写法: (1)以key:value的方式构建struct实例是更严谨的写法 (2)如果是在包内使用,直接写value是可以的 (3)如果在包外使用,直接写value会出错:composite literal uses unkeyed fields 注意,在命令行执行前,区块链已经创建好了,当然创始区块也挖好了。 运行结果: ![](https://img.kancloud.cn/23/c3/23c3e9425316cff993735063a997de6b_1007x452.png)