【原始碼解讀】EOS測試外掛:txn_test_gen_plugin.cpp
本文內容本屬於《【精解】EOS TPS 多維實測》的內容,但由於在編寫時篇幅過長,所以我決定將這一部分單獨成文撰寫,以便於理解。
關鍵字:eos, txn_test_gen_plugin, signed_transaction, ordered_action_result, C++, EOS外掛
txn_test_gen_plugin 外掛
這個外掛是官方開發用來測試塊打包交易量的,這種方式由於是直接系統內部呼叫來模擬transaction,沒有中間通訊的損耗,因此效率是非常高的,官方稱通過這個外掛測試到了8000的tps結果,而就我的測試結果來講,沒有這麼恐怖,但也能到2000了,熟不知,其他的測試手段,例如cleos,eosjs可能只有百級的量。下面,我們一同來研究一下這個外掛是如何實現以上功能的,過程中,我們也會思考EOS外掛的架構體系,以及實現方法。通過本文的學習,如果有好的想法,我們也可以自己開發一個功能強大的外掛pr給eos,為EOS社群做出我們自己的貢獻。
關於txn_test_gen_plugin外掛的使用,非常易於上手,本文不做分析,這方面可以直接參考官方文件。
外掛的整體架構
外掛程式碼整體結構中,我們上面介紹的核心功能的實現函式都是包含在一個結構體struct txn_test_gen_plugin_impl中。剩餘的其他程式碼都是對外掛本身的通訊進行描述,包括如何呼叫,如何響應等,以及整個外掛的生命週期的控制:
- set_program_options,設定引數的階段,是最開始的階段,內容只設置了txn-reference-block-lag的值,預設是0,-1代表最新頭區塊。
- plugin_initialize,這一時期就把包含核心功能的結構體txn_test_gen_plugin_impl載入到程式執行時記憶體中了,同時初始化標誌位txn_reference_block_lag為txn-reference-block-lag的值。
- plugin_startup,我們通過基礎外掛http_plugin的支援獲得了http介面的能力,這一時期,就暴露出來本外掛的對外介面。
- plugin_shutdown,呼叫stop_generation函式,重置標誌位running為false,計時器關閉,列印關閉提示日誌。
下面是對外暴露的三個介面之一的stop_generation函式的原始碼:
void stop_generation() { if(!running) throw fc::exception(fc::invalid_operation_exception_code); timer.cancel(); running = false; ilog("Stopping transaction generation test"); }
接下來,我們主要集中精力在結構體txn_test_gen_plugin_impl上,研究路線是以剩餘兩個介面分別為入口進行逐一分析。
create_test_accounts 介面
關於這個介面,呼叫方法是
curl --data-binary '["eosio", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"]' http://localhost:8888/v1/txn_test_gen/create_test_accounts
傳入的引數是eosio以及其私鑰。我們進入到函式create_test_accounts中去分析原始碼。
準備知識
首先,整個函式涉及到的所有transaction都是打包存入到一個vector集合std::vector中去。
trxs是一個事務集,它包含很多的trx,而其中每一個trx包含一個actions集合vector
一、準備賬戶
trxs的第一個trx,內容為賬戶建立:
- 定義3個賬戶:txn.test.a,txn.test.b, txn.test.t
- 輔助功能:controller& cc = app().get_plugin<chain_plugin>().chain();,通過cc可以隨時呼叫本地區塊鏈上的任意資訊。
- 通過fc::crypto::private_key::regenerate函式分別生成他們的私鑰,要傳入生成祕鑰的seed。
- 通過私鑰直接呼叫get_public_key()即可獲得公鑰
- 設定每個賬戶的owner和active許可權對應的公鑰,一般來講他們是相同的
- 賬戶的建立者均為我們外部呼叫create_test_accounts介面時傳入的賬戶eosio,注意:eosio的私鑰是通過字串傳入的,要通過fc::crypto::private_key轉換成私鑰物件
- 將每一個賬戶的建立組裝好成為一個action,存入trx的actions集合中去。
- trx的actions成員已經設定完畢,完成剩餘trx的組裝工作,包括
- expiration,通過cc獲得當前頭區塊的時間,加上延遲時間,這裡是30s,fc::seconds(30)
- reference_block,值為通過cc獲取當前的頭區塊,意思為本transaction的引用區塊,所有的資訊是引用的這個區塊為頭區塊的環境
- sign,簽名,使用的是建立者eosio的私鑰物件,上面我們已經準備好了,簽名的資料是data的摘要
- 當前trx的actions中的元素的data並不是如文首的transaction中的data的加密串的結構,而是明文的,這裡的加密是數字摘要技術,感興趣的朋友可以去《應用密碼學初探》進行了解。
- 摘要的原始碼函式是:sig_digest(chain_id, context_free_data),其中引數使用到了chain_id,而context_free_data就是上面提到的明文data內容,所以它是要與鏈id一起做數字摘要的(這一點我在使用eosjs嘗試自己做摘要的時候並未想到)
這一部分的原始碼展示如下:
name newaccountA("txn.test.a");
name newaccountB("txn.test.b");
name newaccountC("txn.test.t");
name creator(init_name);
abi_def currency_abi_def = fc::json::from_string(eosio_token_abi).as<abi_def>();
controller& cc = app().get_plugin<chain_plugin>().chain();
auto chainid = app().get_plugin<chain_plugin>().get_chain_id();
fc::crypto::private_key txn_test_receiver_A_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'a')));
fc::crypto::private_key txn_test_receiver_B_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'b')));
fc::crypto::private_key txn_test_receiver_C_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'c')));
fc::crypto::public_key txn_text_receiver_A_pub_key = txn_test_receiver_A_priv_key.get_public_key();
fc::crypto::public_key txn_text_receiver_B_pub_key = txn_test_receiver_B_priv_key.get_public_key();
fc::crypto::public_key txn_text_receiver_C_pub_key = txn_test_receiver_C_priv_key.get_public_key();
fc::crypto::private_key creator_priv_key = fc::crypto::private_key(init_priv_key);
//create some test accounts
{
signed_transaction trx;
//create "A" account
{
auto owner_auth = eosio::chain::authority{1, {{txn_text_receiver_A_pub_key, 1}}, {}};
auto active_auth = eosio::chain::authority{1, {{txn_text_receiver_A_pub_key, 1}}, {}};
trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountA, owner_auth, active_auth});
}
//create "B" account
{
auto owner_auth = eosio::chain::authority{1, {{txn_text_receiver_B_pub_key, 1}}, {}};
auto active_auth = eosio::chain::authority{1, {{txn_text_receiver_B_pub_key, 1}}, {}};
trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountB, owner_auth, active_auth});
}
//create "txn.test.t" account
{
auto owner_auth = eosio::chain::authority{1, {{txn_text_receiver_C_pub_key, 1}}, {}};
auto active_auth = eosio::chain::authority{1, {{txn_text_receiver_C_pub_key, 1}}, {}};
trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountC, owner_auth, active_auth});
}
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.set_reference_block(cc.head_block_id());
trx.sign(creator_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}
二、token相關
trxs的第二個trx,內容為token建立和issue,為賬戶轉賬為之後的測試做準備
- 為賬戶txn.test.t設定eosio.token合約,之前在操作cleos set contract的時候可以通過列印結果發現,是有setcode和setabi兩個步驟的。
- setcode handler:
- 設定handler的賬戶為txn.test.t
- 將wasm設定為handler的code,wasm是通過eosio.token合約的eosio_token_wast檔案獲取的,vector<uint8_t> wasm = wast_to_wasm(std::string(eosio_token_wast))
- 將handler加上相關許可權組裝成action裝入trx的actions集合中。
- setabi handler:
- 設定handler的賬戶為txn.test.t
- 設定handler的abi,將檔案eosio_token_abi(json格式的)轉成json轉儲為abi_def結構,然後通過fc::raw::pack操作將結果賦值給abi
- 將handler加上相關許可權組裝成action裝入trx的actions集合中。
- setcode handler:
- 使用賬戶txn.test.t建立token,標誌位CUR,總髮行量十億,裝成action裝入trx的actions集合中。
- issue CUR 給txn.test.t 600枚CUR,裝成action裝入trx的actions集合中。
- 從txn.test.t轉賬給txn.test.a 200枚CUR,裝成action裝入trx的actions集合中。
- 從txn.test.t轉賬給txn.test.b 200枚CUR,裝成action裝入trx的actions集合中。
- trx的actions成員已經設定完畢,完成剩餘trx的組裝工作(同上),這裡只介紹不同的部分
- max_net_usage_words,指定了網路資源的最大使用限制為5000個詞。
這一部分的原始碼展示如下:
//set txn.test.t contract to eosio.token & initialize it
{
signed_transaction trx;
vector<uint8_t> wasm = wast_to_wasm(std::string(eosio_token_wast));
setcode handler;
handler.account = newaccountC;
handler.code.assign(wasm.begin(), wasm.end());
trx.actions.emplace_back( vector<chain::permission_level>{{newaccountC,"active"}}, handler);
{
setabi handler;
handler.account = newaccountC;
handler.abi = fc::raw::pack(json::from_string(eosio_token_abi).as<abi_def>());
trx.actions.emplace_back( vector<chain::permission_level>{{newaccountC,"active"}}, handler);
}
{
action act;
act.account = N(txn.test.t);
act.name = N(create);
act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
act.data = eosio_token_serializer.variant_to_binary("create", fc::json::from_string("{\"issuer\":\"txn.test.t\",\"maximum_supply\":\"1000000000.0000 CUR\"}}"));
trx.actions.push_back(act);
}
{
action act;
act.account = N(txn.test.t);
act.name = N(issue);
act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
act.data = eosio_token_serializer.variant_to_binary("issue", fc::json::from_string("{\"to\":\"txn.test.t\",\"quantity\":\"600.0000 CUR\",\"memo\":\"\"}"));
trx.actions.push_back(act);
}
{
action act;
act.account = N(txn.test.t);
act.name = N(transfer);
act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.a\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}"));
trx.actions.push_back(act);
}
{
action act;
act.account = N(txn.test.t);
act.name = N(transfer);
act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.b\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}"));
trx.actions.push_back(act);
}
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.set_reference_block(cc.head_block_id());
trx.max_net_usage_words = 5000;
trx.sign(txn_test_receiver_C_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}
發起請求
目前trxs集合已經包含了兩個trx元素,其中每個trx包含了多個action。下面要將trxs推送到鏈上執行
- push_transactions函式,遍歷trxs元素,每個trx單獨傳送push_next_transaction
- push_next_transaction函式,首先將trx取出通過packed_transaction函式進行組裝成post的結構
- packed_transaction函式,通過set_transaction函式對trx進行摘撿,使用pack_transaction函式進行組裝
- pack_transaction函式,就是呼叫了一下上面提過的fc::raw::pack操作,然後通過accept_transaction函式向鏈發起請求
- accept_transaction函式,是chain_plugin的一個函式,它內部呼叫了incoming_transaction_async_method非同步發起交易請求。
這部分程式碼比較雜,分為幾個部分:
push_transactions函式:
void push_transactions( std::vector<signed_transaction>&& trxs, const std::function<void(fc::exception_ptr)>& next ) {
auto trxs_copy = std::make_shared<std::decay_t<decltype(trxs)>>(std::move(trxs));
push_next_transaction(trxs_copy, 0, next);
}
push_next_transaction函式:
static void push_next_transaction(const std::shared_ptr<std::vector<signed_transaction>>& trxs, size_t index, const std::function<void(const fc::exception_ptr&)>& next ) {
chain_plugin& cp = app().get_plugin<chain_plugin>();
cp.accept_transaction( packed_transaction(trxs->at(index)), [=](const fc::static_variant<fc::exception_ptr, transaction_trace_ptr>& result){
if (result.contains<fc::exception_ptr>()) {
next(result.get<fc::exception_ptr>());
} else {
if (index + 1 < trxs->size()) {
push_next_transaction(trxs, index + 1, next);
} else {
next(nullptr);
}
}
});
}
packed_transaction函式,set_transaction函式以及pack_transaction函式的程式碼都屬於本外掛原始碼之外的EOS庫原始碼,由於本身程式碼量也較少,含義在上面已經完全解釋過了,這裡不再貼上原始碼。
accept_transaction函式也是EOS的庫原始碼
void chain_plugin::accept_transaction(const chain::packed_transaction& trx, next_function<chain::transaction_trace_ptr> next) {
my->incoming_transaction_async_method(std::make_shared<packed_transaction>(trx), false, std::forward<decltype(next)>(next));
}
incoming_transaction_async_method(app().get_method<incoming::methods::transaction_async>())
start_generation 介面
該介面的呼叫方法是:
curl --data-binary '["", 20, 20]' http://localhost:8888/v1/txn_test_gen/start_generation
引數列表為:
- 第一個引數為 salt,一般用於“加鹽”加密演算法的值,這裡我們可以留空。
- 第二個引數為 period,傳送交易的間隔時間,單位為ms,這裡是20。
- 第三個引數為 batch_size,每個傳送間隔週期內打包交易的數量,這裡也是20。
翻譯過來就是:每20ms提交20筆交易。
接下來,以start_generation 函式為入口進行原始碼分析。
start_generation 函式
- 校驗:
- period的取值範圍為(1, 2500)
- batch_size的取值範圍為(1, 250)
- batch_size必須是2的倍數,batch_size & 1結果為假0才可以,這是一個位運算,與&,所以batch_size的值轉為二進位制時末位不能為1,所以就是2的倍數即可。
- 對標誌位running的控制。
這部分程式碼展示如下:
if(running)
throw fc::exception(fc::invalid_operation_exception_code);
if(period < 1 || period > 2500)
throw fc::exception(fc::invalid_operation_exception_code);
if(batch_size < 1 || batch_size > 250)
throw fc::exception(fc::invalid_operation_exception_code);
if(batch_size & 1)
throw fc::exception(fc::invalid_operation_exception_code);
running = true;
- 定義兩個action,分別是:
- 賬戶txn.test.a給txn.test.b轉賬1000枚CUR
- txn.test.b轉給txn.test.a同樣1000枚CUR
這部分程式碼展示如下:
//create the actions here
act_a_to_b.account = N(txn.test.t);
act_a_to_b.name = N(transfer);
act_a_to_b.authorization = vector<permission_level>{{name("txn.test.a"),config::active_name}};
act_a_to_b.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string(fc::format_string("{\"from\":\"txn.test.a\",\"to\":\"txn.test.b\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutable_variant_object()("l", salt))));
act_b_to_a.account = N(txn.test.t);
act_b_to_a.name = N(transfer);
act_b_to_a.authorization = vector<permission_level>{{name("txn.test.b"),config::active_name}};
act_b_to_a.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string(fc::format_string("{\"from\":\"txn.test.b\",\"to\":\"txn.test.a\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutable_variant_object()("l", salt))));
接下來,是對引數period和batch_size的儲存為結構體作用域的變數以供結構體內其他函式呼叫,然後列印日誌,最後呼叫arm_timer函式。
timer_timeout = period; // timer_timeout是結構體的成員變數
batch = batch_size/2; // batch是結構體的成員變數
ilog("Started transaction test plugin; performing ${p} transactions every ${m}ms", ("p", batch_size)("m", period));
arm_timer(boost::asio::high_resolution_timer::clock_type::now());
arm_timer 函式
從start_generation 函式過來,傳入的引數是當前時間now,該函式主要功能是對計時器的初始化操作(計時器與文首的stop_generation函式中的關閉計時器呼應)。具體內容可分為兩部分:
- 設定計時器的過期時間,值為start_generation 介面的引數period與now相加的值,即從現在開始,過period這麼久,當前計時器物件timer就過期。
- 設定計時器的非同步定時任務,任務體直接呼叫send_transaction函式,對函式的返回值進行處理,如果有報錯資訊(一般是服務中止)則呼叫stop_generation函式關閉外掛。
注意stop_generation函式關閉的是定時任務的無限遞迴,中止定時任務,停止傳送測試交易。但它並沒有停止外掛服務,我們仍舊可以通過再次請求外掛介面啟動無限測試交易。
這部分程式碼如下:
void arm_timer(boost::asio::high_resolution_timer::time_point s) {
timer.expires_at(s + std::chrono::milliseconds(timer_timeout));
timer.async_wait([this](const boost::system::error_code& ec) {
if(!running || ec)
return;
send_transaction([this](const fc::exception_ptr& e){
if (e) {
elog("pushing transaction failed: ${e}", ("e", e->to_detail_string()));
stop_generation();
} else { // 如果沒有終止報錯,則無限遞迴呼叫arm_timer函式,遞迴時傳入的引數代替上面的now是當前timer物件的過期時間,這樣在新的遞迴呼叫中,timer的建立會以這個時間再加上period,無間隔繼續執行。
arm_timer(timer.expires_at());
}
});
});
}
send_transaction 函式
這個函式是本外掛的核心功能部分,主要是傳送測試交易,對transaction的處理,將我們上面start_generation 函式中設定的兩個action打包到transaction中去,以及對transaction各項屬性的設定。具體步驟為:
- 宣告trxs,併為其設定大小為start_generation 介面中batch_size的值。
std::vector<signed_transaction> trxs;
trxs.reserve(2*batch);
接下來,與上面介紹的create_test_accounts 介面的賬戶準備過程相同,準備私鑰公鑰,不多介紹。繼續準備trx的引數:
- nonce,是用來賦值context_free_actions的
- context_free_actions:官方介紹一大堆,總之就是正常action是需要代價的,要確權,要佔用主網資源什麼的,所以搞了一個context_free_actions,字面意思就是上下文免費的action,這裡權當測試用,填入的資料也是隨機nonce組裝的。
- abi_serializer,用來序列化abi的,傳入的system_account_name的abi值,它是在這裡被賦值,然而是在結構體的作用域中被呼叫的。
- reference_block_num的處理,引用區塊,上面我們也提到過,而這裡面增加了一層判斷,是根據標誌位txn_reference_block_lag的值來比較,也就是說reference_block_num最後的值是最新區塊號減去txn_reference_block_lag的值,但是最小值為0,不可為負數。
- 通過reference_block_num獲得reference_block_id
這部分程式碼如下:
controller& cc = app().get_plugin<chain_plugin>().chain();
auto chainid = app().get_plugin<chain_plugin>().get_chain_id();
fc::crypto::private_key a_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'a')));
fc::crypto::private_key b_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'b')));
static uint64_t nonce = static_cast<uint64_t>(fc::time_point::now().sec_since_epoch()) << 32;
abi_serializer eosio_serializer(cc.db().find<account_object, by_name>(config::system_account_name)->get_abi());
uint32_t reference_block_num = cc.last_irreversible_block_num();
if (txn_reference_block_lag >= 0) {
reference_block_num = cc.head_block_num();
if (reference_block_num <= (uint32_t)txn_reference_block_lag) {
reference_block_num = 0;
} else {
reference_block_num -= (uint32_t)txn_reference_block_lag;
}
}
block_id_type reference_block_id = cc.get_block_id_for_num(reference_block_num);
接下來,就是迴圈打包trx,我們設定的batch_size好比是20,現在我們已有兩個action,每個action對應一個trx,則迴圈只需要執行10次,每次執行兩個trx即可實現,每個trx相關的屬性在上一階段都已準備好。直接看程式碼吧。
for(unsigned int i = 0; i < batch; ++i) {
{
signed_transaction trx;
trx.actions.push_back(act_a_to_b);
trx.context_free_actions.emplace_back(action({}, config::null_account_name, "nonce", fc::raw::pack(nonce++)));
trx.set_reference_block(reference_block_id);
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.max_net_usage_words = 100;
trx.sign(a_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}
{
signed_transaction trx;
trx.actions.push_back(act_b_to_a);
trx.context_free_actions.emplace_back(action({}, config::null_account_name, "nonce", fc::raw::pack(nonce++)));
trx.set_reference_block(reference_block_id);
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.max_net_usage_words = 100;
trx.sign(b_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}
}
最後,執行
push_transactions(std::move(trxs), next);
這個部分與create_test_accounts 介面發起請求的部分一致,這裡不再重複展示。
總結
到這裡為止,我們已經完全分析透了txn_test_gen_plugin 外掛的內容。本文首先從大體上介紹了外掛的架構,生命週期,通訊請求與返回。接著介紹了核心結構體的內容,然後以對外介面為入口,沿著一條線將每個功能的實現完整地研究清楚。通過本文的學習,我們對於EOS外掛的體系有了初步深刻的理解,同時我們也完全搞清楚了txn_test_gen_plugin 外掛的功能,以及它為什麼會達到一個比較高的tps的表現。
參考資料
- EOSIO/eos
- eos官方文件