NodeJS實現簡易區塊鏈

godbmw發表於2019-05-03

之前由於課程要求,基於Nodejs做了一個實現簡易區塊鏈。要求非常簡單,結構體記錄區塊結構,順便能向鏈中插入新的區塊即可。

但是如果要支援多使用者使用,就需要考慮“可信度”的問題。那麼按照區塊鏈要求,鏈上的資料不能被篡改,除非算力超過除了攻擊者本身之外其餘所以機器的算力。

想了想,就動手做試試咯。

?檢視全部教程 / 閱讀原文?

技術調研

在google上搜了搜,發現有個專案不錯: github.com/lhartikk/na… 。大概只有200行,但是其中幾十行都是關於搭建ws和http伺服器,美中不足的是沒有實現批量插入區塊鏈和計算可信度。

結合這個專案,基本上可以確定每個區塊會封裝成一個class(結構化表示),區塊鏈也封裝成一個class,再對外暴露介面。

區塊定義

為了方便表示區塊,將其封裝為一個class,它沒有任何方法:

/**
 * 區塊資訊的結構化定義
 */
class Block {
  /**
   * 建構函式
   * @param {Number} index 
   * @param {String} previousHash 
   * @param {Number} timestamp 
   * @param {*} data 
   * @param {String} hash 
   */
  constructor(index, previousHash, timestamp, data, hash) {
    this.index = index // 區塊的位置
    this.previousHash = previousHash + '' // 前一個區塊的hash
    this.timestamp = timestamp // 生成區塊時候的時間戳
    this.data = data // 區塊本身攜帶的資料
    this.hash = hash + '' // 區塊根據自身資訊和規則生成的hash
  }
}
複製程式碼

至於怎麼生成hash,這裡採用的規則比較簡單:

  1. 拼接index、previouHash、timestamp和data,將其字串化
  2. 利用sha256演算法,計算出的記過就是hash

為了方便,會引入一個加密庫:

const CryptoJS = require('crypto-js')
複製程式碼

鏈結構定義

很多區塊連結在一起,就組成了一條鏈。這條鏈,也用class來表示。並且其中實現了很多方法:

  1. 按照加密規則生成hash
  2. 插入新塊和檢查操作
  3. 批量插入塊和檢查操作以及可信度計算

1. 起源塊

起源塊是“硬編碼”,因為它前面沒資料呀。並且規定它不能被篡改,即不能強制覆蓋。我們在建構函式中,直接將生成的起源塊放入鏈中。

class BlockChain {
  constructor() {
    this.blocks = [this.getGenesisBlock()]
  }

  /**
   * 建立區塊鏈起源塊, 此塊是硬編碼
   */
  getGenesisBlock() {
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')
  }
}
複製程式碼

2. 計算下一個區塊

BlockChain物件可以根據當前鏈,自動計算下一個區塊。並且與使用者傳來的區塊資訊比較,如果一樣,說明合法,可以插入;否則,使用者的區塊就是非法的,不允許插入。

// 方法都是BlockChain物件方法
  /**
   * 根據資訊計算hash值
   */
  calcuteHash(index, previousHash, timestamp, data) {
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''
  }

  /**
   * 得到區塊鏈中最後一個塊節點
   */
  getLatestBlock() {
    return this.blocks[this.blocks.length - 1]
  }

  /**
   * 計算當前連結串列的下一個區塊
   * @param {*} blockData 
   */
  generateNextBlock(blockData) {
    const previousBlock = this.getLatestBlock()
    const nextIndex = previousBlock.index + 1
    const nextTimeStamp = new Date().getTime()
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
  }
複製程式碼

3. 插入區塊

插入區塊的時候,需要檢查當前塊是否合法,如果合法,那麼插入並且返回true;否則返回false。

  /**
   * 向區塊鏈新增新節點
   * @param {Block} newBlock 
   */
  addBlock(newBlock) {
    // 合法區塊
    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {
      this.blocks.push(newBlock)
      return true  
    }
    return false
  }
複製程式碼

