P2P網絡 - 比特幣開發指南
原文鏈接: https://bitcoin.org/en/developer-guide#operating-modes
翻譯: terryc007
版本:1.0
比特幣開發指南
比特幣網絡協議允許全節點一起協作維持p2p網絡,實現區塊,交易數據的交換。全節點下載,并驗證每個個區塊,交易,然后在轉發給其他節點。檔案節點是一種全節點,它會存儲各個區塊鏈,并為其他節點提供歷史區塊服務。裁剪節點是一種不會保存這個區塊鏈的全節點。很多SPV客戶端也使用比特幣網絡協議來連接全節點。
因為共識規則不涉及到網絡,因此比特幣程序可以選擇不同的網絡,不同的協議,比如一些礦工使用高速區塊轉發網絡,一些提供SPV級別安全的錢包,使用專業的交易信息服務器。
為提高一個可實操的比特幣P2P網絡例子,這個章節使用比特幣內核作為典型的全節點,BitcoinJ作為典型SPV客戶端。這兩個程序都是靈活的,因此這里只討論默認他們的行為。同時,考慮到隱私,下面例子中的ip地址已經被替換成RFC5737預留的IP地址。
節點發現
當程序第一啟動時,它并不知道任何活躍節點的ip地址。為了發現一些全節點的ip地址,他們會查詢硬編碼在比特幣內核或BitCoinJ中的,一個或多個DNS域名,在返回的結果中應該包含一個或多個DNS A記錄,里面有一些可接受新連接的全節點的ip地址。 比如,使用Unix命令 dig
:
;;QUESTION SECTION:
;seed.bitcoin.sipa.be. IN A
;; ANSWER SECTION:
seed.bitcoin.sipa.be. 60 IN A 192.0.2.113
seed.bitcoin.sipa.be. 60 IN A 198.51.100.231
seed.bitcoin.sipa.be. 60 IN A 203.0.113.183
[...]
DNS 種子由比特幣社區成員維護。其中一部分提供動態DNS種子服務器,它通過掃描比特幣網絡,自動獲取活動節點的ip地址;其他的提供一些靜態DNS種子,這需要手動更新,不過他們很有可能提供不活躍節點的ip地址。不管是動態的,還是靜態的DNS種子,如果節點在主網上運行在端口號8333,或在測試網絡運行在端口號18333,就會被加入到DNS種子。
DNS種子結果沒有被授權,一個惡意的DNS種子運營者或網絡中間人攻擊者能返回僅被攻擊者控制的節點的ip地址,在攻擊者自己的網絡中,孤立節點,并給他們假的交易,區塊數據。因為這個原因,程序不應該只依賴一個DNS種子。
一但程序連接上比特幣網絡,它的節點就可以開始發送,帶有網絡中其他節點IP地址,端口號的addr
消息給其他節點。這個提供了一個完整的去中心化節點發現方法。比特幣內核會在本地數據庫中保存已知節點的信息,通常,等下一次程序啟動時,它就不需要使用DNS種子,直接可以跟這些節點連接即可。
然而,節點通常會離開網絡或者改變ip地址,這樣程序在啟動時,在需要多次嘗試才有可能連接到比特幣網絡。這了會增加連接到比特幣網絡的延遲時間,使得用戶在發送交易或檢查支付狀態前,不得不等待一段時間。
為避免這種延遲,BitcoinJ總是使用動態DNS種子,來獲取那些被確定為活躍節點的IP地址。比特幣處內核也嘗試在降低延遲,避免使用不必要的DNS節點中權衡。如果比特幣內核在它的節點數據庫中有記錄,它就會用11秒時間去連接至少其中一個節點,失敗后,才使用DNS節點獲取ip地址;如果在11秒內成功建立連接,則不在向DNS種子查詢。
比特幣內核跟BitcoinJ在其他特定版本的第一版本發布時,他們在代碼里面都硬編碼了一些節點當時活躍節點的IP地址跟端口號。比特幣內核內置了一個自動回調選項,當沒有DNS種子服務器在60秒內回應查詢,比特幣內核會開始嘗試跟這些節點連接。
作為一個手段回調選項,比特幣內核也提供了好幾個命令行連接選項,包括從一個指定節點,通過其IP地址獲取一個節點列表,或直接跟一個指定節點,通過IP地址建立持久連接。通過-help
獲取命令行詳情。BitcoinJ也可以通過編程實現這樣的功能。
資源: Bitcoin種子, 這個程序管理了好幾個比特幣內核,BitcoinJ都有用到的DNS種子。比特幣內核DNS種子政策。比特幣內核,BitcoinJ里硬編碼的節點IP地址是使用makeseeds script生成的。
連接到節點
通過給遠程節點,發送version
消息跟他節點建立連接,消息里面包括軟件版本號,區塊,當前時間。遠程節點也返回一個version
消息。然后,他們再給其他節點發送verack
消息,表示他們已經建立連接。
一但建立連接,客戶端就能給遠程節點發送getaddr
和addr
消息獲取其他節點信息。
客戶端為維持跟節點的連接,在其離線前30分鐘,它會給其他節點發送一個消息。如果節點在90分鐘內,沒有返回消息,那么這個客戶端就認為連接已經關閉。
初始化區塊下載
在全節點驗證非確認交易,最近挖出的區塊前,它必須下載,驗證最佳區塊鏈上所有的區塊(從創世區塊到最頂部的區塊)。這叫做初始化區塊下載(IBD)或者叫初始化同步。
雖然“初始化” 意味著這個方法只會使用一次,但可以在任何時候,在下載大量區塊的時候,用到它,比如當一個之前連接過的節點下線了很長一段時間。這種情況,節點能使用IBD方法去下載從它最近上線以來,所有的挖出的區塊。
在任何時候,只要比特幣內核本地最佳區塊鏈上,它最新的區塊的區塊頭時間,以及超過了24小時,它就會使用IBD方法獲取新的區塊。如果本地最佳區塊頭鏈,比本地最佳區塊鏈多出144個區塊(這也就意味著,本地最佳區塊鏈已經有24小時沒有更新了),比特幣內核0.10.0也會使用IBD方法。
區塊優先
比特幣內核(一直到0.9.3版本為止)使用簡單的初始化區塊下載(IBD)方法,我們稱之為區塊優先。它的目標是從最佳區塊鏈按序下載區塊。
節點第一啟動時,在它本地最佳區塊鏈只有一個區塊 — 硬編碼的創世區塊(區塊0)。節點了會選擇一個遠程節點(也叫同步節點),并給它發送一個getblocks
消息,如下圖所示:
在getblocks
消息頭部哈希域,這個新節點發這個區塊僅有的頭哈希 - 創世區塊(6fe2...0000 內部字節序)。同時它把停止哈希域全設置為0以獲取最大返回結果。
同步節點一但收到這個getblocks
消息,它使用第一個哈希頭去它本地最佳區塊鏈搜索帶有這個哈希頭的區塊。如果找到block 0與之匹配,它就從區塊1開始,返回500個區塊清單(getblocks
消息返回的最大個數)。它使用inv
消息來發送這些區塊清單。 如下面所示:
存貨清單是一些在比特幣網絡上獨一無二的身份標識信息。每個存貨包含一個類型,一個對象實例的唯一標識。對于區塊而言,這個唯一標識是區塊頭的哈希值。
inv
消息中區塊存貨信息的順序跟其在區塊鏈中的是一樣的,因此第一個inv
消息包含了區塊1到區塊501存貨信息。(比如,如上面所示,區塊1的哈希值是4860...0000)
IBD節點使用收到存貨清單,通過getdata
消息,從同步節點獲取128個區塊。如下圖所示:
對于區塊優先的節點而言,按序請求,發送區塊是非常重要的,因為每個區塊頭會引用它前面的區塊頭。這就意味IBD節點,必須在父區塊還沒有接收完之前,是不能完全的驗證區塊的。之所以不能驗證區塊,是因為那些沒有收到父區塊的區塊是孤塊;下小節會詳細地介紹到。
一但收到getdata
消息,同步節點就會把請求的區塊返回給IBD節點。每個區塊被序列化成區塊格式,并發送各自的block
消息。發送的第一個block
消息(block1),如下圖所示。
IBD節點下載每個區塊,并驗證,然后獲取下一個還未請求過的區塊,并維持一個128個區塊的下載隊列。當它獲取完存貨清單中所有的區塊后,它就發送另外一個getblocks
消息給同步節點,以獲取最多500個區塊的存貨清單。第二個getblocks
消息包含多個區塊頭哈希,如下圖所示:
同步節點一但收到第二個getblocks
消息,它就按照收到區塊頭哈希的順序,挨個在它本地最佳的區塊鏈中尋找匹配的區塊。如果它找到一個匹配的區塊,它會從該區塊的下個區塊開始,返回500個區塊存貨清單。 如果沒有找到一個匹配的哈希(除了那個截止哈希外),它會假定這個兩個節點只有block0是一樣的,因此它會發送一個從block1開始的inv
消息。
通過這樣的重復查找,允許同步節點發送有用的存貨清單,即使IDB節點本地的區塊鏈是從同步節點的本地區塊鏈分叉而來的。IBD節點上的區塊離區塊鏈頂部區塊越近,分叉檢測就變的越有用。 [?]
當IBD節點收到第二個inv
消息后,它會使用getdata
消息請求這些區塊。同步節點會給IBD節點返回block
消息。然后IBD節點會使用getblocks
消息,繼續請求更多的存貨清單。 不斷的重復這個過程直到IBD節點同步完整個區塊鏈。到這后,IBD節點通過普通的區塊廣播(后續小節會講到)來接受新的區塊。
區塊優先的優缺點
區塊優先主要優點在于簡單。其主要缺點在于只依賴于一個同步節點來下載區塊數據。這會帶來幾個影響:
速度受限: 因為所有的請求都指向一個同步節點,這樣如果同步節點的上傳帶寬有限,那么IBD節點的下載速度就很慢。注意:如果同步節點離線,比特幣內核會從另外一個同步節點下載— 但是它仍然一次只從一個同步節點下載。
下載重起:同步節點可能給IBD節點發送非最佳(但是其他的都有效)區塊鏈。IBD節點是無法驗證它到底是不是最佳的區塊鏈,直到初始化區塊下載接近完成時才可以。這會強制IBD節點重新從另外一個節點下載區塊鏈。開發者在比特幣內核中,在多個不同區塊高度,加了好幾個區塊鏈檢測點,來幫組IBD節點監測它是否在下載一條非最佳區塊鏈。 這可以讓IBD節點盡早的重啟下載。
硬盤充滿攻擊:這個跟下載重起關系很大,如果同步節點發送了一個非最佳區塊鏈,這條鏈會存儲在硬盤上,浪費磁盤空間,可能會使磁盤上充滿無用的數據。
高內存消耗:不管是故意的,還是意外,同步節點可能無序地發送區塊,這會導致一些孤塊,只有收到并驗證了其父塊后,才能驗證這些孤塊。孤塊在等待驗證期間會一直存在內存中,這會消耗大量內存。
在比特幣內核0.10.0中,所有的這些問題在頭部優先IBD方法中,部分或全部的得以解決。
資源: 下面的的表格總結了這節提到的消息。點擊消息欄的鏈接可以查看對于消息的參考頁面。
消息 | getblocks |
inv |
getdata |
block |
---|---|---|---|---|
From→To | IBD→Sync | Sync→IBD | IBD→Sync | Sync→IBD |
內容 | 一個/多個頭哈希 | 最多500個區塊存貨(唯一id) | 一個/多個區塊存貨 | 一個 序列化區塊 |
區塊頭優先
比特幣內核0.10.0使用區塊頭優先IBD的初始化區塊下載方法。其目標是先下載最佳區塊鏈頭,部分驗證,然后并行下載相應的區塊。這解決了幾個區塊優先IBD方法中的問題。
節點第一次啟動時,它本地最佳區塊鏈只要一個區塊 - 硬編碼的創世區塊(block0)。 它會選擇一個遠程節點,這里我們稱之為同步節點,然后給同步節點發送getheaders
消息。如下圖所示:
在getheaders
消息的區塊頭哈希域,新節點只發送了它本地僅有的區塊頭哈希 - 創世區塊哈希( 6fe2…0000 內部字節序)。同時會截止哈希域全設為0,以獲取最多哈希值。
同步節點一但收到getheaders
消息,它會取出第一個區塊頭哈希,然后用它在本地最佳區塊鏈中搜索區塊。如果block0匹配,同步節點就會從block1開始,返回2000個區塊頭哈希。它以headers
消息的方式,來發送這些區塊頭。如下圖所示:
IBD節點可以部分驗證區塊頭,它是通過確保區塊頭所有字段遵循共識規則,同時區塊頭的哈希值要低于nBits字段的目標閥值來實現。(要完整驗證區塊,仍需要獲得該區塊所有交易后才可以)
當IBD節點部分驗證完區塊頭之后,它可以并行做兩件事情:
-
下載更多的區塊頭: IBD節點可以發送另外一個
getheaders
消息到同步節點,以獲取最佳區塊鏈上,下一批2000個區塊頭。這些區塊頭可以被立即驗證,同時不斷的重復批量發送請求,直到從同步節點收到的headers
消息中所包含的頭少于2000個,這表示已沒有更多的區塊頭。在撰寫本文時,少于200個來回,就可以完成整個區塊頭同步,大約需要下載32MB數據。一但IBD節點收到一個少于2000個區塊頭的
headers
消息,它就給它所有外連的節點發送一個getheaders
消息,看看他們最佳區塊鏈的情況。通過對比它們返回的消息,它很容易通過它外聯的節點,判斷它所下載的區塊頭是不是屬于最佳區塊鏈上的。這就意味一個不誠實的節點很快會被發現,即使不用檢測點(只要IBD節點連接到至少一個誠實節點,如果找不到誠實節點,比特幣內核會繼續提供檢查點)。 下載區塊: 當IBD繼續下載區塊頭時,以及完成區塊頭下載后,IBD節點會請求并下載每個區塊。IBD節點通過區塊頭鏈中區塊的哈希,來創建
getdata
消息。而在區塊優先中,getdata
中的區塊頭哈希需要通過inv
消息中的區塊存貨清單來提供。但在區塊頭先中,就不必從同步節點獲取區塊, 它可以從其他任何全節點獲取。(雖然并不是所有的全節點存儲所有的區塊。) 這就運行它能夠并行獲取區塊,同時避免下載速度受限于單個同步節點的帶寬速度。
為從更多的節點加載數據,比特幣內核一次最多從單個節點獲取16個區塊,最多8個外向連接。這就意味采用區塊頭優先的比特幣內核, 在IBD階段,同時最多可以同時發起128個區塊請求。(跟采用區塊優先的比特幣內核最大請求數是一樣的)
比特幣內核采用的區塊頭優先模式,采用1024個區塊為一移動下載時間窗口,以最大化下載速度。在下載時間窗口中最低區塊,是下一個即將被驗證的區塊。 如果輪到區塊驗證了,但區塊還未下載完成,比特幣內核會至少會等2秒,等待區塊從失速節點下載完成,如果還沒有下載完成,比特幣內核會斷開跟失速節點的連接,并嘗試給另外一個節點連接。比如,如上圖所示,如果在2秒后,節點A不能發送區塊3,那么節點A會被斷開連接。
一但IBD節點同步完整個區塊鏈,他會接收來那些通過正常區塊廣播而來的區塊,這個會在后面的小節會講到。
資源: 下面的的表格總結了這節提到的消息。點擊消息欄的鏈接可以查看對于消息的參考頁面。
消息 | getheaders |
headers |
getdata |
block |
---|---|---|---|---|
發送→接 | IBD→Sync | Sync→IBD | IBD→Many | Many→IBD |
內容 | 一個/多個區塊頭哈希 | 最大2000個區塊頭哈希 | 一個/多個從哈希頭派生出來的區塊存貨清單 | 一個序列化區塊 |
區塊廣播
當礦工發現一個新區塊后,它會使用下面的方法把區塊廣播給它的節點:
主動推送區塊:礦工給它的每個一個全節點發送一個帶有新區塊的
block
消息。在這種情況,礦工不采用標準的轉發方法,因為它知道跟它連接的節點沒有一個正好發現這個區塊。-
標準轉發區塊: 礦工扮演一個標準的轉發節點,給它每一個節點(全節點,SPV),通過發送帶有新區塊存貨訂單的
inv
消息。 通常節點會有以下返回:每個區塊優先(BF)節點,想要從全節點通過
getdata
消息中獲取區塊信息。每個區塊頭優先(HF)節點,想要從全節點通過
getheaders
消息獲取區塊,消息中應包括其最佳區塊鏈上,最高區塊頭的哈希頭,以及可能帶有一些,用于檢測分叉的,在最佳區塊鏈上的后續區塊頭。然后緊接著發送一個getdata
消息去請一個完整的區塊。通過先請求區塊頭,一個區塊頭優先的節點可能會拒絕孤塊,這會在下面小節會講到。-
每個SPV客戶端,通常想通過
getdata
消息獲取默克爾區塊。礦工根據每個請求相應的給他們返回消息。 通過
block
消息發送區塊,通過headers
消息發送一個或多個區塊頭,在0/多個tx消息后,通過merkleblock
消息,發送與SPV客戶端bloom過濾器相應的默克爾區塊,交易。-
直接公告區塊頭:中轉節點可跳過
getheaders
跟inv
消息之間來回切換的方式,獲取新區塊信息,直接立即發送一個包含完整新區塊頭的headers
消息。HF(區塊頭優先)節點收到這個消息后,當它在區塊頭優先IBD階段時,它會部分驗證區塊頭,如果區塊頭是有限的,它就會通過getdata
消息,來請求整個區塊內容。 中轉節點會給getdata
請求,相應的以block
或merkleblock
消息返回完整的,或過濾后的區塊數據。HF節點在握手連接時,可以通過發送一個特殊的sendheaders
消息來發出它更喜歡接收headers
消息,而非inv
消息的信號。這個區塊廣播協議已經在BIP130被提議,自從比特幣內核0.12后,都實現了這個協議。
-
在默認情況下,比特幣內核使用直接廣播區塊的方式給那些發送了sendheaders
信號的節點廣播區塊,而對其他節點使用標準區塊轉發方式。比特幣內核接受以上所有方式轉發的區塊。
全節點驗證收到的區塊,然后使用標準區塊轉發方式轉發給它的節點。下面精簡表格,重點羅列了這個過程中用到的消息(Relay, BH, HF, SPV 分別對應 轉發節點,區塊優先節點,區塊頭優先節點,SPV客戶端;Any - 表示一個使用任何獲取區塊方法的節點)
消息 | inv |
getdata |
getheaders |
headers |
---|---|---|---|---|
From→To | Relay→Any | BF→Relay | HF→Relay | Relay→HF |
內容 | 新區塊清單 | 新區塊清單 | 在HF節點最佳區塊頭鏈(BHC)上,一個/多個區塊頭哈希 | 最多2000個區塊頭,把HF節點的BHC跟轉發節點的BHC連接起來 |
消息 | block |
merkleblock |
tx |
|
From→To | Relay→BF/HF | Relay→SPV | Relay→SPV | |
內容 | 新序列化區塊 | 對新區塊修改后的默克爾區塊 | 來自新區塊,跟bloom過濾器匹配的序列化的交易 |
孤塊
區塊優先節點可能會下載孤塊。所謂的孤塊就是指之前區塊頭哈希字段,所指向的區塊還未看到。也就是說,孤塊沒有可知的父區塊(跟陳腐區塊不一樣,它們有父區塊,但是它們不屬于最近區塊鏈)。
當區塊優先節點下載了一個孤塊,節點不會立刻去驗證它,而是給發送孤塊的節點(廣播節點)發送一個getblocks
消息,這個廣播節點會返回一個帶有區塊清單的inv
消息,這個清單里面是節點丟失區塊的信息(最多500條);下載節點使用getdata
消息請求這些區塊;然后廣播節點會以block
消息形式發送這些區塊。然后下載節點會驗證這些區塊,一但之前孤塊的父區塊下載完成,并被驗證,下載節點就會驗證之前的孤塊。
區塊頭優先節點為避免復雜,它在使用getdata
消息請求一個區塊前,經常先使用getheaders
消息請求區塊頭。 廣播節點會給下載節點發送一個headers
消息,這個消息里面包含了它認為的,下載節點要達到最佳區塊鏈頂部,所需要的所有區塊頭(做多2000條);每個區塊頭都會指向它的父區塊,因此當下載節點收到block
消息時,該區塊不應該是一個孤塊 — 因為它所有的父塊哈希都已經知道(即使他們還未被驗證)。如果在block
消息中,收到的區塊是孤塊,那么區塊頭優先節點會立即丟棄它。
然而,丟棄孤塊意味著,區塊頭優先節點會忽略掉礦工以主動推送方法發出的孤塊。
交易廣播
要發送一個交易到另外一個節點,需要發送一個inv
消息。如果節點收到一個getdata
消息,就會使用tx
發送這個交易。節點收到這個交易后,如果是一個有效的交易,也會以同樣的方式轉發交易。
內存池
全節點可以跟蹤那些可以打包進下一區塊的未確認交易。這對于那些挖取這些或全部交易的礦工來說,是非常有必要的,但是它對于任何想要跟蹤未被確認交易的節點來說,也是很有用,比如為SPV節點提供未確認交易信息服務的節點。
因為在比特幣中,未確認交易沒有一種持久狀態,比特幣內核會把他們保存在內存里,也叫做內存池。當一個節點關掉后,除了那些保存到錢包的交易外,其他在內存池中的交易全部會丟失。這就意味著,但節點重起后,那些沒有被挖出的未確認交易會慢慢的從網絡中消息掉,或因為內存不足時,把其中的一些未確認交易從內存池中刪除掉。
那些沒有被打包進去區塊的交易會變成陳腐區塊,它們可能會被重新加到內存池中。如果替代區塊已包含這些交易,這些從新加入到內存池的交易會立即被刪除掉。在比特幣內核中,這種情況就是,從最佳區塊鏈最頂部開始(最高區塊),把鏈上的陳腐區塊一個個刪掉。當每個區塊被刪除時,它的交易會重新加到內存池中。刪掉完陳腐區塊后,在區塊鏈頂部,逐個加上替代區塊。當添加完一個區塊后,區塊中確認的交易就會從內存中刪除掉。
SPV客戶端因為不需要轉發交易,所以他們就沒有內存池。他們不能獨立的驗證一個交易是否包含在一個區塊里面,同時SPV客戶端只能花UTXOs,所以他們不知道哪個交易是合格的,是可以打包到下一個區塊的。
作弊節點
要注意的是,對于區塊,交易這兩種廣播,系統有一個機制來懲罰那些作弊節點。 他們會通過發送錯誤信息來占用帶寬,計算資源。如果一個節點的banscore值大于-banscore=<n>
設置的閥值,它就會被禁止-bantime=<n>
中所設置的時長,這個默認是86400秒(24小時)。
警告
在早期的比特幣內核版本,是允許開發者,可信的社區成員給用戶發布比特幣警告,以通知用戶比特幣網絡出現嚴重的問題。這個消息系統在比特幣內核0.13.0就已經作廢掉;然而,內部警告,分叉檢測警告,-alertnofity
功能還保留著。
聲明:
文中帶有[?]的地方,表示我對此翻譯明顯感覺不太對的,后續會不斷修正。
有些地方可能會翻譯的不好,不地道,甚至錯誤,如果有發現,還請留言,指出,以便我好修正,謝謝!