深入淺出區塊鏈技術
本文爲純粹區塊鏈技術分享,沒有任何投資建議。希望大家喜歡~
一、故事導讀
開始分享之前,引用自網上一個段子來引導大家。
《小明的故事》
小明是誰?小明是一名前端工程師,也是一個足球迷。
他有一項神奇的技能:他對足球有很深的理解,能夠在每屆世界盃開賽之前準確預測出最終奪冠的球隊
比如,在 2010 年的那屆世界盃,小明就預測出了正確的結果。大賽閉幕,小明難掩興奮之情,想在女朋友面前顯擺一下。
女朋友很自然地提出質疑,而小明並沒有證據證明自己,只能啞口無言。
小明痛定思痛,決定寫一個網站來提前記錄自己的預言。
-
小明自己設計了網頁界面。
-
找小夥伴幫忙寫了一個後端服務,提供兩個接口。
-
小明基於這兩個接口,寫了一個純前端渲染的網站。
最終網站看起來是這個樣子的:
接下來,小明靜靜等待下一屆世界盃的到來。
時間過得很快,轉眼到了 2014 年。這一次,小明再次正確預測出了冠軍得主。
有網站記錄預言,小明心想,這次女朋友應該會相信自己了吧!
然而……
女朋友也是懂技術的,她這次仍然提出了一個合理的質疑。小明再次無言以對。
那麼問題來了,該怎麼辦能夠讓女朋友相信自己呢?
如果現在還有沒結論,可以繼續向下看。
二、基礎概念
區塊鏈技術中有很多新的概念,對於一些並不深入這個領域的同學來說,相對不是很友好。本文先對一些技術的概念進行講解。作爲前置的知識。
區塊鏈的概念
特殊的分佈式數據庫。
一種鏈表結構,鏈表中元素作爲一個區塊。而每個鏈表的結構包括:
-
timestamp: 區塊產生時間戳
-
nonce: 與區塊頭的 hash 值共同證明計算量(工作量)
-
data: 區塊鏈上存儲的數據
-
previousHash: 上一個區塊的 hash
-
hash: 本區塊鏈的 hash,由上述幾個屬性進行哈希計算而得
暫時無法在飛書文檔外展示此內容
一些特點
- 去中心化存儲
分佈式數據庫很早之前就已經出現,但與之不同的是區塊鏈是一個沒有管理者的、無中心化的分佈式數據庫。其起初的設計目標就是防止出現位於中心地位的管理者當局。
那麼下一個問題就來了,如果沒有一個管理者進行數據的管理,如何保證這個分佈式數據庫中的數據是可信任的呢?這就要提到下一個不可修改的特性了。
- 不可篡改
區塊鏈上的數據是不可篡改的,大家都這樣說。但其實,數據是可以改的,只是說改了以後就你自己認,而且被修改數據所在區塊之後的所有區塊都會失效。區塊鏈網絡有一個同步邏輯,整個區塊鏈網絡總是保持所有節點使用最長的鏈,那麼你修改完之後,一聯網同步,修改的東西又會被覆蓋。這是不可篡改的一個方面。
更有意思的是,區塊鏈通過加密校驗,保證了數據存取需要經過嚴格的驗證,而這些驗證幾乎又是不可僞造的,所以也很難篡改。加密並不代表不可篡改,但不可篡改是通過加密以及經濟學原理搭配實現的。這還有點玄學的味道,一個純技術實現的東西,還要靠理論來維持。但事實就是這樣。這就是傳說中的挖礦。
挖礦過程其實是礦工爭取創建一個區塊的過程,一旦挖到礦,也就代表這個礦工有資格創建新區塊。怎麼算挖到礦呢?通過一系列複雜的加密算法,從 0 開始到∞,找到一個滿足難度的 hash 值,得到這個值,就是挖到礦。這個算法過程被稱爲 “共識機制”,也就是通過什麼形式來決定誰擁有記賬權,共識機制有很多種,區塊鏈採用哪種共識機制最佳,完全是由區塊鏈的實際目的結合經濟學道理來選擇。
除了這些,區塊鏈裏面的加密比比皆是,這些加密規則和算法,使得整個區塊鏈遵循一種規律,讓篡改數據的成本特別高,以至於參與的人對篡改數據都沒有興趣,甚至忌憚。這又是玄學的地方。
針對這些不可篡改的特性,我們是不是能夠解決一開始提出的問題呢。
用 js 來寫一段區塊鏈的代碼,來解決小明的困惑。
三、【實戰】用 JavaScript 來寫一個基本的區塊鏈 demo。
實現一個基本的區塊鏈
- 創建區塊
區塊鏈是由許許多多的區塊鏈接在一起的(這聽上去好像沒毛病..)。鏈上的區塊通過某種方式允許我們檢測到是否有人操縱了之前的任何區塊。
那麼我們如何確保數據的完整性呢?每個區塊都包含一個基於其內容計算出來的 hash。同時也包含了前一個區塊的 hash。
下面是一個區塊類用 JavaScript 寫出來大致的樣子:採用構造函數初始化區塊的屬性。
在這裏的哈希值是無法修改的。我們能夠看到,哈希值是由多個元素組成的,一旦一個哈希值受到了修改,意味着previousHash
被修改了,這個時候如果想要繼續修改就要對下一個區塊進行操作,否則修改的區塊就不具有意義了。而哈希值的計算非常耗時,同時修改 51% 以上的節點基本不可能,所以,這種聯動機制也就保證了其不可修改的特性。
const crypto = require('crypto');
class Block {
constructor(previousHash, timestamp, data) {
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
this.nonce = 0;
this.hash = this.calculateHash();
}
// 計算區塊的哈希值
calculateHash() {
return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
}
}
- 創建鏈
我們通過創建包含創世區塊的數組來初始化整個鏈。這樣一來,第一個區塊是特殊的,因爲他並沒有指向前一個區塊。並且添加了兩個方法:
-
getLatestBlock()
返回我們區塊鏈上最新的區塊。 -
addBlock()
負責將新的區塊添加到我們的鏈上。爲此,我們將前一個區塊的 hash 添加到我們新的區塊中。這樣我們就可以保持整個鏈的完整性。因爲只要我們變更了最新區塊的內容,我們就需要重新計算它的 hash。當計算完成後,我將把這個區塊推進鏈裏(一個數組)。
最後,我創建一個isChainValid()
來確保沒有人篡改過區塊鏈。它會遍歷所有的區塊來檢查每個區塊的 hash 是否正確。它會通過比較previousHash
來檢查每個區塊是否指向正確的上一個區塊。如果一切都沒有問題它會返回true
否則會返回false
。
class Blockchain{
constructor() {
this.chain = [this.createGenesisBlock()];
}
// 創建當前時間下的區塊(創世塊)
createGenesisBlock() {
return new Block(0, "20/05/2022", "Genesis block", "0");
}
// 獲得區塊鏈上最新的區塊
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// 將新的區塊添加到鏈上
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.hash = newBlock.calculateHash();
this.chain.push(newBlock);
}
// 驗證區塊鏈是否被篡改。
// 遍歷每個區塊的hash值是否正確&&每個區塊的指向previousHash是否正確。
isChainValid() {
for (let i = 1; i < this.chain.length; i++){
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
}
- 使用
我們的區塊鏈類已經寫完啦,可以真正的開始使用它了!
這裏,我們創建了一個區塊鏈的實例,並在其中添加區塊。其中的數據就寫成了小明對於世界盃冠軍的預言。
let firstClain = new Blockchain();
firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));
firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));
// 檢查是否有效(將會返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);
// 現在嘗試操作變更數據
firstClain.chain[1].data = { champion: 'korea' };
// 再次檢查是否有效 (將會返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
- 嘗試修改數據
我會在一開始通過運行isChainValid()
來驗證整個鏈的完整性。我們操作過任何區塊,所以它會返回 true。
之後我將鏈上的第一個(索引爲 1)區塊的數據進行了變更。之後我再次檢查整個鏈的完整性,發現它返回了 false。我們的整個鏈不再有效了。
// 檢查是否有效(將會返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);
// 現在嘗試操作變更數據
firstClain.chain[1].data = { champion: 'korea' };
// 再次檢查是否有效 (將會返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
POW(proof-of-work)工作量證明
POW 是在第一個區塊鏈被創造之前就已經存在的一種機制。這是一項簡單的技術,通過一定數量的計算來防止濫用。工作量是防止垃圾填充和篡改的關鍵。如果它需要大量的算力,那麼填充垃圾就不再值得。
比特幣通過要求 hash 以特定 0 的數目來實現 POW。這也被稱之爲難度
不過等一下!一個區塊的 hash 怎麼可以改變呢?在比特幣的場景下,一個區塊包含有各種金融交易信息。我們肯定不希望爲了獲取正確的 hash 而混淆了那些數據。
爲了解決這個問題,區塊鏈添加了一個nonce
值。Nonce 是用來查找一個有效 Hash 的次數。而且,因爲無法預測 hash 函數的輸出,因此在獲得滿足難度條件的 hash 之前,只能大量組合嘗試。尋找到一個有效的 hash(創建一個新的區塊)在圈內稱之爲挖礦。
在比特幣的場景下,POW 確保每 10 分鐘只能添加一個區塊。你可以想象垃圾填充者需要多大的算力來創造一個新區塊,他們很難欺騙網絡,更不要說篡改整個鏈。
暫時無法在飛書文檔外展示此內容
我們該如何實現呢?我們先來修改我們區塊類並在其構造函數中添加 Nonce 變量。我會初始化它並將其值設置爲 0。
constructor(previousHash, timestamp, data) {
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
// 工作量
this.nonce = 0;
this.hash = this.calculateHash();
}
我們還需要一個新的方法來增加 Nonce,直到我們獲得一個有效 hash。強調一下,這是由難度決定的。所以我們會收到作爲參數的難度。
// 工作量計算
mineBlock(difficulty) {
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) {
this.nonce++;
this.hash = this.calculateHash();
}
最後,我們還需要更改一下calculateHash()
函數。因爲目前他還沒有使用 Nonce 來計算 hash。
// 計算區塊的哈希值
calculateHash() {
return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
}
將它們結合在一起,你會得到如下所示的區塊類:
class Block {
constructor(previousHash, timestamp, data) {
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
// 工作量
this.nonce = 0;
this.hash = this.calculateHash();
}
// 計算區塊的哈希值
calculateHash() {
return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
}
// 工作量計算
mineBlock(difficulty) {
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) {
this.nonce++;
this.hash = this.calculateHash();
}
}
}
修改區塊鏈
現在,我們的區塊已經擁有 Nonce 並且可以被開採了,我們還需要確保我們的區塊鏈支持這種新的行爲。讓我們先在區塊鏈中添加一個新的屬性來跟蹤整條鏈的難度。我會將它設置爲 2(這意味着區塊的 hash 必須以 2 個 0 開頭)。
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 2;
}
現在剩下要做的就是改變addBlock()
方法,以便在將其添加到鏈中之前確保實際挖到該區塊。下面我們將難度傳給區塊。
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
大功告成!我們的區塊鏈現在擁有了 POW 來抵禦攻擊了。
測試
現在讓我們來測試一下我們的區塊鏈,看看在 POW 下添加一個新區塊會有什麼效果。我將會使用之前的代碼。我們將創建一個新的區塊鏈實例然後往裏添加 2 個區塊。
let firstClain = new Blockchain();
firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));
firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));
// 檢查是否有效(將會返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);
// 現在嘗試操作變更數據
firstClain.chain[1].data = { champion: 'korea' };
// 再次檢查是否有效 (將會返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
如果你運行了上面的代碼,你會發現添加新區塊依舊非常快。這是因爲目前的難度只有 2(或者你的電腦性能非常好)。
如果你創建了一個難度爲 5 的區塊鏈實例,你會發現你的電腦會花費大概十秒鐘來挖礦。隨着難度的提升,你的防禦攻擊的保護程度越高。
實際的難度係數與 hash 值
上面計算 hash 的過程其實就是一個簡略版本的挖礦過程,也就是計算機來計算出一個相應的 hash 值,但就像上面的所提及的並不是所有的 hash 都能夠滿足,這個條件比較苛刻,使得絕大多數的 hash 都不能夠滿足要求,需要重新計算。
在區塊鏈的協議中,有一個標準的常量和一個目標值。只有小於目標值的 hash 纔可以被使用。用常量除以難度係數,可以得到目標值,顯然,難度係數越大,目標值越小。
target = const / diffculty
否則,hash 無效只能重新計算,而 nonce 的大小就計算了相應的工作量證明。
整體代碼貼在下方
const crypto = require('crypto');
class Block {
constructor(previousHash, timestamp, data) {
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
// 工作量
this.nonce = 0;
this.hash = this.calculateHash();
}
// 計算區塊的哈希值
calculateHash() {
return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
}
// 工作量計算
mineBlock(difficulty) {
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) {
this.nonce++;
this.hash = this.calculateHash();
}
}
}
class Blockchain{
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 5;
}
// 創建當前時間下的區塊(創世塊)
createGenesisBlock() {
return new Block(0, "20/05/2022", "Genesis block", "0");
}
// 獲得區塊鏈上最新的區塊
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// 將新的區塊添加到鏈上
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
// 驗證區塊鏈是否被篡改。
// 遍歷每個區塊的hash值是否正確&&每個區塊的指向previousHash是否正確。
isChainValid() {
for (let i = 1; i < this.chain.length; i++){
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
}
module.exports.Blockchain = Blockchain;
module.exports.Block = Block;
const { Block, Blockchain } = require('./block-chain');
let firstClain = new Blockchain();
firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));
firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));
// 檢查是否有效(將會返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);
// 現在嘗試操作變更數據
firstClain.chain[1].data = { champion: 'korea' };
// 再次檢查是否有效 (將會返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
四、總結
回到一開始的問題.
小明用 js 用區塊鏈的形式在世界本的開始之前把預測的內容存儲在了這裏。並且成功預測.
這一次,終於沒有之一,成功的在女朋友面前秀了一把。
本文從一個小故事引出區塊鏈的相關內容,其作爲一門新的技術和思路,提供了一些不可篡改,分佈式數據庫的觀念,並用前端的 js 代碼來寫了一個小的 demo。
當然其作爲一種無人管理的不可隨意篡改的分佈式數據庫確實沒有很大的問題,但也有一些弊端,首先是鏈表的結構與 hash 值計算的困難導致其寫入是數據的效率並不高,需要一定的時間才能保證所有的節點同步。第二、區塊的計算所需要的一些無意義的計算,也是較爲消耗能源的。
最後本文作爲純技術分享,無任何投資建議。希望大家喜歡~
參考文章
-
https://juejin.cn/post/6844903541903982606
-
https://juejin.cn/post/6844903557649399821
-
https://juejin.cn/post/6844903575617798157
-
https://juejin.cn/post/6844903734837772301
-
https://mp.weixin.qq.com/s/feo6YuBv4x-UcsLOooLGlA
-
https://juejin.cn/post/6844903607343513613
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dJ8jeAUVyuDLo8AkxbPTBQ