blockchain in js (3) : 持久化和命令行接口

持久化和命令行接口

引言

到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前距离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。

选择数据库

目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。

那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。而我们将要使用的是...

incache

因为它:

  1. 非常简洁
  2. 用 JavaScript 实现
  3. 不需要运行一个服务器
  4. 能够允许我们构造想要的数据结构

当然,如果想要承担更复杂的功能,该db将不在考虑范围之内。该DB仅仅作为参考使用,不建议在production环境使用。但是因为该DB支持,key value的存储结构,非常适合用于教程。

数据库结构

在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:

简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:

  1. 其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
  2. 另一个 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

chainstatekey -> 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)相关的东西。最终,我们会用到的键值对有:

  1. 32 字节的 block-hash -> block 结构
  2. 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 实例,并向其中加入创世块。而现在,我们希望它做的事情有:

  1. 打开一个数据库文件
  2. 检查文件里面是否已经存储了一个区块链
  3. 如果已经存储了一个区块链:
    1. 创建一个新的 Blockchain 实例
    2. 设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
  4. 如果没有区块链:
    1. 创建创世块
    2. 存储到数据库
    3. 将创世块哈希保存为最后一个块的哈希
    4. 创建一个新的 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

到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是简单执行了 NewBlockchainbc.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来对命令行参数进行解析。首先,我们创建了两个命令 addblockprintchain。对于 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 }

  

链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容