1. 程式人生 > >NodeJS區塊鏈實踐(1)Nodejs搭建簡易區塊鏈

NodeJS區塊鏈實踐(1)Nodejs搭建簡易區塊鏈

區塊鏈的目的之一是讓我們所需要的“有價值”的資訊得以儲存且不可更改,這些資訊都儲存在一個叫做“區塊(block)”的結構中。以比特幣為例,被認為是有價值的資訊是“交易”,所有的交易儲存在區塊中,通過區塊的hash、時間戳等實現資訊的可追溯以及不可更改性。

我們這裡首先實現的是一個簡易的區塊鏈,並不是像比特幣這樣成熟的區塊鏈,暫不涉及交易結構、驗證以及UTXO。

一、區塊(block)

首先,建立一個區塊(Block)類。我們的區塊僅包含了一部分關鍵資訊,它的結構如下:

class Block{
  constructor(data){
     this.hash = "",  // 當前區塊的Hash值
     this.height = 0,  // 當前區塊的高度
     this.body = data,  // 區塊實際儲存的有效資訊
     this.time = 0,  // 時間戳,即區塊建立的時間
     this.previousBlockHash = ""。// 前一個區塊的Hash值
  }
}

這裡,hash、height、time和previousBlockHash在比特幣規範中屬於區塊頭(header),區塊頭是一個單獨的資料結構,而body部分用於儲存交易資訊或者其他資料。

二、區塊鏈(blockchain)

首先,我們要把區塊存在哪裡?我們可以把區塊直接放進一個數組(let blockchain = arr[]),但是這顯然不是我們想要的。這裡我們選擇leveldb作為底層的資料儲存方案。

const level = require('level');
const chainDB = './.data/blockchain';
const db = level(chainDB);

然後,我們要思考一下,一個blockchain例項需要具有哪些功能?

1、getBlockHeight():獲取當前區塊的高度,即該區塊鏈的高度;
2、getBlockByHeight(height):通過區塊高度來獲取區塊;
3、getBlockByHash(hash):通過區塊雜湊來獲取區塊;
4、validateBlock(height):驗證某高度的區塊是否有效;
5、validateChain():驗證整條鏈的每個區塊是否均有效;

通過上面幾個函式,我們定義了對一個區塊鏈(blockchain)的基本描述,我們通過當前區塊的高度獲取當前區塊的hash和前一個區塊的hash,進而可以向前回溯出整個區塊鏈。

下面我們看看怎麼實現?

首先,Blockchain要包括如下的方法:

class Blockchain {
  addBlock();
  getBlockHeight();
  getBlockByHeight();
  getBlockByHash();
  validateBlock();
  validateChain();
}

我們要保證系統中只有一個Blockchain例項,可以使用ES6語法class的static關鍵詞來設定一個靜態方法來儲存Blockchain例項。在ES6的class中,加了static關鍵詞的方法不會被例項所繼承,只能通過class來呼叫,如Blockchain.getInstance()。

static getInstance() {
   if (!Blockchain.instance) {
      Blockchain.instance = new Blockchain();
         return Blockchain.instance.getBlockHeight()
              .then((height) => {
                  if (height === 0) {
                      const initialBlock = new Block('First block of the blockchain');
                      return Blockchain.instance.addBlock(initialBlock);
                  }
              })
              .then(() => Blockchain.instance)
    } 
    return Promise.resolve(Blockchain.instance);    
}

getBlockHeight()

通過遞迴呼叫count函式,從創始區塊一直計數到最後一個區塊,得到待增新區塊的高度。

getBlockHeight() {
    let count = function (key) {
       return db.get(key)
            .then(() => count(key + 1))
            .catch(() => key);
    };
    return count(0);
}

getBlockByHeight()

getBlockByHeight(){
    return db.get(height).then((value) => JSON.parse(value));
}

addBlock(newBlock)

