深入淺出區(qū)塊鏈技術(shù)

以下文章來源于ELab團(tuán)隊(duì) ,作者ELab.houxinyu


本文為純粹區(qū)塊鏈技術(shù)分享,沒有任何投資建議。希望大家喜歡~

一、故事導(dǎo)讀
開始分享之前,引用自網(wǎng)上一個(gè)段子來引導(dǎo)大家。

《小明的故事》

小明是誰?小明是一名前端工程師,也是一個(gè)足球迷。

他有一項(xiàng)神奇的技能:他對足球有很深的理解,能夠在每屆世界杯開賽之前準(zhǔn)確預(yù)測出最終奪冠的球隊(duì)

比如,在 2010 年的那屆世界杯,小明就預(yù)測出了正確的結(jié)果。大賽閉幕,小明難掩興奮之情,想在女朋友面前顯擺一下。

女朋友很自然地提出質(zhì)疑,而小明并沒有證據(jù)證明自己,只能啞口無言。

小明痛定思痛,決定寫一個(gè)網(wǎng)站來提前記錄自己的預(yù)言。

小明自己設(shè)計(jì)了網(wǎng)頁界面。
找小伙伴幫忙寫了一個(gè)后端服務(wù),提供兩個(gè)接口。
小明基于這兩個(gè)接口,寫了一個(gè)純前端渲染的網(wǎng)站。
最終網(wǎng)站看起來是這個(gè)樣子的:

接下來,小明靜靜等待下一屆世界杯的到來。

時(shí)間過得很快,轉(zhuǎn)眼到了 2014 年。這一次,小明再次正確預(yù)測出了冠軍得主。

有網(wǎng)站記錄預(yù)言,小明心想,這次女朋友應(yīng)該會(huì)相信自己了吧!

然而……

女朋友也是懂技術(shù)的,她這次仍然提出了一個(gè)合理的質(zhì)疑。小明再次無言以對。

那么問題來了,該怎么辦能夠讓女朋友相信自己呢?

如果現(xiàn)在還有沒結(jié)論,可以繼續(xù)向下看。

二、基礎(chǔ)概念
區(qū)塊鏈技術(shù)中有很多新的概念,對于一些并不深入這個(gè)領(lǐng)域的同學(xué)來說,相對不是很友好。本文先對一些技術(shù)的概念進(jìn)行講解。作為前置的知識。

區(qū)塊鏈的概念
特殊的分布式數(shù)據(jù)庫。

一種鏈表結(jié)構(gòu),鏈表中元素作為一個(gè)區(qū)塊。而每個(gè)鏈表的結(jié)構(gòu)包括:

timestamp: 區(qū)塊產(chǎn)生時(shí)間戳
nonce: 與區(qū)塊頭的hash值共同證明計(jì)算量(工作量)
data: 區(qū)塊鏈上存儲(chǔ)的數(shù)據(jù)
previousHash: 上一個(gè)區(qū)塊的hash
hash: 本區(qū)塊鏈的hash,由上述幾個(gè)屬性進(jìn)行哈希計(jì)算而得
暫時(shí)無法在飛書文檔外展示此內(nèi)容

一些特點(diǎn)
去中心化存儲(chǔ)
分布式數(shù)據(jù)庫很早之前就已經(jīng)出現(xiàn),但與之不同的是區(qū)塊鏈?zhǔn)且粋€(gè)沒有管理者的、無中心化的分布式數(shù)據(jù)庫。其起初的設(shè)計(jì)目標(biāo)就是防止出現(xiàn)位于中心地位的管理者當(dāng)局。

那么下一個(gè)問題就來了,如果沒有一個(gè)管理者進(jìn)行數(shù)據(jù)的管理,如何保證這個(gè)分布式數(shù)據(jù)庫中的數(shù)據(jù)是可信任的呢?這就要提到下一個(gè)不可修改的特性了。

不可篡改

區(qū)塊鏈上的數(shù)據(jù)是不可篡改的,大家都這樣說。但其實(shí),數(shù)據(jù)是可以改的,只是說改了以后就你自己認(rèn),而且被修改數(shù)據(jù)所在區(qū)塊之后的所有區(qū)塊都會(huì)失效。區(qū)塊鏈網(wǎng)絡(luò)有一個(gè)同步邏輯,整個(gè)區(qū)塊鏈網(wǎng)絡(luò)總是保持所有節(jié)點(diǎn)使用最長的鏈,那么你修改完之后,一聯(lián)網(wǎng)同步,修改的東西又會(huì)被覆蓋。這是不可篡改的一個(gè)方面。

