原文鏈接:http://www.lotushy.com/?p=115
數據結構
在任意給定時刻,一個節點總是連接到多個其他節點。默認情況下,一個節點連接到8個其他節點(鏈出),並允許多達125個鏈入節點連接進來。
相關常量定義在net.h
文件中。
節點集合則由全局變量vNodes維護。
CNode用於表示一個節點。
CNode包含許多屬性,其中大部分屬性都與底層鏈路(如套接字,字節流等)有關。
CNode的關鍵屬性如下:
-
nServices: 通常被稱為“服務位”。這是一個bitmap數據結構,用於表示peer提供的服務種類。具體種類如下。
-
fClient: 表示節點是否是SPV節點
-
fWhiteListed: 是否是白名單節點。如果是的話,該節點不會因為
壞
的行為被屏避掉。 -
vSendMsg: 隊列中待發送的消息隊列
-
vRecvMsg: 我們從Peer接收到的消息隊列
節點發現和節點連接
地址管理
地址管理器用於管理節點的IP地址和端口。(見addrman.h
)
設計目標:
- 將地址表保存在內存中,並將整個表異步轉儲到peers.dat。
- 確保沒有(本地化的)攻擊者可以用它的節點/地址填充整個表格。
為此:
- 地址以Bucket的方式組織,即地址被放到桶里。
- 尚未嘗試過的地址進入1024個“new”桶。
- 已知可訪問的節點地址進入256個“tried”桶。
- 每個地址范圍隨機選擇8個桶
- 根據完整地址從實際存儲桶中選擇實際存儲桶。
- 當為一個完整的桶添加一個新的好地址時,一個隨機選擇的entry(偏向於最近嘗試過的)會被彈出,回到“新”桶。
- 分組選擇基於密碼散列,使用隨機生成的256位密鑰,這不應該被攻擊者觀察到。
- 多個索引用於提升性能。
** 時間戳 **
地址管理器也要持續跟蹤每個節點的最近活躍情況。時間戳僅在一個地址上更新,並且在時間戳超過20分鍾時保存到數據庫。 通過理解時間戳的作用,將更加清楚的理解為什么要保持時間戳。
節點發現
節點發現從鏈路層來講,就是發現一個節點的IP地址和端口號。有多種方式:
- 地址數據庫,即peers.dat文件
- 在節點啟動時讀入並加載到節點管理器中。該方法在節點首次運行時是不支持的。因為peers.dat文件還不存在。
- 用戶指定(--addnode, --connect)
- DNS種子(DNS seeding)
- 僅當peers.dat文件為空時,DNS種子才會被使用。
- 默認DNS種子有6個:seed.bitcoin.sipa.be, dnsseed.bluematt.me, dnsseed.bitcoin.dashjr.org, seed.bitcoinstats.com, seed.bitcoin.jonasschnelli.ch, seed.btc.petertodd.org
- 硬編碼的種子
- 如果DNS seeding失敗,客戶端使用硬編碼的種子。[chainparamsseeds.h] 這些地址僅用作最后的手段。理想情況是盡快遠離種子節點,以避免過載這些節點。DNS種子和硬編碼的種子的時間戳均為0,這樣可以避免此類地址被廣播到網絡中或響應
getaddr
。
- 如果DNS seeding失敗,客戶端使用硬編碼的種子。[chainparamsseeds.h] 這些地址僅用作最后的手段。理想情況是盡快遠離種子節點,以避免過載這些節點。DNS種子和硬編碼的種子的時間戳均為0,這樣可以避免此類地址被廣播到網絡中或響應
- 從其他Peer獲取(
getaddr
和addr
消息)- 節點間通過
getaddr
和addr
交換IP地址信息。 - 但是,“addr”消息也可能會不請自來,因為節點在以下情況時無償地發布地址:
- 當節點轉發地址時
- 周期性廣播節點自己的地址(每24小時)
- 當連接建立時(響應
version
消息時帶回)
- 節點發送
getaddr
消息的時機- 在響應
version
消息,並且節點自身的地址量小於1000時。
- 在響應
- 當接收到
addr
時:- 發送消息的節點版本太老,並且我們已經有1000個節點地址了,該消息被忽略。
- 如果節點是當前版本,並且嘗試向我們發送超過1000個節點地址,則該節點會被懲罰。
- 如果該地址24小時內已經發現,並且當前時間戳超過60分種,則時間戳更新為60分種。
- 如果該地址24小時內沒有出現過,而時間戳是24小時之前,則更新為24小時。
- 當響應
getaddr
消息時:- 該節點計算出在過去3個小時內有多少個地址有時間戳。
- 它發送這些地址,但如果有超過2500個地址,它隨機選擇2500。
- 它清除我們認為遠程節點所擁有的地址列表,這將觸發發送到節點的刷新。 請參閱SendMessages。
- 地址中繼:
- 一旦新地址被添加
- 地址的時間戳在10分鍾之內
addr
消息包括10個地址或少一些- fGetAddr==false
- 該地址是可路由的
- 滿足以上條件,則新地址會被隨機的發送出去
- 一旦新地址被添加
- 節點間通過
節點連接
節點鏈接由線程ThreadOpenConnections管理。它負責選擇可用的地址,建立鏈接並在適當的時候釋放鏈接。
而inbound鏈接則由ThreadSocketHandler負責處理[ThreadMessageHandler]。
對於inbound鏈接,系統通過select執行IO操作。所以,其支持的inbound數量不會太多,目前,是125個。
插口(Sockets)和消息
Socket線程 (net.cpp)
消息線程
ProcessMessages (net_processing.cpp)
ProcessMessages是net_processing.cpp中的處理和驗證交易和區塊等相關代碼代碼的入口點,同時它也處理getaddr, addr等比特幣協議相關的消息。該方法主要完成消息的復制並調用ProcessMessage處理消息。
ProcessMessage基本上是一個大型的“開關”,它根據消息類型[NetMsgType]來采取行動。
消息類型列表如下[protocol.cpp]:
namespace NetMsgType {
const char *VERSION="version";
const char *VERACK="verack";
const char *ADDR="addr";
const char *INV="inv";
const char *GETDATA="getdata";
const char *MERKLEBLOCK="merkleblock";
const char *GETBLOCKS="getblocks";
const char *GETHEADERS="getheaders";
const char *TX="tx";
const char *HEADERS="headers";
const char *BLOCK="block";
const char *GETADDR="getaddr";
const char *MEMPOOL="mempool";
const char *PING="ping";
const char *PONG="pong";
const char *NOTFOUND="notfound";
const char *FILTERLOAD="filterload";
const char *FILTERADD="filteradd";
const char *FILTERCLEAR="filterclear";
const char *REJECT="reject";
const char *SENDHEADERS="sendheaders";
const char *FEEFILTER="feefilter";
const char *SENDCMPCT="sendcmpct";
const char *CMPCTBLOCK="cmpctblock";
const char *GETBLOCKTXN="getblocktxn";
const char *BLOCKTXN="blocktxn";
}
SendMessages (main.cpp)
SendMessages創建消息並在peer的vSendMsg隊列(雙端隊列或C ++中的“deque”)中對其進行排隊。vSendMsg對象基本上只是序列化的數據。
Locks
P2P層主要的鎖有:
- cs_vNodes: 控制對CNode對象的訪問
- cs_vSend: 控制對節點的發送緩存的訪問
- cs_vRecvMsg: 控制對節點的接收緩存的訪問
- cs_inventory
拒絕服務的防范措施
一旦發現異常行為的節點,則直接ban掉。
DoS預防框架在2011年引入。
具體見:https://github.com/bitcoin/bitcoin/pull/517
** 被禁節點 **
被禁節點存儲在setBanned中。setBanned是map類型的數據結構,定義為typedef std::map<CSubNet, CBanEntry> banmap_t
。
默認情況下,一個節點被禁止24小時,但可以使用-bantime選項進行配置。