【劉文彬】 EOS行為核心:解析外掛chain_plugin

圓方圓區塊鏈發表於2018-12-07

原文連結:醒者呆的部落格園,www.cnblogs.com/Evsward/p/c…

EOS提供了大量的rpc介面,其中功能性最強,使用最頻繁的一部分介面是EOS的行為核心,由chain_api_plugin提供,具體實現是在chain_plugin。 關鍵字:EOS,區塊鏈,chain_plugin,chain_api_plugin,rpc,FC_REFLECT,反射,method模板,channel模板

一、介面列表chain_api_plugin

rpc呼叫邏輯,chainbase資料庫底層原理,nodeos啟動流程,plugin生命週期在前文都有介紹。本節直接研究chain_plugin的內容,研究入口會從chain_api_plugin中暴漏的rpc介面切入,這些介面是非常熟悉的,因為之前演練cleos相關命令時呼叫也是rpc。首先展示一下所有的介面內容:

_http_plugin.add_api({
  CHAIN_RO_CALL(get_info, 200l),
  CHAIN_RO_CALL(get_block, 200),
  CHAIN_RO_CALL(get_block_header_state, 200),
  CHAIN_RO_CALL(get_account, 200),
  CHAIN_RO_CALL(get_code, 200),
  CHAIN_RO_CALL(get_code_hash, 200),
  CHAIN_RO_CALL(get_abi, 200),
  CHAIN_RO_CALL(get_raw_code_and_abi, 200),
  CHAIN_RO_CALL(get_raw_abi, 200),
  CHAIN_RO_CALL(get_table_rows, 200),
  CHAIN_RO_CALL(get_table_by_scope, 200),
  CHAIN_RO_CALL(get_currency_balance, 200),
  CHAIN_RO_CALL(get_currency_stats, 200),
  CHAIN_RO_CALL(get_producers, 200),
  CHAIN_RO_CALL(get_producer_schedule, 200),
  CHAIN_RO_CALL(get_scheduled_transactions, 200),
  CHAIN_RO_CALL(abi_json_to_bin, 200),
  CHAIN_RO_CALL(abi_bin_to_json, 200),
  CHAIN_RO_CALL(get_required_keys, 200),
  CHAIN_RO_CALL(get_transaction_id, 200),
  CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202),
  CHAIN_RW_CALL_ASYNC(push_transaction, chain_apis::read_write::push_transaction_results, 202),
  CHAIN_RW_CALL_ASYNC(push_transactions, chain_apis::read_write::push_transactions_results, 202)
});
複製程式碼

這些介面可以分為兩類,一類是通過巨集CHAIN_RO_CALL呼叫的,另一類是通過巨集CHAIN_RW_CALL_ASYNC呼叫。

(1) CHAIN_RO_CALL

#define CHAIN_RO_CALL(call_name, http_response_code) CALL(chain, ro_api, chain_apis::read_only, call_name, http_response_code)
複製程式碼

採用同步只讀的方式呼叫巨集CALL。call_name是呼叫的函式名,http_response_code是響應碼。下面進入巨集CALL。

/**
 *  @attention 目前呼叫CALL函式的只有read_only應用。
 *  @param api_name "chain'
 *  @param api_handle app().get_plugin<chain_plugin>().get_read_only_api();
 *  @param api_namespace chain_apis::read_only
 *  @param call_name -INHERIT
 *  @param http_response_code -INHERIT
 */
#define CALL(api_name, api_handle, api_namespace, call_name, http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \ /*拼接介面url:http://ip:port/v1/chain/{call_name}*/ \
    /*
     * @param body:http請求體
     * @param cb:回撥函式,用於返回處理結果
     */ \
   [api_handle](string, string body, url_response_callback cb) mutable { \
       api_handle.validate(); \
       try { \
          if (body.empty()) body = "{}"; \
          /*
           * api_handle為chain_plugin中read_only類的例項
           * call_name為函式名,實現體找chain_plugin.cpp檔案
           * 函式引數1個:此處規定了一個命名規則,介面名加入字尾_param即為請求引數結構
           */ \
          auto result = api_handle.call_name(fc::json::from_string(body).as<api_namespace::call_name ## _params>()); \
          /*回撥函式返回處理結果,此處也規定了一個命名規則,介面名加入字尾_result即為返回結構,轉化為json的格式返回。*/ \
          cb(http_response_code, fc::json::to_string(result)); \
       } catch (...) { \
          /*捕捉到異常,呼叫http_plugin的異常處理函式handle_exception*/ \
          http_plugin::handle_exception(#api_name, #call_name, body, cb); \
       } \
    } \
}
複製程式碼

api_handle引數

同步只讀的請求傳入的api_handle引數值為ro_api變數,該變數是在chain_api_plugin外掛啟動chain_api_plugin::plugin_startup時(外掛的生命週期前文已有介紹)初始化的,

auto ro_api = app().get_plugin<chain_plugin>().get_read_only_api();

app()函式以及與application類相關的內容前文已經介紹過,通過get_plugin<chain_plugin>獲取chain_plugin的例項,然後呼叫其成員函式get_read_only_api(),

chain_apis::read_only get_read_only_api() const { return chain_apis::read_only(chain(), get_abi_serializer_max_time()); } //注意const修飾符,函式體內返回值是不可修改的。

返回的是chain_apis::read_only建構函式返回的read_only例項。類read_only中包含了所有基於只讀機制的介面實現,與上面介面列表中宣告的保持一致。

read_only(const controller& db, const fc::microseconds& abi_serializer_max_time) : db(db), abi_serializer_max_time(abi_serializer_max_time) {}

因此,最後傳入CALL巨集的api_handle引數值實際就是這個類read_only的例項。之後使用該例項去呼叫call_name,就是簡單的例項呼叫自身成員函式(一般這個成員函式是宣告和實現都有的)的邏輯了。

(2) CHAIN_RW_CALL_ASYNC

#define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code) CALL_ASYNC(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code)
複製程式碼

採用非同步讀寫的方式呼叫非同步處理巨集CALL_ASYNC。call_name是呼叫的函式名,call_result傳入宣告的結果接收體(例如chain_apis::read_write::push_transaction_results),http_response_code是響應碼。下面進入巨集CALL_ASYNC。

/**
 *  @attention 目前呼叫CALL_ASYNC函式的只有read_write的應用。
 *  @param api_name "chain'
 *  @param api_handle app().get_plugin<chain_plugin>().get_read_write_api();
 *  @param api_namespace chain_apis::read_write
 *  @param call_name -INHERIT
 *  @param call_result -INHERIT
 *  @param http_response_code -INHERIT
 */
#define CALL_ASYNC(api_name, api_handle, api_namespace, call_name, call_result, http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \ /*同上,拼接介面url:http://ip:port/v1/chain/{call_name}*/ \
    /*
     * http處理請求的函式結構不變,同上。
     * @param body:http請求體
     * @param cb:回撥函式,用於返回處理結果
     */ \
   [api_handle](string, string body, url_response_callback cb) mutable { \
      if (body.empty()) body = "{}"; \
      api_handle.validate(); \
      /*
       * api_handle為chain_plugin中read_only類的例項
       * call_name為函式名,實現體找chain_plugin.cpp檔案
       * 函式引數2個:
       * @param 此處規定了一個命名規則,介面名加入字尾_param即為請求引數結構
       * @param lambda表示式,將cb和body按值傳遞進內部函式,該內部函式整體作為非同步操作的回撥函式,注意與http的回撥函式cb區分。
       */ \
      api_handle.call_name(fc::json::from_string(body).as<api_namespace::call_name ## _params>(),\
         [cb, body](const fc::static_variant<fc::exception_ptr, call_result>& result){\
            /*捕獲異常,分發異常處理*/ \
            if (result.contains<fc::exception_ptr>()) {\
               try {\
                  result.get<fc::exception_ptr>()->dynamic_rethrow_exception();\
               } catch (...) {\
                  http_plugin::handle_exception(#api_name, #call_name, body, cb);\
               }\
            } else {\
                /*
                 * 非同步處理成功,通過http的回撥函式cb返回結果。
                 */ \
               cb(http_response_code, result.visit(async_result_visitor()));\
            }\
         });\
   }\
}
複製程式碼

其中最後處理結果的語句比較令人好奇result.visit(async_result_visitor()) result的型別是:const fc::static_variant<fc::exception_ptr, call_result>& async_result_visitor()函式:

struct async_result_visitor : public fc::visitor<std::string> {
   template<typename T>
   std::string operator()(const T& v) const {
      return fc::json::to_string(v); //與CALL處理返回結果相同的是,此處做的也是轉換json的工作。
   }
};
複製程式碼

接著,進入fc庫的static_variant.hpp檔案中尋找類static_variant,它包含一個模板函式visit:

template<typename visitor>
typename visitor::result_type visit(const visitor& v)const {
    return impl::storage_ops<0, Types...>::apply(_tag, storage, v);
}
複製程式碼

非同步處理將處理結果轉型放置在結果容器中。

api_handle引數

非同步讀寫的請求傳入的api_handle引數值為rw_api變數,該變數是在chain_api_plugin外掛啟動chain_api_plugin::plugin_startup時(外掛的生命週期前文已有介紹)初始化的,

auto rw_api = app().get_plugin<chain_plugin>().get_read_write_api();

app()函式以及與application類相關的內容前文已經介紹過,通過get_plugin<chain_plugin>獲取chain_plugin的例項,然後呼叫其成員函式get_read_write_api(),

chain_apis::read_write get_read_write_api() { return chain_apis::read_write(chain(), get_abi_serializer_max_time()); }

返回的是chain_apis::read_write建構函式返回的read_write例項。類read_write中包含了所有基於讀寫機制的介面實現,與上面介面列表中宣告的保持一致。

read_write(controller& db, const fc::microseconds& abi_serializer_max_time): db(db), abi_serializer_max_time(abi_serializer_max_time) {}

因此,最後傳入CALL_ASYNC巨集的api_handle引數值實際就是這個類read_write的例項。之後使用該例項去呼叫call_name,就是簡單的例項呼叫自身成員函式(一般這個成員函式是宣告和實現都有的)的邏輯了。

chain_api_plugin生命週期

  • set_program_options,空
  • plugin_initialize,空
  • plugin_startup,新增rpc介面,請求chain_plugin功能函式。
  • plugin_shutdown,空

二、結構體成員序列化FC_REFLECT

FC_REFLECT為結構體提供序列化成員的能力。
複製程式碼

FC_REFLECT是FC庫中提供反射功能的巨集。反射的意義在於瞭解一個未知的物件,反射是不限程式語言的,通過反射能夠獲取到物件的成員結構。巨集#define FC_REFLECT( TYPE, MEMBERS )內部又呼叫了巨集#define FC_REFLECT_DERIVED( TYPE, INHERITS, MEMBERS ),反射功能的具體實現就不深入探究了。下面來看其應用,舉個例子:

FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_params, (transaction)(available_keys) )
FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_result, (required_keys) )
複製程式碼

