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

原文連接:醒者呆的博客園,www.cnblogs.com/Evsward/p/c…html

Controller是EOS區塊鏈的核心控制器,其功能豐富、責任重大。 關鍵字:EOS,區塊鏈,controller,chainbase,db,namespace,using,信號槽,fork_database,snapshotjava

命名空間namespace

命名空間namespace定義了一個範圍,這個範圍自己可做爲額外的信息,相似於地址,或者位置。若是有兩個名字相同的變量或者函數,例如foshan::linshuhao和nba::linshuhao,命名空間能夠提供:ios

  • 區分性或者歸類性。不一樣命名空間下的內容互相孤立,即便內部函數名稱相同,也不會產生混淆。c++

  • 可讀性,本例中foshan和nba提供了一層語義。web

    C++程序架構中,不一樣的文件能夠經過引入相同的命名空間使用或者擴展功能。進一步理解,不一樣的文件名能夠提供一層語義,這些文件能夠共同維護一個跨文件的命名空間。
    複製代碼

using語法

C++程序設計中,常常會遇到帶有using關鍵字的語句。using正如字面含義,表明了本做用域後續會使用到的內容,這個內容能夠是:算法

  • 其餘命名空間,用using聲明之後,該命名空間下的公有屬性均可被使用。
  • 直接指定其餘命名空間下的某個函數,至關於導入功能,可使用該函數,不過使用時仍舊要帶上包含函數命名空間的完整路徑。
  • 爲某個複雜名字變量起的別名以便於使用。例如using apply_handler = std::function<void(apply_context&)>;

controller依賴功能

經過controller的聲明文件,能夠看到其整個結構。它聲明瞭兩個命名空間:mongodb

  • 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讀取模式是一個枚舉類,包括:json

  • SPECULATIVE,推測模式。內容爲兩個主體的數據:已完成的頭區塊,以及還未上鍊的事務。
  • HEAD,頭塊模式。內容爲當前頭區塊數據。
  • READ_ONLY,只讀模式。內容爲同步進來的區塊數據,不包括推測狀態的事務處理數據。
  • IRREVERSIBLE,不可逆模式。內容爲當前不可逆區塊的數據。

validation_mode,校驗模式也一樣是一個枚舉類,包括:api

  • 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(是否儲存狀態區塊數據)來判斷是否要儲存區塊數據。儲存區塊的方式是調用隊列block_state_\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<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_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 &section ){
     chain_snapshot_header header;
     section.read_row(header, db);
     header.validate();
  });// 先讀取快照頭數據。
  snapshot->read_section<block_state>([this]( auto &section ){
     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, &section, &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的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 &section ){
     section.add_row(chain_snapshot_header(), db);// 向快照中寫入快照頭數據
  });
  snapshot->write_section<genesis_state>([this]( auto &section ){
     section.add_row(conf.genesis, db);// 向快照中寫入創世塊數據
  });
  snapshot->write_section<block_state>([this]( auto &section ){
     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, &section]( 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

相關文章和視頻推薦

圓方圓學院聚集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。

公開課地址:ke.qq.com/course/3451…

相關文章
相關標籤/搜索