新區塊的previousBlockHash應等於前一個區塊的Hash,這樣就將兩個區塊連結到了一起。

addBlock(newBlock) {
    return this.getBlockHeight()
        .then((height) => {
            let PrevBlock;
            newBlock.height = height;  // 新區塊的高度
            newBlock.time = new Date().getTime().toString().slice(0, -3); // 新區塊時間戳
            // 得到新區塊的previousBlockHash
            if (height > 0) {
                PrevBlock = this.getBlock(height - 1) 
                    .then((previousBlock) => {
                        newBlock.previousBlockHash = previousBlock.hash;
                    });
            }
            return Promise.all([PrevBlock])
                .then(() => {
                    newBlock.hash = SHA256(JSON.stringify(newBlock)).toString();
                    return db.put(height, JSON.stringify(newBlock));
                });
        })
        .then(() => Blockchain.instance);
}

getBlockByHash()

可以通過類似於getBlockHeight()的思路進行遞迴,比較該區塊的hash於輸入的引數hash是否相等。然而,我們還有另外一種選擇。leveldb給我提供了db.createReadStream()方法來逐個讀取資料庫中的條目。

getBlockByHash(hash){
  return new Promise((resolve, reject) => {
    let block;
    db.createReadStream()
      .on("data", (data) => {
        if(data.key != 'height'){
          let value = JSON.parse(data.value);
          let blockHash = value.hash;
          if (blockHash == hash) {
            block = value;
          }
        }
      })
      .on("error", (err) => {
        reject(err);
      })
      .on("close", ()=>{
        resolve(block); // 如果沒有滿足條件的,則block值為undefined,在下一步就會丟擲錯誤
      })
  })
}

validateBlock(blockHeight)

驗證一個區塊,就是要重新生成該區塊的hash,比較該hash與區塊本身hash屬性的值是否相等。

validateBlock(blockHeight){
  return db.get(blockHeight).then(function(value){
    let block = JSON.parse(value);
    let blockHash = block.hash;
    // remove block hash to test block integrity
    block.hash = '';
    // generate block hash
    let validBlockHash = SHA256(JSON.stringify(block)).toString();
    // Compare
    if (blockHash === validBlockHash) {
	return true
    } else {
        console.log('Block #'+blockHeight+' invalid hash:\n'+blockHash+'<>'+validBlockHash);
	return  false
    }
  }).catch(function(err){
	console.log('Not found!', err);
  })
}

validateChain()

驗證整個鏈的所有區塊,這就肯定要用到for迴圈。

validateChain(){
  let errorLog = [];
  this.getBlockHeight().then(height =>{
      for (var i = 0; i <= height; i++) {
	  this.getBlock(i).then(block => {
            // validate block
	    let h = block.height
	    this.validateBlock(h).then(val => {
		if(!val) errorLog.push(h)
	    })

	    // compare blocks hash link
            let hash = block.hash;
	    let n = block.height + 1;
	    if( n <= value ) {
		 db.get(n, (err, val) => {
		    let nextBlock = JSON.parse(val)
		    let preHash = nextBlock.previousBlockHash
		    if(hash !== preHash) {
		        errorLog.push(n-1);
		    }
		    if(n == value) {
		       if(errorLog.length>0){
	                  console.log('\n-------Errors Detected!-------\n')
			  console.log('Block errors = ' + errorLog.length);
			  console.log('Blocks: '+ errorLog);
                       } else {
			  console.log('No errors detected');
		       }
		    }
	         })
	    }
          })
       }
    }).catch(function(err){
	console.log('Not found!', err);
    })
}

總結

通過上面簡單的程式碼,我們實現了一個簡單的區塊鏈結構,這是一個private chain,沒有p2p網路,沒有挖礦需要的共識演算法。我們關注的是基本的區塊鏈的儲存結構,即有效資料儲存在block中,每一個block通過hash值關聯成一條鏈,我們通過區塊的高度即可從資料庫leveldb中讀取區塊的資料。