【劉文彬】 Controller:EOS區塊鏈核心控制器

原文鏈接:https://www.cnblogs.com/Evsward/p/controller.html

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的信號

controller類的共有成員屬性以及私有成員介紹完了,還剩下公有成員函數,這部分內容非常多,幾乎包含了整個鏈運行所涉及到的出塊流程相關的一切內容,從區塊本地組裝、校驗簽名,到本地節點應用入狀態庫,經過多節點共識成爲不可逆區塊等函數。其中每個階段都有對應的信號,信號功能使用了boost::signals2::signal庫。controller維護了這些信號內容,共8個:

  • 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中。

1. pre_accepted_block

發射時機: push_block函數,會對已籤區塊校驗,包括不能有pending塊,不能push空塊,區塊狀態不能是incomplete。通過校驗後,會發射該信號,攜帶該區塊。
插件捕捉處理: chain_plugin連接該信號,由信號槽轉播到channel,pre_accepted_block_channel發佈該區塊。但是該channel沒有訂閱者。

2. accepted_block_header

發射時機①: commit_block函數,如果該函數的參數add_to_fork_db爲true,需要添加至fork_db,首先將pending狀態區塊的狀態置爲已校驗,在fork_db中添加pending狀態區塊,然後發射該信號並攜帶pending狀態區塊。
發射時機②: push_block函數,pre_accepted_block發射完以後,獲取區塊的可信狀態並添加至fork_db,然後發射該信號,攜帶fork_db添加成功後返回的狀態區塊。
插件捕捉處理①: net_plugin連接該信號,綁定處理函數,函數體實現了日誌打印。
插件捕捉處理②: chain_plugin連接該信號,由信號槽轉播到channel,accepted_block_header_channel發佈該區塊。bnet_plugin訂閱該channel,綁定bnet_plugin_impl的on_accepted_block_header函數,該函數涉及到線程池等概念,將會在bnet_plugin插件的部分詳細分析。遍歷線程池,轉到session會話下的on_accepted_block_header函數執行。如果傳入區塊與本地時間相差6秒以內則接收,之外不處理。接收處理時先從本地多索引庫表block_status中查找是否已存在,不存在則插入block_status結構對象,如果不是遠程不可逆請求以及不存在該區塊,或者該區塊不是來自其他節點的情況,要在區塊頭通知集合中插入該區塊id。

3. accepted_block

發射時機: commit_block函數,fork_db以及重播的處理結束後,發射承認區塊的信號,攜帶pending狀態區塊數據。
插件捕捉處理①: net_plugin連接該信號,綁定處理函數,打印日誌的同時調用dispatch_manager::bcast_block,傳入區塊數據。send_all向所有連接發送廣播,這部分內容會在net_plugin部分詳細研究。
插件捕捉處理②: chain_plugin連接該信號,由信號槽轉播到channel,accepted_block_channel發佈該區塊。bnet_plugin訂閱該channel,依然有線程池的處理,會話遍歷,執行單個會話的on_accepted_block函數,刪除緩存中的所有事務,遍歷接收到的區塊的事務receipt,獲得事務的打包對象,事務id,在多索引表_transaction_status中查找該id,如果找到了則刪除。接下來如果在空閒狀態下,嘗試發送下一條pingpong心跳連接信息。
插件捕捉處理③: mongo_db_plugin連接該信號,綁定其mongo_db_plugin_impl::accepted_block函數,傳入區塊內容。該函數首先校驗是否達到了mongo配置中的開始處理的區塊號,這項配置是通過參數start_block_num設置的。如果傳入區塊號大於該參數設置的值(默認是0),則將標誌位start_block_reached置爲true。接着根據另一個配置項mongodb-store-blocks(是否儲存區塊數據)以及mongodb-store-block-states(是否儲存狀態區塊數據)來判斷是否要儲存區塊數據。儲存區塊的方式是調用隊列blockstate\queue,傳入區塊數據等待被消費,等待的過程又涉及到一個速度平衡的機制,關於mongo插件的內容請查閱相關篇章。
插件捕捉處理④: producer_plugin連接該信號,執行其on_block函數,傳入區塊數據。函數首先做了校驗,包括時間是否大於最後簽名區塊的時間以及大於當前時間,還有區塊號是否大於最後簽名區塊號。校驗通過以後,活躍生產者賬戶集合active_producers開闢新空間,插入計劃出塊生產者。
接下來利用set_intersection取本地生產者與集合active_producers的交集(如果結果爲空,說明本地生產者沒有出塊權利不屬於活躍生產者的一份子)。將結果存入一個迭代器,迭代執行內部函數,如果交集生產者不等於接收區塊的生產者,說明是校驗別人生產的區塊,如果是相等的不必做特殊處理。校驗別人生產的區塊,首先要在活躍生產者的key中找到匹配的key(本地生產者賬戶公鑰),否則說明該區塊不是合法生產者簽名拋棄不處理。接下來,獲取本地生產者私鑰,組裝生產確認數據字段,包括區塊id,區塊摘要,生產者,簽名。更新producer插件本地標誌位_last_signed_block_time和_last_signed_block_num。最後發射信號confirmed_block,攜帶以上組裝好的數據。但經過搜索,項目中目前沒有對該信號設置槽connection。
在區塊創建之前要爲該區塊的生產者設置水印用來標示該區塊的生產者是誰。

