第二章 工做量證實和挖礦

概覽

本章節咱們將會在咱們的玩具版區塊鏈的基礎上加入工做量證實(POW)的支持。在第一章節的版本中, 任何人都均可以在沒有任何工做量證實的狀況下添加一個區塊到區塊鏈中。 當咱們引入工做量證實機制以後,一個節點必需要解開一個有至關計算量的拼圖(POW Puzzle)以後,才能往區塊鏈上添加一個新的區塊。而去解開該拼圖,一般就被稱爲挖礦。node

引入工做量證實機制以後,咱們還能夠對一個新區塊的產出時間做出大體的控制。大概的作法就是動態的改變拼圖的難易程度來達到控制的效果:若是最近的區塊產生的太快了,那麼就將拼圖的難度提高,反之,則將拼圖的難度下降。git

須要點出來的時,本章節中咱們尚未引入 交易(Transaction) 這個概念。這就意味着礦工挖出一個區塊後,並不會得到相應的獎勵。 通常來講,在加密貨幣中,若是礦工挖到了一個區塊,是應該得到必定量的幣做爲激勵的。github

工做量證實拼圖和難易度

在上一個章節的區塊鏈的基礎上,咱們將會爲區塊結構加入兩個新的屬性:difficulty和nonce。要弄清楚這倆貨是幹嗎用的,咱們必須先對工做量證實拼圖做一些必要的闡述。算法

工做量證實拼圖是一個什麼樣的任務呢?其實就是去計算出一個知足條件的區塊哈希。怎麼纔算知足條件呢?若是這個計算出來的哈希的前面的0的數目知足指定的個數,那麼就算知足條件。那麼這個個數又是誰指定的呢?對,就是上面的這個difficulty指定的。若是difficulty指定說哈希前面要有4個0,但你計算出來的哈希前面只有3個0,那麼這個哈希就不是個有效的哈希。typescript

下圖展現的就是不一樣難易程度的狀況下,哈希是否有效:shell

image

如下代碼用來檢查指定difficulty下哈希是否有效:npm

const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => {
    const hashInBinary: string = hexToBinary(hash);
    const requiredPrefix: string = '0'.repeat(difficulty);
    return hashInBinary.startsWith(requiredPrefix);
};

區塊的哈希是經過對區塊的內容算sha256來得到的,經過相同的區塊內容作哈希,咱們是沒法算出符合指定difficulty的哈希, 由於內容一直沒有變,哈希也就一直不變, 這就是爲何咱們引入了nonce這個屬性到區塊中,咱們能夠控制nonce的改變來得到不一樣的哈希值。只要咱們的內容有任何一點點的改變,算出來的哈希就確定是不同的。一旦相應的nonce修改後讓咱們得到到指定difficulty的哈希,咱們挖礦也就成功了!api

既然加入了difficulty和nonce到區塊,咱們仍是先看看區塊結構如今長什麼樣吧:網絡

class Block {

    public index: number;
    public hash: string;
    public previousHash: string;
    public timestamp: number;
    public data: string;
    public difficulty: number;
    public nonce: number;

    constructor(index: number, hash: string, previousHash: string,
                timestamp: number, data: string, difficulty: number, nonce: number) {
        this.index = index;
        this.previousHash = previousHash;
        this.timestamp = timestamp;
        this.data = data;
        this.hash = hash;
        this.difficulty = difficulty;
        this.nonce = nonce;
    }
}

固然,咱們的創世區塊是硬編碼的,記得把它也給更新成相應的結構哦。curl

挖礦

如上所述, 爲了解開咱們的工做量證實拼圖這個任務,咱們須要不停的修改區塊中的nonce而後計算修改後的哈希,直到算出知足條件的哈希。這個知足條件的哈希何時纔會跑出來則徹底是個隨機的事情。因此咱們要作的就是給nonce一個初始值,而後不停的循環,修改,計算哈希,直到知足條件的心儀的它的出現。

const findBlock = (index: number, previousHash: string, timestamp: number, data: string, difficulty: number): Block => {
    let nonce = 0;
    while (true) {
        const hash: string = calculateHash(index, previousHash, timestamp, data, difficulty, nonce);
        if (hashMatchesDifficulty(hash, difficulty)) {
            return new Block(index, hash, previousHash, timestamp, data, difficulty, nonce);
        }
        nonce++;
    }
};

