1. 程式人生 > >比特幣原始碼閱讀筆記【網路篇】

比特幣原始碼閱讀筆記【網路篇】

這篇文章總結下比特幣網路相關內容。

2008年中本聰創造比特幣時在白皮書中這樣定義比特幣:一個點對點的電子現金系統,那時還沒有“區塊鏈”這個說法。那段時間,點對點(P2P)網路已經有了廣泛的應用,例如Bittorrent和迅雷。P2P網路最大的特點就是網路中沒有“特權節點”,所有節點共同承擔P2P服務,這就是所謂“去中心化”。比特幣的設計初衷就是創造一個去中心化的共識網路。比特幣網路指的是執行比特幣P2P協議的節點集合。隨著比特幣生態的發展,除了比特幣P2P協議,比特幣的節點還可能執行著其他協議, 用於挖礦和輕量級錢包等應用。礦機與礦池軟體之間的通訊協議是Stratum,而礦池軟體與錢包之間的通訊是bitcoinrpc介面。閘道器路由伺服器在執行比特幣P2P協議的同時,還執行著附加協議用於將Stratum協議節點橋接到比特幣網路中。這些橋接進比特幣網路中的節點屬於擴充套件比特幣網路

一、比特幣網路

1.1 節點類別

比特幣P2P網路的去中心,準確地說是節點之間的關係對等,而不是所有節點功能完全一致。比特幣網路中的節點有如下幾種功能:1.網路路由(Network Route, 簡寫為N) 2.完整區塊鏈(Full Blockchain, 簡寫為B) 3.礦工(Miner, 簡寫為M) 4.錢包(Wallet, 簡寫為Wallet)。如果一個比特幣節點具有上述全部四種功能,那麼這個節點叫做全節點。如果一個節點只儲存部分割槽塊,同時使用簡化支付驗證 (Simplified Payment Verification, SPV)的方法驗證交易,那麼這個節點叫做SPV節點

或者輕量級節點。比特幣網路中的大部分節點都不是全節點, 本地資料庫只儲存部分割槽塊鏈的情況下,節點依然可以具有錢包功能或者挖礦功能。概括下來,擴充套件比特幣網路中的節點主要分為以下幾種:

  • 參考客戶端 包含錢包,礦工,完整區塊鏈資料庫,比特幣P2P網路路由節點 比特幣節點型別-參考客戶的.png-57.3kB
  • 完整區塊鏈節點 包含完整區塊鏈資料庫,比特幣P2P網路路由節點 完整區塊鏈節點.png-38.3kB
  • 獨立礦工 包含挖礦函式,完整區塊鏈資料庫,比特幣P2P網路路由節點純礦工.png-46.5kB
  • 輕量級(SPV)錢包 包含錢包和比特幣P2P網路路由節點,不包含區塊鏈資料庫 輕量級(SPV)錢包.png-38kB
  • 礦池協議伺服器 閘道器路由,用於連線比特幣P2P協議網路和其他協議的節點 礦池協議伺服器.png-30.6kB
  • 礦池礦工節點 包含挖礦函式,不包含完整區塊鏈資料庫,但執行著其他礦池協議礦池礦工節點.png-27.8kB
  • 輕量級(SPV) Stratum錢包 包含錢包和Stratum協議節點,不包含區塊鏈資料 輕量級Stratum錢包.png-38.9kB

比特幣協議,Stratum協議和礦池協議組成了比特幣網路的基礎,擴充套件比特幣網路結構如下所示。少量的全節點客戶端,少量的獨立礦工,礦池及其背後大量礦池礦工,
完整網路.png-776.6kB

1.2 接力網路

比特幣礦工爭分奪秒地進行著工作量證明競賽,為了成功參與這項競賽,礦工們需要儘可能地縮短網路延遲,包括挖到區塊時對外廣播和接收其他節點發起的下一個區塊的競賽。礦工間的挖礦競賽實質上是計算能力和網路水平的雙重比拼。為了優化礦工的網路,2015年,Matt Corallo建立了一個接力網路(Relay Network)向全球比特幣礦工提供低延遲網路服務。網路維護了一批亞馬遜雲主機(Amazon Web Services, AWS),連線著全球大部分的礦工和礦池節點。接力網路如下圖所示。
接力網路.png-1007.2kB