4. irreversible_block

發射時機①: push_block函數,當推送的區塊狀態爲irreversible不可逆時,發射該信號,攜帶狀態區塊數據。
發射時機②: on_irreversible函數,更改區塊狀態爲irreversible的函數,操作成功最後發射該信號。
插件捕捉處理①: net_plugin連接該信號,綁定函數irreversible_block,打印日誌。
插件捕捉處理②: chain_plugin連接該信號,由信號槽轉播到channel,irreversible_block_channel發佈該區塊。 bnet_plugin訂閱該channel,依然線程池遍歷會話,執行on_new_lib函數,當本地庫領先時可以清除歷史直到滿足當前庫,或者直到最後一個被遠端節點所知道的區塊。最後如果空閒,嘗試發送下一條pingpong心跳連接信息。
插件捕捉處理③: mongo_db_plugin連接該信號,執行applied_irreversible_block函數,仍舊參照mongo配置項的值決定是否儲存區塊、狀態區塊以及事務數據,然後將區塊數據塞入隊列等待消費。同上不贅述。
插件捕捉處理④: producer_plugin連接該信號,綁定執行函數on_irreversible_block,設置producer成員_irreversible_block_time的值爲區塊的時間。

5. accepted_transaction

發射時機①: push_scheduled_transaction函數,推送計劃事務時,將事務體經過一系列轉型以及校驗,當事務超時時間小於pending區塊時間時的處理,接着發射該信號,承認事務。當事務超時時間大於等於pending區塊時間時的處理,最後發射該信號,承認事務。當事務的sender發送者不是空且沒有主觀失敗的處理,最後發射該信號,承認事務。基於生產和校驗的主觀修改,主觀時的處理之後發射該信號,承認事務。當不是主觀問題而是硬邏輯錯誤時的處理,接着發射該信號,承認事務。
發射時機②: push_transaction函數,新事務到大狀態區塊,要經過身份認證以及決定是否現在執行還是延期執行,最後要插入到pending區塊的receipt接收事務中去。當檢查事務未被承認時,發射一次該信號。最後全部函數處理完畢,再次發射該信號。
插件捕捉處理①: net_plugin連接該信號,綁定函數accepted_transaction,打印日誌。
插件捕捉處理②: chain_plugin連接該信號,由信號槽轉播到channel,accepted_transaction_channel發佈該事務。bnet_plugin訂閱該channel,線程池遍歷會話,執行函數on_accepted_transaction。在可能是多個的投機塊中一個事務被承認,當一個區塊包含該承認事務或者切換分叉時,該事務狀態變爲“receive now”,被添加至數據庫表中,作爲發送給其他節點的證據。當該事務被髮送給其他節點時,根據這個狀態可以保證之後不會重複發送。每一次事務被“accepted”,都會延時5秒鐘。每次一個區塊被應用,所有超過5秒未被應用的但被承認的事務都將被清除。
插件捕捉處理③: mongo_db_plugin連接該信號,執行函數accepted_transaction,校驗加入隊列待消費。

6. applied_transaction

發射時機①: push_scheduled_transaction函數,事務過期時間小於pending區塊時間處理後發射該信號。反之大於等於處理後發射該信號。當事務的sender發送者不爲空且沒有主觀失敗的處理後發射該信號。基於生產和校驗的主觀修改,主觀處理後發射該信號,非主觀處理髮射該信號。
發射時機② :push_transaction函數,發射兩次該信號,邏輯較多,這段包括以上那個函數的可讀性很差,註釋幾乎沒有。
插件捕捉處理①: net_plugin連接該信號,綁定函數applied_transaction,打印日誌。
插件捕捉處理② : chain_plugin連接該信號,由信號槽轉播到channel,原理基本同上,不再重複。
插件捕捉處理③ : mongo_db_plugin同上。

7. accepted_confirmation

發射時機 : push_confirmation函數,推送確認信息,在此階段不允許有pending區塊存在,接着fork_db添加確認信息,發射該信號。
插件捕捉處理① : net_plugin連接該信號,綁定函數accepted_confirmation,打印日誌。
插件捕捉處理② : chain_plugin連接該信號,由信號槽轉播到channel,基本同上。

8. bad_alloc

發射時機 : 與前面七種不同,該信號沒有發射,是屬於boost::interprocess::bad_alloc,用於捕捉內存分配錯誤的異常。
插件捕捉處理 : 無connect。

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&lt;void(apply_context&)&gt;爲值的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)。

這個信號本來是與這一小節的內容不相干,但既然分析到這了,還是希望能有個閉環,那麼來看一下該信號的連接槽位置,如圖所示。

![pic1]()

可以看到,區塊不可逆的信號在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_readerostream_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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章