一旦知足條件的區塊的哈希給找到了,挖礦成功!而後咱們就須要將該區塊廣播到網絡上,讓其餘節點接受咱們的區塊,並更新最新的帳本。這個和第一章節的狀況並沒有二致。

難易度共識

咱們如今已經擁有找出和驗證知足指定難易度的區塊的哈希的手段了,可是,這個難易程度是如何決定的呢?必需要有一個方法讓全網各個節點一致認同這個決定。否則個人難易度是要挖半天才能出來,別人的是幾毫秒就出來,那這個區塊鏈網絡就有問題。

因此,這裏必需要有一個方法來讓全部的節點都一致認同當前挖礦的難易度。爲了作到這一點,咱們首先引入一些用於計算當前難易度的規則。

先定義如下的一些常量:

  • BLOCK_GENERATION_INTERVAL: 定義 一個區塊產出的 頻率(比特幣中該值是設置成10分鐘的, 即每10分鐘產出一個區塊)
  • DIFFICULTY_ADJUSTMENT_INTERVAL: 定義 修改難易度以適應網絡不斷增長或者下降的網絡算力(hashRate, 即每秒能計算的哈希的數量)的 頻率(比特幣中是設置成2016個區塊, 即每2016個區塊,而後根據這些區塊生成的耗時,作一次難易度調整)

這裏咱們將會把區塊產出間隔設置成10秒,難易度調整間隔設置成10個區塊。這些常量是不會隨着時間而改變的,因此咱們將其硬編碼以下:

// in seconds
const BLOCK_GENERATION_INTERVAL: number = 10;

// in blocks
const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10;

有了這些規則,咱們就能夠在咱們的網絡上達成難易度的一致性了。每產出10個區塊以後,咱們就去檢查生成全部這10個區塊所消耗的時間,而後和預期時間進行對比,而後對難易度進行動態的調整。

這裏的預期時間是這樣指定的:BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL。 該預期時間表明瞭當前網絡的算力剛恰好和當前的難易度吻合。

若是耗時超過或者不足預期時間的兩倍,那麼咱們就會將difficulty進行對應的減1或者加1。該算法以下:

const getDifficulty = (aBlockchain: Block[]): number => {
    const latestBlock: Block = aBlockchain[blockchain.length - 1];
    if (latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && latestBlock.index !== 0) {
        return getAdjustedDifficulty(latestBlock, aBlockchain);
    } else {
        return latestBlock.difficulty;
    }
};

const getAdjustedDifficulty = (latestBlock: Block, aBlockchain: Block[]) => {
    const prevAdjustmentBlock: Block = aBlockchain[blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL];
    const timeExpected: number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL;
    const timeTaken: number = latestBlock.timestamp - prevAdjustmentBlock.timestamp;
    if (timeTaken < timeExpected / 2) {
        return prevAdjustmentBlock.difficulty + 1;
    } else if (timeTaken > timeExpected * 2) {
        return prevAdjustmentBlock.difficulty - 1;
    } else {
        return prevAdjustmentBlock.difficulty;
    }
};

時間戳校驗

第一章節中的區塊鏈版本中,區塊結構中的時間戳屬性是沒有任何意義的,由於咱們不會對其做任何校驗的工做,也就是說,咱們其實能夠給該時間戳賦以任何內容。 但如今狀況變了,由於咱們這裏引入了難易度的動態調整,而該調整是基於前10個區塊產出的耗時總長度的。因此時間戳咱們就不能像以前同樣隨意賦值了。

既然時間戳的大小會影響區塊產出難易度的調整,因此別有用心的人就會考慮去惡意設置一個錯誤的時間戳來嘗試操縱咱們的難易度,以實現對咱們的區塊鏈網絡進行攻擊。爲了規避這種風險,咱們須要引入如下的規則:

  • 新區塊時間戳比當前時間最晚不晚過1分鐘,那麼咱們認爲該區塊有效。
  • 新區塊時間戳比前一區塊的時間戳最先不早過1分鐘,那麼咱們任務該區塊在區塊鏈中是有效的。
