基於Java語言構建區塊鏈(二)—— 工作量證明

wangwei_hz發表於2018-03-22

基於Java語言構建區塊鏈(二)—— 工作量證明

最終內容請以原文為準:https://wangwei.one/posts/7890ab7e.html

引言

上一篇文章中,我們實現了區塊鏈最基本的資料結構模型,新增區塊以及和前一個區塊連線在一起。但是,我們的實現方式非常簡單,而真實的比特幣區塊鏈中,每一個區塊的新增都是需要經過大量的計算才可以完成,這個過程就是我們熟知的挖礦

工作量證明機制

區塊鏈最關鍵的一個思想就是,必須進行大量且困難的計算工作才能將交易資料存放到區塊鏈上。這種工作機制才能保證整個區塊鏈資料的安全性和一致性。同時,完成這個計算工作的礦工會得到相應的Token獎勵。

這套機制和我們的現實生活非常相似:我們必須努力工作來賺取報酬用以維持我們的生活。在區塊鏈中,網路中的礦工們努力工作來維持區塊鏈網路,為其新增區塊,並且獲得一定的Token獎勵。作為他們工作的成果,一個區塊以安全的方式被組合進了區塊鏈中,這樣就保證了整個區塊鏈資料庫的穩定性。還有一個必須要注意的是,某個礦工完成了計算工作的結果,還必須得到其他所有礦工的認同(證明是正確的),這樣才算完成。

這一整套的計算和證明機制,就稱為Proof-of-Work(工作量證明)。計算工作是非常非常困難的,因為它需要消耗大量的計算機算力資源,即使是效能非常高的計算機都不能非常快地計算出正確的結果。此外,隨著時間的推移,這項計算工作的難度也會隨之增加,目的是為了保證每小時6個新區塊的出塊率。在比特幣中,這種工作的目標是找到滿足某個特定要求的區塊Hash(雜湊值)。這個區塊雜湊值就是工作結果的一個證明。因此,計算工作的目的就是為了尋找到這個證明值。

最後要注意的是,計算出這個特定的Hash(雜湊值)是非常困難的,但是別人來驗證這個Hash值是否正確的時候,是非常簡單的,一下子就能完成。

Hashing

Hash:雜湊 | 雜湊

我們來討論一下Hashing(雜湊),對這一塊非常熟悉的朋友可以直接跳過這一段內容。

雜湊是一種計算機演算法,該演算法能夠計算出任意大小資料的雜湊值,並且這個雜湊值的長度是固定的,256bit。這個被計算出來的雜湊值能夠作為這個資料的唯一代表。雜湊演算法有幾個關鍵的特性:

  • 不可逆性。不能根據一個雜湊值推匯出原始資料。所以,雜湊不是加密。
  • 唯一性。每個資料有且僅有一個唯一的雜湊值。
  • 迥異性。原始資料一丁點的變化都將得到完全不一樣的雜湊值。

例如:

SHA256("wangwei1") ——> 1e898b7c9adaad86c20139a302ccd5277f81040cab68dc2aecfc684773532652
SHA256("wangwei2") ——> c9cc7417c17318c8aab448cc8ace24c53b6dcf350f5c5fd8e91cbc3b011a179d
複製程式碼

雜湊演算法被廣泛用於驗證檔案的一致性上。比如軟體提供商通常會在安裝包上附加一個檢驗碼(checksums),當我們下載完一個軟體安裝包後,可以用雜湊函式計算一下這個軟體安裝包的雜湊值,然後再和軟體安裝包的檢驗碼做個對比,就可以知道下載的安裝包是否完整、是否有資料丟失。

在區塊鏈中,雜湊值用於保證區塊的一致性。每一個區塊被用於進行雜湊計算的資料,都包含前一個區塊鏈的雜湊值,因此任何人想要修改區塊的資料幾乎是不可能的,他必須要把整個區塊鏈中從創世區塊到最新的區塊的所有雜湊值全部重新計算一遍。

你可以腦補一下這個工作量有多大,按照目前計算機的算力來看,幾乎不可能

Hashcash

比特幣的工作量證明是使用的是Hashcash演算法,一種最初被用於反垃圾郵件的演算法,它可以被拆解為以下幾步:

  1. 獲取某種公開可知的資料data(在郵件案例中,指的是收件人郵件地址;比特幣案例中,指的是區塊頭)
  2. 新增一個計數器counter,初始值設定為0;
  3. 計算 data 與 counter拼接字串的雜湊值;
  4. 檢查上一步的雜湊值是否滿足某個條件,滿足則停止計算,不滿足則 counter 加1,然後重複第3步和第4步,直到滿足這個特定的條件為止。

