持久化和命令行接口
引言
到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前距离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。
选择数据库
目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。而我们将要使用的是...
incache
因为它:
- 非常简洁
- 用 JavaScript 实现
- 不需要运行一个服务器
- 能够允许我们构造想要的数据结构
当然,如果想要承担更复杂的功能,该db将不在考虑范围之内。该DB仅仅作为参考使用,不建议在production环境使用。但是因为该DB支持,key value的存储结构,非常适合用于教程。
数据库结构
在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:
简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:
- 其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
- 另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。
在 blocks 中,key -> value 为:
key | value |
---|---|
b + 32 字节的 block hash |
block index record |
f + 4 字节的 file number |
file information record |
l + 4 字节的 file number |
the last block file number used |
R + 1 字节的 boolean |
是否正在 reindex |
F + 1 字节的 flag name length + flag name string |
1 byte boolean: various flags that can be on or off |
t + 32 字节的 transaction hash |
transaction index record |
在 chainstate,key -> value 为:
key | value |
---|---|
c + 32 字节的 transaction hash |
unspent transaction output record for that transaction |
B |
32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs |
详情可见 这里。
因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
- 32 字节的 block-hash -> block 结构
-
l
-> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了。
准备工作
$ npm install --save incache
序列化
因为采用的是纯JS的key value数据库,所以很方便存储。
在Block增加以下代码:
toString() {
return JSON.stringify(
this
);
}
static fromString(data) {
let payload = JSON.parse(data);
let block = new Block(payload.timestamp, payload.data, payload.prevBlockHash, payload.hash, payload.nonce);
return block;
}
持久化
让我们从 NewBlockchain
函数开始。在之前的实现中,NewBlockchain
会创建一个新的 Blockchain
实例,并向其中加入创世块。而现在,我们希望它做的事情有:
- 打开一个数据库文件
- 检查文件里面是否已经存储了一个区块链
- 如果已经存储了一个区块链:
- 创建一个新的
Blockchain
实例 - 设置
Blockchain
实例的 tip 为数据库中存储的最后一个块的哈希
- 创建一个新的
- 如果没有区块链:
- 创建创世块
- 存储到数据库
- 将创世块哈希保存为最后一个块的哈希
- 创建一个新的
Blockchain
实例,初始时 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)
代码大概是这样:
static NewBlockChain() {
// get db instance
let store = new InCache({
storeName: "blockchain",
autoSave: true,
autoSaveMode: "timer"
});
let bc;
// check db
let lastHash = store.get('l');
if (!lastHash) {
bc = new BlockChain();
let block = bc.newGenesisBlock();
store.set(block.hash, block.toString());
store.set('l', block.hash);
lastHash = block.hash;
} else
bc = new BlockChain();
bc.db = store;
bc.tip = lastHash;
return bc;
}
相对之前的代码,修改了几个部分
1. Blockchain这个class不保存block的array
2. Blockchain每次创建实例都会检查本地数据
3. Blockchain只存储最后一个block的hash,以及对应的db实例
因为修改了数据结构和存储方式,所以对应的 addBlock 方法也必须做修改
addBlock(data) {
// modify in part3
// let prevBlock = this.blocks[this.blocks.length - 1];
// let newBlock = Block.NewBlock(data, prevBlock.hash);
// this.blocks.push(newBlock);
// save to db
let lastHash = this.tip;
let newBlock = Block.NewBlock(data, lastHash);
this.db.set(newBlock.hash, newBlock.toString());
this.db.set('l', newBlock.hash);
this.tip = newBlock.hash;
}
检查区块链
现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!
先上代码:
class BlockChainIterator {
constructor(bc) {
this.blockchain = bc;
this.tip = this.blockchain.tip;
}
curr() {
let data = this.blockchain.db.get(this.tip);
let block = Block.fromString(data);
return block;
}
next() {
let block = this.curr();
this.tip = block.prevBlockHash;
if (!this.tip || this.tip == '')
return null;
return this.curr();
}
hasNext() {
let block = this.curr();
let prevBlockHash = block.prevBlockHash;
if (!prevBlockHash || prevBlockHash == '')
return false;
return true;
}
}
通过BlockChain里面的方法获取到Iterator实例,可以调用 curr()
以及 prevBlock()
获取对应的block。注意,迭代器的初始状态为链中的 tip,因此区块将从尾到头(创世块为头),也就是从最新的到最旧的进行获取。实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。
这就是数据库部分的内容了!
CLI
到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是简单执行了 NewBlockchain
和 bc.AddBlock
。是时候改变了!现在我们想要拥有这些命令:
// cd to current project folder
node src/cli.js --addblock "Pay 0.031337 for a coffee"
node src/cli.js --printchain
所有的命令行相关的操作都会通过 cli.js 来进行处理。
commander
详情可见 这里:nodejs命令行工具)。
代码部分
const program = require('commander');
const Block = require("./block.js");
const BlockChain = require("./blockchain.js");
program
.version('0.1.0')
.option('-a, --addblock', 'Default empty',)
.option('-p, --printchain')
.parse(process.argv);
let addblock = program.addblock;
console.log(`addBlock: ${addblock}`);
if (addblock && addblock != '') {
let bc = BlockChain.NewBlockChain();
bc.addBlock(addblock);
let iterator = bc.getBlockIterator();
let currBlock = iterator.curr();
console.log(currBlock);
}
let printchain = program.printchain;
console.log(`printchain: ${printchain}`);
if (printchain && printchain != '') {
let bc = BlockChain.NewBlockChain();
let iterator = bc.getBlockIterator();
let currBlock = iterator.curr();
console.log(currBlock);
while(iterator.hasNext()) {
let prev = iterator.next();
console.log(prev);
}
}
我们用 commander
来对命令行参数进行解析。首先,我们创建了两个命令 addblock
和 printchain
。对于 addblock
,需要传递参数,作为block的data。而对于 printchain
,只验证该参数是否传递,如果传递,则打印整个blockchain。
其中,printchain
部分代码,就是利用了Iterator迭代器对blockchain的block进行遍历,最终一个一个打印出来。
尝试运行下前文提及的两个命令。
## input addblock "Pay 0.031337 for a coffee"
node src/cli.js --addblock "Pay 0.031337 for a coffee"
addBlock: Pay 0.031337 for a coffee
Mining the block: Pay 0.031337 for a coffee
Mining end! hash: 00d29174de8232af298c07a867eff2bfd538ae8b3cb104c737f1bcb1d9bacb69
Block {
timestamp: 1534753677791,
data: 'Pay 0.031337 for a coffee',
prevBlockHash: '0007b433932931ee20d1cd8bd1d99f682ce453d374f08b726f2fcc96e6b5d7d9',
hash: '00d29174de8232af298c07a867eff2bfd538ae8b3cb104c737f1bcb1d9bacb69',
nonce: 0 }
printchain: undefined
## input printchain
node src/cli.js --printchain
addBlock: undefined
printchain: true
Block {
timestamp: 1534754549174,
data: true,
prevBlockHash: '007e0c70cd625bb198db22d2dab5ab7716502f0d48382bf77f89ec3b1f9f4b99',
hash: '009d9ab8ba29c24cfa0307729f33595951073e304ae0860a7cd87080c7a160b6',
nonce: 0 }
Block {
timestamp: 1534754514339,
data: 'Send 1 BTC to another friend',
prevBlockHash: '0032cd95416119244dc37fa0013b2a6e85252566718fbae447de338e45ccc7bf',
hash: '007e0c70cd625bb198db22d2dab5ab7716502f0d48382bf77f89ec3b1f9f4b99',
nonce: 0 }
Block {
timestamp: 1534754514310,
data: 'Send 1 BTC to a friend',
prevBlockHash: '0032953c8509cbe60b3d8f0fbbd5cece6de63247e336e85fbeee95d15e14b206',
hash: '0032cd95416119244dc37fa0013b2a6e85252566718fbae447de338e45ccc7bf',
nonce: 0 }
Block {
timestamp: 1534754490482,
data: 'Genesis Block',
prevBlockHash: '',
hash: '0032953c8509cbe60b3d8f0fbbd5cece6de63247e336e85fbeee95d15e14b206',
nonce: 0 }