兩行程式碼分別包含了關於get_required_keys的兩個結構體,

struct get_required_keys_params {
  fc::variant transaction;
  flat_set<public_key_type> available_keys;
};
struct get_required_keys_result {
  flat_set<public_key_type> required_keys;
};
複製程式碼

get_required_keys是chain的RPC介面,結構體get_required_keys_params是該介面的請求引數的結構,而另一個get_required_keys_result是介面處理後返回的結構。

回過頭繼續看FC_REFLECT的兩行程式碼,第一個引數傳入的是結構體。第二個引數用圓括號包含,可以有多個,內容與結構體的成員一致。

FC_REFLECT實際上實現了物件導向程式設計中類成員的getter/setter方法。
複製程式碼

三、chain_plugin生命週期

與基類定義的生命週期相同,也包含四個階段。

chain_plugin::set_program_options

在nodeos程式除錯部分有詳細介紹。主要是新增chain_plugin相關的配置引數,一組是命令列的,另一組是來自配置檔案的,其中命令列的配置項優先順序更高。

chain_plugin::plugin_initialize

這個函式也是從nodeos程式入口而來,會傳入配置項呼叫chain_plugin的初始化函式。初始胡函式獲取到來自命令列和配置檔案的中和配置引數以後,結合創世塊配置,逐一處理相關引數邏輯。這些引數對應的處理邏輯如下表(對應controller的成員屬性介紹)所示:

param explanation detail
action-blacklist 新增action黑名單 每一條資料是有賬戶和action名組成
key-blacklist 公鑰黑名單 公鑰集合
blocks-dir 設定資料目錄 最終會處理為絕對路徑儲存到記憶體
checkpoint 檢查點 快取區塊的檢查點,用於快速掃描
wasm-runtime 虛擬機器型別 可以指定執行時webassembly虛擬機器型別
abi-serializer-max-time-ms abi序列化最大時間 要提高這個數值防止abi序列化失敗
chain-state-db-size-mb 鏈狀態庫大小 基於chainbase的狀態主庫的大小
chain-state-db-guard-size-mb 鏈狀態庫守衛大小 也是controller中提到的未包含在公開屬性中的
reversible-blocks-db-size-mb 鏈可逆區塊庫大小 鏈可逆區塊庫也是基於chainbase的狀態資料庫
reversible-blocks-db-guard-size-mb 鏈可逆區塊庫守衛大小 也是controller中提到的未包含在公開屬性中的
force-all-checks 是否強制執行所有檢查 預設為false
disable-replay-opts 是否禁止重播引數 預設為false
contracts-console 是否允許合約輸出到控制檯 一般為了除錯合約使用,預設為false
disable-ram-billing-notify-checks 是否允許記憶體賬單通知 預設為false
extract-genesis-json/print-genesis-json 輸出創世塊配置 以json格式輸出
export-reversible-blocks 匯出可逆區塊到路徑 將可逆區塊目錄reversible中資料匯入到指定路徑
delete-all-blocks 刪除所有區塊資料 重置區塊鏈
truncate-at-blocks 區塊擷取點 所有生效的指令都要截止到本引數設定的區塊號
hard-replay-blockchain 強制重播區塊鏈 清空狀態庫,通過repair_log獲得backup,搭配fix-reversible-blocks從backup中恢復可逆區塊到區塊目錄。
replay-blockchain 重播區塊鏈 清空狀態庫,搭配fix-reversible-blocks從原區塊目錄的可逆區塊目錄自我修復
fix-reversible-blocks 修復可逆區塊 呼叫函式recover_reversible_blocks傳入源路徑和新路徑,可逆快取大小,以及是否有擷取點truncate-at-blocks
import-reversible-blocks 匯入可逆區塊路徑(必須獨立使用,沒有其他引數命令) 清空可逆區塊目錄,呼叫import_reversible_blocks函式匯入
snapshot 指定匯入的快照路徑 在controller的快照部分有詳述
genesis-json 指定創世塊配置檔案 從檔案中匯出創世塊的配置項到記憶體
genesis-timestamp 指定創世塊的時間 同樣將該時間配置到記憶體中對應的變數
read-mode 狀態主庫的讀取模式 controller部分有詳述
validation-mode 校驗模式 controller部分有詳述

