LaravelZero 從零實現區塊鏈(二)工作量證明

鼓樓的夜色中發表於2020-04-30

上一節我們提到區塊鏈的核心技術點之一就是需要一種分散式一致性演算法,或者說需要一種共識機制,來實現區塊鏈這個分散式系統的對外一致性。
在比特幣中,採用的這種共識機制被稱為工作量證明(POW)

簡單來說,想要將區塊(block)新增到區塊鏈中,需要付出一定的成本。對於比特幣而言,需要做的就是為當前要新增的區塊計算一個滿足難度條件的Hash值,
這是一個困難的過程,會消耗大量的機器計算能力。當然,完成這個工作也會獲得獎勵,這個過程也被稱為挖礦。也正是由於這種困難的工作,才保證了區塊鏈的安全和一致。

首先,讓我們來定義挖礦的難度值,在config目錄下新建一個 blockchain.php

<?php
return [
    // 定義挖礦難度
    'targetBits' => 24
];

我們知道SHA256方法計算出來的結果是一個256位的二進位制數,這裡的 24 指的是算出來的雜湊前 24 位必須是 0,如果用 16 進製表示,就是前 6 位必須是 0,目前我們並不會實現一個動態調整目標的演算法,所以將難度定義為一個配置即可。

新建一個 ProofOfWork.php

<?php
namespace App\Services;

use GMP;

class ProofOfWork
{
    /**
     * @var Block $block
     */
    public $block;

    /**
     * 目標值(計算結果要小於這個目標值才有效)
     * @var GMP $target
     */
    public $target;

    public function __construct(Block $block)
    {
        $targetBits = config('blockchain.targetBits');

        $this->target = gmp_pow('2', (256 - $targetBits));

        $this->block = $block;
    }
}

由於數字很大,所以使用PHP的GMP來存放與比較。

// 當 targetBits = 0 時,targe(目標)值
0x1000000000000000000000000000000000000000000000000000000000000000
// 當 targetBits = 24 時,targe(目標)值
0x0000010000000000000000000000000000000000000000000000000000000000

可以看到,當 targetBits 越來越大,target 會越來越小,這導致要尋找一個小於 target 的雜湊會變得越來越困難。比特幣會根據全網出塊的時間動態調整挖礦難度,當全網算力變大,難度也會逐步加大。

下面,繼續為 ProofOfWork 新增兩個方法:

    public function prepareData(int $nonce): string
    {
        return implode('', [
            $this->block->prevBlockHash,
            $this->block->data,
            $this->block->timestamp,
            $this->block->data,
            $nonce
        ]);
    }

    public function run(): array
    {
        $nonce = 0;
        $hash = '';
        while (true) {
            $data = $this->prepareData($nonce);
            $hash = hash('sha256', $data);
            if (gmp_cmp('0x' . $hash, $this->target) == -1) {
                break;
            }
            $nonce++;
        }
        return [$nonce, $hash];
    }

prepareData 方法是準備要雜湊的資料,run 則是尋找小於 target 的雜湊。這裡我們引入了新變數 nonce,也就是不斷修改 nonce 的值,來看匹配出有效的雜湊。

最後我們要修改一下 Block 的建構函式

    public function __construct(string $data, string $prevBlockHash)
    {
        $this->prevBlockHash = $prevBlockHash;
        $this->data = $data;
        $this->timestamp = time();

        $pow = new ProofOfWork($this);
        list($nonce, $hash) = $pow->run();

        $this->nonce = $nonce;
        $this->hash = $hash;
    }

下面我們來測試一下之前的修改

$time1 = time();
$bc = BlockChain::NewGenesisBlock();
$bc->addBlock('i am second block');
$bc->addBlock('i am third block');
$time2 = time();

$spend = $time2 - $time1;
dd($bc->blocks, '花費時間(s):' . $spend);

// 結果:
array:3 [
  0 => App\Services\Block {#161
    +timestamp: 1587802143
    +data: "Genesis Block"
    +prevBlockHash: ""
    +hash: "000000c983da2e30e3110e40c8a9acbc321a93cef2a8409000deaeb784778f1c"
    +nonce: 3898561
  }
  1 => App\Services\Block {#159
    +timestamp: 1587802149
    +data: "i am second block"
    +prevBlockHash: "000000c983da2e30e3110e40c8a9acbc321a93cef2a8409000deaeb784778f1c"
    +hash: "000000d22a3ed3aed46da3b566e0f0705d3e540fc2061ca12a9a79369711f595"
    +nonce: 1018747
  }
  2 => App\Services\Block {#166
    +timestamp: 1587802151
    +data: "i am third block"
    +prevBlockHash: "000000d22a3ed3aed46da3b566e0f0705d3e540fc2061ca12a9a79369711f595"
    +hash: "0000002b2867b3fd52ea94c41950ed1a827fcb3515e5305f9b310c7571a89db9"
    +nonce: 10739207
  }
]
"花費時間(s):26"

可以看到,現在加入區塊已經需要一些時間了,在我的電腦上算出三個有效塊需要26s,並且算出來的 hash值都是小於目標值的。

0x0000010000000000000000000000000000000000000000000000000000000000  // target 
0x000000c983da2e30e3110e40c8a9acbc321a93cef2a8409000deaeb784778f1c  // Genesis Block
0x000000d22a3ed3aed46da3b566e0f0705d3e540fc2061ca12a9a79369711f595  // second block
0x0000002b2867b3fd52ea94c41950ed1a827fcb3515e5305f9b310c7571a89db9  // third block

Yeah!
下一篇,我們會一起實現區塊鏈資料持久化,並且構建出命令列。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章