比特幣原始碼閱讀筆記【網路篇】
這篇文章總結下比特幣網路相關內容。
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網路路由節點
- 完整區塊鏈節點 包含完整區塊鏈資料庫,比特幣P2P網路路由節點
- 獨立礦工 包含挖礦函式,完整區塊鏈資料庫,比特幣P2P網路路由節點
- 輕量級(SPV)錢包 包含錢包和比特幣P2P網路路由節點,不包含區塊鏈資料庫
- 礦池協議伺服器 閘道器路由,用於連線比特幣P2P協議網路和其他協議的節點
- 礦池礦工節點 包含挖礦函式,不包含完整區塊鏈資料庫,但執行著其他礦池協議
- 輕量級(SPV) Stratum錢包 包含錢包和Stratum協議節點,不包含區塊鏈資料
比特幣協議,Stratum協議和礦池協議組成了比特幣網路的基礎,擴充套件比特幣網路結構如下所示。少量的全節點客戶端,少量的獨立礦工,礦池及其背後大量礦池礦工,
1.2 接力網路
比特幣礦工爭分奪秒地進行著工作量證明競賽,為了成功參與這項競賽,礦工們需要儘可能地縮短網路延遲,包括挖到區塊時對外廣播和接收其他節點發起的下一個區塊的競賽。礦工間的挖礦競賽實質上是計算能力和網路水平的雙重比拼。為了優化礦工的網路,2015年,Matt Corallo建立了一個接力網路(Relay Network)向全球比特幣礦工提供低延遲網路服務。網路維護了一批亞馬遜雲主機(Amazon Web Services, AWS),連線著全球大部分的礦工和礦池節點。接力網路如下圖所示。
此後,接力網路還推出了付費升級版,叫做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
訊息用於建立連線;ADDR
和GETADDR
訊息用於地址傳播;GETBLOCKS
, INV
和GETDATA
訊息用於同步區塊鏈資料。感興趣的讀者,建議完整地檢視所有比特幣網路訊息型別,分析其應用場景。
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, 成功建立連線。整個過程如下圖所示:
建立連線部分的更多細節見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請求,獲取鄰居節點可以連線的節點列表。整個過程如下圖所示。
2.3 同步區塊資料
連線建立後,兩個節點會互相傳送同步請求getblocks, 節點比較對方的BestHeight後,區塊數較多的一方向區塊較少的一方傳送inv響應,讓落後的節點追上。收到inv響應後,落後的節點開始傳送getdata請求資料。整個過程如下圖所示:
2.4 斷開連線
如果兩個節點建立網路連線後沒有流量,節點之間會定期傳送訊息保持連線,如果兩個節點超過一定時間沒有傳送訊息,那麼認為節點已經斷開,並開始尋找新的節點。
考慮到文章篇幅因素,此處省略2.2,2.3和2.4的程式碼分析部分。
三、簡化支付驗證 SPV
最後我們介紹比特幣網路中的重要組成部分,輕量級錢包。
上面提到過,並不是所有節點都在本地資料庫儲存著完整區塊鏈,畢竟完整的賬本非常消耗儲存空間。很多比特幣客戶端是在智慧手機等裝置上執行的。為了支援這些裝置(最主要就是智慧手機),中本聰在白皮書中提到了簡化支付驗證SPV, 使用者客戶端只需要儲存區塊header就可以驗證支付。使用者如果能夠從區塊鏈的某處找到相符的交易,他就可以知道網路已經認可了這筆交易,而且得到了網路的多少個確認。輕量級錢包節點只同步header也大大減少了客戶端的網路開銷。
SPV背後的技術原理是Bloom Filter。Bloom Filter是一種概率搜尋器,它在搜尋時不需要完整描述被搜尋的模式,這種查詢雖然存在一定比例的偽命中,但效率遠遠高出普通查詢。例如,查詢名字以字母g結尾的,交易金額的小數部分是0.618的交易。搜尋條件的模糊程度和交易隱私洩露程度構成了一種權衡關係。有了SPV, 我們可以在不暴露地址的情況下完成支付驗證。
這裡需要注意,SPV指的是“支付驗證“,而不是“交易驗證”。這兩種驗證有很大區別。”交易驗證”非常複雜,涉及到驗證是否有足夠餘額可供支出、是否存在雙花、指令碼能否通過等等,通常由執行完全節點的礦工來完成。“支付驗證”則比較簡單,只判斷用於“支付”的那筆交易是否已經被驗證過,並得到了多少的算力保護(多少確認數)。