chain_plugin引數處理完畢後,設定方法提供者(並沒有找到該provider的應用)。接著轉播訊號到頻道,為chain_plugin_impl的唯一指標my的connection屬性賦值,建立訊號槽。

  • pre_accepted_block_connection,連線訊號pre_accepted_block,更新loaded_checkpoints區塊檢查點位置。
  • accepted_block_header_connection,連線訊號accepted_block_header,承認區塊頭訊號。
  • accepted_block_connection,連線訊號accepted_block,承認區塊訊號。
  • irreversible_block_connection,連線訊號irreversible_block,區塊不可逆。
  • accepted_transaction_connection,連線訊號accepted_transaction,承認事務。
  • applied_transaction_connection,連線訊號applied_transaction,應用事務。
  • accepted_confirmation_connection,連線訊號accepted_confirmation,承認確認。

chain_plugin的外掛初始化工作完畢,主要是對chain_plugin的配置引數的處理,以及訊號槽的實現。

chain_plugin::plugin_startup

chain_plugin外掛的啟動,首先是快照的處理,這部分在快照的內容中有介紹,是根據nodeos過來的快照引數,判斷是否要加入快照引數呼叫controller的startup。這期間如有捕捉到異常,則執行controller的reset重置操作。然後根據controller的屬性輸出鏈日誌資訊。

chain_plugin::plugin_shutdown

重置所有的訊號槽,重置controller。

四、RPC介面實現

外部rpc呼叫通過chain_api_plugin外掛包裹的介面服務,內部介面的實現是在chain_plugin中,對應關係是在chain_api_plugin的介面列表,通過函式名字匹配。

1. 獲取基本資訊 get_info

// 返回值為read_only的實體成員get_info_results結構的例項。
read_only::get_info_results read_only::get_info(const read_only::get_info_params&) const {
   const auto& rm = db.get_resource_limits_manager();
   return {
      // 以下欄位都與get_info_results結構匹配,最終構造出get_info_results例項返回。
      eosio::utilities::common::itoh(static_cast<uint32_t>(app().version())), // server_version
      db.get_chain_id(), // chain_id
      db.fork_db_head_block_num(), // head_block_num
      db.last_irreversible_block_num(), // last_irreversible_block_num
      db.last_irreversible_block_id(), // last_irreversible_block_id
      db.fork_db_head_block_id(), // head_block_id
      db.fork_db_head_block_time(), // head_block_time
      db.fork_db_head_block_producer(), // head_block_producer
      rm.get_virtual_block_cpu_limit(), // virtual_block_cpu_limit
      rm.get_virtual_block_net_limit(), // virtual_block_net_limit
      rm.get_block_cpu_limit(), // block_cpu_limit
      rm.get_block_net_limit(), // block_net_limit
      //std::bitset<64>(db.get_dynamic_global_properties().recent_slots_filled).to_string(), // recent_slots
      //__builtin_popcountll(db.get_dynamic_global_properties().recent_slots_filled) / 64.0, // participation_rate
      app().version_string(), // server_version_string
   };
}
複製程式碼

可以看到get_info_results的部分欄位是通過read_only::db物件獲取,還有一部分資源相關的內容是通過db的資源限制管理器獲得,而關於版本方面的資料是從application例項獲得。

2. 獲取區塊資訊 get_block

// 特殊的是,此處並沒有建立一個get_block_result的結構體作為返回值的容器,是利用了variant語法將signed_block_ptr轉換成可輸出的狀態。
fc::variant read_only::get_block(const read_only::get_block_params& params) const {
   signed_block_ptr block;
   // 如果引數block_num_or_id為空或者block_num_or_id的長度大於64,屬於非法引數不處理,會報錯。
   EOS_ASSERT(!params.block_num_or_id.empty() && params.block_num_or_id.size() <= 64, chain::block_id_type_exception, "Invalid Block number or ID, must be greater than 0 and less than 64 characters" );
   try {
      // 通過variant語法將引數block_num_or_id型別擦除然後通過as語法轉化為block_id_type型別,
      block = db.fetch_block_by_id(fc::variant(params.block_num_or_id).as<block_id_type>());
      if (!block) {// 如果通過id的方法獲得的block為空,則嘗試使用區塊號的方式獲取。
         block = db.fetch_block_by_number(fc::to_uint64(params.block_num_or_id));// 利用to_uint64將引數轉型。
      }// 如果獲取失敗,丟擲異常,無效的引數block_num_or_id
   } EOS_RETHROW_EXCEPTIONS(chain::block_id_type_exception, "Invalid block ID: ${block_num_or_id}", ("block_num_or_id", params.block_num_or_id))

   EOS_ASSERT( block, unknown_block_exception, "Could not find block: ${block}", ("block", params.block_num_or_id));
   // 通過校驗,開始返回物件。
   fc::variant pretty_output;
   // i將結果block的資料通過resolver解析到pretty_output
   abi_serializer::to_variant(*block, pretty_output, make_resolver(this, abi_serializer_max_time), abi_serializer_max_time);
   // 引用區塊的字首設定
   uint32_t ref_block_prefix = block->id()._hash[1];

   return fc::mutable_variant_object(pretty_output.get_object())
           ("id", block->id())
           ("block_num",block->block_num())
           ("ref_block_prefix", ref_block_prefix);
}
複製程式碼

進一步研究區塊的id是如何生成的,以及如何通過id獲得區塊號。是在block_heade.cpp中定義:

namespace eosio { namespace chain {
   digest_type block_header::digest()const
   {
      return digest_type::hash(*this);// hash演算法為sha256,然後使用fc::raw::pack打包獲取結果
   }
   
   uint32_t block_header::num_from_id(const block_id_type& id)
   {
      return fc::endian_reverse_u32(id._hash[0]);// 實際上是對區塊id併入區塊號的演算法的逆向工程,獲得區塊號。
   }
   
   // id的型別為block_id_type
   block_id_type block_header::id()const
   {
      // id不包括簽名區塊頭屬性,尤其是生產者簽名除外。
      block_id_type result = digest();//digest_type::hash(*this),this是id()的呼叫者。
      result._hash[0] &= 0xffffffff00000000;//對結果進行位操作,併入一個十六進位制頭。
      result._hash[0] += fc::endian_reverse_u32(block_num()); // 通過上一個區塊id找到其區塊號然後自增獲得當前區塊號。併入id資料成為其一部分。
      return result;
   }
} }
複製程式碼

get_block拼接好id、block_num、ref_block_prefix最後三個欄位以後,返回的資料結構如下圖所示:

【劉文彬】 EOS行為核心:解析外掛chain_plugin

3. 獲取區塊頭狀態 get_block_header_state

注意與上面的get_block的實現區分,get_block_header_state是通過fetch_block_state_by_numberfetch_block_state_by_id函式獲取到的是狀態庫中的區塊物件,也就是說是可逆區塊資料,而不是get_block通過fetch_block_by_numberfetch_block_by_id函式獲取到的不可逆區塊。 get_block_header_state獲取到可逆區塊以後,通過以下程式碼得到其區塊頭資料並返回。

fc::variant vo;
fc::to_variant( static_cast<const block_header_state&>(*b), vo );// block_state_ptr b;
return vo;
複製程式碼

4. 獲取賬戶資訊 get_account

這個功能的實現函式程式碼較長,但做的工作實際上並不複雜,可以採用從返回的account資料結構來逆向分析該功能的實現方法:

struct get_account_results {
  name                       account_name; // 賬戶名,入參的值。
  uint32_t                   head_block_num = 0; // 頭塊號,controller的狀態主庫db獲取
  fc::time_point             head_block_time; // 頭塊時間,controller的狀態主庫db獲取

  bool                       privileged = false; // 是否超級賬戶,預設false。controller的狀態主庫db獲取賬戶的屬性之一。
  fc::time_point             last_code_update; // 最後的code修改時間,例如給賬戶set contract的時間。controller的狀態主庫db獲取賬戶的屬性之一。
  fc::time_point             created; // 賬戶建立時間。controller的狀態主庫db獲取賬戶的屬性之一。

  optional<asset>            core_liquid_balance; // 主幣的餘額,在accounts狀態表裡查到的