這是一種粗暴的演算法:你改變計數器,計算一個新的雜湊值,檢查它,增加計數器,計算一個新的雜湊值,迴圈往復,這就是為什麼它需要花費大量計算機算力資源的原因所在。

讓我們來近距離看一下這個特定的條件指的是什麼。在原始的Hashcash演算法中,這個特殊的要求指的是計算出來的雜湊值的前20bit必須全是零,

在比特幣種,這種要求雜湊值前面有多少個零打頭的要求是隨著時間的推移而不斷調整的,這是出於設計的目的,儘管在計算機的算力會不斷的提升和越來越多的礦工加入這個網路中的情況下,都要保證每10min生產一個區塊。

我們演示一下這個演算法,

# 計算字串'I like donuts'的雜湊值
SHA256("I like donuts") 
——> f80867f6efd4484c23b0e7184e53fe4af6ab49b97f5293fcd50d5b2bfa73a4d0

# 拼接一個計數器值(ca07ca),再次進行Hash計算
SHA256("I like donutsca07ca") 
——> 0000002f7c1fe31cb82acdc082cfec47620b7e4ab94f2bf9e096c436fc8cee06
複製程式碼

這裡的ca07ca是計數器值的十六進位制,他表示的十進位制值為13240266

即,從0開始,總共計算了13240266次,才計算出I like donuts這個資料的Hash值,滿足前6位(3位元組)全是零。

程式碼實現

思路:

1)每次區塊被新增到區塊鏈之前,先要進行挖礦(Pow)

2)挖礦過程中,產生的 Hash 值,如果小於難度目標值則新增進區塊,否則繼續挖礦,直到找到正確的Hash為止

3)最後,驗證區塊Hash是否有效

定義Pow類

/**
 * 工作量證明
 *
 * @author wangwei
 * @date 2018/02/04
 */
@Data
public class ProofOfWork {

    /**
     * 難度目標位
     */
    public static final int TARGET_BITS = 20;

    /**
     * 區塊
     */
    private Block block;
    /**
     * 難度目標值
     */
    private BigInteger target;

    private ProofOfWork(Block block, BigInteger target) {
        this.block = block;
        this.target = target;
    }
  
    /**
     * 建立新的工作量證明,設定難度目標值
     * <p>
     * 對1進行移位運算,將1向左移動 (256 - TARGET_BITS) 位,得到我們的難度目標值
     * 
     * @param block
     * @return 
     */
    public static ProofOfWork newProofOfWork(Block block) {
        BigInteger targetValue = BigInteger.valueOf(1).shiftLeft((256 - TARGET_BITS));
        return new ProofOfWork(block, targetValue);
    }
}
複製程式碼
  • 設定一個難度目標位TARGET_BITS,表示最終挖礦挖出來Hash值,轉化為二進位制後,與256相比,長度少了多少bit,也即二進位制前面有多少bit是零.

    • TARGET_BITS 越大,最終targetValue就越小,要求計算出來的Hash越來越小,也就是挖礦的難度越來越大。
    • 我們這裡的TARGET_BITS是固定的,但是在真實的比特幣中,難度目標是隨著時間的推推,會動態調整的。詳見:《精通比特幣 (第二版)》第10章
  • 由於數值比較大,這裡要使用BitInteger型別。

準備資料

/**
 * 準備資料
 * <p>
 * 注意:在準備區塊資料時,一定要從原始資料型別轉化為byte[],不能直接從字串進行轉換
 * @param nonce
 * @return
 */
private String prepareData(long nonce) {
   byte[] prevBlockHashBytes = {};
   if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) {
       prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray();
   }

   return ByteUtils.merge(
           prevBlockHashBytes,
           this.getBlock().getData().getBytes(),
           ByteUtils.toBytes(this.getBlock().getTimeStamp()),
           ByteUtils.toBytes(TARGET_BITS),
           ByteUtils.toBytes(nonce)
    );
}
複製程式碼
  • 參與Hash運算的如下幾個資訊:
    • 前一個區塊(父區塊)的Hash值;
    • 區塊中的交易資料;
    • 區塊生成的時間;
    • 難度目標;
    • 用於工作量證明演算法的計數器