更有意思的是,區(qū)塊鏈通過加密校驗(yàn),保證了數(shù)據(jù)存取需要經(jīng)過嚴(yán)格的驗(yàn)證,而這些驗(yàn)證幾乎又是不可偽造的,所以也很難篡改。加密并不代表不可篡改,但不可篡改是通過加密以及經(jīng)濟(jì)學(xué)原理搭配實(shí)現(xiàn)的。這還有點(diǎn)玄學(xué)的味道,一個(gè)純技術(shù)實(shí)現(xiàn)的東西,還要靠理論來維持。但事實(shí)就是這樣。這就是傳說中的挖礦。

挖礦過程其實(shí)是礦工爭取創(chuàng)建一個(gè)區(qū)塊的過程,一旦挖到礦,也就代表這個(gè)礦工有資格創(chuàng)建新區(qū)塊。怎么算挖到礦呢?通過一系列復(fù)雜的加密算法,從0開始到∞,找到一個(gè)滿足難度的hash值,得到這個(gè)值,就是挖到礦。這個(gè)算法過程被稱為“共識機(jī)制”,也就是通過什么形式來決定誰擁有記賬權(quán),共識機(jī)制有很多種,區(qū)塊鏈采用哪種共識機(jī)制最佳,完全是由區(qū)塊鏈的實(shí)際目的結(jié)合經(jīng)濟(jì)學(xué)道理來選擇。

除了這些,區(qū)塊鏈里面的加密比比皆是,這些加密規(guī)則和算法,使得整個(gè)區(qū)塊鏈遵循一種規(guī)律,讓篡改數(shù)據(jù)的成本特別高,以至于參與的人對篡改數(shù)據(jù)都沒有興趣,甚至忌憚。這又是玄學(xué)的地方。

針對這些不可篡改的特性,我們是不是能夠解決一開始提出的問題呢。

用js來寫一段區(qū)塊鏈的代碼,來解決小明的困惑。






三、【實(shí)戰(zhàn)】用JavaScript來寫一個(gè)基本的區(qū)塊鏈demo。
實(shí)現(xiàn)一個(gè)基本的區(qū)塊鏈
創(chuàng)建區(qū)塊
區(qū)塊鏈?zhǔn)怯稍S許多多的區(qū)塊鏈接在一起的(這聽上去好像沒毛病..)。鏈上的區(qū)塊通過某種方式允許我們檢測到是否有人操縱了之前的任何區(qū)塊。

那么我們?nèi)绾未_保數(shù)據(jù)的完整性呢?每個(gè)區(qū)塊都包含一個(gè)基于其內(nèi)容計(jì)算出來的hash。同時(shí)也包含了前一個(gè)區(qū)塊的hash。

下面是一個(gè)區(qū)塊類用JavaScript寫出來大致的樣子:采用構(gòu)造函數(shù)初始化區(qū)塊的屬性。

在這里的哈希值是無法修改的。我們能夠看到,哈希值是由多個(gè)元素組成的,一旦一個(gè)哈希值受到了修改,意味著previousHash被修改了,這個(gè)時(shí)候如果想要繼續(xù)修改就要對下一個(gè)區(qū)塊進(jìn)行操作,否則修改的區(qū)塊就不具有意義了。而哈希值的計(jì)算非常耗時(shí),同時(shí)修改51%以上的節(jié)點(diǎn)基本不可能,所以,這種聯(lián)動(dòng)機(jī)制也就保證了其不可修改的特性。

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();
  }

    // 計(jì)算區(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ù)組來初始化整個(gè)鏈。這樣一來,第一個(gè)區(qū)塊是特殊的,因?yàn)樗]有指向前一個(gè)區(qū)塊。并且添加了兩個(gè)方法:

getLatestBlock()返回我們區(qū)塊鏈上最新的區(qū)塊。
addBlock()負(fù)責(zé)將新的區(qū)塊添加到我們的鏈上。為此,我們將前一個(gè)區(qū)塊的hash添加到我們新的區(qū)塊中。這樣我們就可以保持整個(gè)鏈的完整性。因?yàn)橹灰覀冏兏俗钚聟^(qū)塊的內(nèi)容,我們就需要重新計(jì)算它的hash。當(dāng)計(jì)算完成后,我將把這個(gè)區(qū)塊推進(jìn)鏈里(一個(gè)數(shù)組)。
最后,我創(chuàng)建一個(gè)isChainValid()來確保沒有人篡改過區(qū)塊鏈。它會(huì)遍歷所有的區(qū)塊來檢查每個(gè)區(qū)塊的hash是否正確。它會(huì)通過比較previousHash來檢查每個(gè)區(qū)塊是否指向正確的上一個(gè)區(qū)塊。如果一切都沒有問題它會(huì)返回true否則會(huì)返回false。