  int64_t                    ram_quota  = 0; // 記憶體限額(資源相關部分有詳細介紹),從controller的資源管理器獲取
  int64_t                    net_weight = 0; // 網路頻寬資源權重,從controller的資源管理器獲取
  int64_t                    cpu_weight = 0; // cpu資源權重,從controller的資源管理器獲取

  account_resource_limit     net_limit; // 網路頻寬資源,包括已使用、剩餘可用、總量。從controller的資源管理器獲取
  account_resource_limit     cpu_limit; // cpu頻寬資源,包括已使用、剩餘可用、總量。從controller的資源管理器獲取
  int64_t                    ram_usage = 0; // 記憶體已使用量。從controller的資源管理器獲取

  vector<permission>         permissions; // 賬戶的許可權內容(賬戶多簽名部分有詳細介紹),在狀態主庫的表裡查到的。

  fc::variant                total_resources; // 總資源量,包括網路、cpu、記憶體資源總量。在userres狀態表裡查到的
  fc::variant                self_delegated_bandwidth; // 自我抵押頻寬。在delband狀態表裡查到的
  fc::variant                refund_request; // 退款請求。在refunds狀態表裡查到的
  fc::variant                voter_info; // 投票相關。在voters狀態表裡查到的
};
複製程式碼

5. 獲取賬戶code資訊 get_code

注意該介面修改了原始碼,不支援返回wast資料了。因此在請求該介面的時候,要使用的引數如下:

{
    "account_name": "eosio.token",
    "code_as_wasm": true
}
複製程式碼

返回的資料將包括

  • 該賬戶的名字。
  • code的hash值,先通過controller狀態庫查詢到賬戶物件,然後將其code的data和size值做sha256雜湊得到的值。
  • wasm的資料,就是完整的原始code的資料。
  • abi資料,通過abi_serializer將賬戶的abi資料解析出來。
read_only::get_code_results read_only::get_code( const get_code_params& params )const {
   get_code_results result;
   result.account_name = params.account_name;
   const auto& d = db.db();
   const auto& accnt  = d.get<account_object,by_name>( params.account_name );// 從controller狀態庫中獲取賬戶資訊
   // 當前預設不支援返回wast資料
   EOS_ASSERT( params.code_as_wasm, unsupported_feature, "Returning WAST from get_code is no longer supported" );

   if( accnt.code.size() ) {
      if (params.code_as_wasm) {
          // 完整的原始賬戶的code的資料
         result.wasm = string(accnt.code.begin(), accnt.code.end());
      }
      // 獲得code的雜湊值:將賬戶資訊下的code的data和size值做sha256雜湊得到的值
      result.code_hash = fc::sha256::hash( accnt.code.data(), accnt.code.size() );
   }
   // 獲取賬戶的abi資料:通過abi_serializer將賬戶的abi資料解析出來。
   abi_def abi;
   if( abi_serializer::to_abi(accnt.abi, abi) ) {
      result.abi = std::move(abi);
   }

   return result;
}
複製程式碼

6. 獲得賬戶的code雜湊值 get_code_hash

實現方法參照get_code,返回資料只包括code的hash值。

###7. 獲得賬戶的abi資料 get_abi

實現方法參照get_code,返回資料只包括賬戶的abi資料。

8. 獲得賬戶的原始code和abi資料 get_raw_code_and_abi

read_only::get_raw_code_and_abi_results read_only::get_raw_code_and_abi( const get_raw_code_and_abi_params& params)const {
   get_raw_code_and_abi_results result;
   result.account_name = params.account_name;

   const auto& d = db.db();
   const auto& accnt = d.get<account_object,by_name>(params.account_name);
   result.wasm = blob{{accnt.code.begin(), accnt.code.end()}}; // 原始wasm值,完整取出即可。
   result.abi = blob{{accnt.abi.begin(), accnt.abi.end()}}; // 原始abi值,完整取出即可。

   return result;
}
複製程式碼

9. 獲得賬戶的原始abi資料 get_raw_abi

實現方法參照get_raw_code_and_abi,返回資料只包括賬戶的原始abi資料。

10. 獲得一條狀態庫表的值 get_table_rows

首先檢視該介面的傳入引數的資料結構:

struct get_table_rows_params {
  bool        json = false; // 是否是json的格式
  name        code; // 傳入code值,即擁有該table的賬戶名
  string      scope; // 傳入scope值,即查詢條件
  name        table; // 傳入table的名字
  string      table_key; // table主鍵
  string      lower_bound; // 設定檢索資料的下限,預設是first
  string      upper_bound; // 設定檢索資料的上限,預設是last
  uint32_t    limit = 10; // 資料結果的最大條目限制
  string      key_type;  // 通過指定鍵的資料型別,定位查詢依賴的鍵
  string      index_position; // 通過傳入鍵的位置,,定位查詢依賴的鍵。1 - 主鍵(first), 2 - 二級索引 (multi_index定義), 3 - 三級索引,等等
  string      encode_type{"dec"}; //加密型別,有十進位制還是十六進位制,預設是十進位制dec。
};
複製程式碼

除了code、scope、table以外都是可選的引數,這三個引數是定位檢索資料的關鍵,所以不可省略。下面來看該介面的返回值型別:

struct get_table_rows_result {
  vector<fc::variant> rows; // 資料集合。一行是一條,無論是十六進位制加密串還是解析成json物件,都代表一行。
  bool                more = false; // 如果最後顯示的元素(受制於limit)並不是資料庫中最後一個,則該欄位會置為true
};
複製程式碼

進入介面實現的函式體,內容較多。首先通過傳入引數物件中的index_position欄位來確定查詢依賴的鍵,這是通過函式get_table_index_name完成的工作,同時會修改primary原物件的值,返回table名字的同時告知是否是主鍵(table的鍵的名字是與table名字相關的)。 接著,如果是主鍵:

// 對比入參物件的table名字是否與通過index反查的table名字保持一致。
EOS_ASSERT( p.table == table_with_index, chain::contract_table_query_exception, "Invalid table name ${t}", ( "t", p.table ));
auto table_type = get_table_type( abi, p.table );// 獲得table型別
if( table_type == KEYi64 || p.key_type == "i64" || p.key_type == "name" ) {//支援這三種table型別
 return get_table_rows_ex<key_value_index>(p,abi);// 具體檢索table的函式。
}
// 如果是已支援的三種table型別之外的,則會報錯。
EOS_ASSERT( false, chain::contract_table_query_exception,  "Invalid table type ${type}", ("type",table_type)("abi",abi));
複製程式碼

具體檢索table的函式get_table_rows_ex,這是非常重要的一個函式,需要原始碼分析:

