只用120行Java代碼寫一個自己的區塊鏈-3挖礦算法


在本系列前兩篇文章中,我們向大家展示了如何通過精煉的Java代碼實現一個簡單的區塊鏈。包括生成塊,驗證塊數據,廣播通信等等,這一篇讓我們聚焦在如何實現 PoW算法。

 

大家都無不驚呼比特幣、以太坊及其他加密電子貨幣的持續狂熱,特別是對於剛接觸這個領域的新手,不斷得聽到張三李四通過 GPU “挖礦”而聚集價值數萬乃至數百萬加密電子貨幣。那么“挖礦”到底是什么? 它是如何工作的? 相信對於程序員來說,沒有什么比自己動手實踐一遍“挖礦”算法更好的學習辦法了。

在這篇文章中,讓我們一起逐個解讀每一個問題,並最終編寫出自己的“挖礦”算法。這個算法稱為工作證明算法(Proof-of-Work),它是比特幣和以太坊這兩種最流行的加密貨幣的基礎。

什么是“挖礦”?

加密電子貨幣因為稀缺才具有價值。以現在的比特幣為例,如果任何人任何時候都可以隨意“制造”比特幣,那么作為電子貨幣它會變得毫無價值。比特幣通過算法來控制產出的速率並在大約122年內達到最大量。這種隨着時間推移緩慢、穩定並逐步產出的方式有效避免了通貨膨脹。

比特幣的產出是通過給予“獲勝礦工”獎勵來實現,為了獲取比特幣獎勵礦工之間會進行競爭。這個過程之所以被稱為“挖礦”,是因為它類似於“Gold Rush”時每個黃金礦工通過辛苦勞作並最終(希望)找到一點黃金。

“挖礦”是如何工作的?

如果 Google 一下這個問題,你會得到大量的結果。簡單來說,“挖礦”就是“解決一個數學難題”的過程。我們先來了解一些密碼學和哈希算法的知識。

密碼學簡要介紹

單向加密以人類可讀的文本(明文)作為輸入,比如“Hello world”這個字符串,再通過一個數學函數產生出難以辨認的輸出(密文)。 這類函數或算法的性質和復雜性各不相同。 算法越復雜,逆向工程就越困難。 

以流行的 SHA-256 算法為例。 通過這個網站可以讓你計算任意給定輸入的輸出,也就是 SHA-256 哈希值。比如讓我們輸入“Hello world”,看看得到了什么:

 

通過不斷嘗試計算“Hello world”的哈希值。你會發現每次的結果都完全相同。 這個過程稱為冪等性。


加密算法一個最基本的特性是,非常難以通過反向工程來求解輸入,但是非常容易驗證輸出。比如上面的例子,你可以很容易驗證給定輸入“Hello world”的SHA-256哈希值是否正確,但很難通過給定的哈希值判斷它的輸入是什么。這就是為什么將這種類型的算法稱為單向加密。

比特幣使用 Double SHA-256,它將 SHA-256 求得的哈希值作為輸入再次計算 SHA-256 哈希值。 為了簡化,我們只使用一次SHA-256。

挖礦

 

回到加密電子貨幣中,比特幣就是通過讓參與者利用這樣的加密算法求解出符合特定條件的哈希值來實現“挖礦”過程。具體來說,比特幣要求參與者通過 double SHA-256 算法計算出“前導0”超過若干位的哈希值,第一個求解出來的參與者就是“獲勝的礦工”。

比如,我們求“886”這個字符串的 SHA-256 哈希值:

可以看到,是一個“前導0”為3位的哈希值(前三位是0)。


回憶我們前面說到的“單向加密”的特點:

任何人都可以很容易地驗證“886”是否產生3位“前導0”的哈希值。但為了找到這樣一個能產生3位“前導0”的輸入(就是這里的“886”),我們做了大量繁瑣的計算工作:從一個很大的數字和字母集合中逐個計算它們的哈希值並判斷是否滿足上述條件。如果我是第一個找到“886”的人,那其他人通過驗證就能判斷我做了這樣大量繁瑣的工作。在比特幣、以太坊中這樣的過程就稱為工作證明算法。

“如果我運氣非常好,第一次嘗試就找到了一個符合條件的(輸入)值呢?” —— 這是非常不可能的,你可以試試隨意輸入一些字母和數字。

