前言node
最近準備學習區塊鏈的底層技術,打算以Bitshares公鏈爲學習例子。爲何要選擇 Bitshares 呢?主要是由於本身接觸的第一個區塊鏈就是 Bitshares ,而它跟目前很火的公鏈 EOS 以及公信寶(GXB)都是用一個叫 Graphene 的底層工具庫開發的,同屬 BM 的傑做。另外 Graphene 做爲高性能區塊鏈工具庫催生了不少優秀的區塊鏈項目,以 Bitshares 爲切入點去了解 Graphene 也是至關不錯的。在學習之餘,分享一下本身的學習筆記,一方面但願能爲社區作一點貢獻,另外一個方面是但願有大佬能在小編理解錯時指點一下,小編目前對c++還不熟悉,可能犯不少低級錯誤,望見諒。c++
適合什麼人看git
整個學習流程小編只會從比較宏觀的角度去分析一個區塊鏈程序是怎樣跑起來的,其中涉及什麼模塊,模塊之間又是如何交織支撐起區塊鏈運行的。適合對區塊鏈行業感興趣但仍是持着觀望態度的程序員,但願這些受衆在看完整個系列後,得到的知識可以幫助我的在合適的時機轉到區塊鏈行業。程序員
本章主要是講節點啓動流程,以及涉及的數據庫和網絡模塊分析。github
代碼地址:https://github.com/bitshares/bitshares-coreweb
首先看一下witness_node的程序入口(對於沒必要要的代碼都隱藏了,用備註來代替)數據庫
文件programs/witness_node/main.cpp
int main(int argc, char** argv) {
app::application* node = new app::application();
// 讀取命令行帶上的配置項
// 加載插件,後面分析插件機制
// 解析命令行指令
// 加載本地config.ini配置文件 ps:命令行帶上的配置項優先級比config.ini配置的要高
// 節點程序初始化(主要是查看須要加載哪些插件)
// 節點插件初始化
// 節點程序啓動
node->startup();
// 節點程序插件啓動
// 收到ctrl+c命令後關閉節點程序,進行關閉前的數據保存操做
return 0;
}複製代碼
轉到node->startup()內部api
文件libraries/app/application.cpp
void startup()
{
// 建立保存區塊數據的目錄
auto initial_state = // 初始化創世塊內容的匿名函數
// 打開數據庫
_chain_db->open( _data_dir / "blockchain", initial_state, GRAPHENE_CURRENT_DB_VERSION );
// api訪問權限設置讀取
// 重置p2p網絡狀態
reset_p2p_node(_data_dir);
// 重置websocket服務狀態
}複製代碼
這裏咱們重點關注一下數據庫和p2p的啓動過程,畢竟這兩個是區塊鏈骨架的核心。bash
先從數據庫着手:(我的習慣帶着問題或猜測去閱讀源碼,這樣更加有目的性和趣味性,而不是單純的看和記)websocket
問題1:既然區塊鏈的數據是以一個個連續的區塊來保存,那麼若是要查詢一個用戶的餘額,要怎麼查詢,這個數據存在了哪?
數據庫啓動過程:
文件libraries/chain/db_management.cpp
void database::open(
const fc::path& data_dir,
std::function<genesis_state_type()> genesis_loader,
const std::string& db_version)
{
// 檢查版本
// 打開 object 數據庫(爲了能避免混淆使用英文名來講明)
object_database::open(data_dir);
// 打開 block 數據庫( object 數據庫和 block 數據庫是有區別的)
_block_id_to_block.open(data_dir / "database" / "block_num_to_block");
// 若是數據爲空,則用genesis_loader創世塊配置初始化數據庫
// 驗證 block 數據庫最新的區塊是否與 object 數據庫一致,不一致則將沒記錄的區塊處理並更新 object 數據庫
reindex( data_dir );
}
複製代碼
能夠看出數據庫分爲兩個部分,一個是 block(區塊)數據庫,存儲的是每個區塊的原始數據;另外一個是 object(對象)數據庫,它是從 block 數據庫解析每個區塊數據後得出的區塊鏈中各個對象當前狀態的數據庫。打個比方,區塊1包含建立用戶b,區塊2包含用戶b做爲見證人得到10bts, object 數據庫解析了區塊1以後,用戶列表多了一個用戶b,解析區塊2後狀態變成用戶b存款有10bts。
用一個表達式來表示:
parse(nextblock, state) = nextstate
object 數據庫的啓動過程:
文件libraries/db/object_database.cpp
void object_database::open(const fc::path& data_dir)
{
// 讀取不一樣object的數據,bts中不一樣的數據類型都會有對應的object_id
// 格式是x.x.x,分別表明spaceID,typeID,index,spaceID的區別還沒明白,typeID就是區分不一樣對象類型,index就是同一對象類型不一樣實體
// 例如1.3.前綴表明資產,1.2.前綴表明用戶,1.2.99表明第99個用戶,1.3.113表明第113個資產
// 這裏能夠看出每一個對象類型的數據都是由單獨的文件保存的
_index[space][type]->open( _data_dir / "object_database" / space/ type);
}
文件libraries/db/include/graphene/db/index.hpp
virtual void open( const path& db )override
{
// 建立內存映射文件
// 從內存映射文件反序列化數據並保存起來,具體序列號和反序列化的實現是在/libraries/fc/include/fc/io/raw.hpp
}複製代碼
object 數據庫爲了不每次啓動都要從新解析全部塊來生成對象的狀態,把狀態都保存到了文件裏,啓動的時候再從文件中解析對象狀態。
數據庫模塊的數據流向圖:
除了區塊數據庫之外,節點還維護了對象數據庫,用來保存數據對象的狀態,咱們查餘額的時候其實是查詢對象數據庫的內容,而不是直接從區塊數據庫查內容。
p2p啓動流程:
問題1:如何在一開始的時候發現存在的而且有效的節點?(冷啓動問題)
文件libraries/app/application.cpp
void reset_p2p_node(const fc::path& data_dir){
_p2p_network = std::make_shared<net::node>("BitShares Reference Implementation");
// 加載配置
_p2p_network->set_node_delegate(this);
// 若是沒有指定p2p節點則使用默認節點
vector<string> seeds = {
"104.236.144.84:1777", // puppies (USA)
"bts-seed1.abit-more.com:62015", // abit (China)
...省略不少
};
// 添加節點到_p2p_network中
// 配置項讀取
// 監聽p2p網絡,等於向外提供p2p服務
_p2p_network->listen_to_p2p_network();
// 鏈接p2p網絡
_p2p_network->connect_to_p2p_network();
// 從p2p網絡中同步區塊數據
_p2p_network->sync_from(net::item_id(net::core_message_type_enum::block_message_type,
_chain_db->head_block_id()),
std::vector<uint32_t>());
}
複製代碼
原來冷啓動鏈接的p2p節點是寫死的,域名形式的節點可能被牆,ip形式的可能不穩定,二者都有恰好互補。
鏈接到p2p網絡的操做有不少(爲了閱讀體驗,只保留了要說明的代碼,其他的都用一句註釋來歸納了)
文件/bitshares-core/libraries/net/node.cpp
void node_impl::connect_to_p2p_network(){
// 循環監聽是否有p2p鏈接請求,爲了減輕dos攻擊影響,設置10毫米間隔
// 循環鏈接,鏈接20個節點後進入10秒睡眠,不斷重複該過程
// 向其餘節點請求同步區塊
_fetch_sync_items_loop_done = fc::async( [=]() { fetch_sync_items_loop(); }, "fetch_sync_items_loop" );
// 向其餘節點請求最新的數據
_fetch_item_loop_done = fc::async( [=]() { fetch_items_loop(); }, "fetch_items_loop" );
// 將節點收到的交易廣播到區塊鏈網絡中
_advertise_inventory_loop_done = fc::async( [=]() { advertise_inventory_loop(); }, "advertise_inventory_loop" );
// 循環關閉超時沒響應的節點,向其餘有效節點發送心跳包
// 循環請求當前p2p網絡節點信息,15分鐘一次
// 統計網絡下載和上傳速率,每秒一次
// 節點的狀態log,每分鐘一次
}
}
複製代碼
網絡鏈接時建立了不少定時任務來維護p2p網絡和保證數據同步,其中fetch_sync_items_loop、fetch_items_loop、advertise_inventory_loop這三個任務跟區塊鏈的業務邏輯比較相關,分別是用於從其餘節點同步區塊數據、從其餘節點獲取區塊數據、廣播數據到其餘節點。
看到這裏,大體知道了節點啓動時是走了怎麼樣的流程,涉及了什麼:
1.啓動過程主要涉及數據庫加載和p2p網絡加載
2.數據庫分爲區塊數據庫和對象數據庫,對象數據庫是經過解析區塊數據庫得出的
3.p2p網絡模塊主要負責維護p2p網絡和保證數據同步
那麼Bitshares是怎麼校驗數據的正確性?數據不正確的時候怎麼處理?
這個下一篇再講了,腦袋疼,小編不多寫文章,會存在不少自身沒法發現的問題條,但願各位路過的大佬多多斧正,謝謝~
打個公衆號廣告~