/**
* 檢索table的核心函式
* @tparam IndexType 模板類,支援不同的索引型別
* @param p get_table_rows介面入參物件
* @param abi 通過controller查詢入參code對應的程式abi
* @return 查詢結果
*/
template <typename IndexType>
read_only::get_table_rows_result get_table_rows_ex( const read_only::get_table_rows_params& p, const abi_def& abi )const {
  read_only::get_table_rows_result result; // 首先定義結果容器
  const auto& d = db.db(); // 狀態主庫物件
  uint64_t scope = convert_to_type<uint64_t>(p.scope, "scope"); // 獲得查詢條件。

  abi_serializer abis;
  abis.set_abi(abi, abi_serializer_max_time);// 將abi_def型別的abi通過序列化轉到abi_serializer型別的物件abis。

  // 查詢狀態庫表的標準正規化,返回的是通過code、scope、table檢索到的結果集的資料迭代器,
  const auto* t_id = d.find<chain::table_id_object, chain::by_code_scope_table>(boost::make_tuple(p.code, scope, p.table));

  if (t_id != nullptr) { // 迭代器不為空
     const auto& idx = d.get_index<IndexType, chain::by_scope_primary>(); // 傳入查詢依賴的鍵,指定迭代器的索引。
     decltype(t_id->id) next_tid(t_id->id._id + 1);
     auto lower = idx.lower_bound(boost::make_tuple(t_id->id)); // 獲取結果集上限
     auto upper = idx.lower_bound(boost::make_tuple(next_tid)); // 獲取結果集下限

     if (p.lower_bound.size()) {// 如果入參物件設定了結果集下限
        if (p.key_type == "name") { // 主鍵型別是賬戶名字,設定下限物件
           name s(p.lower_bound);
           lower = idx.lower_bound( boost::make_tuple( t_id->id, s.value ));
        } else {// 主鍵型別是其他型別,設定下限物件
           auto lv = convert_to_type<typename IndexType::value_type::key_type>( p.lower_bound, "lower_bound" );
           lower = idx.lower_bound( boost::make_tuple( t_id->id, lv ));
        }
     }
     if (p.upper_bound.size()) {// 如果入參物件設定了結果集上限
        if (p.key_type == "name") {// 主鍵型別是賬戶名字,設定上限物件
           name s(p.upper_bound);
           upper = idx.lower_bound( boost::make_tuple( t_id->id, s.value ));
        } else {// 主鍵型別是其他型別,設定上限物件
           auto uv = convert_to_type<typename IndexType::value_type::key_type>( p.upper_bound, "upper_bound" );
           upper = idx.lower_bound( boost::make_tuple( t_id->id, uv ));
        }
     }
     // 迭代器啟動迭代,開始檢索
     vector<char> data;
     auto end = fc::time_point::now() + fc::microseconds(1000 * 10); /// 10ms 是最長時間
     unsigned int count = 0;
     auto itr = lower;
     for (; itr != upper; ++itr) {
        copy_inline_row(*itr, data); // 將迭代器當前指標指向的物件複製到data容器中去。

        if (p.json) { // 處理data為json格式,通過方法binary_to_variant,向result的結果集rows中插入解析後的明文json格式的data
           result.rows.emplace_back( abis.binary_to_variant( abis.get_table_type(p.table), data, abi_serializer_max_time, shorten_abi_errors ) );
        } else { // 未要求json格式,則直接返回data,data不是可讀的。
           result.rows.emplace_back(fc::variant(data));
        }
        if (++count == p.limit || fc::time_point::now() > end) { // 兩個限制:一是結果集行數limit限制,二是執行時間是否超時
           ++itr;
           break;
        }
     }
     if (itr != upper) { // 如果實際返回的結果集並沒有完全輸出所有符合要求的資料,則將more欄位置為true,提醒使用者還有符合要求的資料沒顯示。
        result.more = true;
     }
  }
  return result;
}
複製程式碼

繼續回到get_table_rows介面函式體,如果不是主鍵,則需要按照鍵型別來區分處理,鍵型別包括i64, i128, i256, float64, float128, ripemd160, sha256。這裡與主鍵不同的是,檢索table的核心函式改為get_table_rows_by_seckey,該函式與主鍵處理函式大部分邏輯是一致的,只是特殊在鍵的處理上,由於該函式是處理二級索引的,因此要先通過程式碼const auto& secidx = d.get_index<IndexType, chain::by_secondary>();獲得二級索引。然後對迭代器資料集進行處理,獲得結果集的迴圈起止位,最後迴圈匯出結果集即可。

11. 指定範圍獲取table資料 get_table_by_scope

此處的scope並不是前面理解的查詢條件,而是字面意思,表示一個範圍,上面提到了,在表資料中,範圍就是上限和下限以及條目限制。因此不難猜出get_table_by_scope介面的入參物件結構:

struct get_table_by_scope_params {
  name        code; // 必須欄位,傳入賬戶名
  name        table = 0; // 可選,作為過濾器
  string      lower_bound; // 範圍下限,可選
  string      upper_bound; // 範圍上限,可選
  uint32_t    limit = 10; // 範圍數量,限制條目
};
複製程式碼

那麼處理結果集就簡單了,實際上就是上面函式get_table_rows_ex的一部分,取出相關結果集返回即可。

12. 獲取貨幣餘額 get_currency_balance

介面入參結構:

struct get_currency_balance_params {
  name             code; // 賬戶名,token合約的owner,一般是eosio.token賬戶
  name             account; // 賬戶名,檢索條件,查詢該賬戶的餘額
  optional<string> symbol; // 檢索條件,需要的token符號,例如SYS(主幣),EOS等。
};
複製程式碼

函式的處理邏輯:

vector<asset> read_only::get_currency_balance( const read_only::get_currency_balance_params& p )const {
   const abi_def abi = eosio::chain_apis::get_abi( db, p.code ); // get_abi與前面RPC介面實現函式為同一個。先通過賬戶code獲取eosio.token合約物件abi資料。
   auto table_type = get_table_type( abi, "accounts" ); // 在abi中找到accounts表,返回該表的索引型別。

   vector<asset> results; // 結果容器
   walk_key_value_table(p.code, p.account, N(accounts), [&](const key_value_object& obj){
       // 表資料的value值超過了assert資料型別的大小,說明是無效資料。
      EOS_ASSERT( obj.value.size() >= sizeof(asset), chain::asset_type_exception, "Invalid data on table");

      asset cursor;
      // obj.value.data()是原始資料。
      fc::datastream<const char *> ds(obj.value.data(), obj.value.size());
      fc::raw::unpack(ds, cursor); // 將datastream資料解包到cursor

      EOS_ASSERT( cursor.get_symbol().valid(), chain::asset_type_exception, "Invalid asset");

      if( !p.symbol || boost::iequals(cursor.symbol_name(), *p.symbol) ) { // 對比token符號,一致的新增至結果集。
        results.emplace_back(cursor);
      }
      return !(p.symbol && boost::iequals(cursor.symbol_name(), *p.symbol));
   });

   return results;
}
複製程式碼

get_table_type函式:

string get_table_type( const abi_def& abi, const name& table_name ) {
   for( const auto& t : abi.tables ) { //遍歷abi下的所有table
      if( t.name == table_name ){ // 找到符合條件的table
         return t.index_type; // 返回該table的索引型別。
      }
   }
   // 如果沒查到,報錯提示當前ABI中並未找到目標table。
   EOS_ASSERT( false, chain::contract_table_query_exception, "Table ${table} is not specified in the ABI", ("table",table_name) );
}
複製程式碼

13. 獲取貨幣狀態 get_currency_stats

傳入eosio.token合約owner賬戶以及token符號即可請求到該token的狀態資訊。

fc::variant read_only::get_currency_stats( const read_only::get_currency_stats_params& p )const {
   fc::mutable_variant_object results; // 結果容器
   const abi_def abi = eosio::chain_apis::get_abi( db, p.code );
   auto table_type = get_table_type( abi, "stat" ); // 在abi的表中找到stat表,返回其索引型別。
   uint64_t scope = ( eosio::chain::string_to_symbol( 0, boost::algorithm::to_upper_copy(p.symbol).c_str() ) >> 8 );

   walk_key_value_table(p.code, scope, N(stat), [&](const key_value_object& obj){
      EOS_ASSERT( obj.value.size() >= sizeof(read_only::get_currency_stats_result), chain::asset_type_exception, "Invalid data on table");
      fc::datastream<const char *> ds(obj.value.data(), obj.value.size());
      read_only::get_currency_stats_result result; // 介面的返回物件
      fc::raw::unpack(ds, result.supply);// 已發行量
      fc::raw::unpack(ds, result.max_supply);// 最大發行量
      fc::raw::unpack(ds, result.issuer); // 發行人
      results[result.supply.symbol_name()] = result; // 陣列下標為token符號,內容是token資訊。
      return true;
   });
   return results;
}
複製程式碼

###14. 獲取生產者資訊 get_producers