class Blockchain{
    constructor() {
      this.chain = [this.createGenesisBlock()];
    }
 
    // 創(chuàng)建當(dāng)前時(shí)間下的區(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);
    }
 
    // 驗(yàn)證區(qū)塊鏈?zhǔn)欠癖淮鄹摹?br>    // 遍歷每個(gè)區(qū)塊的hash值是否正確&&每個(gè)區(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)建了一個(gè)區(qū)塊鏈的實(shí)例,并在其中添加區(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'}));

// 檢查是否有效(將會(huì)返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);

// 現(xiàn)在嘗試操作變更數(shù)據(jù)
firstClain.chain[1].data = {  champion: 'korea'  };

// 再次檢查是否有效 (將會(huì)返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
嘗試修改數(shù)據(jù)
我會(huì)在一開始通過運(yùn)行isChainValid()來驗(yàn)證整個(gè)鏈的完整性。我們操作過任何區(qū)塊,所以它會(huì)返回true。

之后我將鏈上的第一個(gè)(索引為1)區(qū)塊的數(shù)據(jù)進(jìn)行了變更。之后我再次檢查整個(gè)鏈的完整性,發(fā)現(xiàn)它返回了false。我們的整個(gè)鏈不再有效了。

// 檢查是否有效(將會(huì)返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);

// 現(xiàn)在嘗試操作變更數(shù)據(jù)
firstClain.chain[1].data = {  champion: 'korea'  };

// 再次檢查是否有效 (將會(huì)返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
POW(proof-of-work)工作量證明
POW是在第一個(gè)區(qū)塊鏈被創(chuàng)造之前就已經(jīng)存在的一種機(jī)制。這是一項(xiàng)簡單的技術(shù),通過一定數(shù)量的計(jì)算來防止濫用。工作量是防止垃圾填充和篡改的關(guān)鍵。如果它需要大量的算力,那么填充垃圾就不再值得。

比特幣通過要求hash以特定0的數(shù)目來實(shí)現(xiàn)POW。這也被稱之為難度

不過等一下!一個(gè)區(qū)塊的hash怎么可以改變呢?在比特幣的場景下,一個(gè)區(qū)塊包含有各種金融交易信息。我們肯定不希望為了獲取正確的hash而混淆了那些數(shù)據(jù)。

為了解決這個(gè)問題,區(qū)塊鏈添加了一個(gè)nonce值。Nonce是用來查找一個(gè)有效Hash的次數(shù)。而且,因?yàn)闊o法預(yù)測hash函數(shù)的輸出,因此在獲得滿足難度條件的hash之前,只能大量組合嘗試。尋找到一個(gè)有效的hash(創(chuàng)建一個(gè)新的區(qū)塊)在圈內(nèi)稱之為挖礦。

在比特幣的場景下,POW確保每10分鐘只能添加一個(gè)區(qū)塊。你可以想象垃圾填充者需要多大的算力來創(chuàng)造一個(gè)新區(qū)塊,他們很難欺騙網(wǎng)絡(luò),更不要說篡改整個(gè)鏈。

暫時(shí)無法在飛書文檔外展示此內(nèi)容

我們該如何實(shí)現(xiàn)呢?我們先來修改我們區(qū)塊類并在其構(gòu)造函數(shù)中添加Nonce變量。我會(huì)初始化它并將其值設(shè)置為0。

  constructor(previousHash, timestamp, data) {
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.data = data;
    // 工作量
    this.nonce = 0;
    this.hash = this.calculateHash();
 }
我們還需要一個(gè)新的方法來增加Nonce,直到我們獲得一個(gè)有效hash。強(qiáng)調(diào)一下,這是由難度決定的。所以我們會(huì)收到作為參數(shù)的難度。

    // 工作量計(jì)算
    mineBlock(difficulty) {
      while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
最后,我們還需要更改一下calculateHash()函數(shù)。因?yàn)槟壳八€沒有使用Nonce來計(jì)算hash。

    // 計(jì)算區(qū)塊的哈希值
    calculateHash() {
      return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
    }
將它們結(jié)合在一起,你會(huì)得到如下所示的區(qū)塊類:

