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