入參的結構有是否以json格式輸出的布林型別物件、資料集下限、資料集條目限制,三個都是可選引數。該介面獲得的是當前鏈的生產者資訊。該介面的返回值是一個顯示所有生產者資訊的列表,以及生產者投票總權重資訊,最後也有一個more欄位用於說明是否有更多未展示的符合條件的資料。

生產者資訊是在system合約的producers表中儲存。
複製程式碼

具體介面的實現函式較長且與前面獲取其他狀態庫表資料的邏輯相似,不在這裡重複分析原始碼。原始碼中複雜的部分在於對各種二級索引的處理。

15. 獲取生產者出塊安排 get_producer_schedule

無請求引數,返回引數的結構有三個欄位:

  • active,活躍的。直接取自controller的active_producers函式獲得,實際上返回的就是controller_impl的屬性my->head->active_schedule或者是如果存在pending塊時my->pending->_pending_block_state->active_schedule。
  • pending,等待中的。與上面一項相似來自pending_producers()函式,my->head->pending_schedule或者是如果存在pending塊時my->pending->_pending_block_state->pending_schedule。
  • proposed,計劃中的。來自proposed_producers()函式,返回my->db.get<global_property_object>()獲取的全域性屬性中的proposed_schedule欄位。

16. 獲取日程安排上鍊的事務資訊 get_scheduled_transactions

請求引數的結構:

struct get_scheduled_transactions_params {
  bool        json = false; 
  string      lower_bound;  // 傳入時間戳或者交易id。
  uint32_t    limit = 50;
};
複製程式碼

返回值結構:

struct get_scheduled_transactions_result {
  fc::variants  transactions; // 事務陣列
  string        more;
};
複製程式碼

transactions的一個元素的結構為:

auto row = fc::mutable_variant_object()
          ("trx_id", itr->trx_id)
          ("sender", itr->sender)
          ("sender_id", itr->sender_id)
          ("payer", itr->payer)
          ("delay_until", itr->delay_until)
          ("expiration", itr->expiration)
          ("published", itr->published)
    ;
複製程式碼

結果集會根據是否按照json格式輸出而做出相應處理,如果不是json格式,要進行事務打包packed,這個之前也分析過。本介面實現函式內容較多,鑑於介面本身使用並不頻繁,這裡不展開研究。

17. abi資料明文json轉二進位制 abi_json_to_bin

入參結構:

struct abi_json_to_bin_params {
  name         code; // 合約owner賬戶
  name         action; // action名字
  fc::variant  args; // action引數,json明文格式
};
複製程式碼

返回值就是二進位制串集合。實現函式:

read_only::abi_json_to_bin_result read_only::abi_json_to_bin( const read_only::abi_json_to_bin_params& params )const try {
   abi_json_to_bin_result result;
   const auto code_account = db.db().find<account_object,by_name>( params.code ); // 找到合約owner賬戶
   EOS_ASSERT(code_account != nullptr, contract_query_exception, "Contract can't be found ${contract}", ("contract", params.code));

   abi_def abi;
   if( abi_serializer::to_abi(code_account->abi, abi) ) {// 反序列化解析abi
      abi_serializer abis( abi, abi_serializer_max_time );
      auto action_type = abis.get_action_type(params.action); // 獲得action型別,在abi的action中尋找目標action
      EOS_ASSERT(!action_type.empty(), action_validate_exception, "Unknown action ${action} in contract ${contract}", ("action", params.action)("contract", params.code));
      try {
         result.binargs = abis.variant_to_binary( action_type, params.args, abi_serializer_max_time, shorten_abi_errors ); //將入參args由json轉為二進位制
      } EOS_RETHROW_EXCEPTIONS(chain::invalid_action_args_exception,
                                "'${args}' is invalid args for action '${action}' code '${code}'. expected '${proto}'",
                                ("args", params.args)("action", params.action)("code", params.code)("proto", action_abi_to_variant(abi, action_type)))
   } else {
      EOS_ASSERT(false, abi_not_found_exception, "No ABI found for ${contract}", ("contract", params.code));
   }
   return result;
} FC_RETHROW_EXCEPTIONS( warn, "code: ${code}, action: ${action}, args: ${args}",
                         ("code", params.code)( "action", params.action )( "args", params.args ))
複製程式碼

實際上的轉換工作是由variant_to_binary函式執行的。

18. abi資料二進位制轉明文json abi_bin_to_json

功能正好與上一個介面相反。入參結構中唯一不同的欄位是json格式的args改為了二進位制型別的binargs,實際上這個二進位制是字元的集合vector。返回值是json格式。函式實現與上面類似,不再展示。實際的轉換工作是由binary_to_variant函式執行的。總結這兩個介面實現函式可以發現,binary對應的就是二進位制資料格式,而variant變體對應的是json格式。

19. 獲取必須金鑰 get_required_keys

傳入使用金鑰的transaction(json格式),以及當前支援的金鑰集合。

read_only::get_required_keys_result read_only::get_required_keys( const get_required_keys_params& params )const {
   transaction pretty_input;
   auto resolver = make_resolver(this, abi_serializer_max_time);
   try {
      abi_serializer::from_variant(params.transaction, pretty_input, resolver, abi_serializer_max_time);//根據明文json事務,通過abi序列化器將資料輸出到pretty_input,轉為transaction物件。
   } EOS_RETHROW_EXCEPTIONS(chain::transaction_type_exception, "Invalid transaction")
   // 通過認證管理器獲得必須金鑰
   auto required_keys_set = db.get_authorization_manager().get_required_keys( pretty_input, params.available_keys, fc::seconds( pretty_input.delay_sec ));
   get_required_keys_result result;
   result.required_keys = required_keys_set;
   return result;
}
複製程式碼

所以核心處理函式為認證管理器authorization_manager的get_required_keys函式:

flat_set<public_key_type> authorization_manager::get_required_keys( const transaction& trx,
                                                                       const flat_set<public_key_type>& candidate_keys,
                                                                       fc::microseconds provided_delay
                                                                     )const
   {
      auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; },// 獲取許可權內容
                                        _control.get_global_properties().configuration.max_authority_depth, // 當前全域性屬性的最大許可權深度
                                        candidate_keys,
                                        {},
                                        provided_delay,
                                        _noop_checktime
                                      ); // 獲取認證檢查器

      for (const auto& act : trx.actions ) { // 遍歷事務的action
         for (const auto& declared_auth : act.authorization) {
            EOS_ASSERT( checker.satisfied(declared_auth), unsatisfied_authorization,
                        "transaction declares authority '${auth}', but does not have signatures for it.",
                        ("auth", declared_auth) );// 如果在金鑰集合中發現沒有能滿足任意action需要的許可權的,即報錯提醒。
         }
      }

      return checker.used_keys();
   }
複製程式碼

20. 獲取事務id get_transaction_id

入參物件會轉為transaction結構,返回物件是transaction_id_type,過程就很簡單了,因為本身transaction_id_type就是transaction的成員,因此將入參轉型後直接返回物件的呼叫即可。

21. 非同步讀寫操作:推送區塊 push_block

入參為chain::signed_block型別:

struct signed_block : public signed_block_header {
  using signed_block_header::signed_block_header; // 簽名區塊頭
  signed_block() = default; // 預設構造器
  signed_block( const signed_block_header& h ):signed_block_header(h){} // 構造器,傳入簽名區塊頭
  vector<transaction_receipt>   transactions; // 包含收到事務的集合
  extensions_type               block_extensions; // 區塊擴充套件
};
複製程式碼

該介面的返回值push_block_results為空,沒有返回值。介面的函式實現:

void read_write::push_block(const read_write::push_block_params& params, next_function<read_write::push_block_results> next) {
   try {
      app().get_method<incoming::methods::block_sync>()(std::make_shared<signed_block>(params));// 名稱空間incoming::methods下的成員block_sync
      next(read_write::push_block_results{});// 呼叫next寫入結果,實際上結果為空。
   } catch ( boost::interprocess::bad_alloc& ) {
      chain_plugin::handle_db_exhaustion();
   } CATCH_AND_CALL(next);
}
複製程式碼

檢視incoming::methods名稱空間下的成員block_sync:

namespace incoming {
  namespace methods {
     // 推送block到一個獨立的provider
     using block_sync = method_decl<chain_plugin_interface, void(const signed_block_ptr&), first_provider_policy>; 
  }
}
複製程式碼

繼續看method_decl的定義:

/**
* @tparam Tag - API鑑定器,用於區分相同方法的不同簽名
* @tparam FunctionSig - 方法簽名
* @tparam DispatchPolicy - 分發策略,規定了provider是如何被訪問的
*/
template< typename Tag, typename FunctionSig, template <typename> class DispatchPolicy = first_success_policy>
struct  method_decl {
  using method_type = method<FunctionSig, DispatchPolicy<FunctionSig>>;
  using tag_type = Tag;
};
複製程式碼

method_decl中呼叫了method模板,該特性是由appbase/method提供,它是一個鬆散的連結應用程式層級的函式。呼叫者Caller可以抓取一個方法並且呼叫它,而提供者Providers能夠抓取一個方法然後註冊它。method模板消除了應用程式中不同外掛之間的耦合度,可以在不同外掛之間完成鬆散地函式呼叫。

method模板的使用方式如下圖:

【劉文彬】 EOS行為核心:解析外掛chain_plugin

實體A註冊了一個函式到method裡,使用FunctionSig作為key。實體B傳入FunctionSig在method中尋找method並呼叫。同樣的,實體C、實體D都可以來呼叫,實體A並不關心誰來呼叫,它不會與呼叫者發生強關係。 回到:push_block,這一行程式碼:

app().get_method
複製程式碼

block_sync就是key,通過該鍵能夠找到對應的method: app().get_method<incoming::methods::block_sync>()。獲取到method以後,可以直接呼叫,傳入引數,通過make_shared將rpc引數轉成signed_block物件的(共享)指標: std::make_shared<signed_block>(params)。下面去找到key為block_sync的method的位置,查詢其相關的register語句:

my->_incoming_block_sync_provider = app().get_method<incoming::methods::block_sync>().register_provider([this](const signed_block_ptr& block){
  my->on_incoming_block(block);
});
複製程式碼

在producer_plugin中找到了method的註冊位置,真實呼叫的函式為生產外掛中的on_incoming_block函式,引數在外部處理傳入符合signed_block指標型別。

on_incoming_block函式

下面來看on_incoming_block函式。首先列印日誌,提醒告知接收到區塊的區塊號。然後區塊時間與本地節點時間對時,超過本地7秒開外的就終止程式,日誌提示。接著,獲取節點當前鏈環境:

chain::controller& chain = app().get_plugin<chain_plugin>().chain();
複製程式碼

接下來,判斷本地節點是否已包含該區塊,

signed_block_ptr controller::fetch_block_by_id( block_id_type id )const {// 傳入區塊id
   auto state = my->fork_db.get_block(id);// 在本地fork_db庫中查詢,是否之前已接收到分叉庫了。
   if( state && state->block ) return state->block; //&emsp;如果找到了,則返回區塊。
   auto bptr = fetch_block_by_number( block_header::num_from_id(id) ); //將id轉為區塊號,嘗試以區塊號來查詢。
   if( bptr && bptr->id() == id ) return bptr; // 以區塊號來查詢並且找到了,則直接返回區塊。
   return signed_block_ptr(); // 返回為空的signed_block物件。
}
複製程式碼

如果判斷存在,則終止程式。不存在可以繼續處理。處理接收新區塊時,仍舊要丟棄掉pending狀態的區塊。

pending狀態區塊的優先順序有時候很低,前面講到在寫入快照時,此處又提到接收新區塊時,都要將pending區塊先丟棄再進行。
複製程式碼

總結所有需要先丟棄pending區塊的操作還有:

  • producer_plugin_impl::maybe_produce_block
  • producer_plugin_impl::start_block,
  • producer_plugin::get_integrity_hash,獲取完整hash
  • producer_plugin::update_runtime_options,更新環境引數
  • producer_plugin::resume
  • producer_plugin::create_snapshot
  • producer_plugin_impl::on_incoming_block

接著設定異常回撥,如果發生異常則執行回撥函式,迴歸正常計劃的出塊迴圈節奏:

auto ensure = fc::make_scoped_exit([this](){
   schedule_production_loop(); // 正常計劃的出塊迴圈節奏。
});
複製程式碼

接下來,向鏈推送目標區塊chain.push_block(block);。異常處理,相關標誌位處理,日誌輸出結果。繼續回到push_block函式。

push_block函式

首先要判斷是否是pending狀態,推送區塊前要保證沒有pending區塊。接著校驗是否為空區塊,區塊狀態是否為incomplete。通過校驗後,發射預承認區塊訊號,攜帶區塊物件。

emit( self.pre_accepted_block, b ); // 預承認區塊訊號

接著,如果節點未特殊配置強制檢查以及區塊狀態為不可逆irreversible或者檢驗過validated,則將區塊構建為可信block_state物件加入到fork_db。經歷一系列校驗,執行auto inserted = my->index.insert(n)新增區塊到分叉庫建立的多索引庫fork_multi_index_type中,返回狀態區塊物件。回到push_block,檢查區塊生產者是否在可信生產者列表中,如果在,則將可信的生產者執行輕量級校驗的標誌位置為true。然後發射承認區塊頭訊號,並攜帶區塊狀態資料。

emit( self.accepted_block_header, new_header_state ); // 承認區塊頭訊號

接著判斷如果當前資料庫讀取模式不是IRREVERSIBLE不可逆,則需要呼叫maybe_switch_forks處理分叉合併的事務。最後判斷如果區塊狀態為irreversible,則發出第三個訊號,不可逆區塊訊號,並攜帶區塊資料。

emit( self.irreversible_block, new_header_state ); // 不可逆區塊訊號

push_block函式執行完畢,共發射了三個訊號,對應的是前文提到的controller維護的訊號,通過訊號槽機制,找到connection,並執行對應函式操作即可,訊號槽機制曾多次分析闡述,此處不展開。 push_block介面是推送本地的區塊處理,並未涉及到區塊鏈網路節點的廣播。

22. 非同步讀寫操作:推送事務 push_transaction

該介面的函式實現方式以及採用語法特性與push_block相似,本段不重複介紹。該介面的入參型別是一個變體物件variant_object,也就是說它沒有像其他介面那樣特別宣告引數結構,而是在函式實現中,加入了物件的構造過程,引數物件最終通過abi_serializer::from_variant被構造成packed_transaction打包事務型別。返回值結構是有定義的:

struct push_transaction_results {
  chain::transaction_id_type  transaction_id; // 事務id
  fc::variant                 processed; // 加工過的事務物件
};
複製程式碼

回到函式體,同樣是基於method模板的功能,在producer_plugin中找到transaction_async註冊的函式,傳入了處理好的打包事務物件,是否存留標誌位,用來接收返回值的next函式。實際呼叫了producer_plugin_impl::on_incoming_transaction_async函式。

on_incoming_transaction_async函式

該函式內容較多。首先,仍及是獲取節點當前鏈環境:

chain::controller& chain = app().get_plugin<chain_plugin>().chain();

接著,判斷當前鏈若是不存在pending塊,則增加到pending塊。接著推送區塊是通過channel模板的機制,這是與method模板想類似的機制。首先來看函式中該機制首次出現的位置:

_transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>(response.get<fc::exception_ptr>(), trx)); // 傳入事務物件trx

