Controller:EOS區塊鏈核心控制器
Controller是EOS區塊鏈的核心控制器,其功能豐富、責任重大。
關鍵字:EOS,區塊鏈,controller,chainbase,db,namespace,using,訊號槽,fork_database,snapshot
名稱空間namespace
名稱空間namespace定義了一個範圍,這個範圍本身可作為額外的資訊,類似於地址,或者位置。如果有兩個名字相同的變數或者函式,例如foshan::linshuhao和nba::linshuhao,名稱空間可以提供:
- 區分性或者歸類性。不同名稱空間下的內容互相孤立,即使內部函式名稱相同,也不會產生混淆。
- 可讀性,本例中foshan和nba提供了一層語義。
C++程式架構中,不同的檔案可以通過引入相同的名稱空間使用或者擴充套件功能。進一步理解,不同的檔名可以提供一層語義,這些檔案可以共同維護一個跨檔案的名稱空間。
using語法
C++程式設計中,經常會遇到帶有using關鍵字的語句。using正如字面含義,代表了本作用域後續會使用到的內容,這個內容可以是:
- 其他名稱空間,用using宣告以後,該名稱空間下的公有屬性都可被使用。
- 直接指定其他名稱空間下的某個函式,相當於匯入功能,可以使用該函式,不過使用時仍舊要帶上包含函式名稱空間的完整路徑。
- 為某個複雜名字變數起的別名以便於使用。例如
using apply_handler = std::function<void(apply_context&)>;
controller依賴功能
通過controller的宣告檔案,可以看到其整個結構。它聲明瞭兩個名稱空間:
- chainbase,這項宣告為controller提供了基於chainbase的狀態資料庫能力。該名稱空間是chainbase元件定義的,聲明瞭database類,在chainbase原始碼中可以找到database類,這個類在前文chainbase的章節已經介紹過。
- eosio::chain,該命名函式是EOSIO專案中內容最豐富的,在很多其他元件都有定義與使用。Controller引用了其他元件在相同名稱空間下定義的功能,包括:
- authorization_manager,提供許可權管理的功能,許可權內容有認證資訊、依賴金鑰、關聯許可權、許可。管理操作包括增刪改查。
- resource_limits::resource_limits_manager,完全的名稱空間為eosio::chain::resource_limits,為controller提供了資源限制管理的功能。此處的資源指的是基於chainbase的資料庫的儲存資源。例如,增加索引、資料庫初始化、快照增加和讀取、賬戶初始化、設定區塊引數、更新賬戶使用等。
- dynamic_global_property_object,動態維護全域性狀態資訊,繼承自chainbase::object。它的值是在正常的鏈操作期間計算的,以及反映全域性區塊鏈屬性的當前值。
- global_property_object,維護全域性狀態資訊,同樣繼承自chainbase::object。它的的值由委員會成員設定,以調優區塊鏈引數。與上面的區別是一個是動態計算,一個是靜態指定。
- permission_object,同樣繼承自chainbase::object。增加了屬於許可權範疇的屬性,包括id主鍵、parent父許可權id、許可權使用id,賬戶名、許可權名、最後更新時間、許可權認證。另外提供了檢查傳入許可權是否等效或大於其他許可權。許可權是按層次結構組織的,因此父許可權嚴格地比子許可權以及孫子許可權更強大。
- account_object,同樣繼承自chainbase::object。增加了屬於賬戶範疇的屬性,包括id主鍵、賬戶名、是否擁有超級許可權能力、最後code更新時間、code版本、建立時間、code、abi。另外提供了abi設定函式set_abi()和abi查詢函式get_abi()。
- fork_database,分叉資料庫。下面會詳細介紹。
controller擴充套件
在controller.hpp中,最重要的部分就是類controller的內容,它是對名稱空間eosio::chain內容的擴充套件。在展開介紹controller類之前,先要說明在eosio::chain名稱空間下,有兩個列舉類的定義,這也是對名稱空間功能的擴充套件,因為下面介紹controller類的時候會使用:
db_read_mode,db讀取模式是一個列舉類,包括:
- SPECULATIVE,推測模式。內容為兩個主體的資料:已完成的頭區塊,以及還未上鍊的事務。
- HEAD,頭塊模式。內容為當前頭區塊資料。
- READ_ONLY,只讀模式。內容為同步進來的區塊資料,不包括推測狀態的事務處理資料。
- IRREVERSIBLE,不可逆模式。內容為當前不可逆區塊的資料。
validation_mode,校驗模式也同樣是一個列舉類,包括:
- FULL,完全模式。所有同步進來的區塊都將被完整地校驗。
- LIGHT,輕量模式。所有同步進來的區塊頭都將被完整的校驗,通過校驗的區塊頭所在區塊的全部事務被認為可信。
下面進入controller類,內容很多,首先包含了一個公有的成員config,它是一個結構體,包含了大量鏈配置項,可在配置檔案或者鏈啟動命令中配置。controller中的config結構體是動態執行時的引數配置,而EOSIO提供了另外一個eosio::chain::config名稱空間,這裡定義了系統初始化預設的一些配置項的值,controller中的config結構體的某些配置項的初始化會使用到這些預設值。
config的配置項中大量使用到了一個容器:flat_set。這是一個使用鍵儲存物件,且經過排序的容器,同時它是一個去重容器,也就是說容器中不會包含兩個相同的元素。
其中被序列化公開的屬性有:
FC_REFLECT( eosio::chain::controller::config,
(actor_whitelist) // 賬戶集合,作為actor白名單
(actor_blacklist) // 賬戶集合,作為actor黑名單
(contract_whitelist) // 賬戶集合,作為合約白名單
(contract_blacklist) // 賬戶集合,作為合約黑名單
(blocks_dir) // 儲存區塊資料的目錄名字,有預設值為"blocks"
(state_dir) // 儲存狀態資料的目錄名字,有預設值為"state"
(state_size) // 狀態資料的大小,有預設值為1GB
(reversible_cache_size) // 可逆去快資料的快取大小,有預設值為340MB
(read_only) // 是否只讀,預設為false。
(force_all_checks) // 是否強制執行所有檢查,預設為false。
(disable_replay_opts) // 是否禁止重播引數,預設為false。
(contracts_console) // 是否允許合約輸出到控制檯,一般為了除錯合約使用,預設為false。
(genesis) // eosio::chain::genesis_state結構體的例項,包含了創世塊的初始化配置內容。
(wasm_runtime) // 執行時webassembly虛擬機器的型別,預設值為eosio::chain::wasm_interface::vm_type::wabt
(resource_greylist) // 賬戶集合,是資源灰名單。
(trusted_producers) // 賬戶集合,為可信生產者。
)
未包含在內的屬性有:
flat_set< pair<account_name, action_name> > action_blacklist; // 賬戶和action組成一個二元組作為元素的集合,儲存了action的黑名單
flat_set<public_key_type> key_blacklist; // 公鑰集合,公鑰黑名單
uint64_t state_guard_size = chain::config::default_state_guard_size; // 狀態守衛大小,預設為128MB
uint64_t reversible_guard_size = chain::config::default_reversible_guard_size; // 可逆區塊守衛大小,預設為2MB
bool allow_ram_billing_in_notify = false; // 是否允許記憶體賬單通知,預設為false。
db_read_mode read_mode = db_read_mode::SPECULATIVE; // db只讀模式,預設為SPECULATIVE
validation_mode block_validation_mode = validation_mode::FULL; // 區塊校驗模式,預設為FULL
controller::block_status,區塊狀態列舉類,包括:
- irreversible = 0,該區塊已經被當前節點應用,並且被認為是不可逆的。
- validated = 1,這是由一個有效生產者簽名的完整區塊,並且之前已經被當前節點應用,因此該區塊已被驗證但未成為不可逆。
- complete = 2,這是一個由有效生產者簽名的完整區塊,但是還沒有成為不可逆,也沒有被當前節點應用。
- incomplete = 3,這是一個未完成的區塊,未被生產者簽名也沒有被某個節點生產。
接下來,檢視controller的私有成員:
- apply_context類物件,處理節點應用區塊的上下文環境。其中包含了迭代器快取、二級索引管理、通用索引管理、構造器等內容。
- transaction_context類物件,事務上下文環境。包含了構造器,轉型,事務的生命週期(包括初始化、執行、完成、刷入磁碟、撤銷操作),事務資源管理、分發action、定時事務、資源賬單等內容。
- mutable_db(),返回一個可變db,型別與正常db相同,都是chainbase::database,但這個函式返回的是一個常量引用。
- controller_impl結構體的例項的唯一指標my。這是整個controller的環境物件,controller_impl結構體包含了眾多controller功能的實現。通過my都可以快取在同一個環境下使用。
controller類的共有成員屬性以及私有成員介紹完了,還剩下公有成員函式,這部分內容非常多,幾乎包含了整個鏈執行所涉及到的出塊流程相關的一切內容,從區塊本地組裝、校驗簽名,到本地節點應用入狀態庫,經過多節點共識成為不可逆區塊等函式。其中每個階段都有對應的訊號,訊號功能使用了boost::signals2::signal
庫。controller維護了這些訊號內容:
- signal<void(const signed_block_ptr&)> pre_accepted_block; // 預承認區塊(承認其他節點廣播過來的區塊是正確的)
- signal<void(const block_state_ptr&)> accepted_block_header; // 承認區塊頭(對區塊頭做過校驗)
- signal<void(const block_state_ptr&)> accepted_block; // 承認區塊
- signal<void(const block_state_ptr&)> irreversible_block; // 不可逆區塊
- signal<void(const transaction_metadata_ptr&)> accepted_transaction; // 承認事務
- signal<void(const transaction_trace_ptr&)> applied_transaction; // 應用事務(承認其他節點資料要先校驗,通過以後可以應用在本地節點)
- signal<void(const header_confirmation&)> accepted_confirmation; // 承認確認
- signal<void(const int&)> bad_alloc; // 記憶體分配錯誤訊號
controller的具體實現
controller函式的具體實現內容,一般是對引數的校驗,然後通過my來呼叫controller_impl結構體的具體函式來處理。所以controller的核心功能實現是在controller_impl結構體中,下面檢視其成員屬性:
- self,controller例項的引用。
- db, chainbase::database的一個例項,用於儲存區塊全資料,是區塊進入不可修改的block_log之前的緩衝地帶,包括本地的,同步過來的,未承認的,已承認的等等。
- reversible_blocks,同樣也是chainbase::database的一個例項,但它是用來儲存那些已經成功被應用但仍舊是可逆的特殊區塊。
- blog,block_log類例項,是區塊鏈不可逆資料的儲存物件。這部分內容在資料儲存結構部分已有詳細解釋,此處不再贅述。
- pending,處於pending狀態的一個區塊的包裝。
- head,block_state_ptr結構體是所有區塊的統一資料結構,head代表頭區塊物件。
- fork_db,fork_database類例項,分叉庫。
- wasmif,wasm_interface類例項,是webassembly虛擬機器介面的例項。
- resource_limits,resource_limits_manager資源限制管理器例項。
- authorization,authorization_manager認證許可權管理器例項。
- conf,controller::config前文介紹的配置config的例項。
- chain_id,chain_id_type型別,代表區塊鏈當前id。
- replaying,是否允許重播,預設初始化為false。
- replay_head_time,重播的頭區塊時間。
- read_mode,資料庫讀取模式,預設初始話為SPECULATIVE
- in_trx_requiring_checks,事務中是否需要檢查,預設為false。如果為true的話,通常會被跳過的檢查不會被跳過。例如身份驗證。
- subjective_cpu_leeway,剩餘的cpu資源,以微妙計算。
- trusted_producer_light_validation,可信的生產者執行輕量級校驗,預設為false。
- snapshot_head_block,快照的頭區塊號。
- handler_key,處理者的鍵,元素為scope和action組成的二元組。
- apply_handlers,應用操作的處理者,元素為以handler_key為鍵,
std::function<void(apply_context&)>
為值的map作為值,賬戶名作為鍵的複雜map。 - unapplied_transactions,未應用的事務map,以sha256加密串作為鍵,transaction_metadata_ptr為值。pop_block函式或者abort_block函式為執行完畢的事務,如果再次被其他區塊應用會從這個列表中移除,生產者在排程新事務打包到區塊裡時可以查詢這個列表。
剩下的內容為controller_impl的眾多功能函式的實現了,這些內容都是需要與其他程式組合使用,例如外掛程式,或者智慧合約,因此在接下來的篇章中,將會重新按照一個功能入口研究完整的使用脈絡。而在這些功能中有兩個內容需要在此處研究清楚,一個是fork_database,另一個是snapshot。下面逐一展開分析。
fork_database
在fork_database.hpp檔案中宣告。管理了輕量級狀態資料,是由未確認的潛在區塊產生的。當本地節點接收receive到新的區塊時,它們將被推入fork資料庫。fork資料庫跟蹤最長的鏈,以及最新不可逆塊號。所有大於最新不可逆塊號的區塊將會在發出“irreversible”不可逆訊號以後被釋放掉,區塊已經成功上鍊變為不可逆,因此fork庫沒必要再儲存。分叉庫提供了很多函式,例如通過區塊id獲取區塊、通過區塊號獲取區塊、插入區塊包括set和add各種過載函式、刪除區塊、獲取頭區塊、通過id獲取兩個分支、設定區塊標誌位等。
1. fork_database構造器
在controller_impl的建構函式體中會被呼叫。
controller_impl( const controller::config& cfg, controller& s )
:self(s),
db( cfg.state_dir,
cfg.read_only ? database::read_only : database::read_write,
cfg.state_size ),
reversible_blocks( cfg.blocks_dir/config::reversible_blocks_dir_name,
cfg.read_only ? database::read_only : database::read_write,
cfg.reversible_cache_size ),
blog( cfg.blocks_dir ),
fork_db( cfg.state_dir ), // 呼叫fork_db構造器,傳入一個檔案路徑。
wasmif( cfg.wasm_runtime ),
resource_limits( db ),
authorization( s, db ),
conf( cfg ),
chain_id( cfg.genesis.compute_chain_id() ),
read_mode( cfg.read_mode )
進入構造器。
fork_database::fork_database( const fc::path& data_dir ):my( new fork_database_impl() ) {
my->datadir = data_dir;
if (!fc::is_directory(my->datadir))
fc::create_directories(my->datadir);
auto fork_db_dat = my->datadir / config::forkdb_filename; // 在該目錄下建立一個檔案forkdb.dat
if( fc::exists( fork_db_dat ) ) { // 如果該檔案已存在
string content;
fc::read_file_contents( fork_db_dat, content ); // 將其讀到記憶體中
fc::datastream<const char*> ds( content.data(), content.size() );
unsigned_int size; fc::raw::unpack( ds, size ); // 按照區塊結構解析
for( uint32_t i = 0, n = size.value; i < n; ++i ) { // 遍歷所有區塊
block_state s;
fc::raw::unpack( ds, s );
set( std::make_shared<block_state>( move( s ) ) ); // 逐一插入到資料庫fork_database中
}
block_id_type head_id;
fc::raw::unpack( ds, head_id );
my->head = get_block( head_id ); // 處理fork_database的頭區塊資料
fc::remove( fork_db_dat ); // 刪除持久化檔案forkdb.dat。
}
}
檔案forkdb.dat也位於節點資料目錄中,是前文介紹唯一沒有說到的檔案,這裡補齊。
2. irreversible訊號
上面講到了,fork_database擁有一個公有成員irreversible訊號。這個訊號在controller_impl結構體的巨集SET_APP_HANDLER中被使用:
fork_db.irreversible.connect( [&]( auto b ) {
on_irreversible(b);
});
這段程式碼其實是boost的訊號槽機制,訊號有一個connect操作,其引數是一個slot插槽,可將插槽連線到訊號上,最終返回一個connection物件代表這段連線關係,可以靈活控制連線開關。插槽的型別可以是任意物件,這段程式碼中是一個lambda表示式,呼叫了on_irreversible函式。
接下來,去fork_database查詢該訊號的觸發位置,出現在prune函式中的一段程式碼,
auto itr = my->index.find( h->id ); // h是prune入參,const block_state_ptr& h
if( itr != my->index.end() ) {
irreversible(*itr);
my->index.erase(itr);
}
在table中查詢入參區塊,查詢到以後,會觸發訊號irreversible並攜帶區塊源資料發射。然後執行fork_database的刪除操作將目標區塊從分叉庫中刪除。
irreversible訊號攜帶區塊被髮射後,由於上面巨集的作用,會呼叫controller_impl的on_irreversible函式,並按照lambda表示式的規則將區塊傳入。該函式會將入參區塊變為不可逆,處理成功以後,下面截取了這部分相關程式碼:
...
fork_db.mark_in_current_chain(head, true);
fork_db.set_validity(head, true);
}
emit(self.irreversible_block, s);
這兩行是該函式對fork_db的全部操作,將fork_db的屬性in_current_chain和validated置為true。在on_irreversible函式的最後,它也發射了一個自己的訊號,注意發射方式採用了關鍵字emit,也攜帶了操作的區塊資料。
訊號觸發可以有兩種方式,使用關鍵字emit(signal,param)和直接呼叫signal(param)。
這個訊號本來是與這一小節的內容不相干,但既然分析到這了,還是希望能有個閉環,那麼來看一下該訊號的連線槽位置,如圖所示。
可以看到,區塊不可逆的訊號在net_plugin,chain_plugin,mongo_db_plugin,producer_plugin四個外掛程式碼中得到了運用,也說明這四個外掛是非常關心區塊不可逆的狀態變化的。至於他們具體是如何運用的,在相關部分會有詳細介紹。
3. initialize_fork_db
初始化fork_db,主要工作是從創世塊狀態設定fork_db的頭塊。頭塊的資料結構是區塊狀態物件,構造頭塊時,要先構造區塊頭狀態物件,包括:
- active_schedule,活動的出塊安排,預設為初始出塊安排。
- pending_schedule,等待中的出塊安排,預設為初始出塊安排。
- pending_schedule_hash,等待中的出塊安排的單向雜湊值。
- header.timestamp,等於創世塊配置檔案genesis中的timestamp值。
- header.action_mroot,action的Merkel樹根,創世塊的值為鏈id值,該值是通過加密演算法計算出的。
- id,塊id。
- block_num,塊號。
構建好區塊頭以後,接著構建區塊體,構建完成以後,將完整頭塊插入到空的fork_db中。
4. commit_block -> add_to_fork_db
提交區塊函式,無論提交是否成功,都不再保留活動的pending塊。該函式有一個引數add_to_fork_db,是否加入fork_db。在producer_plugin生產者生產區塊的邏輯中,提交區塊呼叫controller物件的commit_block函式:
void controller::commit_block() {
validate_db_available_size(); // 校驗db資料庫的大小
validate_reversible_available_size(); // 校驗reversible資料庫的大小
my->commit_block(true); // 呼叫controller_impl結構體中的的commit_block函式,並且傳入true
}
從這條邏輯過來的提交區塊,會執行add_to_fork_db,而commit_block函式的另一處呼叫是在應用區塊部分,沒有觸發add_to_fork_db。至於commit_block函式的內容不在此處展開,只看fork_db相關的內容:
if (add_to_fork_db) {
pending->_pending_block_state->validated = true; // 將pending區塊物件的狀態屬性validated置為true,標記已校驗。
auto new_bsp = fork_db.add(pending->_pending_block_state); // 將pending區塊新增至fork_db。
emit(self.accepted_block_header, pending->_pending_block_state); // 發射controller的accepted_block_header訊號,攜帶pending區塊狀態物件。
head = fork_db.head(); // 將當前節點的頭塊設定為fork_db的頭塊。
// 校驗pending區塊是否最終成功同時變為fork_db以及主節點的頭塊。
EOS_ASSERT(new_bsp == head, fork_database_exception, "committed block did not become the new head in fork database");
}
以上程式碼中又發射一個訊號accepted_block_header,仍舊檢視一下該訊號的連線槽在哪裡,經過查詢,發現是在net_plugin和chain_plugin兩個外掛中,說明這兩個外掛是要對這個訊號感興趣並捕捉該訊號。
5. maybe_switch_forks
或許要切換分叉庫到主庫。該函式會在controller_impl結構體中的push_block和push_confirmation兩個函式中被呼叫。
if ( read_mode != db_read_mode::IRREVERSIBLE ) { // 在db讀取模式不等於IRREVERSIBLE時,要呼叫maybe_switch_forks函式。
maybe_switch_forks( s );
}
db讀取模式為IRREVERSIBLE時,只關心當前不可逆區塊的資料,而fork_db中不存在不可逆區塊的資料。而其他三種讀取模式都涉及到可逆區塊以及未被確認的資料,因此要去maybe_switch_forks函式檢查處理一番。
- 當fork_db頭塊的上一個塊等於當前節點的頭塊時,說明有新塊被接收,先到達fork_db中,執行:
apply_block( new_head->block, s ); // 將新塊應用到主庫中去。
fork_db.mark_in_current_chain( new_head, true ); // 在fork_db中將新塊的屬性in_current_chain標記為true。
fork_db.set_validity( new_head, true ); // 在fork_db中將新塊的屬性validity標記為true。
head = new_head; // 更新節點主庫的頭塊為當前塊。
- 當fork_db頭塊的前一個塊不等於主庫頭塊且fork_db頭塊id也不等於當前節點的頭塊id時,說明fork_db最新的兩個塊都不等於主庫頭塊。這時候fork_db是更長的一條鏈,因此要切換主庫為fork_db鏈。切換的過程很複雜,此處不展開。
6. controller析構對fork_db的處理
my->fork_db.close();
在controller析構時將fork_db關掉,因為它會生成irreversible訊號到這個controller。如果db讀取模式為IRREVERSIBLE,將應用最後一個不可逆區塊,my需要成為指向有效controller_impl的指標。
void fork_database::close() {
if( my->index.size() == 0 ) return;
auto fork_db_dat = my->datadir / config::forkdb_filename;
// 獲取檔案輸出流。
std::ofstream out( fork_db_dat.generic_string().c_str(), std::ios::out | std::ios::binary | std::ofstream::trunc );
uint32_t num_blocks_in_fork_db = my->index.size();
// 將當前fork_db的區塊資料打包到輸出流,持久化到fork_db.dat檔案中。
fc::raw::pack( out, unsigned_int{num_blocks_in_fork_db} );
for( const auto& s : my->index ) {
fc::raw::pack( out, *s );
}
if( my->head )
fc::raw::pack( out, my->head->id );
else
fc::raw::pack( out, block_id_type() );
// 通常頭塊不是不可逆的。如果fork_db中只剩一個塊就是頭塊,一般不會將它刪除因為下一個區塊需要從頭塊建立。不過可以在退出之前將這個區塊作為不可逆區塊從fork_db中刪除。
auto lib = my->head->dpos_irreversible_blocknum;
auto oldest = *my->index.get<by_block_num>().begin();
if( oldest->block_num <= lib ) {
prune( oldest );
}
my->index.clear();
}
7. controller::startup對fork_db的處理
my->head = my->fork_db.head();
controller的startup週期時,會將fork_db的頭塊設定為主庫頭塊(頭塊一般不是不可逆的)。
snapshot
快照,顧名思義,可以為區塊鏈提供臨時快速備份的功能。
1. abstract_snapshot_row_writer
該結構體位於名稱空間eosio::chain::detail。提供了寫入snapshot快照的能力,是所有關於快照寫入的結構的基類。該結構體是一個抽象型別,包含四個成員函式:
- write,引數為ostream_wrapper例項(同樣在detail名稱空間下定義)的引用。
- write,過載引數為sha256的加密器。
- to_variant,轉型變體。
- row_type_name,行型別名,字串型別。
snapshot_row_writer繼承了abstract_snapshot_row_writer,在構造該結構體例項時,要傳入data資料被快取在函式體。接著,實際上,write向兩種資料型別的輸出流中寫入的時候,物件就是data,寫入方法都是fc::raw::pack(out, data);
,最終將記憶體中的data資料寫入到輸出流。to_variant函式也被實現了,轉型的目標是data,返回轉型後的variant物件。data型別是模板型別,row_type_name實現了通過boost::core::demangle庫獲得data的具體型別名。最後,對外提供了make_row_writer函式,接收任何型別的資料,初始化以上快照行寫入的功能。
snapshot_writer進一步封裝了寫入功能,對外提供了write_row寫入介面以及其他輔助功能介面。該類使用到了detail的內容,包括make_row_writer函式的類。
接著,定義了snapshot_writer_ptr是snapshot_writer例項的共享指標。
variant_snapshot_writer和ostream_snapshot_writer都是snapshot_writer的子類,根據不同的資料型別實現了不同的處理邏輯。
2. abstract_snapshot_row_reader
與上面相對的,是讀取的部分,所有關於快照讀取結構的基類。其包含三個成員虛擬函式:
- provide,引數是std::istream的例項引用,說明是對標準庫輸入流的讀取。
- provide,過載引數是fc::variant的引用,對變體的讀取。
- row_type_name,行型別名,同上,字串型別。
snapshot_row_reader繼承了abstract_snapshot_row_reader,在構造該結構體例項時,要傳入data資料被快取在函式體。接著,分別對應不同輸入流的處理不同,最終會將不同輸入流的資料讀取到記憶體的data例項中。row_type_name的實現同上。make_row_reader的意義同上。
snapshot_reader進一步封裝了讀取功能,對外提供了read_row讀取介面以及其他輔助功能介面。該類使用到了detail的內容,包括make_row_reader函式的類。
接著,定義了snapshot_reader_ptr是snapshot_reader例項的共享指標。
variant_snapshot_reader和ostream_snapshot_reader,還有integrity_hash_snapshot_writer(處理的是hash演算法sha256的加密串)都是snapshot_writer的子類,根據不同的資料型別實現了不同的處理邏輯。
3. controller::startup對snapshot的處理
void controller::startup( const snapshot_reader_ptr& snapshot ) {
my->head = my->fork_db.head(); // 將fork_db的頭塊設定為狀態主庫頭塊
if( !my->head ) { // 如果狀態主庫頭塊為空,則說明fork_db沒有資料,可能需要重播block_log生成這些資料。
elog( "No head block in fork db, perhaps we need to replay" );
}
my->init(snapshot); // 根據startup的入參snapshot呼叫controller_impl的初始化函式init。
}
進入controller_impl的初始化函式init。
void init(const snapshot_reader_ptr& snapshot) {
if (snapshot) { // 如果入參snapshot不為空
EOS_ASSERT(!head, fork_database_exception, "");//快照存在而狀態主庫頭塊不存在是個異常狀態。
snapshot->validate();// 校驗快照
read_from_snapshot(snapshot);// 執行read_from_snapshot函式
auto end = blog.read_head();// 從日誌檔案中獲取不可逆區塊頭塊。
if( !end ) {// 如果不可逆區塊頭塊為空,重置日誌檔案,清除所有資料,重新初始化block_log狀態。
blog.reset(conf.genesis, signed_block_ptr(), head->block_num + 1);
} else if ( end->block_num() > head->block_num) {// 如果不可逆區塊頭塊號大於狀態主庫頭塊號。
replay();// 狀態庫的資料與真實資料不同步,版本過舊,需要重播修復狀態主庫資料。
} else {
// 校驗提示報錯:區塊日誌提供了快照,但不包含主庫頭塊號
EOS_ASSERT(end->block_num() == head->block_num, fork_database_exception,
"Block log is provided with snapshot but does not contain the head block from the snapshot");
}
} else if( !head ) {如果入參snapshot為空且狀態主庫的頭塊也不存在,說明狀態庫完全是空的。
initialize_fork_db(); // 重新初始化fork_db
auto end = blog.read_head();// 讀取區塊日誌中的不可逆區塊頭塊。
if( end && end->block_num() > 1 ) {// 如果頭塊存在且頭塊號大於1
replay();// 重播生成狀態庫。
} else if( !end ) {// 如果頭塊不存在
blog.reset( conf.genesis, head->block );// 重置日誌檔案,清除所有資料,重新初始化block_log狀態。
}
}
...
if( snapshot ) {//快照存在,計算完整hash值。通過sha256演算法計算,將結果寫入快照,同時將結果列印到控制檯。
const auto hash = calculate_integrity_hash();
ilog( "database initialized with hash: ${hash}", ("hash", hash) );
}
}
EOS為snapshot定義了一個chain_snapshot_header結構體,用來儲存快照版本資訊。
執行read_from_snapshot函式:
void read_from_snapshot( const snapshot_reader_ptr& snapshot ) {
snapshot->read_section<chain_snapshot_header>([this]( auto §ion ){
chain_snapshot_header header;
section.read_row(header, db);
header.validate();
});// 先讀取快照頭資料。
snapshot->read_section<block_state>([this]( auto §ion ){
block_header_state head_header_state;
section.read_row(head_header_state, db);// 讀取區塊頭狀態資料
auto head_state = std::make_shared<block_state>(head_header_state);
// 對fork_db的設定。
fork_db.set(head_state);
fork_db.set_validity(head_state, true);
fork_db.mark_in_current_chain(head_state, true);
head = head_state;
snapshot_head_block = head->block_num;// 設定快照的頭塊號為主庫頭塊號
});
controller_index_set::walk_indices([this, &snapshot]( auto utils ){
using value_t = typename decltype(utils)::index_t::value_type;
// 跳過table_id_object(內聯的合同表格部分)
if (std::is_same<value_t, table_id_object>::value) {
return;
}
snapshot->read_section<value_t>([this]( auto& section ) {//按照value_t型別讀取快照到section
bool more = !section.empty();
while(more) {// 迴圈讀取section內容,知道全部讀取完畢。
decltype(utils)::create(db, [this, §ion, &more]( auto &row ) {
more = section.read_row(row, db);// 按行讀取資料,回撥逐行寫入主庫。
});
}
});
});
read_contract_tables_from_snapshot(snapshot);//從快照中同步合約資料
authorization.read_from_snapshot(snapshot);//從快照中同步認證資料
resource_limits.read_from_snapshot(snapshot);//從快照中同步資源限制資料
db.set_revision( head->block_num );// 更新頭塊
}
同步快照資料的操作是在controller的startup週期中執行的,根據傳入的snapshot,會調整區塊鏈的基於block_log的不可逆日誌資料,基於chainbase的狀態主庫資料。在controller的startup完畢後,可以保證三者資料的健康同步。
在chain_plugin的外掛配置項中有一個“snapshot”的引數,該配置項可以指定讀取的快照檔案。幾個關鍵校驗:
- 注意不能同時配置“genesis-json”和“genesis-timestamp”兩項,因為快照中已經存在這兩項的值,會發生衝突。
- 不能存在已有狀態檔案data/state/shared_memory.bin,因為快照只能被用來初始化一個空的狀態資料庫。
- 校驗block_log日誌中不可逆區塊的創世塊是否與快照中的保持一致。
引數設定完畢,在chain_plugin的startup階段,會檢查快照地址,如果存在,則會帶上該快照檔案啟動鏈。
if (my->snapshot_path) {
auto infile = std::ifstream(my->snapshot_path->generic_string(), (std::ios::in | std::ios::binary));
auto reader = std::make_shared<istream_snapshot_reader>(infile);
my->chain->startup(reader);// 帶上該快照檔案啟動鏈。
infile.close();
}
my->chain的型別是fc::optional<controller>,所以會執行controller的startup函式,這樣就與上面的流程掛鉤了,形成了一個完整的邏輯閉環。
4. controller::write_snapshot
void controller::write_snapshot( const snapshot_writer_ptr& snapshot ) const {
// 寫入快照時,不允許存在pending區塊。
EOS_ASSERT( !my->pending, block_validate_exception, "cannot take a consistent snapshot with a pending block" );
return my->add_to_snapshot(snapshot);
}
呼叫add_to_snapshot函式。
void add_to_snapshot( const snapshot_writer_ptr& snapshot ) const {
snapshot->write_section<chain_snapshot_header>([this]( auto §ion ){
section.add_row(chain_snapshot_header(), db);// 向快照中寫入快照頭資料
});
snapshot->write_section<genesis_state>([this]( auto §ion ){
section.add_row(conf.genesis, db);// 向快照中寫入創世塊資料
});
snapshot->write_section<block_state>([this]( auto §ion ){
section.template add_row<block_header_state>(*fork_db.head(), db);// 向快照中寫入頭塊區塊頭資料。
});
controller_index_set::walk_indices([this, &snapshot]( auto utils ){
using value_t = typename decltype(utils)::index_t::value_type;
if (std::is_same<value_t, table_id_object>::value) {// 跳過table_id_object(內聯的合同表格部分)
return;
}
snapshot->write_section<value_t>([this]( auto& section ){ // 遍歷主庫db區塊。
decltype(utils)::walk(db, [this, §ion]( const auto &row ) {
section.add_row(row, db); // 向快照中逐行寫入快照
});
});
});
add_contract_tables_to_snapshot(snapshot);// 向快照中寫入合約資料
authorization.add_to_snapshot(snapshot);// 向快照中寫入認證資料
resource_limits.add_to_snapshot(snapshot);// 向快照中寫入資源限制資料
}
5. producer_plugin的create_snapshot()功能
controller::write_snapshot函式在外部由producer_plugin所呼叫。producer_plugin通過rpc api介面create_snapshot對外提供了建立快照的功能。這個功能無疑是非常實用的,可以為生產者提供快速資料備份的能力,為整個EOS區塊鏈的運維工作增加了健壯性。producer_plugin的具體的實現程式碼:
producer_plugin::snapshot_information producer_plugin::create_snapshot() const {
chain::controller& chain = app().get_plugin<chain_plugin>().chain();// 獲取chain_plugin的外掛例項
auto reschedule = fc::make_scoped_exit([this](){// 獲取生產者出塊計劃
my->schedule_production_loop();
});
if (chain.pending_block_state()) {// 快照大忌:如果有pending塊,不可生成快照。
// abort the pending block
chain.abort_block();// 將pending塊幹掉
} else {
reschedule.cancel();// 無pending塊,則取消出塊計劃。
}
// 開始寫快照。
auto head_id = chain.head_block_id();
// 快照目錄:可通過配置producer_plugin的snapshots-dir項來指定快照目錄,會在節點資料目錄下生成該快照目錄,如果未特殊指定,預設目錄名字為“snapshots”
// 在快照目錄下生成格式為“snapshot-${id}.bin”的快照檔案。id是當前鏈的頭塊id
std::string snapshot_path = (my->_snapshots_dir / fc::format_string("snapshot-${id}.bin", fc::mutable_variant_object()("id", head_id))).generic_string();
EOS_ASSERT( !fc::is_regular_file(snapshot_path), snapshot_exists_exception,
"snapshot named ${name} already exists", ("name", snapshot_path));
auto snap_out = std::ofstream(snapshot_path, (std::ios::out | std::ios::binary));// 構造快照檔案輸出流
auto writer = std::make_shared<ostream_snapshot_writer>(snap_out);// 構造快照寫入器
chain.write_snapshot(writer);// 備份當前鏈寫入快照
// 資源釋放。
writer->finalize();
snap_out.flush();
snap_out.close();
return {head_id, snapshot_path};// 返回快照檔案路徑
}
快照的部分就介紹完畢了,區塊生產者可以根據需要呼叫producer_plugin的rpc介面create_snapshot為當前鏈建立快照。經過以上研究可以得出,EOS的快照是對狀態資料庫的備份,而不是block_log日誌檔案的備份,不可逆區塊在全網有很多節點作為備份,不必本地備份,而狀態資料庫很可能是本地唯一的,與其他節點都不同,如果有損壞會造成很多未上到不可逆區塊日誌的事務丟失。
當需要使用快照恢復時,可以重新啟動鏈,同時設定chain_plugin的引數“snapshot”,傳入快照檔案路徑,通過快照恢狀態資料庫。
總結
本節重點介紹了EOS中的核心控制器controller的功能和使用。controller的功能是非常多的,貫穿整個鏈生命週期的大部分行為,深入研究會發現controller實際上是對資料的控制,正如java中的mvc模式,控制器的功能就是對持久化資料的操作。本節首先介紹了兩個c++的語法使用,一個是名稱空間另一個是using關鍵字,另外文中也提到了boost的訊號槽機制。接著瀏覽了controller的宣告和實現的程式碼結構,最後,在眾多功能中挑選了fork_database分叉庫和snapshot快照進行了詳細的研究與分析。其他的眾多功能由於他們與外掛的緊密互動性,將會在相關外掛的部分詳細分析。
參考資料
- EOSIO/eos
- boost