以前因爲課程要求,基於Nodejs作了一個實現簡易區塊鏈。要求很是簡單,結構體記錄區塊結構,順便能向鏈中插入新的區塊便可。javascript
可是若是要支持多用戶使用,就須要考慮「可信度」的問題。那麼按照區塊鏈要求,鏈上的數據不能被篡改,除非算力超過除了攻擊者自己以外其他因此機器的算力。css
想了想,就動手作試試咯。前端
在google上搜了搜,發現有個項目不錯: https://github.com/lhartikk/naivechain 。大概只有200行,可是其中幾十行都是關於搭建ws和http服務器,美中不足的是沒有實現批量插入區塊鏈和計算可信度。node
結合這個項目,基本上能夠肯定每一個區塊會封裝成一個class(結構化表示),區塊鏈也封裝成一個class,再對外暴露接口。webpack
爲了方便表示區塊,將其封裝爲一個class,它沒有任何方法:css3
/** * 區塊信息的結構化定義 */ 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,這裏採用的規則比較簡單:git
爲了方便,會引入一個加密庫:es6
const CryptoJS = require('crypto-js')
不少區塊連接在一塊兒,就組成了一條鏈。這條鏈,也用class來表示。而且其中實現了不少方法:github
起源塊是「硬編碼」,由於它前面沒數據呀。而且規定它不能被篡改,即不能強制覆蓋。咱們在構造函數中,直接將生成的起源塊放入鏈中。
class BlockChain { constructor() { this.blocks = [this.getGenesisBlock()] } /** * 建立區塊鏈起源塊, 此塊是硬編碼 */ getGenesisBlock() { return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4') } }
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) }
插入區塊的時候,須要檢查當前塊是否合法,若是合法,那麼插入而且返回true;不然返回false。
/** * 向區塊鏈添加新節點 * @param {Block} newBlock */ addBlock(newBlock) { // 合法區塊 if(this.isValidNewBlock(newBlock, this.getLatestBlock())) { this.blocks.push(newBlock) return true } return false }
檢查的邏輯就就放在了 isValidNewBlock
方法中, 它主要完成3件事情:
/** * 判斷新加入的塊是否合法 * @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個區塊的下標是: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 }
我當時很奇怪,爲何須要「批量插入」這個方法。後來想明白了(但願沒想錯)。假設服務器S,以及兩個用戶A與B。
A與B同時拉取到已知鏈的數據,而後各自生成。A網速較快,可是算力低,就生成了1個區塊,放入了S上。注意:此時S上的區塊已經更新。
而B比較慘了,它在本地生成了2個區塊,可是受限於網速,只能等網速恢復了傳入區塊。這時候,按照規則,它是能夠覆蓋的(算力高嘛)。因此這種狀況下,服務器S接受到B的2個區塊,更新後的鏈長度是3(算上起源塊),而且A的那個區塊已經被覆蓋了。
雖然沒有寫服務器,可是仍是模擬了上面講述的第5種狀況。代碼在 test.js
文件中,直接run便可。看下效果截圖吧:
紅線上面就是先算出來的,紅線下面就是被算力更高的客戶端篡改後的區塊鏈。具體模擬過程能夠看代碼,這裏再也不冗贅了。
所有代碼在都放在: https://github.com/dongyuanxin/node-blockchain
《前端知識體系》
《設計模式手冊》
《Webpack4漸進式教程》