此後,接力網路還推出了付費升級版,叫做FIBRE網路,每月需要支付約50美元。FIBRA是一個基於UDP協議的網路,它實現了一種“緊湊塊”優化策略,進一步減少了資料傳輸時間。目前FIBRA網路有6個節點。整個網路維護了一份IP白名單,只有白名單中的礦工才能連線到FIBRE網路,且每個礦工節點只能連線6個節點中的1個。到目前為止,接力網路並沒有成為Bitcoin Core的正式部分。接力網路的更多資訊,可以參考這裡

二、典型場景

下面我們從更微觀的角度解釋比特幣網路。比特幣的網路模組主要包括以下幾個功能:

  • 建立初始連線
  • 地址傳播發現
  • 同步區塊資料
  • 斷開連線

比特幣網路相關的程式碼主要在src/net.cpp, src/netbase.cpp,src/net_processing。比特幣全部網路訊息型別定義見src/protocol.h

理解每一種訊息的場景和含義,我們就掌握了比特幣網路的核心內容。例如,VERSION訊息和VERACK訊息用於建立連線;ADDRGETADDR訊息用於地址傳播;GETBLOCKS, INVGETDATA訊息用於同步區塊鏈資料。感興趣的讀者,建議完整地檢視所有比特幣網路訊息型別,分析其應用場景。

namespace NetMsgType {
/**
 * The version message provides information about the transmitting node to the
 * receiving node at the beginning of a connection.
 * @see https://bitcoin.org/en/developer-reference#version
 */
extern const char *VERSION;
/**
 * The verack message acknowledges a previously-received version message,
 * informing the connecting node that it can begin to send other messages.
 * @see https://bitcoin.org/en/developer-reference#verack
 */
extern const char *VERACK;
/**
 * The addr (IP address) message relays connection information for peers on the
 * network.
 * @see https://bitcoin.org/en/developer-reference#addr
 */
extern const char *ADDR;
// 其他訊息型別省略

2.1 建立連線

在比特幣客戶端,可以用bitcoin-cli getpeerinfo命令檢視可以連線到的網路節點資訊:

$ bitcoin-cli getpeerinfo
[{
    "addr": "85.213.199.39:8333",
    "services": "00000001",
    "lastsend": 1405634126,
    "lastrecv": 1405634127,
    "bytessent": 23487651,
    "bytesrecv": 138679099,
    "conntime": 1405021768,
    "pingtime": 0.00000000,
    "version": 70002,
    "subver": "/Satoshi:0.9.2.1/",
    "inbound": false,
    "startingheight": 310131,
    "banscore": 0,
    "syncnode": true
}, {
    "addr": "58.23.244.20:8333",
    "services": "00000001",
    "lastsend": 1405634127,
    "lastrecv": 1405634124,
    "bytessent": 4460918,
    "bytesrecv": 8903575,
    "conntime": 1405559628,
    "pingtime": 0.00000000,
    "version": 70001,
    "subver": "/Satoshi:0.8.6/",
    "inbound": false,
    "startingheight": 311074,
    "banscore": 0,
    "syncnode": false
}]

找到同伴後,客戶端就開始與已知的同伴建立TCP連線,預設埠是8333。
比特幣客戶端之間的連線過程和TCP三次握手一模一樣。節點A向節點B傳送自己的版本號ver,B收到A的版本號後,如果與自己相容則確認連線,B返回verack,同時向A傳送B自己的版本號,如果A也相容,A再次返回verack, 成功建立連線。整個過程如下圖所示:
初始連線.png-95.9kB

建立連線部分的更多細節見src/net.cpp。輸入待連線的地址addrConnect,返回該地址的節點pnode。如果這個節點已經連線,直接返回這個節點。如果是新的節點,嘗試建立Socket連線或者通過代理連線。

CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure)
{
    if (pszDest == nullptr) {
        if (IsLocal(addrConnect))
            return nullptr;

        // Look for an existing connection
        CNode* pnode = FindNode(static_cast<CService>(addrConnect));
        if (pnode)
        {
            LogPrintf("Failed to open new connection, already connected\n");
            return nullptr;
        }
    }
    // 省略部分程式碼
    bool connected = false;
    SOCKET hSocket = INVALID_SOCKET;
    proxyType proxy;
    if (addrConnect.IsValid()) {
        bool proxyConnectionFailed = false;
        // 網路代理部分程式碼省略
        std::string host;
        int port = default_port;
        SplitHostPort(std::string(pszDest), port, host);
        connected = ConnectThroughProxy(proxy, host, port, hSocket, nConnectTimeout, nullptr);
    }
    if (!connected) {
        CloseSocket(hSocket);
        return nullptr;
    }
    NodeId id = GetNewNodeId();
    uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize();
    CAddress addr_bind = GetBindAddress(hSocket);
    CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", false);
    pnode->AddRef();

    return pnode;
}