詳見:《精通比特幣 (第二版)》第09章

Pow演算法

/**
 * 執行工作量證明,開始挖礦,找到小於難度目標值的Hash
 *
 * @return
 */
public PowResult run() {
    long nonce = 0;
    String shaHex = "";
    System.out.printf("Mining the block containing:%s \n", this.getBlock().getData());

    long startTime = System.currentTimeMillis();
    while (nonce < Long.MAX_VALUE) {
        String data = this.prepareData(nonce);
        shaHex = DigestUtils.sha256Hex(data);
        if (new BigInteger(shaHex, 16).compareTo(this.target) == -1) {
            System.out.printf("Elapsed Time: %s seconds \n", (float) (System.currentTimeMillis() - startTime) / 1000);
            System.out.printf("correct hash Hex: %s \n\n", shaHex);
            break;
         } else {
            nonce++;
         }
     }
     return new PowResult(nonce, shaHex);
}
複製程式碼
  • 迴圈體裡面主要以下四步:
    • 準備資料
    • 進行sha256運算
    • 轉化為BigInter型別
    • 與target進行比較
  • 最後,返回正確的Hash值以及運算計數器nonce

驗證區塊Hash有效性

/**
 * 驗證區塊是否有效
 *
 * @return
 */
public boolean validate() {
    String data = this.prepareData(this.getBlock().getNonce());
    return new BigInteger(DigestUtils.sha256Hex(data), 16).compareTo(this.target) == -1;
}
複製程式碼

修改區塊新增邏輯

/**
 * <p> 建立新區塊 </p>
 *
 * @param previousHash
 * @param data
 * @return
 */
public static Block newBlock(String previousHash, String data) {
    Block block = new Block("", previousHash, data, Instant.now().getEpochSecond(), 0);
    ProofOfWork pow = ProofOfWork.newProofOfWork(block);
    PowResult powResult = pow.run();
    block.setHash(powResult.getHash());
    block.setNonce(powResult.getNonce());
    return block;
}
複製程式碼
  • 建立區塊
  • 建立Pow演算法物件
  • 執行Pow演算法
  • 儲存返回的Hash以及運算計數器

測試執行

/**
 * 測試
 *
 * @author wangwei
 * @date 2018/02/05
 */
public class BlockchainTest {

    public static void main(String[] args) {

        Blockchain blockchain = Blockchain.newBlockchain();

        blockchain.addBlock("Send 1 BTC to Ivan");
        blockchain.addBlock("Send 2 more BTC to Ivan");

        for (Block block : blockchain.getBlockList()) {
            System.out.println("Prev.hash: " + block.getPrevBlockHash());
            System.out.println("Data: " + block.getData());
            System.out.println("Hash: " + block.getHash());
            System.out.println("Nonce: " + block.getNonce());

            ProofOfWork pow = ProofOfWork.newProofOfWork(block);
            System.out.println("Pow valid: " +  pow.validate() + "\n");
        }
    }
}

/**
 * 設定TARGET_BITS = 20,得到如下結果:
 */
Mining the block containing:Genesis Block 
Elapsed Time: 2.118 seconds 
correct hash Hex: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442 

Mining the block containing:Send 1 BTC to Ivan 
Elapsed Time: 1.069 seconds 
correct hash Hex: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5 

Mining the block containing:Send 2 more BTC to Ivan 
Elapsed Time: 4.258 seconds 
correct hash Hex: 00000777f93efe91d9aabcba14ab3d8ab8e0255b89818cdb9b93cfa844ad0c7f 

Prev.hash: 
Data: Genesis Block
Hash: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442
Nonce: 522163
Pow valid: true

Prev.hash: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442
Data: Send 1 BTC to Ivan
Hash: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5
Nonce: 474758
Pow valid: true

Prev.hash: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5
Data: Send 2 more BTC to Ivan
Hash: 00000777f93efe91d9aabcba14ab3d8ab8e0255b89818cdb9b93cfa844ad0c7f
Nonce: 1853839
Pow valid: true
複製程式碼

總結

我們正在一步一步接近真實的區塊鏈架構,本篇我們實現了挖礦機制,但是我們還有很多關鍵性的功能沒有實現:區塊鏈資料庫的永續性、錢包、地址、交易、共識機制,這些我們後面一步一步來實現

資料

基於Java語言構建區塊鏈(二)—— 工作量證明

相關文章