const isValidTimestamp = (newBlock: Block, previousBlock: Block): boolean => {
    return ( newBlock.timestamp - 60 < getCurrentTimestamp() )
        && ( previousBlock.timestamp - 60 < newBlock.timestamp );
};

天地會珠海分舵注:這裏爲何要有一分鐘的緩衝呢?估計是爲了既考慮必定程度的容錯,也減緩了時間戳惡意修改的攻擊。若是還有其餘緣由的,請不吝指教。

累積難易度

還記得第一章節的區塊鏈的版本中,咱們定的規則是:一旦發生衝突,咱們老是選擇最長的區塊鏈做爲有效的鏈進行更新,以示對更多區塊生產者的努力的確定。由於咱們如今引入了挖礦的難易度,因此咱們不該該再這樣子作了,咱們更應該對投入資源更多計算而出的那條鏈進行確定。也就是說,如今正確的鏈,不是最長的那條鏈,而是累積難易度最大的那條鏈。什麼意思呢?換個說法就是,正確的鏈,應該是那條消耗了最多計算資源(網絡算力*耗時)而產生出來的鏈。

那麼怎麼算出一條鏈的累積難易度呢?咱們首先對區塊鏈中每一個區塊的difficulty進行2^difficulty計算,而後將每一個區塊的計算結果加起來,最終就是這條鏈的累積難易度。

這裏爲何咱們用2^difficulty來計算呢?由於difficulty表明了哈希的二進制表示中前面的0的位數。試想下,一個difficulty是11的塊和一個difficulty是5的塊相比,總的來講,咱們須要多計算2^(11-5) = 2^6 次哈希才能得到想要的結果。由於每一位在二進制中都有0和1兩種變化,他們之間相差6個位,固有2^6個變化。

下圖中,雖然鏈A比鏈B長,但由於鏈B的累積難易度比鏈A大,因此鏈B纔是正確的鏈.

image

同時咱們還要注意的是,咱們累積難易度只和區塊的difficulty屬性有關係,和區塊的真實哈希即其前面的位數是沒有任何關係的。 拿一個difficulty爲4的區塊來講,假如它的哈希是000000a34c...(該哈希同時也知足difficulty爲5和6的狀況),咱們計算累積難易度是仍是會以2^4來算,而不是2^5或者2^6, 即便它前面有6個0.

這種根據難易度選擇正確的鏈的策略也叫作中本聰共識(Nakamoto consensus), 這也是中本聰的比特幣中最重要的一個發明之一。一旦帳本出現分叉,礦工們就必須選擇一條鏈來投入資源繼續進行挖礦,由於這個關係到礦工挖礦後的激勵,因此選擇正確的鏈必須全網達成共識。

驗證測試

  • 安裝運行
npm install
npm run node1
npm run node2
  • 挖礦

咱們能夠嘗試經過curl或者postman調用/mineBlock這個api接口來進行挖礦,並查看對應的輸出.

  • 難易度調整

在經過不一樣的時間間隔調用完10次/mineBlock這個接口以後,難易度就會相應的進行變化

  • 選擇最大累積難易度的鏈

這個真實狀況很差模擬出來。 可是咱們能夠去驗證對應的算法。咱們能夠先開一個節點,建立3個以上的區塊以後,再開第二個節點。這時節點2就會去獲取節點1的全部區塊,而後運行對應的邏輯去選擇最大累積難易度的鏈。

小結

工做量證實拼圖的一個重要特色就是難以解開但易於驗證。因此不斷調整nonce以算出一個知足必定難度的SHA256哈希,而後簡單的驗證前面幾位是不是0,每每就是這種問題最簡單的解決方案。

有了工做量證實機制後,節點如今就須要挖礦,也就是解決工做量證實拼圖,才能往區塊鏈中新增長一個區塊了。在下一章節中,咱們將會爲咱們的區塊鏈引入交易(Transaction)功能。

本章節代碼請查看這裏

第三章

本文由天地會珠海分舵編譯,轉載需受權,喜歡點個贊,吐槽請評論,如能給Github上的項目給個星,將不勝感激.

相關文章
相關標籤/搜索