2.2 地址傳播發現

一旦建立了連線,節點會向它的所有鄰居節點發送addr訊息,這條訊息包含著節點自己的IP地址,用於向更多節點告知自己。此外,節點還會向它所有的鄰居節點發送getaddr請求,獲取鄰居節點可以連線的節點列表。整個過程如下圖所示。
地址傳播發現.png-96.2kB

2.3 同步區塊資料

連線建立後,兩個節點會互相傳送同步請求getblocks, 節點比較對方的BestHeight後,區塊數較多的一方向區塊較少的一方傳送inv響應,讓落後的節點追上。收到inv響應後,落後的節點開始傳送getdata請求資料。整個過程如下圖所示:
同步區塊鏈資料.png-158.7kB

2.4 斷開連線

如果兩個節點建立網路連線後沒有流量,節點之間會定期傳送訊息保持連線,如果兩個節點超過一定時間沒有傳送訊息,那麼認為節點已經斷開,並開始尋找新的節點。

考慮到文章篇幅因素,此處省略2.2,2.3和2.4的程式碼分析部分。

三、簡化支付驗證 SPV

最後我們介紹比特幣網路中的重要組成部分,輕量級錢包
上面提到過,並不是所有節點都在本地資料庫儲存著完整區塊鏈,畢竟完整的賬本非常消耗儲存空間。很多比特幣客戶端是在智慧手機等裝置上執行的。為了支援這些裝置(最主要就是智慧手機),中本聰在白皮書中提到了簡化支付驗證SPV, 使用者客戶端只需要儲存區塊header就可以驗證支付。使用者如果能夠從區塊鏈的某處找到相符的交易,他就可以知道網路已經認可了這筆交易,而且得到了網路的多少個確認。輕量級錢包節點只同步header也大大減少了客戶端的網路開銷。

SPV節點同步區塊header.png-107kB

SPV背後的技術原理是Bloom Filter。Bloom Filter是一種概率搜尋器,它在搜尋時不需要完整描述被搜尋的模式,這種查詢雖然存在一定比例的偽命中,但效率遠遠高出普通查詢。例如,查詢名字以字母g結尾的,交易金額的小數部分是0.618的交易。搜尋條件的模糊程度和交易隱私洩露程度構成了一種權衡關係。有了SPV, 我們可以在不暴露地址的情況下完成支付驗證。

這裡需要注意,SPV指的是“支付驗證“,而不是“交易驗證”。這兩種驗證有很大區別。”交易驗證”非常複雜,涉及到驗證是否有足夠餘額可供支出、是否存在雙花、指令碼能否通過等等,通常由執行完全節點的礦工來完成。“支付驗證”則比較簡單,只判斷用於“支付”的那筆交易是否已經被驗證過,並得到了多少的算力保護(多少確認數)。

四、參考資料