_transaction_ack_channel是當前例項的成員,找到當前例項的建構函式,發現該成員的初始化資訊:

_transaction_ack_channel(app().get_channel<compat::channels::transaction_ack>())

app().get_channel的結構與上面介紹method的機制非常相似,檢視transaction_ack的宣告:

namespace compat {
  namespace channels {
     using transaction_ack       = channel_decl<struct accepted_transaction_tag, std::pair<fc::exception_ptr, packed_transaction_ptr>>;
  }
}
複製程式碼

該宣告與上面method相關key的宣告在同一個檔案中,說明設計者的思路是有將他們歸為一類的:都屬於解耦呼叫的橋樑。接著檢視channel_decl:

/**
* @tparam Tag - API鑑定器,用於區分相同資料型別
* @tparam Data - channel攜帶的資料型別
* @tparam DispatchPolicy - 當前channel的分發策略。預設是drop_exceptions
*/
template< typename Tag, typename Data, typename DispatchPolicy = drop_exceptions >
struct channel_decl {
  using channel_type = channel<Data, DispatchPolicy>;
  using tag_type = Tag;
};
複製程式碼

與method_decl非常相似了。具體channel機制的分析如下圖所示。

【劉文彬】 EOS行為核心:解析外掛chain_plugin

可以看得出,channel的訂閱與釋出的模式,對應的是method的註冊和呼叫,主要區別在於主體的角色轉換。

channel的訂閱是要依賴頻道本身的內容釋出的,也就是說頻道是要先存在的,主體A可以來訂閱,主體C、主體D都可以來訂閱,而與作為釋出方的主體B無關,主體B不用知道有誰訂閱了。而method的註冊和呼叫正好是相反的。實際上對於本文研究到的channel和method,主體A都是producer_plugin。本例中,一個區塊被廣播出來,需要所有的訂閱者來執行本地的區塊接收操作,因此需要採用channel機制。
複製程式碼

下面搜尋transaction_ack頻道的訂閱處:

my->incoming_transaction_ack_subscription = app().get_channel<channels::transaction_ack> ().subscribe(boost::bind(&net_plugin_impl::transaction_ack, my.get(), _1));

延伸到實際的呼叫函式net_plugin_impl::transaction_ack

void net_plugin_impl::transaction_ack(const std::pair<fc::exception_ptr, packed_transaction_ptr>& results) {
  transaction_id_type id = results.second->id();
  if (results.first) { // first位置是用來放異常資訊的,如果first不為空則說明有異常。
     fc_ilog(logger,"signaled NACK, trx-id = ${id} : ${why}",("id", id)("why", results.first->to_detail_string()));
     dispatcher->rejected_transaction(id);// 呼叫rejected_transaction,從received_transactions接收事務集合中將其刪除。
  } else {
     fc_ilog(logger,"signaled ACK, trx-id = ${id}",("id", id));
     dispatcher->bcast_transaction(*results.second);
  }
}
複製程式碼

bcast_transaction函式

呼叫廣播事務函式dispatch_manager::bcast_transaction。

void dispatch_manager::bcast_transaction (const packed_transaction& trx) {
  std::set<connection_ptr> skips; // 跳過的資料集合
  transaction_id_type id = trx.id();
  auto range = received_transactions.equal_range(id);
  for (auto org = range.first; org != range.second; ++org) {
     skips.insert(org->second); // 在接收事務集合中找到對應id的事務資料遍歷放於skips。
  }
  received_transactions.erase(range.first, range.second); // 從received_transactions接收事務集合中將其刪除。
  for (auto ref = req_trx.begin(); ref != req_trx.end(); ++ref) {
     if (*ref == id) { // 本地請求事務集合中,找到目標事務刪除
        req_trx.erase(ref);
        break;
     }
  }
  if( my_impl->local_txns.get<by_id>().find( id ) != my_impl->local_txns.end( ) ) {
     fc_dlog(logger, "found trxid in local_trxs" );
     return;// 在本地事務集合中找到目標事務了,終止不必重複處理。
  }
  uint32_t packsiz = 0;
  uint32_t bufsiz = 0;
  time_point_sec trx_expiration = trx.expiration();
  net_message msg(trx);
  packsiz = fc::raw::pack_size(msg);
  bufsiz = packsiz + sizeof(packsiz);
  vector<char> buff(bufsiz);
  fc::datastream<char*> ds( buff.data(), bufsiz);
  ds.write( reinterpret_cast<char*>(&packsiz), sizeof(packsiz) );
  fc::raw::pack( ds, msg );// trx轉為net_message結構,打包通過資料流ds到快取buff中。
  node_transaction_state nts = {id,
                                trx_expiration,
                                trx,
                                std::move(buff),
                                0, 0, 0};
  my_impl->local_txns.insert(std::move(nts)); // 插入到本地事務集,net_plugin自定義的多索引庫node_transaction_index中。

  if( !large_msg_notify || bufsiz <= just_send_it_max) { // max-implicit-request引數決定just_send_it_max,最大請求數量
     my_impl->send_all( trx, [id, &skips, trx_expiration](connection_ptr c) -> bool {
           if( skips.find(c) != skips.end() || c->syncing ) {// skips中一旦有了當前連線,或者connection正在同步中,則退出。
              return false;
           }
           const auto& bs = c->trx_state.find(id); // 連線中的事務狀態多索引庫中尋找目標事務,返回事務資料
           bool unknown = bs == c->trx_state.end();
           if( unknown) {// 沒找到則插入
              c->trx_state.insert(transaction_state({id,true,true,0,trx_expiration,time_point() }));
              fc_dlog(logger, "sending whole trx to ${n}", ("n",c->peer_name() ) );
           } else { // 找到則更新過期時間、狀態庫資料
              update_txn_expiry ute(trx_expiration);
              c->trx_state.modify(bs, ute);
           }
           return unknown;
        });
  }else {// 超過最大請求數量以後,不處理trx,而是pending_notify
     notice_message pending_notify;
     pending_notify.known_trx.mode = normal;
     pending_notify.known_trx.ids.push_back( id );
     pending_notify.known_blocks.mode = none;
     my_impl->send_all(pending_notify, [id, &skips, trx_expiration](connection_ptr c) -> bool {
           if (skips.find(c) != skips.end() || c->syncing) {
              return false;
           }
           const auto& bs = c->trx_state.find(id);
           bool unknown = bs == c->trx_state.end();
           if( unknown) {
              fc_dlog(logger, "sending notice to ${n}", ("n",c->peer_name() ) );
              c->trx_state.insert(transaction_state({id,false,true,0,trx_expiration,time_point() }));
           } else {
              update_txn_expiry ute(trx_expiration);
              c->trx_state.modify(bs, ute);
           }
           return unknown;
        });
  }
}
複製程式碼

23. 非同步讀寫操作:推送事務陣列 push_transactions

這個介面是針對網路情況不理想,一次請求希望攜帶更多事務的場景而設計的,實現函式是呼叫一個遞迴函式push_recurse遍歷傳入的transactions陣列,每個transaction最終仍舊會通過以上push_transaction函式逐一處理。

目前事務陣列最多支援1000筆,多了報錯。
複製程式碼

總結

chain_plugin是EOS的核心,承載了大部分鏈相關的功能。本文按照rpc訪問的脈絡分析,從chain_api_plugin的rpc介面列表展開介紹,延伸到chain_plugin的介面實現,深入分析了所有的rpc介面的背後實現邏輯,其中涉及到了FC_REFLECT反射技術,通過method模板關聯到了producer_plugin,通過channel模板技術關聯到了net_plugin。chain_plugin是核心鏈處理外掛,本文在該範疇下進行了詳盡地調研,加深了對於fork_db,多索引庫以及各種出現的資料結構的理解。

參考資料

  • EOSIO\eos

相關文章和視訊推薦

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

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

相關文章