class Block {

  constructor(previousHash, timestamp, data) {
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.data = data;
    // 工作量
    this.nonce = 0;
    this.hash = this.calculateHash();
  }

    // 計(jì)算區(qū)塊的哈希值
    calculateHash() {
      return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
    }

    // 工作量計(jì)算
    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ū)塊鏈中添加一個(gè)新的屬性來跟蹤整條鏈的難度。我會(huì)將它設(shè)置為2(這意味著區(qū)塊的hash必須以2個(gè)0開頭)。






constructor() {
  this.chain = [this.createGenesisBlock()];
  this.difficulty = 2;
}
現(xiàn)在剩下要做的就是改變addBlock()方法,以便在將其添加到鏈中之前確保實(shí)際挖到該區(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下添加一個(gè)新區(qū)塊會(huì)有什么效果。我將會(huì)使用之前的代碼。我們將創(chuàng)建一個(gè)新的區(qū)塊鏈實(shí)例然后往里添加2個(gè)區(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'}));

// 檢查是否有效(將會(huì)返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);

// 現(xiàn)在嘗試操作變更數(shù)據(jù)
firstClain.chain[1].data = {  champion: 'korea'  };

// 再次檢查是否有效 (將會(huì)返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
如果你運(yùn)行了上面的代碼,你會(huì)發(fā)現(xiàn)添加新區(qū)塊依舊非常快。這是因?yàn)槟壳暗碾y度只有2(或者你的電腦性能非常好)。

如果你創(chuàng)建了一個(gè)難度為5的區(qū)塊鏈實(shí)例,你會(huì)發(fā)現(xiàn)你的電腦會(huì)花費(fèi)大概十秒鐘來挖礦。隨著難度的提升,你的防御攻擊的保護(hù)程度越高。

實(shí)際的難度系數(shù)與hash值
上面計(jì)算hash的過程其實(shí)就是一個(gè)簡略版本的挖礦過程,也就是計(jì)算機(jī)來計(jì)算出一個(gè)相應(yīng)的hash值,但就像上面的所提及的并不是所有的hash都能夠滿足,這個(gè)條件比較苛刻,使得絕大多數(shù)的hash都不能夠滿足要求,需要重新計(jì)算。

在區(qū)塊鏈的協(xié)議中,有一個(gè)標(biāo)準(zhǔn)的常量和一個(gè)目標(biāo)值。只有小于目標(biāo)值的hash才可以被使用。用常量除以難度系數(shù),可以得到目標(biāo)值,顯然,難度系數(shù)越大,目標(biāo)值越小。

target = const / diffculty
否則,hash無效只能重新計(jì)算,而nonce的大小就計(jì)算了相應(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();
  }

    // 計(jì)算區(qū)塊的哈希值
    calculateHash() {
      return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex');
    }

    // 工作量計(jì)算
    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)前時(shí)間下的區(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);
    }
 
    // 驗(yàn)證區(qū)塊鏈?zhǔn)欠癖淮鄹摹?br>    // 遍歷每個(gè)區(qū)塊的hash值是否正確&&每個(gè)區(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'}));

// 檢查是否有效(將會(huì)返回true)
console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);

// 現(xiàn)在嘗試操作變更數(shù)據(jù)
firstClain.chain[1].data = {  champion: 'korea'  };

// 再次檢查是否有效 (將會(huì)返回false)
console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);
四、總結(jié)
回到一開始的問題.

小明用js用區(qū)塊鏈的形式在世界本的開始之前把預(yù)測的內(nèi)容存儲(chǔ)在了這里。并且成功預(yù)測.

這一次,終于沒有之一,成功的在女朋友面前秀了一把。

本文從一個(gè)小故事引出區(qū)塊鏈的相關(guān)內(nèi)容,其作為一門新的技術(shù)和思路,提供了一些不可篡改,分布式數(shù)據(jù)庫的觀念,并用前端的js代碼來寫了一個(gè)小的demo。

當(dāng)然其作為一種無人管理的不可隨意篡改的分布式數(shù)據(jù)庫確實(shí)沒有很大的問題,但也有一些弊端,首先是鏈表的結(jié)構(gòu)與hash值計(jì)算的困難導(dǎo)致其寫入是數(shù)據(jù)的效率并不高,需要一定的時(shí)間才能保證所有的節(jié)點(diǎn)同步。第二、區(qū)塊的計(jì)算所需要的一些無意義的計(jì)算,也是較為消耗能源的。

最后本文作為純技術(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)小兵