在之前的版本中,我们已经实现了区块链的基本模型,包括区块链的基本结构、不同的共识机制与简易的P2P网络。但是仍有一些重要的特性没有完成,在这篇文章中,我们将会实现区块链的持久化功能,因为区块链本质上是一个”分布式数据库“,所以存储功能必不可少。
在数据库的选择上,我们选择leveldb来实现存储功能。leveldb是Google开源的key-value数据库,存储效率高,主要有get/put/delete三个操作,对于我们学习区块链来说可以把更多的精力关注在如何运用数据库来实现区块链功能上,而非数据库相关问题的研究和处理上。同时leveldb也是比特币所使用的数据库。
存储结构
参考比特币核心的介绍,比特币使用两个bucket来存储数据,一个是blocks
,存储所有区块的数据,另一个是chainstate
,存储交易数据。比特币核心会将每个区块存储为不同的文件,这样读取一个区块只需要加载一个文件,减少内存消耗。我们在这里只实现一个简化的模型,在leveldb中存储两个部分:
- (hash, block_data)
- ("last_block", block_data)
第一个部分是将所有经过验证的区块存入数据库中,key为区块的hash值,value为区块的二进制数据。
第二个部分是当前链的最后一个区块,每存入一个区块,都需要对“last_block”
进行更新。
这里的两部分都是blocks
中的内容,由于还没有实现交易,所以我们这里暂且没有存储chainstate
。
持久化
存储区块
更新save_last_block
函数,处理逻辑并没有改变,不过将原来存入chain_
数组的block数据用put
操作存入leveldb,还要存储last_block
。这里使用level
模块来实现对leveldb的操作,但是每个操作都是异步的,所以这里将save_last_block
函数设置为async函数,这样db的put操作可以写成同步的形式。
// blockchain.js
async save_last_block() {
if (this.pending_block_[this.last_block_.hash]) {
delete this.pending_block_[this.last_block_.hash];
}
await this.db_.put(this.last_block_.hash, JSON.stringify(this.last_block_));
await this.db_.put("last_block", JSON.stringify(this.last_block_));
加载区块
我们既然已经可以将区块持久化,那么启动节点时,节点就可以从数据库中加载已有区块,而不是每次都是从头开始挖矿。其逻辑是:
- 打开数据库文件
- 通过
"last_block"
来查找是否已经存在一个区块链 - 如果已经存在一个区块链,那就读取并设置
last_block_
- 如果不存在区块链,那么就创建传世区块,设置为
last_block_
并存入数据库
我们将这段逻辑加入到start
函数中。
// blockchain.js
async start() {
this.db_ = level(`/tmp/data_${this.get_account_id()}`);
try {
// load blocks
let last = await this.db_.get("last_block");
this.last_block_ = JSON.parse(last);
console.log(`node: ${this.get_account_id()} last block: ${this.last_block_.height}`);
} catch (err) {
// empty chain
this.last_block_ = genesis_block;
this.save_last_block();
console.log(`node: ${this.get_account_id()} empty`);
}
this.node_ = new Node(this.get_account_id());
this.node_.on("message", this.on_data.bind(this));
this.node_.start();
// start loop
var self = this;
setTimeout(function next_loop() {
self.loop(function () {
setTimeout(next_loop, 1000);
});
}, 5000);
}
遍历区块
在之前的版本中,挖矿或者接收到的区块顺序存入内存数组中,遍历区块链时直接遍历数组即可。但是我们现在将区块存入了数据库,数据库并不是按照存入顺序来排序的,所以我们没有办法直接顺序遍历。可是我们能够获得last_block,并且区块中含有previous_hash,所以我们可以通过previous_hash将所有区块串联起来,从最后一个区块开始反向遍历。相比于正向遍历有如下好处:
- 每条区块链都有若干分叉,正向遍历时对于分叉情况的判断不好处理,但是选定了一个last_block,就相当于明确确定了一条分支,遍历时不会出现分叉。(分叉的处理我们在后续篇章会详细讲解)
- 实际应用中区块链的高度可达几万甚至几十万,从头开始全部遍历将会耗费大量内存,而且前面的区块经过确认后是无法被篡改的,我们常关心的其实是最新产生的若干区块,而反向遍历正好优先显示最新的区块。
当然在测试调试时,因为区块比较少,正向遍历反而比较方便。所以这里实现了两种方法供参考:
async get_block(hash) {
// query block with hash value
try {
let block_data = await this.db_.get(hash);
let block = JSON.parse(block_data);
return block;
} catch (err) {
return null;
}
}
async iterator_back(cb, hash) {
if (!hash) {
return;
}
let block = await this.get_block(hash);
let res = cb(block);
if (res)
await this.iterator_back(cb, block.previous_hash);
}
async iterator_forward(cb, hash) {
if (!hash) {
return;
}
let block = await this.get_block(hash);
await this.iterator_forward(cb, block.previous_hash);
cb(block);
}
正向遍历和反向遍历都利用递归来实现,但是在反向遍历中,我们可以根据回调函数的返回值来及时停止遍历,例如只需要查询最新的n个区块的信息;而正向遍历我们一般用于测试调试,没有添加递归控制。
代码地址:https://github.com/yjjnls/awesome-blockchain/tree/v0.3.0/src/js
网友评论