本章源代码地址:[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)
- 重要更新说明
- linechain发布
- linechain新版设计
- 引言一
- 引言二
- 引言三
- vs-code设置及开发环境设置
- BoltDB数据库应用
- 关于Go语言、VS-code的一些Tips
- 区块链的架构
- 网络通信与区块链
- 单元测试
- 比特币脚本语言
- 关于区块链的一些概念
- 区块链组件
- 区块链第一版:基本原型
- 区块链第二版:增加工作量证明
- 区块链第三版:持久化
- 区块链第四版:交易
- 区块链第五版:实现钱包
- 区块链第六版:实现UTXO集
- 区块链第七版:网络
- 阶段小结
- 区块链第八版:P2P
- P2P网络架构
- 区块链网络层
- P2P区块链最简体验
- libp2p建立P2P网络的关键概念
- 区块链结构层设计与实现
- 用户交互层设计与实现
- 网络层设计与实现
- 建立节点发现机制
- 向区块链网络请求区块信息
- 向区块链网络发布消息
- 运行区块链
- LineChain
- 系统运行流程
- Multihash
- 区块链网络的节点发现机制深入探讨
- DHT
- Bootstrap
- 连接到所有引导节点
- Advertise
- 搜索其它peers
- 连接到搜到的其它peers
- 区块链网络的消息订发布-订阅机制深入探讨
- LineChain:适用于智能合约编程的脚本语言支持
- LineChain:解决分叉问题
- LineChain:多重签名
- libp2p升级到v0.22版本
- 以太坊基础
- 重温以太坊的树结构
- 世界状态树
- (智能合约)账户存储树
- 交易树
- 交易收据树
- 小结
- 以太坊的存储结构
- 以太坊状态数据库
- MPT
- 以太坊POW共识算法
- 智能合约存储
- Polygon Edge
- block结构
- transaction数据结构
- 数据结构小结
- 关于本区块链的一些说明
- UML工具-PlantUML
- libp2p介绍
- JSON-RPC
- docker制作:启动多个应用系统
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基础操作整理
- jupyter计算交互环境
- git技巧一
- git技巧二
- 使用github项目的最佳实践
- windows下package管理工具