比特幣中實際的算法和約束要比上說要求復雜,當然也更難(要求更多位的“前導0”)。同時它也可以動態調整難度,目標是確保每隔10分鍾產出一次比特幣,不管參與“挖礦”的人多還是少。

差不多可以動手了

了解了足夠的背景知識,接着我們就用 Java語言來編碼實踐下工作量證明(Proof-of-Work)算法。建議你閱讀之前的系列文章,因為下面工作證明算法部分會涉及之前的代碼。

Proof-of-work

創建新塊並加入到鏈上之前需要完成“工作量證明”過程。我們先寫一個簡單的函數來檢查給定的哈希值是否滿足要求。

  • 哈希值必須具有給定位的“前導0”

  • “前導0”的位數是由難度(difficulty)決定的

  • 可以動態調整難度(difficulty)來確保 Proof-of-Work 更難解

下面就是 isHashValid 這個函數:

    /**
     * 校驗HASH的合法性
     * 
     * @param hash
     * @param difficulty
     * @return
     */
    public static boolean isHashValid(String hash, int difficulty) {
        String prefix = repeat("0", difficulty);
        return hash.startsWith(prefix);
    }

  private static String repeat(String str, int repeat) {
        final StringBuilder buf = new StringBuilder();
        for (int i = 0; i < repeat; i++) {
            buf.append(str);
        }
        return buf.toString();
    }

 

我們定義prefix 變量,它代表“前導0”,接着檢查哈希值是否具有滿足條件的“前導0”,然后返回 True 或 False 。

我們修改之前生成塊的generateBlock 函數:

 

public static Block generateBlock(Block oldBlock, int vac) {
        Block newBlock = new Block();

        newBlock.setIndex(oldBlock.getIndex() + 1);
        newBlock.setTimestamp(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        newBlock.setVac(vac);
        newBlock.setPrevHash(oldBlock.getHash());
        newBlock.setDifficulty(difficulty);

        /*
         * 這里的 for 循環很重要: 獲得 i 的十六進制表示 ,將 Nonce 設置為這個值,並傳入 calculateHash 計算哈希值。
         * 之后通過上面的 isHashValid 函數判斷是否滿足難度要求,如果不滿足就重復嘗試。 這個計算過程會一直持續,直到求得了滿足要求的
         * Nonce 值,之后通過 handleWriteBlock 函數將新塊加入到鏈上。
         */
        for (int i = 0;; i++) {
            String hex = String.format("%x", i);
            newBlock.setNonce(hex);
            if (!isHashValid(calculateHash(newBlock), newBlock.getDifficulty())) {
                System.out.printf("%s do more work!\n", calculateHash(newBlock));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    LOGGER.error("error:", e);
                }
                continue;
            } else {
                System.out.printf("%s work done!\n", calculateHash(newBlock));
                newBlock.setHash(calculateHash(newBlock));
                break;
            }
        }
        return newBlock;
    }

篇幅有限,我已經將完整代碼發布在 Github上,可以從這里獲得。

 

跑起來看看

 啟動程序,在瀏覽器中訪問 http://localhost:4567

 

接着通過 RestClient 來發送一個包含vac數據的POST 請求。我們觀察命令行窗口,不斷得計算哈希值,如果不滿足難度要求就繼續重試,直到找到滿足要求的哈希值及 Nonce

 

可以看到最后一個哈希值滿足我們設定的難度要求(1位“前導0”)。我們再來刷新下瀏覽器:

 

可以看到第二個塊創建成功並加到鏈上了,其中Nonce 就是通過Proof-of-Work計算出來滿足難度要求的值。

下一步

 

到這里要先祝賀你,上面的內容很有價值。盡管我們的示例中使用了非常低的難度,但本質上,工作證明算法就是比特幣、以太坊等區塊鏈的重要組成根本。

對於下一步應該深入區塊鏈的哪個方向,我們推薦可以學習如何通過 IPFS存取大文件並與區塊鏈打通。[IPFS]

此外相比 Proof-of-Work,Proof-of-Stake 算法正越來越受到關注和青睞,你也可以學習如何將本文的 PoW 算法改為實現 PoS 算法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM