之前由於課程要求,基於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,這裡採用的規則比較簡單:
- 拼接index、previouHash、timestamp和data,將其字串化
- 利用sha256演算法,計算出的記過就是hash
為了方便,會引入一個加密庫:
const CryptoJS = require('crypto-js')
複製程式碼
鏈結構定義
很多區塊連結在一起,就組成了一條鏈。這條鏈,也用class來表示。並且其中實現了很多方法:
- 按照加密規則生成hash
- 插入新塊和檢查操作
- 批量插入塊和檢查操作以及可信度計算
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件事情:
- 判斷新區塊的index是否是遞增的
- 判斷previousHash是否和前一個區塊的hash相等
- 判斷新區塊的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即可。看下效果截圖吧:
紅線上面就是先算出來的,紅線下面就是被算力更高的客戶端篡改後的區塊鏈。具體模擬過程可以看程式碼,這裡不再冗贅了。
全部程式碼在都放在: github.com/dongyuanxin…
更多系列文章
《前端知識體系》
- JavaScript基礎知識梳理(上)
- JavaScript基礎知識梳理(下)
- ES6重難點整理
- 談談promise/async/await的執行順序與V8引擎的BUG
- 前端面試中常考的原始碼實現
- Flex上手與實戰
- ......
《設計模式手冊》
《Webpack4漸進式教程》