檢查的邏輯就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:

  1. 判斷新區塊的index是否是遞增的
  2. 判斷previousHash是否和前一個區塊的hash相等
  3. 判斷新區塊的hash是否按約束好的規則生成
  /**
   * 判斷新加入的塊是否合法
   * @param {Block} newBlock 
   * @param {Block} previousBlock 
   */
  isValidNewBlock(newBlock, previousBlock) {
    if(
      !(newBlock instanceof Block) ||
      !(previousBlock instanceof Block)
    ) {
      return false
    }

    // 判斷index
    if(newBlock.index !== previousBlock.index + 1) { 
      return false
    }

    // 判斷hash值
    if(newBlock.previousHash !== previousBlock.hash) { 
      return false
    }

    // 計算新塊的hash值是否符合規則
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) { 
      return false
    }

    return true
  }
複製程式碼

4. 批量插入

批量插入的邏輯比較複雜,比如當前鏈上有4個區塊的下標是:0->1->2->3。除了起源塊0不能被覆蓋,當插入一條新的下標為“1->2->3->4”的鏈時候,就可以替換原來的區塊。最終結果是:0->1->2->3->4。

在下標index的處理上,假設還是上面的情況,如果傳入的鏈的下標是從大於4的整數開始,顯然無法拼接原來的區塊鏈的下標,直接扔掉。

但是如何保證可信度呢?就是當新鏈(B鏈)替換原來的鏈(A鏈)後,生成新的鏈(C鏈)。如果 length(C) > length(A),那麼即可覆蓋要替換的部分。 這就保證了,只有在算力超過所有算力50%的時候,才能篡改這條鏈

插入新鏈的方法如下:

  /**
   * 插入新連結串列
   * @param {Array} newChain 
   */
  addChain(newChain) {
    if(this.isValidNewChain(newChain)) {
      const index = newChain[0].index
      this.blocks.splice(index)
      this.blocks = this.blocks.concat(newChain)
      return true
    }
    return false
  }
複製程式碼

實現上面所述邏輯的方法如下:

  /**
   * 判斷新插入的區塊鏈是否合法而且可以覆蓋原來的節點
   * @param {Array} newChain 
   */
  isValidNewChain(newChain) {
    if(Array.isArray(newChain) === false || newChain.length === 0) {
      return false
    }

    let newChainLength = newChain.length,
      firstBlock = newChain[0]

    // 硬編碼的起源塊不能改變
    if(firstBlock.index === 0) {
      return false
    }

    // 移植新的鏈的長度 <= 現有鏈的長度
    // 新的鏈不可信
    if(newChainLength + firstBlock.index <= this.blocks.length) {
      return false
    }

    // 下面檢查新的鏈能否移植
    // 以及新的鏈的每個節點是否符合規則
    if(!this.isValidNewBlock(firstBlock, this.blocks[firstBlock.index - 1])) {
      return false
    }

    for(let i = 1; i < newChainLength; ++i) {
      if(!this.isValidNewBlock(newChain[i], newChain[i - 1])) {
        return false
      }
    }

    return true
  }
複製程式碼

5. 為什麼需要批量插入?

我當時很奇怪,為什麼需要“批量插入”這個方法。後來想明白了(希望沒想錯)。假設伺服器S,以及兩個使用者A與B。

A與B同時拉取到已知鏈的資料,然後各自生成。A網速較快,但是算力低,就生成了1個區塊,放入了S上。注意:此時S上的區塊已經更新。

而B比較慘了,它在本地生成了2個區塊,但是受限於網速,只能等網速恢復了傳入區塊。這時候,按照規則,它是可以覆蓋的(算力高嘛)。所以這種情況下,伺服器S接受到B的2個區塊,更新後的鏈長度是3(算上起源塊),並且A的那個區塊已經被覆蓋了。

效果測試

雖然沒有寫伺服器,但是還是模擬了上面講述的第5種情況。程式碼在 test.js 檔案中,直接run即可。看下效果截圖吧:

image.png

紅線上面就是先算出來的,紅線下面就是被算力更高的客戶端篡改後的區塊鏈。具體模擬過程可以看程式碼,這裡不再冗贅了。

全部程式碼在都放在: github.com/dongyuanxin…

更多系列文章

⭐在GitHub上收藏/訂閱⭐

《前端知識體系》

《設計模式手冊》

《Webpack4漸進式教程》

⭐在GitHub上收藏/訂閱⭐

相關文章