近期有個國內著名技術協會的約稿,正好向技術圈分享一下我對區塊鏈系統的拙見。我發現一件有趣的事情,即使是有計算機背景,懂編程的同學,都也不怎麼清楚區塊鏈到底是怎麼回事。今天這裏,我打算用計算機語言和大家溝通,爭取可以至少讓計算機背景的同學,徹底弄明白區塊鏈是咋回事,是怎麼工作的。

不過在開始之前,需要明確的一件事情是,同之前的計算機技術不同,區塊鏈技術核心關乎的是一個計算系統的自動化監管和治理,而不是爲了讓計算更高效或更大規模地發生。需要明確這個期望,才方便我們去理解,爲什麼區塊鏈是這樣設計的,這樣工作的。

僞代碼雜糅了 C++和 Javascript 的語法,一點亂,歡迎大家來改進 (逃 ...

================= 預警分割線 ==============
好吧,這裏開始,前方高能。

我們將以最簡化的加密數字貨幣爲例介紹區塊鏈的精確工作原理,爲了便於理解將省略手續費,大部分優化,互操作性等層面的東西。這裏會用到強類型的僞代碼,來精確定義其數據結構和執行邏輯。這裏我們將從零開始實現一個類似以太坊數字貨幣那樣的區塊鏈系統,爲了便於理解,我們將採用以太坊所採用的賬戶-狀態模型來表示賬簿,而不是比特幣的那種 UTXO。

我們先從一系列基礎實體和原語的定義開始 :
基礎數據類型

    class String;  // 基礎字符串數據結構  
    class Blob;    // 基礎二進制數據用來表示對象序列化之後的線性二進制數據  
    class CriticalSection; // 臨界區多線程互斥對象  
    class BigInt; // 區塊鏈中很多地方的數值採用大整數來表示例如餘額挖礦難度等。  
    // 例如用一個 32 字節的無符號大整數表示 0  2^256-1 的整數

數字簽名原語
標準的非對稱加密系統裏面的函數,公私鑰對可以在不聯網的情況下,任意生成,並且全球唯一。通常爲 32 到 64 字節的無結構二進制數據。其中公鑰會公開,在區塊鏈系統中用來表明特定身份,供他人驗證其對特定賬戶的控制權。而私鑰則用來通過數字簽名來證明其對賬戶的控制。VerifySignature 原語,用來對於給定數據和簽名,驗證是不是對應的簽名者簽署的。

    typedef BYTE PublicKey[32]; // 公鑰數據  
    typedef BYTE PrivateKey[64];// 私鑰數據  
    typedef BYTE Signature[64]; // 數字簽名數據  
    void    Sign(Blob data, PrivateKey sk, Signature sigdata); // 數字簽名  
    bool    VerifySignature(Blob data, PublicKey pk, Signature sigdata); // 檢查數字簽名是否正確  

賬戶地址
在我們這裏的例子中,所有哈希函數都採用 SHA256,其將產生一個 32 字節的哈希值。地址是賬戶的標識符,是一個 32 字節的無結構二進制數據,由公鑰的哈希值 SHA256(PublicKey) 得到。那麼也就是說每個公鑰,對應一個唯一的地址,對應一個唯一的賬戶。

    typedef     BYTE HashValue[32]; //SHA256 的哈希值  
    typedef     HashValue Address;  // 賬戶地址  
    HashValue   SHA256(Blob data); // SHA256 哈希函數  

智能合約 (Smart Contract)
這個有點像一個 C++的類,定義了一些狀態,以及修改這些狀態的函數。一個區塊鏈系統中,可以有多個智能合約同時存在,但是每個僅會有一個實例。這裏我們就數字貨幣給出一個極度簡化的智能合約的例子:

    class MyCoin  
    {  
        // internal state  
        hash_map<Address, BigInt>_Ledger;  

        // internal function  
        BigInt_GetBalance(Address addr)  
        {     
            if(_Ledger.has(addr))return_Ledger[addr];  
            else return 0;  
        }  

        // 轉賬函數  
        void Transfer(Address signer, Address from, Address to, BigInt amount)  
        {  
            if(signer != from)return;  
            if(amount > 0 &&_GetBalance(from) >= amount)  
            {     
                _Ledger[from] -= amount;  
                amount +=_GetBalance(to);  
                _Ledger[to] = amount;  
            }  
        }  

        // 挖礦獎勵函數  
        void CoinBase(int height, Address miner)  
        {  
            BigInt reward = 5000000000; // 這裏簡化爲每次獎勵 50 個幣  
            if(reward > 0)  
            {  
                reward +=_GetBalance(miner);  
                _Ledger[miner] = reward;  
            }  
        }  
    };  

交易 (Transaction)
一個交易表示對特定相關賬戶一次狀態修改請求。交易中不攜帶任何邏輯代碼,僅僅是指定這個交易將調用智能合約裏面的哪個公開函數及其調用參數。當然在我們這個極度簡化的系統中,只有一種交易,即前面的轉賬 (Transfer)。交易的發起方必須爲扣款方 (from),並且整個交易攜帶對交易內容的數字簽名,以確信該交易由扣款方發起。基於我們這裏的例子,一個交易至少含有以下結構 :

    struct Transaction  
    {  
        String      InvokeFunctionName;    // 在我們這裏 始終爲 "Transfer"  
        Blob        InvokeArguments; // 序列化之後的調用參數  
        PublicKey   Signer; // 發起者的公鑰,注意這裏不是地址  
        Signature   SignData; // 由發起者的私鑰對交易的簽名  
    };  

區塊 (Block)
一個區塊表示區塊鏈接力執行中的一步,裏面主要包含這一步中確認的一批交易,以及共識機制驗證數據和塊頭元數據。一個最簡化的定義可以是這樣 :

    struct Block  
    {  
        int     Timestamp; // 出塊時間  
        HashValue   PrevBlock; // 上一個塊的哈希值  
        Address     Miner; // 礦工地址  
        int     TxnCount; // 這個塊中包含的交易個數  
        Transaction     Txns[TxnCount]; // 完整的交易列表  
        BigInt      PowTarget; // 工作量證明的目標 (共識驗證數據)  
        int     PowNonce; // 工作量證明的 Nonce 值 (共識驗證數據)  
    };  

這裏我們給出了最簡化的工作量證明 (Proof-of-Work) 的驗證數據結構,如果採用其他共識算法,這個部分會有變化。從這個結構可以看出,區塊鏈之所以稱爲鏈,就是因爲區塊結構中包含一個指向上一個區塊的 " 指針 ",PrevBlock。任何一個被確認的區塊,同時也意味着承認其全部的前驅區塊,以及這些區塊所攜帶的全部交易。一個區塊被確認有三個條件 :
1. 這個區塊的共識驗證要滿足其特定共識算法的要求。在工作量證明算法中,PowTarget 必須小於當前挖礦難度的要求,同時 ((BigInt&)SHA256(Block)) < Block::PowTarget。
2. 這個塊所包含的交易必須沒有被之前的區塊包含過,並且每個交易必須能夠保證其數字簽名能夠被其 Signer 的公鑰正確驗證。至於交易所執行的邏輯是否正確,是否出錯則無關緊要。
3. 在所有分叉塊中,即具有相同 PrevBlock 的塊,只有優先的塊會被確認。這一點不同的共識算法有不同的情況。

P2P 通訊原語
區塊鏈的網絡層僅用到了 P2P 網絡技術中簡單的部分,用基於 TCP 長連接的 Gossip 協議實現一個數據塊的全網廣播 (Flooding)。我們這裏將其抽象下面的通訊原語 :

    interface BroadcastNetwork  
    {  
        template<typename T>  
        void Broadcast(const T& object); // 將對象序列化並廣播出去  

        function    OnRecvBlock; // 接收到一個區塊的回調函數  
        function    OnRecvTransaction; // 接收到一個交易的回調函數  
    };  

內存池 (Mempool) 原語

內存池在區塊鏈系統中用來記錄尚未被確認的交易,很容易用比如哈希表來實現。

    interface Mempool  
    {  
        bool    Has(Transaction txn);  
        void    Insert(Transaction new_txn);  
        void    Remove(Transaction txns[count]);  
        int Collect(Transaction txns[max_count]);  
    };  

其中 Collect 原語用於挖礦時合成新的區塊,從 mempool 中挑出一系列交易來填充 Txns 數組,最多挑 TxnMaxCount 個,並返回實際填充的個數。

區塊歸檔數據庫原語

區塊鏈系統中的區塊以及交易,在被確認之後,將從內存中移除,並寫入歸檔數據庫中。這個部分很容易用一個 Key-value storage 系統來實現,當然用 SQL 數據可也是可以的,就是效率低一些。

    interface ArchiveDatabase  
    {  
        void    Archive(Transactiontxns[count]);  
        void    Archive(Block blk);  
        void    Has(Transaction txn);  
        void    Has(Block blk);  
    }  

有了這些定義之後,我們可以給出一個不考慮分叉情況下最簡單的基於工作量證明的區塊鏈系統的僞代碼 :

    static const int TARGET_ADJUST_INTERVAL = 256;  // 每隔 256 個塊調整一次算力難度  
    static const int BLOCK_CREATION_INTERVAL = 600*1000; // 每十分鐘出一個塊  
    static const int TRANSCATION_PERBLOCK_MAX = 1024; // 每塊最多包含 1024 個交易  

    BroadcastNetwork*   g_pNet = BroadcastNetwork::Create(...);  
    Mempool*        g_pMempool = Mempool::Create(...);  
    ArchiveDatabase*    g_pArchiDB = ArchiveDatabase::Create(...);  

    MyCoin          g_MyLedger;  // 賬簿  

    // 當前區塊鏈的頭  
    Block       g_BlockHead = Block::GenesisBlock(6); // 初始化爲創始區塊  
    HashValue   g_BlockHeadHash = SHA256(g_BlockHead);  
    int     g_BlockNextHeight = 1;  
    CriticalSection g_BlockHeadCS;  

    // 下一個塊的共識相關信息 (工作量證明)  
    PowTarget   g_NextPowTarget = Block::InitialPowTarget(); // 初始挖礦難度  
    int     g_LastTargetAdjustedTime;  


    // 收到來自網絡廣播的交易  
    g_pNet-> OnRecvTransaction = [](Transaction txn) {  
        if(g_pMempool->Has(txn) || g_pArchiDB->Has(txn))  
            return;   // 忽略已經存在的交易  
        if(!VerifySignature(  
                txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,  
                txn.Signer,   
                txn.Signature  
            )  
        )return;// 驗證簽名是否正確  

        g_pNet->Broadcast(txn);  // 基本驗證合法之後,接力這個交易的廣播  
        g_pMempool->Insert(txn);  
    };  

    // 收到來自網絡廣播的區塊  
    g_pNet-> OnRecvBlock = [](Block blk) {  
        if(blk.PrevBlock != g_BlockHeadHash)  
            return;  // 忽略亂序到達的塊,忽略分叉塊  
        if(blk.PowTarget > g_NextPowTarget)  
            return;  // 忽略不滿足當前算力要求的塊  
        if(blk.TxnCount > TRANSCATION_PERBLOCK_MAX)  
            return;  // 忽略過於大的塊  

        HashValue h = SHA256(blk);  
        if( ((BigInt&)h) >= blk.PowTarget )  
            return;  // 忽略未達到當前標稱算力要求的塊  

        //  校驗全部塊中的交易  
        for(Int32 i=0; i<blk.TxnsCount; i++)  
        {  
            auto& txn = blk.Txns[i];  
            if( g_pArchiDB->Has(txn) || // 包含之前已經被確認過的交易  
                !VerifySignature(  
                    txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,  
                    txn.Signer,   
                    txn.Signature  
                 ) // 包含驗籤失敗的交易  
            )return;  
        }  

        // 至此這個區塊被確認  
        g_pNet->Broadcast(txn);  // 確認之後儘快接力這個區塊的廣播  

        g_MyLedger.CoinBase(g_BlockNextHeight, Miner); // 執行出塊獎勵  
        for(auto& txn : blk.Txns)   // 執行每一條交易然後歸檔  
        {  
            // 調用交易中指定的函數  
            g_MyLedger[txn.InvokeFunctionName](txn.Signer, txn.InvokeArguments…);  
            g_pArchiDB->Archive(txn);  
            g_pMempool->Remove(txn); // 從內存池中刪除,如果存在的話  
        }  

        g_pArchiDB->Archive(g_BlockHead);  // 歸檔上一個區塊  

        // 更新區塊鏈頭這部分代碼需要和挖礦過程中構造新的塊的步驟互斥  
        g_BlockHeadCS.Lock();  
        {  
            if(g_BlockNextHeight%TARGET_ADJUST_INTERVAL == 1)  
            {// 進行算力調整,週期爲 TARGET_ADJUST_INTERVAL 個區塊  
                if(g_BlockNextHeight > 1)  
                {   g_NextPowTarget = PowTargetAdjustment(  
                        g_NextPowTarget,   
                        blk.Timestamp - g_LastTargetAdjustedTime  
                    );  
                }  

                g_LastTargetAdjustedTime = blk.Timestamp;  
            }  

            // 更新區塊鏈頭在最新的這個塊  
            g_BlockHeadHash = h;  
            g_BlockHead = blk;  
            g_BlockNextHeight++;  
        }  
        g_BlockHeadCS.Unlock();  
    };  

這裏涉及到一個上面沒有定義的算法,PowTargetAdjustment 是用來根據近期出塊速度來調整出塊算力難度要求,從而使得出塊的平均間隔的期望可以大體穩定在一個預先設定的值 (BLOCK_CREATION_INTERVAL)。這是一個和工作量證明共識算法有關的算法,並不是所有區塊鏈系統都有。這個算法的一個最簡化定義如下 :

算力難度調整

    BigInt PowTargetAdjustment(BigInt cur_target, int nth_block_interval)  
    {  
        return cur_target*nth_block_interval/(BLOCK_CREATION_INTERVAL*TARGET_ADJUST_INTERVAL);  
    }  

到這裏一個不出塊的區塊鏈節點,即全節點就可以工作了。全節點是區塊鏈網絡中的大多數節點,是區塊鏈底層 P2P 網絡得以穩定魯棒運行的保障,同時也實現了區塊數據和交易數據的高度冗餘的全網存儲。雖然不出塊,全節點不同於互聯網架構的客戶端。一個全節點不需要信賴其他節點,更不存在一個服務器。全節點能夠獨立自主地驗證區塊鏈完整的歷史演進過程,進而重構其上的狀態 (例如一個賬戶的餘額),而不是去向一個需要信賴的服務器查詢。

當然,區塊鏈網絡計算接力過程是由出塊節點完成了,也就是所謂的礦工節點。這些少數節點,和大量的全節點混在一起,大部分節點收到最新的區塊是來自於其他全節點的接力廣播,而不是直接來自於一個出塊節點。當然,作爲接受方,也無從判斷髮送方是中繼的全節點,還是剛剛出塊的礦工節點。這也有效地保護了真正出塊節點的安全性,避免暴露礦工節點的物理 IP 地址。

一個出塊節點,首先是一個全節點,除了上面定義的這些行爲之外,還需要一個額外的過程,運行在一個或者多個線程上。我們定義最簡化的出塊過程如下 :

    void Mining()  
    {  
        while(g_KeepMining)  
        {  
            // 構造新的塊,這個部分需要和區塊鏈頭更新代碼互斥  
            g_BlockHeadCS.Lock();  
            {  
                int next_height = g_BlockNextHeight;  
                Block new_block;  
                new_block.Timestamp = os::GetCurrentTime();  
                new_block.PrevBlock = g_BlockHeadHash;   // 指向最新的塊  
                new_block.Miner = g_MyAddress;  
                new_block.TxnCount = g_pMempool->Collect(new_block.Txns[TRANSCATION_PERBLOCK_MAX]);  
                new_block.PowTarget = g_NextPowTarget;  
                new_block.PowNonce = os::Random<Int64>();  // 隨機初始值  
            }  
            g_BlockHeadCS.Unlock();  

            // 開始挖礦  
            while(next_height ==  g_BlockNextHeight)  
            {  
                if( ((BigInt64&)SHA256(new_block)) < new_block.PowTarget )  
                {  
                    // 挖礦成功  
                    g_pNet->Broadcast(new_block);  // 立即廣播出去  
                    g_pNet->OnRecvBlock(new_block); // 更新本節點的區塊鏈頭  
                    break; // 開始去挖下一個塊  
                }  
                new_block.PowNonce++; // 嘗試下一個 Nonce  
            }  
            // 大多數情況下其他節點出了新的塊並更新了區塊高度  
            // 則挖礦被打斷去挖更新之後的下一個塊  
        }  
    }  

發佈於 2020-01-07

來源鏈接:mp.weixin.qq.com