跟原力一起玩轉(zhuǎn)EOS源碼-Push Transaction機(jī)制區(qū)塊鏈
跟原力一起玩轉(zhuǎn)EOS源碼-PushTransaction機(jī)制
EOS源碼備忘-Push Transaction機(jī)制
這里我們討論EOS Push Transaction 的邏輯,這塊EOS與Eosforce實(shí)現(xiàn)有一些區(qū)別,我們會(huì)著重點(diǎn)出。 關(guān)于wasm相關(guān)的內(nèi)容我們會(huì)有一片專門的文檔分析。
我們這里通常將Transaction譯做交易,其實(shí)這里應(yīng)該是事務(wù)的意思。
1. Transaction與Action
在EOS中Transaction與Action是最重要的幾個(gè)類型, 在EOS中,所有的鏈上行為都是Action,Transaction是一系列Action組成的事務(wù)。
EOS中使用繼承體系劃分trx與action結(jié)構(gòu),關(guān)系圖如下:
transaction_header <- transaction <- signed_transaction <- deferred_transaction
|
packed_transaction
1.1 Action
我們這里先看一下Action的聲明:
// 權(quán)限結(jié)構(gòu)structpermission_level{account_name actor; permission_name permission; }; ...structaction{account_name account; action_name name;// 執(zhí)行所需的權(quán)限vector authorization; bytes data; ...// 打包成二進(jìn)制templateTdata_as()const{ ... } };
Action沒(méi)有什么特別的內(nèi)容,但要注意:
!> 在EOS中一個(gè)transaction中包含很多個(gè)action,而在Eosforce中一個(gè)trx只能包括一個(gè)action。
1.2 Transaction
下面我們分析一下transaction,這里簡(jiǎn)寫為trx。
首先看下
/**
* The transaction header contains the fixed-sized data
* associated with each transaction. It is separated from
* the transaction body to facilitate partial parsing of
* transactions without requiring dynamic memory allocation.
*
* All transactions have an expiration time after which they
* may no longer be included in the blockchain. Once a block
* with a block_header::timestamp greater than expiration is
* deemed irreversible, then a user can safely trust the transaction
* will never be included.
*
* Each region is an independent blockchain, it is included as routing
* information for inter-blockchain communication. A contract in this
* region might generate or authorize a transaction intended for a foreign
* region.
*/structtransaction_header{time_point_sec expiration;///< trx超時(shí)時(shí)間uint16_tref_block_num =0U;// 包含trx的block num 注意這個(gè)值是后2^16個(gè)塊中uint32_tref_block_prefix =0UL;// blockid的低32位fc::unsigned_int max_net_usage_words =0UL;// 網(wǎng)絡(luò)資源上限uint8_tmax_cpu_usage_ms =0;// cpu資源上限fc::unsigned_int delay_sec =0UL;/// 延遲交易的延遲時(shí)間/**
* @return the absolute block number given the relative ref_block_num
* 計(jì)算ref_block_num
*/block_num_typeget_ref_blocknum( block_num_type head_blocknum )const{return((head_blocknum/0xffff)*0xffff) head_blocknum%0xffff; }voidset_reference_block(constblock_id_type& reference_block );boolverify_reference_block(constblock_id_type& reference_block )const;voidvalidate()const; };
transaction_header包含一個(gè)trx中固定長(zhǎng)度的數(shù)據(jù),這里之所以要單獨(dú)提出來(lái)主要是為了優(yōu)化。
transaction視為交易體數(shù)據(jù),這里主要是存儲(chǔ)這個(gè)trx包含的action。
/**
* A transaction consits of a set of messages which must all be applied or
* all are rejected. These messages have access to data within the given
* read and write scopes.
*/
// 在EOS中一個(gè)交易中 action要么全部執(zhí)行,要么都不執(zhí)行
struct transaction : public transaction_header {
vector<action> context_free_actions;
vector<action> actions;
extensions_type transaction_extensions;
// 獲取trx id
transaction_id_type id()const;
digest_type sig_digest( const chain_id_type& chain_id, const vector<bytes>& cfd = vector<bytes>() )const;
...
};
注意這里的context_free_actions,這里指上下文無(wú)關(guān)的Action,具體信息可以參見(jiàn)這里: https://medium.com/@bytemaster/eosio-development-update-272198df22c1 和 https://github.com/EOSIO/eos/issues/1387。 如果一個(gè)Action執(zhí)行時(shí)只依賴與transaction的數(shù)據(jù),而不依賴與鏈上的狀態(tài),這樣的action可以并發(fā)的執(zhí)行。
另外一個(gè)值得注意的是trx id:
transaction_id_type transaction::id()const{ digest_type::encoder enc; fc::raw::pack( enc, *this);returnenc.result();}
!> Eosforce不同
在Eosforce中為了添加手續(xù)費(fèi)信息,trx與EOS結(jié)構(gòu)不同,主要是增加了fee, 在transaction中:
struct transaction : public transaction_header {
vector<action> context_free_actions;
vector<action> actions;
extensions_type transaction_extensions;
asset fee; // EOSForce 增加的手續(xù)費(fèi),在客戶端push trx時(shí)需要寫入
transaction_id_type id()const;
digest_type sig_digest( const chain_id_type& chain_id, const vector<bytes>& cfd = vector<bytes>() )const;
flat_set<public_key_type> get_signature_keys( const vector<signature_type>& signatures,
const chain_id_type& chain_id,
const vector<bytes>& cfd = vector<bytes>(),
bool allow_duplicate_keys = false,
bool use_cache = true )const;
uint32_t total_actions()const { return context_free_actions.size() actions.size(); }
account_name first_authorizor()const {
for( const auto& a : actions ) {
for( const auto& u : a.authorization )
return u.actor;
}
return account_name();
}
};
在 https://eosforce.github.io/Documentation/#/zh-cn/eosforce_client_develop_guild 這篇文檔里也有說(shuō)明。
這里計(jì)算trx id時(shí)完全使用trx的數(shù)據(jù),這意味著,如果是兩個(gè)trx數(shù)據(jù)完全一致,特別的他們?cè)谝粋€(gè)區(qū)塊中,那么這兩個(gè)trx的id就會(huì)是一樣的。
1.3 signed_transaction
一個(gè)trx簽名之后會(huì)得到一個(gè)signed_transaction,
structsigned_transaction:publictransaction { ...vector signatures;// 簽名vector context_free_data;// 上下文無(wú)關(guān)的action所使用的數(shù)據(jù)// 簽名constsignature_type&sign(constprivate_key_type& key,constchain_id_type& chain_id);signature_typesign(constprivate_key_type& key,constchain_id_type& chain_id)const; flat_set get_signature_keys(constchain_id_type& chain_id,boolallow_duplicate_keys =false,booluse_cache =true)const; };
signed_transaction包含簽名數(shù)據(jù)和上下文無(wú)關(guān)的action所使用的數(shù)據(jù),
這里要談一下context_free_data,可以參見(jiàn) https://github.com/EOSIO/eos/commit/a41b4d56b5cbfd0346de34b0e03819f72e834041 ,之前我們看過(guò)context_free_actions, 在上下文無(wú)關(guān)的action中可以去從context_free_data獲取數(shù)據(jù),可以參見(jiàn)在api_tests.cpp中的測(cè)試用例:
... {// back to normal actionactionact1(pl, da); signed_transaction trx; trx.context_free_actions.push_back(act); trx.context_free_data.emplace_back(fc::raw::pack(100));// verify payload matches context free datatrx.context_free_data.emplace_back(fc::raw::pack(200)); trx.actions.push_back(act1);// attempt to access non context free apifor(uint32_ti =200; i <=211; i) { trx.context_free_actions.clear(); trx.context_free_data.clear(); cfa.payload = i; cfa.cfd_idx =1;actioncfa_act({}, cfa); trx.context_free_actions.emplace_back(cfa_act); trx.signatures.clear(); set_transaction_headers(trx); sigs = trx.sign(get_private_key(N(testapi),"active"), control->get_chain_id()); BOOST_CHECK_EXCEPTION(push_transaction(trx), unaccessible_api, [](constfc::exception& e) {returnexpect_assert_message(e,"only context free api's can be used in this context"); } ); }...
這里可以作為context_free_action的一個(gè)例子,在test_api.cpp中的合約會(huì)調(diào)用void test_action::test_cf_action()函數(shù):
// 這個(gè)是測(cè)試`context_free_action`的action
void test_action::test_cf_action() {
eosio::action act = eosio::get_action( 0, 0 );
cf_action cfa = act.data_as<cf_action>();
if ( cfa.payload == 100 ) {
// verify read of get_context_free_data, also verifies system api access
// 測(cè)試在合約中通過(guò) get_context_free_data 獲取 context_free_data
int size = get_context_free_data( cfa.cfd_idx, nullptr, 0 );
eosio_assert( size > 0, "size determination failed" );
eosio::bytes cfd( static_cast<size_t>(size) );
size = get_context_free_data( cfa.cfd_idx, &cfd[0], static_cast<size_t>(size) );
eosio_assert(static_cast<size_t>(size) == cfd.size(), "get_context_free_data failed" );
uint32_t v = eosio::unpack<uint32_t>( &cfd[0], cfd.size() );
eosio_assert( v == cfa.payload, "invalid value" );
// 以下是測(cè)試一些功能
// verify crypto api access
checksum256 hash;
char test[] = "test";
...
// verify context_free_system_api
eosio_assert( true, "verify eosio_assert can be called" );
// 下面是測(cè)試一些在上下文無(wú)關(guān)action中不能使用的功能
} else if ( cfa.payload == 200 ) {
// attempt to access non context free api, privileged_api
is_privileged(act.name);
eosio_assert( false, "privileged_api should not be allowed" );
} else if ( cfa.payload == 201 ) {
// attempt to access non context free api, producer_api
get_active_producers( nullptr, 0 );
eosio_assert( false, "producer_api should not be allowed" );
...
} else if ( cfa.payload == 211 ) {
send_deferred( N(testapi), N(testapi), "hello", 6 );
eosio_assert( false, "transaction_api should not be allowed" );
}
}
接下來(lái)我們來(lái)看一看packed_transaction,通過(guò)這個(gè)類我們可以將trx打包,這樣可以最大的節(jié)省空間,關(guān)于它的功能,會(huì)在下面使用的提到。
2. Transaction的接收和轉(zhuǎn)發(fā)流程
了解Transaction類定義之后,我們先來(lái)看一下trx在EOS系統(tǒng)中的接收和轉(zhuǎn)發(fā)流程,確定發(fā)起trx的入口, 在EOS中,大部分trx都是由用戶所操縱的客戶端發(fā)向同步節(jié)點(diǎn),再通過(guò)同步網(wǎng)絡(luò)發(fā)送給超級(jí)節(jié)點(diǎn),超級(jí)節(jié)點(diǎn)會(huì)把trx打包進(jìn)塊,這里我們梳理一下這里的邏輯,
首先,關(guān)于客戶端提交trx的流程,可以參見(jiàn) https://eosforce.github.io/Documentation/#/zh-cn/eosforce_client_develop_guild , 我們這里從node的角度看是怎么處理收到的trx的。
對(duì)于一個(gè)節(jié)點(diǎn),trx可能是其他節(jié)點(diǎn)同步過(guò)來(lái)的,也可能是客戶端通過(guò)api請(qǐng)求的,我們先看看api:
EOS中通過(guò)http_plugin插件響應(yīng)http請(qǐng)求,這里我們只看處理邏輯,在chain_api_plugin.cpp中注冊(cè)的這兩個(gè):
voidchain_api_plugin::plugin_startup() { ilog("starting chain_api_plugin"); my.reset(newchain_api_plugin_impl(app().get_plugin().chain()));autoro_api = app().get_plugin().get_read_only_api();autorw_api = app().get_plugin().get_read_write_api(); app().get_plugin().add_api({ ... 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) });}
最終實(shí)際調(diào)用的是這里:
// 調(diào)用流程 push_transactions -> push_recurse -> push_transactionvoidread_write::push_transaction(constread_write::push_transaction_params& params, next_function next) {try{autopretty_input =std::make_shared();autoresolver = make_resolver(this, abi_serializer_max_time);try{// 這里在使用 packed_transaction 解包abi_serializer::from_variant(params, *pretty_input, resolver, abi_serializer_max_time); } EOS_RETHROW_EXCEPTIONS(chain::packed_transaction_type_exception,"Invalid packed transaction")// 這里調(diào)用 incoming::methods::transaction_async 函數(shù)app().get_method()(pretty_input,true, [this, next](constfc::static_variant& result) ->void{ ...// 返回返回值, 略去}); }catch( boost::interprocess::bad_alloc& ) { raise(SIGUSR1); } CATCH_AND_CALL(next);}
注意這里的 persist_until_expired 參數(shù),我們?cè)?EOS源碼備忘-Block Produce機(jī)制 這篇文檔中分析過(guò)。 incoming::methods::transaction_async注冊(cè)的是on_incoming_transaction_async函數(shù):
my->_incoming_transaction_async_provider = app().get_method().register_provider([this](constpacked_transaction_ptr& trx,boolpersist_until_expired, next_function next) ->void{returnmy->on_incoming_transaction_async(trx, persist_until_expired, next ); });
on_incoming_transaction_async如下:
voidon_incoming_transaction_async(constpacked_transaction_ptr& trx,boolpersist_until_expired, next_function next){ chain::controller& chain = app().get_plugin().chain();if(!chain.pending_block_state()) { _pending_incoming_transactions.emplace_back(trx, persist_until_expired, next);return; }autoblock_time = chain.pending_block_state()->header.timestamp.to_time_point();// 返回結(jié)果的回調(diào)autosend_response = [this, &trx, &next](constfc::static_variant& response) { next(response);if(response.contains()) { _transaction_ack_channel.publish(std::pair(response.get(), trx)); }else{ _transaction_ack_channel.publish(std::pair(nullptr, trx)); } };autoid = trx->id();// 超時(shí)時(shí)間檢查if( fc::time_point(trx->expiration()) < block_time ) { send_response(std::static_pointer_cast(std::make_shared(FC_LOG_MESSAGE(error,"expired transaction ${id}", ("id", id)) )));return; }// 檢查是否是已處理過(guò)的trxif( chain.is_known_unexpired_transaction(id) ) { send_response(std::static_pointer_cast(std::make_shared(FC_LOG_MESSAGE(error,"duplicate transaction ${id}", ("id", id)) )));return; }// 看看是否超過(guò)最大的執(zhí)行時(shí)間了autodeadline = fc::time_point::now() fc::milliseconds(_max_transaction_time_ms);booldeadline_is_subjective =false;if(_max_transaction_time_ms <0|| (_pending_block_mode == pending_block_mode::producing && block_time < deadline) ) { deadline_is_subjective =true; deadline = block_time; }try{// 這里直接調(diào)用`push_transaction`來(lái)執(zhí)行trxautotrace = chain.push_transaction(std::make_shared(*trx), deadline);if(trace->except) {if(failure_is_subjective(*trace->except, deadline_is_subjective)) { _pending_incoming_transactions.emplace_back(trx, persist_until_expired, next); }else{autoe_ptr = trace->except->dynamic_copy_exception(); send_response(e_ptr); } }else{if(persist_until_expired) {// if this trx didnt fail/soft-fail and the persist flag is set, store its ID so that we can// ensure its applied to all future speculative blocks as well._persistent_transactions.insert(transaction_id_with_expiry{trx->id(), trx->expiration()}); } send_response(trace); } }catch(constguard_exception& e ) { app().get_plugin().handle_guard_exception(e); }catch( boost::interprocess::bad_alloc& ) { raise(SIGUSR1); } CATCH_AND_CALL(send_response); }
注意上面的is_known_unexpired_transaction,代碼如下:
boolcontroller::is_known_unexpired_transaction(consttransaction_id_type& id)const{returndb().find(id);}
與之對(duì)應(yīng)的是這個(gè)函數(shù):
voidtransaction_context::record_transaction(consttransaction_id_type& id, fc::time_point_sec expire ) {try{ control.db().create([&](transaction_object& transaction) { transaction.trx_id = id; transaction.expiration = expire; }); }catch(constboost::interprocess::bad_alloc& ) {throw; }catch( ... ) { EOS_ASSERT(false, tx_duplicate,"duplicate transaction ${id}", ("id", id ) ); } }/// record_transaction
在push_transaction中會(huì)調(diào)用到,記錄trx已經(jīng)被處理過(guò)了。
下面我們來(lái)看看send_response這個(gè)回調(diào):
autosend_response = [this, &trx, &next](constfc::static_variant& response) { next(response);if(response.contains()) { _transaction_ack_channel.publish(std::pair(response.get(), trx)); }else{ _transaction_ack_channel.publish(std::pair(nullptr, trx)); } };
在執(zhí)行之后會(huì)調(diào)用send_response,這里是將結(jié)果發(fā)送到_transaction_ack_channel中,對(duì)于_transaction_ack_channel, 這個(gè)實(shí)際對(duì)應(yīng)的是下面這個(gè)類型:
namespacecompat {namespacechannels {usingtransaction_ack = channel_decl>; } }
在EOS中在net_plugin注冊(cè)響應(yīng)這個(gè)channel的函數(shù):
my->incoming_transaction_ack_subscription =
app().get_channel<channels::transaction_ack>().subscribe(
boost::bind(&net_plugin_impl::transaction_ack, my.get(), _1));
處理的函數(shù)如下:
voidnet_plugin_impl::transaction_ack(conststd::pair& results) { transaction_id_type id = results.second->id();if(results.first) { fc_ilog(logger,"signaled NACK, trx-id = ${id} : ${why}",("id", id)("why", results.first->to_detail_string())); dispatcher->rejected_transaction(id); }else{ fc_ilog(logger,"signaled ACK, trx-id = ${id}",("id", id)); dispatcher->bcast_transaction(*results.second); } }
這里會(huì)將運(yùn)行正常的廣播給其他節(jié)點(diǎn),這其中會(huì)發(fā)送給超級(jí)節(jié)點(diǎn)打包入塊,打包過(guò)程可以參見(jiàn) https://eosforce.github.io/Documentation/#/zh-cn/code/block_produce 。
3. push_transaction代碼分析
這里我們來(lái)分析下push_transaction的過(guò)程,作為執(zhí)行trx的入口,這個(gè)函數(shù)在EOS中非常重要,另一方面,這里EOS與Eosforce有一定區(qū)別,這里會(huì)具體介紹。
TODO 需要一個(gè)流程圖,不過(guò)博客還不支持
3.1 transaction_metadata
我們先來(lái)看下push_transaction的transaction_metadata參數(shù), 這個(gè)參數(shù)統(tǒng)一了各種不同類型,不同行為的trx:
/**
* This data structure should store context-free cached data about a transaction such as
* packed/unpacked/compressed and recovered keys
*/
class transaction_metadata {
public:
transaction_id_type id; // trx ID
transaction_id_type signed_id; // signed trx ID
signed_transaction trx;
packed_transaction packed_trx;
optional<pair<chain_id_type, flat_set<public_key_type>>> signing_keys;
bool accepted = false; // 標(biāo)注是否調(diào)用了accepted信號(hào),確保只調(diào)用一次
bool implicit = false; // 是否忽略檢查
bool scheduled = false; // 是否是延遲trx
explicit transaction_metadata( const signed_transaction& t, packed_transaction::compression_type c = packed_transaction::none )
:trx(t),packed_trx(t, c) {
id = trx.id();
//raw_packed = fc::raw::pack( static_cast<const transaction&>(trx) );
signed_id = digest_type::hash(packed_trx);
}
explicit transaction_metadata( const packed_transaction& ptrx )
:trx( ptrx.get_signed_transaction() ), packed_trx(ptrx) {
id = trx.id();
//raw_packed = fc::raw::pack( static_cast<const transaction&>(trx) );
signed_id = digest_type::hash(packed_trx);
}
const flat_set<public_key_type>& recover_keys( const chain_id_type& chain_id );
uint32_t total_actions()const { return trx.context_free_actions.size() trx.actions.size(); }
};
using transaction_metadata_ptr = std::shared_ptr<transaction_metadata>;
先看一下implicit,這個(gè)參數(shù)指示下面的邏輯是否要忽略對(duì)于trx的各種檢查,一般用于系統(tǒng)內(nèi)部的trx, 對(duì)于EOS,主要是處理on_block_transaction(可以參見(jiàn)出塊文檔),在start_block調(diào)用:
...autoonbtrx =std::make_shared( get_on_block_transaction() ); onbtrx->implicit =true;// on_block trx 會(huì)被無(wú)條件接受autoreset_in_trx_requiring_checks = fc::make_scoped_exit([old_value=in_trx_requiring_checks,this](){ in_trx_requiring_checks = old_value; }); in_trx_requiring_checks =true;// 修改in_trx_requiring_checks變量達(dá)到不將trx寫入?yún)^(qū)塊,一些系統(tǒng)的trx沒(méi)有必要寫入?yún)^(qū)塊。push_transaction( onbtrx, fc::time_point::maximum(), self.get_global_properties().configuration.min_transaction_cpu_usage,true);...
!> Eosforce不同之處
而對(duì)于EOSForce中,除了on_block action之外,onfee合約也是被設(shè)置為implicit==true的,onfee合約是eosforce的系統(tǒng)合約,設(shè)計(jì)用來(lái)收取交易的手續(xù)費(fèi)。
3.2 push_transaction函數(shù)
下面我們逐行分析下代碼,EOS中push_transaction代碼如下:
/**
* This is the entry point for new transactions to the block state. It will check authorization and
* determine whether to execute it now or to delay it. Lastly it inserts a transaction receipt into
* the pending block.
*/transaction_trace_ptrpush_transaction(consttransaction_metadata_ptr& trx, fc::time_point deadline,uint32_tbilled_cpu_time_us,boolexplicit_billed_cpu_time =false){// deadline必須不為空// deadline是trx執(zhí)行時(shí)間的一個(gè)大上限,為了防止某些trx運(yùn)行時(shí)間過(guò)長(zhǎng)導(dǎo)致出塊失敗等問(wèn)題,// 這里必須有一個(gè)嚴(yán)格的上限,一旦超過(guò)上限,交易會(huì)立即失敗。EOS_ASSERT(deadline != fc::time_point(), transaction_exception,"deadline cannot be uninitialized"); transaction_trace_ptr trace;// trace主要用來(lái)保存執(zhí)行中的一些錯(cuò)誤信息。try{// trx_context是執(zhí)行trx的上下文狀態(tài),下面會(huì)專門說(shuō)明transaction_contexttrx_context(self, trx->trx, trx->id);if((bool)subjective_cpu_leeway && pending->_block_status == controller::block_status::incomplete) { trx_context.leeway = *subjective_cpu_leeway; }// 設(shè)置數(shù)據(jù)trx_context.deadline = deadline; trx_context.explicit_billed_cpu_time = explicit_billed_cpu_time; trx_context.billed_cpu_time_us = billed_cpu_time_us; trace = trx_context.trace;try{if( trx->implicit ) {// 如果是implicit的就沒(méi)有必要做下面的一些檢查和記錄,這里的檢查主要是資源方面的trx_context.init_for_implicit_trx(); trx_context.can_subjectively_fail =false; }else{// 如果是重放并且不是重放過(guò)程中接到的新交易,則不去使用`record_transaction`記錄boolskip_recording = replay_head_time && (time_point(trx->trx.expiration) <= *replay_head_time);// 一些trx_context的初始化操作trx_context.init_for_input_trx( trx->packed_trx.get_unprunable_size(), trx->packed_trx.get_prunable_size(), trx->trx.signatures.size(), skip_recording); }if( trx_context.can_subjectively_fail && pending->_block_status == controller::block_status::incomplete ) { check_actor_list( trx_context.bill_to_accounts );// Assumes bill_to_accounts is the set of actors authorizing the transaction} trx_context.delay = fc::seconds(trx->trx.delay_sec);if( !self.skip_auth_check() && !trx->implicit ) {// 檢測(cè)交易所需要的權(quán)限authorization.check_authorization( trx->trx.actions, trx->recover_keys( chain_id ), {}, trx_context.delay, [](){}/*std::bind(&transaction_context::add_cpu_usage_and_check_time, &trx_context,
std::placeholders::_1)*/,false); }// 執(zhí)行,注意這時(shí)trx_context包括所有信息和狀態(tài)trx_context.exec(); trx_context.finalize();// Automatically rounds up network and CPU usage in trace and bills payers if successfulautorestore = make_block_restore_point();if(!trx->implicit) {// 如果是非implicit的交易,則需要進(jìn)入?yún)^(qū)塊。transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0)) ? transaction_receipt::executed : transaction_receipt::delayed; trace->receipt = push_receipt(trx->packed_trx, s, trx_context.billed_cpu_time_us, trace->net_usage); pending->_pending_block_state->trxs.emplace_back(trx); }else{// 注意,這里implicit類的交易是不會(huì)進(jìn)入?yún)^(qū)塊的,只會(huì)計(jì)入資源消耗// 因?yàn)檫@類的trx無(wú)條件運(yùn)行,所以不需要另行記錄。transaction_receipt_header r; r.status = transaction_receipt::executed; r.cpu_usage_us = trx_context.billed_cpu_time_us; r.net_usage_words = trace->net_usage /8; trace->receipt = r; }// 這里會(huì)將執(zhí)行過(guò)的action寫入待出塊狀態(tài)的_actions之中fc::move_append(pending->_actions, move(trx_context.executed));// call the accept signal but only once for this transaction// 為這個(gè)交易調(diào)用accept信號(hào),保證只調(diào)用一次if(!trx->accepted) { trx->accepted =true; emit( self.accepted_transaction, trx); }// 觸發(fā)applied_transaction信號(hào)emit(self.applied_transaction, trace);if( read_mode != db_read_mode::SPECULATIVE && pending->_block_status == controller::block_status::incomplete ) {//this may happen automatically in destructor, but I prefere make it more explicittrx_context.undo(); }else{ restore.cancel(); trx_context.squash(); }// implicit的trx壓根沒(méi)有在unapplied_transactions中if(!trx->implicit) { unapplied_transactions.erase( trx->signed_id ); }returntrace; }catch(constfc::exception& e) { trace->except = e; trace->except_ptr =std::current_exception(); }// 注意這里,如果成功的話上面就返回了這里是失敗的情況// failure_is_subjective 表明if(!failure_is_subjective(*trace->except)) { unapplied_transactions.erase( trx->signed_id ); } emit( self.accepted_transaction, trx ); emit( self.applied_transaction, trace );returntrace; } FC_CAPTURE_AND_RETHROW((trace)) }/// push_transaction
上面注釋中闡述了大致的流程,下面仔細(xì)分析一下:
首先是trx_context,這個(gè)對(duì)象的類聲明如下:
classtransaction_context{...// 省略voiddispatch_action( action_trace& trace,constaction& a, account_name receiver,boolcontext_free =false,uint32_trecurse_depth =0);inlinevoiddispatch_action( action_trace& trace,constaction& a,boolcontext_free =false){ dispatch_action(trace, a, a.account, context_free); };voidschedule_transaction();voidrecord_transaction(consttransaction_id_type& id, fc::time_point_sec expire );voidvalidate_cpu_usage_to_bill(int64_tu,boolcheck_minimum =true)const;public: controller& control;// controller類的引用constsigned_transaction& trx;// 要執(zhí)行的trxtransaction_id_type id; optional undo_session; transaction_trace_ptr trace;// 記錄錯(cuò)誤的tracefc::time_point start;// 起始時(shí)刻fc::time_point published;// publish的時(shí)刻vector executed;// 執(zhí)行完成的actionflat_set bill_to_accounts; flat_set validate_ram_usage;/// the maximum number of virtual CPU instructions of the transaction that can be safely billed to the billable accountsuint64_tinitial_max_billable_cpu =0; fc::microseconds delay;boolis_input =false;boolapply_context_free =true;boolcan_subjectively_fail =true; fc::time_point deadline = fc::time_point::maximum(); fc::microseconds leeway = fc::microseconds(3000);int64_tbilled_cpu_time_us =0;boolexplicit_billed_cpu_time =false;private:boolis_initialized =false;uint64_tnet_limit =0;boolnet_limit_due_to_block =true;boolnet_limit_due_to_greylist =false;uint64_teager_net_limit =0;uint64_t& net_usage;/// reference to trace->net_usageboolcpu_limit_due_to_greylist =false; fc::microseconds initial_objective_duration_limit; fc::microseconds objective_duration_limit; fc::time_point _deadline = fc::time_point::maximum();int64_tdeadline_exception_code = block_cpu_usage_exceeded::code_value;int64_tbilling_timer_exception_code = block_cpu_usage_exceeded::code_value; fc::time_point pseudo_start; fc::microseconds billed_time; fc::microseconds billing_timer_duration_limit; };
我們先看一下init_for_input_trx:
voidtransaction_context::init_for_input_trx(uint64_tpacked_trx_unprunable_size,// 這個(gè)是指trx打包后完整的大小uint64_tpacked_trx_prunable_size,// 這個(gè)指trx額外信息的大小uint32_tnum_signatures,// 這個(gè)參數(shù)沒(méi)用上boolskip_recording )// 是否要跳過(guò)記錄{// 根據(jù)cfg和trx初始化資源constauto& cfg = control.get_global_properties().configuration;// 利用packed_trx_unprunable_size和packed_trx_prunable_size 計(jì)算net資源消耗uint64_tdiscounted_size_for_pruned_data = packed_trx_prunable_size;if( cfg.context_free_discount_net_usage_den >0&& cfg.context_free_discount_net_usage_num < cfg.context_free_discount_net_usage_den ) { discounted_size_for_pruned_data *= cfg.context_free_discount_net_usage_num; discounted_size_for_pruned_data = ( discounted_size_for_pruned_data cfg.context_free_discount_net_usage_den -1) / cfg.context_free_discount_net_usage_den;// rounds up}uint64_tinitial_net_usage =static_cast(cfg.base_per_transaction_net_usage) packed_trx_unprunable_size discounted_size_for_pruned_data;// 對(duì)于delay trx需要額外的net資源if( trx.delay_sec.value >0) {// If delayed, also charge ahead of time for the additional net usage needed to retire the delayed transaction// whether that be by successfully executing, soft failure, hard failure, or expiration.initial_net_usage =static_cast(cfg.base_per_transaction_net_usage) static_cast(config::transaction_id_net_usage); }// 初始化一些信息published = control.pending_block_time(); is_input =true;if(!control.skip_trx_checks()) { control.validate_expiration(trx); control.validate_tapos(trx); control.validate_referenced_accounts(trx); } init( initial_net_usage);// 這里調(diào)用init函數(shù), 在這個(gè)函數(shù)中會(huì)處理cpu資源和ram資源if(!skip_recording)// 將trx添加入記錄中record_transaction( id, trx.expiration );/// checks for dupes}
這里會(huì)先計(jì)算net,再在init函數(shù)中處理其他資源:
voidtransaction_context::init(uint64_tinitial_net_usage) { EOS_ASSERT( !is_initialized, transaction_exception,"cannot initialize twice");conststaticint64_tlarge_number_no_overflow =std::numeric_limits::max()/2;constauto& cfg = control.get_global_properties().configuration;auto& rl = control.get_mutable_resource_limits_manager(); net_limit = rl.get_block_net_limit(); objective_duration_limit = fc::microseconds( rl.get_block_cpu_limit() ); _deadline = start objective_duration_limit;// Possibly lower net_limit to the maximum net usage a transaction is allowed to be billedif( cfg.max_transaction_net_usage <= net_limit ) { net_limit = cfg.max_transaction_net_usage; net_limit_due_to_block =false; }// Possibly lower objective_duration_limit to the maximum cpu usage a transaction is allowed to be billedif( cfg.max_transaction_cpu_usage <= objective_duration_limit.count() ) { objective_duration_limit = fc::microseconds(cfg.max_transaction_cpu_usage); billing_timer_exception_code = tx_cpu_usage_exceeded::code_value; _deadline = start objective_duration_limit; }// Possibly lower net_limit to optional limit set in the transaction headeruint64_ttrx_specified_net_usage_limit =static_cast(trx.max_net_usage_words.value) *8;if( trx_specified_net_usage_limit >0&& trx_specified_net_usage_limit <= net_limit ) { net_limit = trx_specified_net_usage_limit; net_limit_due_to_block =false; }// Possibly lower objective_duration_limit to optional limit set in transaction headerif( trx.max_cpu_usage_ms >0) {autotrx_specified_cpu_usage_limit = fc::milliseconds(trx.max_cpu_usage_ms);if( trx_specified_cpu_usage_limit <= objective_duration_limit ) { objective_duration_limit = trx_specified_cpu_usage_limit; billing_timer_exception_code = tx_cpu_usage_exceeded::code_value; _deadline = start objective_duration_limit; } } initial_objective_duration_limit = objective_duration_limit;if( billed_cpu_time_us >0)// could also call on explicit_billed_cpu_time but it would be redundantvalidate_cpu_usage_to_bill( billed_cpu_time_us,false);// Fail early if the amount to be billed is too high// Record accounts to be billed for network and CPU usagefor(constauto& act : trx.actions ) {for(constauto& auth : act.authorization ) { bill_to_accounts.insert( auth.actor ); } } validate_ram_usage.reserve( bill_to_accounts.size() );// Update usage values of accounts to reflect new timerl.update_account_usage( bill_to_accounts, block_timestamp_type(control.pending_block_time()).slot );// Calculate the highest network usage and CPU time that all of the billed accounts can afford to be billedint64_taccount_net_limit =0;int64_taccount_cpu_limit =0;boolgreylisted_net =false, greylisted_cpu =false;std::tie( account_net_limit, account_cpu_limit, greylisted_net, greylisted_cpu) = max_bandwidth_billed_accounts_can_pay(); net_limit_due_to_greylist |= greylisted_net; cpu_limit_due_to_greylist |= greylisted_cpu; eager_net_limit = net_limit;// Possible lower eager_net_limit to what the billed accounts can pay plus some (objective) leewayautonew_eager_net_limit =std::min( eager_net_limit,static_cast(account_net_limit cfg.net_usage_leeway) );if( new_eager_net_limit < eager_net_limit ) { eager_net_limit = new_eager_net_limit; net_limit_due_to_block =false; }// Possibly limit deadline if the duration accounts can be billed for ( a subjective leeway) does not exceed current deltaif( (fc::microseconds(account_cpu_limit) leeway) <= (_deadline - start) ) { _deadline = start fc::microseconds(account_cpu_limit) leeway; billing_timer_exception_code = leeway_deadline_exception::code_value; } billing_timer_duration_limit = _deadline - start;// Check if deadline is limited by caller-set deadline (only change deadline if billed_cpu_time_us is not set)if( explicit_billed_cpu_time || deadline < _deadline ) { _deadline = deadline; deadline_exception_code = deadline_exception::code_value; }else{ deadline_exception_code = billing_timer_exception_code; } eager_net_limit = (eager_net_limit/8)*8;// Round down to nearest multiple of word size (8 bytes) so check_net_usage can be efficientif( initial_net_usage >0) add_net_usage( initial_net_usage );// Fail early if current net usage is already greater than the calculated limitchecktime();// Fail early if deadline has already been exceededis_initialized =true; }
以上就是transaction_context初始化過(guò)程,這里主要是處理資源消耗。
下面是exec函數(shù),這個(gè)函數(shù)很簡(jiǎn)單:
voidtransaction_context::exec() { EOS_ASSERT( is_initialized, transaction_exception,"must first initialize");// 調(diào)用`dispatch_action`,這里并沒(méi)有對(duì)上下文無(wú)關(guān)trx進(jìn)行特別的操作,只是參數(shù)不同if( apply_context_free ) {for(constauto& act : trx.context_free_actions ) { trace->action_traces.emplace_back(); dispatch_action( trace->action_traces.back(), act,true); } }if( delay == fc::microseconds() ) {for(constauto& act : trx.actions ) { trace->action_traces.emplace_back(); dispatch_action( trace->action_traces.back(), act ); } }else{// 對(duì)于延遲交易,這里特別處理schedule_transaction(); } }
主要執(zhí)行在dispatch_action中,這里會(huì)根據(jù)action不同分別觸發(fā)對(duì)應(yīng)的調(diào)用:
voidtransaction_context::dispatch_action( action_trace& trace,constaction& a, account_name receiver,boolcontext_free,uint32_trecurse_depth ) {// 構(gòu)建apply_context執(zhí)行action, apply_context的分析在下節(jié)進(jìn)行apply_contextacontext( control, *this, a, recurse_depth ); acontext.context_free = context_free; acontext.receiver = receiver;try{ acontext.exec(); }catch( ... ) { trace = move(acontext.trace);throw; }// 匯總結(jié)果到tracetrace = move(acontext.trace); }
對(duì)于延遲交易,執(zhí)行schedule_transaction:
voidtransaction_context::schedule_transaction() {// 因?yàn)榻灰籽舆t執(zhí)行,會(huì)消耗額外的net和ram資源// Charge ahead of time for the additional net usage needed to retire the delayed transaction// whether that be by successfully executing, soft failure, hard failure, or expiration.if( trx.delay_sec.value ==0) {// Do not double bill. Only charge if we have not already charged for the delay.constauto& cfg = control.get_global_properties().configuration; add_net_usage(static_cast(cfg.base_per_transaction_net_usage) static_cast(config::transaction_id_net_usage) );// Will exit early if net usage cannot be payed.}autofirst_auth = trx.first_authorizor();// 將延遲交易寫入節(jié)點(diǎn)運(yùn)行時(shí)狀態(tài)數(shù)據(jù)庫(kù)中,到時(shí)會(huì)從這里查找出來(lái)執(zhí)行uint32_ttrx_size =0;constauto& cgto = control.db().create( [&](auto& gto ) { gto.trx_id = id; gto.payer = first_auth; gto.sender = account_name();/// delayed transactions have no sendergto.sender_id = transaction_id_to_sender_id( gto.trx_id ); gto.published = control.pending_block_time(); gto.delay_until = gto.published delay; gto.expiration = gto.delay_until fc::seconds(control.get_global_properties().configuration.deferred_trx_expiration_window); trx_size = gto.set( trx ); });// 因?yàn)橐獙憙?nèi)存記錄,所以也消耗了一定的ramadd_ram_usage( cgto.payer, (config::billable_size_v trx_size) ); }
調(diào)用完exec之后會(huì)調(diào)用transaction_context::finalize():
// 這里主要是處理資源消耗voidtransaction_context::finalize() { EOS_ASSERT( is_initialized, transaction_exception,"must first initialize");if( is_input ) {auto& am = control.get_mutable_authorization_manager();for(constauto& act : trx.actions ) {for(constauto& auth : act.authorization ) { am.update_permission_usage( am.get_permission(auth) ); } } }auto& rl = control.get_mutable_resource_limits_manager();for(autoa : validate_ram_usage ) { rl.verify_account_ram_usage( a ); }// Calculate the new highest network usage and CPU time that all of the billed accounts can afford to be billedint64_taccount_net_limit =0;int64_taccount_cpu_limit =0;boolgreylisted_net =false, greylisted_cpu =false;std::tie( account_net_limit, account_cpu_limit, greylisted_net, greylisted_cpu) = max_bandwidth_billed_accounts_can_pay(); net_limit_due_to_greylist |= greylisted_net; cpu_limit_due_to_greylist |= greylisted_cpu;// Possibly lower net_limit to what the billed accounts can payif(static_cast(account_net_limit) <= net_limit ) {//NOTE:net_limit may possibly not be objective anymore due to net greylisting, but it should still be no greater than the truly objective net_limitnet_limit =static_cast(account_net_limit); net_limit_due_to_block =false; }// Possibly lower objective_duration_limit to what the billed accounts can payif( account_cpu_limit <= objective_duration_limit.count() ) {//NOTE:objective_duration_limit may possibly not be objective anymore due to cpu greylisting, but it should still be no greater than the truly objective objective_duration_limitobjective_duration_limit = fc::microseconds(account_cpu_limit); billing_timer_exception_code = tx_cpu_usage_exceeded::code_value; } net_usage = ((net_usage 7)/8)*8;// Round up to nearest multiple of word size (8 bytes)eager_net_limit = net_limit; check_net_usage();autonow = fc::time_point::now(); trace->elapsed = now - start; update_billed_cpu_time( now ); validate_cpu_usage_to_bill( billed_cpu_time_us ); rl.add_transaction_usage( bill_to_accounts,static_cast(billed_cpu_time_us), net_usage, block_timestamp_type(control.pending_block_time()).slot );// Should never fail}
接下來(lái)make_block_restore_point,這里添加了一個(gè)檢查點(diǎn):
// The returned scoped_exit should not exceed the lifetime of the pending which existed when make_block_restore_point was called.fc::scoped_exit> make_block_restore_point() {autoorig_block_transactions_size = pending->_pending_block_state->block->transactions.size();autoorig_state_transactions_size = pending->_pending_block_state->trxs.size();autoorig_state_actions_size = pending->_actions.size();std::function callback = [this, orig_block_transactions_size, orig_state_transactions_size, orig_state_actions_size]() { pending->_pending_block_state->block->transactions.resize(orig_block_transactions_size); pending->_pending_block_state->trxs.resize(orig_state_transactions_size); pending->_actions.resize(orig_state_actions_size); };returnfc::make_scoped_exit(std::move(callback) ); }
而后對(duì)于不是implicit的交易會(huì)調(diào)用push_receipt,這里會(huì)將trx寫入?yún)^(qū)塊數(shù)據(jù)中,這也意味著implicit為true的交易雖然執(zhí)行了,但不會(huì)在區(qū)塊中。
/**
* Adds the transaction receipt to the pending block and returns it.
*/templateconsttransaction_receipt&push_receipt(constT& trx, transaction_receipt_header::status_enum status,uint64_tcpu_usage_us,uint64_tnet_usage ){uint64_tnet_usage_words = net_usage /8; EOS_ASSERT( net_usage_words*8== net_usage, transaction_exception,"net_usage is not divisible by 8"); pending->_pending_block_state->block->transactions.emplace_back( trx ); transaction_receipt& r = pending->_pending_block_state->block->transactions.back(); r.cpu_usage_us = cpu_usage_us; r.net_usage_words = net_usage_words; r.status = status;returnr; }
上面的邏輯很大程度上和implicit為true時(shí)的邏輯重復(fù),估計(jì)以后會(huì)重構(gòu)。
接下來(lái)值得注意的是這里:
if( read_mode != db_read_mode::SPECULATIVE && pending->_block_status == controller::block_status::incomplete ) {//this may happen automatically in destructor, but I prefere make it more explicittrx_context.undo(); }else{ restore.cancel(); trx_context.squash(); }
TODO trx_context.undo
這里調(diào)用database::session對(duì)應(yīng)的函數(shù),
!> Eosforce不同之處
以上是EOS的流程,這里我們?cè)賮?lái)看看Eosforce的不同之處,Eosforce與EOS一個(gè)明顯的不同是Eosforce采用了基于手續(xù)費(fèi)的資源模型, 這種模型意味著,如果一個(gè)交易在超級(jí)節(jié)點(diǎn)打包進(jìn)塊時(shí)失敗了,此時(shí)也要收取手續(xù)費(fèi),否則會(huì)造成潛在的攻擊風(fēng)險(xiǎn),所以Eosforce中,執(zhí)行失敗的交易也會(huì)寫入?yún)^(qū)塊中,這樣每次執(zhí)行時(shí)會(huì)調(diào)用對(duì)應(yīng)onfee。 另一方面, Eosforce雖然使用手續(xù)費(fèi),但是還是區(qū)分cpu,net,ram資源,并且在大的限制上依然進(jìn)行檢查。 后續(xù)Eosforce會(huì)完成新的資源模型,這里會(huì)有所改動(dòng)。
Eosforce中的push_transaction函數(shù)如下:
transaction_trace_ptrpush_transaction(consttransaction_metadata_ptr& trx, fc::time_point deadline,uint32_tbilled_cpu_time_us,boolexplicit_billed_cpu_time =false){ EOS_ASSERT(deadline != fc::time_point(), transaction_exception,"deadline cannot be uninitialized");// eosforce暫時(shí)沒(méi)有開(kāi)放延遲交易和上下文無(wú)關(guān)交易EOS_ASSERT(trx->trx.delay_sec.value ==0UL, transaction_exception,"delay,transaction failed"); EOS_ASSERT(trx->trx.context_free_actions.size()==0, transaction_exception,"context free actions size should be zero!");// 在eosforce中,為了安全性,對(duì)于特定一些交易進(jìn)行了額外的驗(yàn)證,主要是考慮到,系統(tǒng)會(huì)將執(zhí)行錯(cuò)誤的交易寫入?yún)^(qū)塊// 此時(shí)就要先驗(yàn)證下交易內(nèi)容,特別是大小上有沒(méi)有超出限制,否則將會(huì)帶來(lái)安全問(wèn)題。check_action(trx->trx.actions); transaction_trace_ptr trace;try{// 一樣的代碼 略去...try{// 一樣的代碼 略去...// 處理手續(xù)費(fèi)EOS_ASSERT(trx->trx.fee == txfee.get_required_fee(trx->trx), transaction_exception,"set tx fee failed"); EOS_ASSERT(txfee.check_transaction(trx->trx) ==true, transaction_exception,"transaction include actor more than one");try{// 這里會(huì)執(zhí)行onfee合約,也是通過(guò)`push_transaction`實(shí)現(xiàn)的autoonftrx =std::make_shared( get_on_fee_transaction(trx->trx.fee, trx->trx.actions[0].authorization[0].actor) ); onftrx->implicit =true;autoonftrace = push_transaction( onftrx, fc::time_point::maximum(), config::default_min_transaction_cpu_usage,true);// 這里如果執(zhí)行失敗直接拋出異常,不會(huì)執(zhí)行下面的東西if( onftrace->except )throw*onftrace->except; }catch(constfc::exception &e) { EOS_ASSERT(false, transaction_exception,"on fee transaction failed, exception: ${e}", ("e", e)); }catch( ... ) { EOS_ASSERT(false, transaction_exception,"on fee transaction failed, but shouldn't enough asset to pay for transaction fee"); } }// 注意這一層try catch,因?yàn)閑os中出錯(cuò)的交易會(huì)被拋棄,所以eos的異常會(huì)被直接拋出到外層// 而在eosforce中出錯(cuò)的交易會(huì)進(jìn)入?yún)^(qū)塊// 但是要注意,這里如果這里并不是在超級(jí)節(jié)點(diǎn)出塊時(shí)調(diào)用,雖然也會(huì)執(zhí)行下面的邏輯,但是不會(huì)被轉(zhuǎn)發(fā)給超級(jí)節(jié)點(diǎn)。try{if(explicit_billed_cpu_time && billed_cpu_time_us ==0){// 在eosforce中 因?yàn)槌?jí)節(jié)點(diǎn)打包區(qū)塊時(shí)失敗的交易也會(huì)被寫入?yún)^(qū)塊中,// 而很多交易失敗的原因不是交易本身有問(wèn)題,而是在執(zhí)行交易時(shí),資源上限被觸發(fā),導(dǎo)致交易被直接判定為失敗,// 這時(shí)寫入?yún)^(qū)塊的交易的cpu消耗是0, 這里是需要失敗的,否則重跑區(qū)塊時(shí)會(huì)出現(xiàn)不同步的情況EOS_ASSERT(false, transaction_exception,"billed_cpu_time_us is 0"); } trx_context.exec(); trx_context.finalize();// Automatically rounds up network and CPU usage in trace and bills payers if successful}catch(constfc::exception &e) { trace->except = e; trace->except_ptr =std::current_exception();// eosforce加了一些日志if(head->block_num !=1) { elog("---trnasction exe failed--------trace: ${trace}", ("trace", trace)); } }autorestore = make_block_restore_point();if(!trx->implicit) {// 這里不太好的地方是,對(duì)于出錯(cuò)的交易也被標(biāo)為`executed`(嚴(yán)格說(shuō)也確實(shí)是executed),后續(xù)eosforce將會(huì)重構(gòu)這里transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0)) ? transaction_receipt::executed : transaction_receipt::delayed; trace->receipt = push_receipt(trx->packed_trx, s, trx_context.billed_cpu_time_us, trace->net_usage); pending->_pending_block_state->trxs.emplace_back(trx); }else{ transaction_receipt_header r; r.status = transaction_receipt::executed; r.cpu_usage_us = trx_context.billed_cpu_time_us; r.net_usage_words = trace->net_usage /8; trace->receipt = r; }// 以下是相同的} FC_CAPTURE_AND_RETHROW((trace)) }/// push_transaction
可以看出主要不同就是手續(xù)費(fèi)導(dǎo)致的,這里必須要注意,就是eosforce中區(qū)塊內(nèi)會(huì)包括一些出錯(cuò)的交易。
4. apply_context代碼分析
這里我們來(lái)看看action的執(zhí)行過(guò)程,上面在dispatch_action中創(chuàng)建apply_context執(zhí)行action,我們這里分析這一塊的代碼。
apply_context結(jié)構(gòu)比較大,主要是數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)內(nèi)容很多,這里我們只分析功能點(diǎn),從這方面入手看結(jié)構(gòu),先從exec開(kāi)始, 在上面push_trx最終調(diào)用的就是這個(gè)函數(shù),執(zhí)行actions:
voidapply_context::exec(){// 先添加receiver,關(guān)于_notified下面分析_notified.push_back(receiver);// 執(zhí)行exec_one,這里是實(shí)際執(zhí)行action的地方,下面單獨(dú)分析trace = exec_one();// 下面處理inline action// 注意不是從0開(kāi)始,會(huì)繞過(guò)上面添加的receiverfor(uint32_ti =1; i < _notified.size(); i ) { receiver = _notified[i];// 通知指定的賬戶 關(guān)于_notified下面分析trace.inline_traces.emplace_back( exec_one() ); }// 防止調(diào)用inline action過(guò)深if( _cfa_inline_actions.size() >0|| _inline_actions.size() >0) { EOS_ASSERT( recurse_depth < control.get_global_properties().configuration.max_inline_action_depth, transaction_exception,"inline action recursion depth reached"); }// 先執(zhí)行_cfa_inline_actionsfor(constauto& inline_action : _cfa_inline_actions ) { trace.inline_traces.emplace_back(); trx_context.dispatch_action( trace.inline_traces.back(), inline_action, inline_action.account,true, recurse_depth 1); }// 再執(zhí)行_inline_actionsfor(constauto& inline_action : _inline_actions ) { trace.inline_traces.emplace_back(); trx_context.dispatch_action( trace.inline_traces.back(), inline_action, inline_action.account,false, recurse_depth 1); }}/// exec()
這里的邏輯基本都是處理inline action,inline action允許在一個(gè)合約中觸發(fā)另外一個(gè)合約的調(diào)用,需要注意的是這里與編程語(yǔ)言中的函數(shù)調(diào)用并不相同,從上面代碼也可以看出,系統(tǒng)會(huì)先執(zhí)行合約對(duì)應(yīng)的action,再執(zhí)行合約中的聲明調(diào)用的inline action,注意recurse_depth,顯然循環(huán)調(diào)用合約次數(shù)深度過(guò)高會(huì)引起錯(cuò)誤。
為了更好的理解代碼過(guò)程我們先來(lái)仔細(xì)看下 inline action。在合約中可以這樣使用,代碼出自dice:
//@abi actionvoiddeposit(constaccount_name from,constasset& quantity ){ ... action( permission_level{ from, N(active) }, N(eosio.token), N(transfer),std::make_tuple(from, _self, quantity,std::string("")) ).send(); ... }
這里send會(huì)把a(bǔ)ction打包并調(diào)用下面的send_inline:
voidsend_inline( array_ptr data,size_tdata_len ){//TODO:Why is this limit even needed? And why is it not consistently checked on actions in input or deferred transactionsEOS_ASSERT( data_len < context.control.get_global_properties().configuration.max_inline_action_size, inline_action_too_big,"inline action too big"); action act; fc::raw::unpack(data, data_len, act); context.execute_inline(std::move(act)); }
可以看到這里調(diào)用的是內(nèi)部的execute_inline函數(shù):
/**
* This will execute an action after checking the authorization. Inline transactions are
* implicitly authorized by the current receiver (running code). This method has significant
* security considerations and several options have been considered:
*
* 1. priviledged accounts (those marked as such by block producers) can authorize any action
* 2. all other actions are only authorized by 'receiver' which means the following:
* a. the user must set permissions on their account to allow the 'receiver' to act on their behalf
*
* Discarded Implemenation: at one point we allowed any account that authorized the current transaction
* to implicitly authorize an inline transaction. This approach would allow privelege escalation and
* make it unsafe for users to interact with certain contracts. We opted instead to have applications
* ask the user for permission to take certain actions rather than making it implicit. This way users
* can better understand the security risk.
*/voidapply_context::execute_inline( action&& a ) {// 先做了一些檢查auto* code = control.db().find(a.account); EOS_ASSERT( code !=nullptr, action_validate_exception,"inline action's code account ${account} does not exist", ("account", a.account) );for(constauto& auth : a.authorization ) {auto* actor = control.db().find(auth.actor); EOS_ASSERT( actor !=nullptr, action_validate_exception,"inline action's authorizing actor ${account} does not exist", ("account", auth.actor) ); EOS_ASSERT( control.get_authorization_manager().find_permission(auth) !=nullptr, action_validate_exception,"inline action's authorizations include a non-existent permission: ${permission}", ("permission", auth) ); }// No need to check authorization if: replaying irreversible blocks; contract is privileged; or, contract is calling itself.// 上面幾種情況下不需要做權(quán)限檢查if( !control.skip_auth_check() && !privileged && a.account != receiver ) { control.get_authorization_manager() .check_authorization( {a}, {}, {{receiver, config::eosio_code_name}}, control.pending_block_time() - trx_context.published,std::bind(&transaction_context::checktime, &this->trx_context),false);//QUESTION: Is it smart to allow a deferred transaction that has been delayed for some time to get away// with sending an inline action that requires a delay even though the decision to send that inline// action was made at the moment the deferred transaction was executed with potentially no forewarning?}// 這里只是把這個(gè)act放入_inline_actions列表中,并沒(méi)有執(zhí)行。_inline_actions.emplace_back( move(a) );}
注意上面代碼中最后的_inline_actions,這里面放著執(zhí)行action時(shí)所觸發(fā)的所有action的數(shù)據(jù),回到exec中:
// 防止調(diào)用inline action過(guò)深if( _cfa_inline_actions.size() >0|| _inline_actions.size() >0) { EOS_ASSERT( recurse_depth < control.get_global_properties().configuration.max_inline_action_depth, transaction_exception,"inline action recursion depth reached"); }// 先執(zhí)行_cfa_inline_actionsfor(constauto& inline_action : _cfa_inline_actions ) { trace.inline_traces.emplace_back(); trx_context.dispatch_action( trace.inline_traces.back(), inline_action, inline_action.account,true, recurse_depth 1); }// 再執(zhí)行_inline_actionsfor(constauto& inline_action : _inline_actions ) { trace.inline_traces.emplace_back(); trx_context.dispatch_action( trace.inline_traces.back(), inline_action, inline_action.account,false, recurse_depth 1); }
這后半部分就是執(zhí)行action,注意上面我們沒(méi)有跟蹤_cfa_inline_actions的流程,這里和_inline_actions的流程是一致的,區(qū)別是在合約中由send_context_free觸發(fā)。
以上我們看了下inline action的處理,上面exec中沒(méi)有提及的是_notified,下面來(lái)看看這個(gè), 在合約中可以調(diào)用require_recipient:
// 把賬戶添加至通知賬戶列表中voidapply_context::require_recipient( account_name recipient ) {if( !has_recipient(recipient) ) { _notified.push_back(recipient); }}
在執(zhí)行完action之后,執(zhí)行inline action之前(嚴(yán)格上說(shuō)inline action 不是action的一部分,所以在這之前)會(huì)通知所有在執(zhí)行合約過(guò)程中添加入_notified的賬戶:
// 注意不是從0開(kāi)始,會(huì)繞過(guò)上面添加的receiverfor(uint32_ti =1; i < _notified.size(); i ) { receiver = _notified[i]; trace.inline_traces.emplace_back( exec_one() ); }
這里可能有疑問(wèn)的是為什么又執(zhí)行了一次exec_one,下面分析exec_one時(shí)會(huì)說(shuō)明。
以上我們分析了一下exec,這里主要是調(diào)用exec_one來(lái)執(zhí)行合約,下面就來(lái)看看exec_one:
// 執(zhí)行action,注意`receiver`action_trace apply_context::exec_one(){autostart = fc::time_point::now();constauto& cfg = control.get_global_properties().configuration;try{// 這里是receiver是作為一個(gè)合約賬戶的情況constauto& a = control.get_account( receiver ); privileged = a.privileged;// 這里檢查action是不是系統(tǒng)內(nèi)部的合約,關(guān)于這方面下面會(huì)單獨(dú)分析autonative = control.find_apply_handler( receiver, act.account, act.name );if( native ) {if( trx_context.can_subjectively_fail && control.is_producing_block()) { control.check_contract_list( receiver ); control.check_action_list( act.account, act.name ); }// 這里會(huì)執(zhí)行cpp中定義的代碼(*native)( *this); }// 如果是合約賬戶的話,這里會(huì)執(zhí)行if( a.code.size() >0// 這里對(duì) setcode 單獨(dú)處理了一下,這是因?yàn)閟etcode和其他合約都使用了code數(shù)據(jù)// 但是 setcode 是在cpp層調(diào)用的,code作為參數(shù),所以這里就不會(huì)調(diào)用code。&& !(act.account == config::system_account_name && act.name == N( setcode ) && receiver == config::system_account_name)) {if( trx_context.can_subjectively_fail && control.is_producing_block()) {// 各種黑白名單檢查control.check_contract_list( receiver );// 這里主要是account黑白名單,不再細(xì)細(xì)說(shuō)明control.check_action_list( act.account, act.name );// 這里主要是action黑名單,不再細(xì)細(xì)說(shuō)明}try{// 這里就會(huì)調(diào)用虛擬機(jī)執(zhí)行code,關(guān)于這方面,我們會(huì)單獨(dú)寫一篇分析文檔control.get_wasm_interface().apply( a.code_version, a.code, *this); }catch(constwasm_exit& ) {} } } FC_RETHROW_EXCEPTIONS(warn,"pending console output: ${console}", ("console", _pending_console_output.str()))// 這里的代碼分成了兩部分,這里其實(shí)應(yīng)該重構(gòu)一下,下面的邏輯應(yīng)該單獨(dú)提出一個(gè)函數(shù)。// 上面對(duì)于`_notified`其實(shí)就是從這里開(kāi)始// 整理action_receipt數(shù)據(jù)action_receipt r; r.receiver = receiver; r.act_digest = digest_type::hash(act); r.global_sequence = next_global_sequence(); r.recv_sequence = next_recv_sequence( receiver );constauto& account_sequence = db.get(act.account); r.code_sequence = account_sequence.code_sequence; r.abi_sequence = account_sequence.abi_sequence;for(constauto& auth : act.authorization ) { r.auth_sequence[auth.actor] = next_auth_sequence( auth.actor ); }// 這里會(huì)生成一個(gè)action_trace結(jié)構(gòu)直接用來(lái)標(biāo)志action_tracet(r); t.trx_id = trx_context.id; t.act = act; t.console = _pending_console_output.str();// 放入以執(zhí)行的列表中trx_context.executed.emplace_back( move(r) );// 日志if( control.contracts_console() ) { print_debug(receiver, t); } reset_console(); t.elapsed = fc::time_point::now() - start;returnt;}
這里先看看對(duì)于加入_notified的賬戶的處理, 正常的邏輯中,執(zhí)行的結(jié)果中會(huì)產(chǎn)生所有_notified(不包含最初的receiver)中賬戶對(duì)應(yīng)的action_trace的列表, 這些會(huì)存入inline_traces中,這里其實(shí)是把通知賬戶的過(guò)程也當(dāng)作了一種“inline action”。
這些trace信息會(huì)被其他插件利用,目前主要是history插件中的on_action_trace函數(shù),這里會(huì)將所有action的執(zhí)行信息和結(jié)果存入action_history_object供api調(diào)用,具體的過(guò)程這里不再消息描述。
以上就是整個(gè)apply_context執(zhí)行合約的過(guò)程。
5. 幾個(gè)內(nèi)置的action
在EOS中有一些action的實(shí)現(xiàn)是在cpp層的,這里單獨(dú)看下。
如果看合約中,會(huì)有這樣幾個(gè)只有定義而沒(méi)有實(shí)現(xiàn)的合約:
/*
* Method parameters commented out to prevent generation of code that parses input data.
*/classnative:publiceosio::contract {public:usingeosio::contract::contract;/**
* Called after a new account is created. This code enforces resource-limits rules
* for new accounts as well as new account naming conventions.
*
* 1. accounts cannot contain '.' symbols which forces all acccounts to be 12
* characters long without '.' until a future account auction process is implemented
* which prevents name squatting.
*
* 2. new accounts must stake a minimal number of tokens (as set in system parameters)
* therefore, this method will execute an inline buyram from receiver for newacnt in
* an amount equal to the current new account creation fee.
*/voidnewaccount( account_name creator, account_name newact/* no need to parse authorites
const authority& owner,
const authority& active*/);voidupdateauth(/*account_name account,
permission_name permission,
permission_name parent,
const authority& data*/){}voiddeleteauth(/*account_name account, permission_name permission*/){}voidlinkauth(/*account_name account,
account_name code,
action_name type,
permission_name requirement*/){}voidunlinkauth(/*account_name account,
account_name code,
action_name type*/){}voidcanceldelay(/*permission_level canceling_auth, transaction_id_type trx_id*/){}voidonerror(/*const bytes&*/){} };
這些合約是在eos項(xiàng)目的cpp中實(shí)現(xiàn)的,這里的聲明是為了適配合約名相關(guān)的api, 這里Eosforce有個(gè)問(wèn)題,就是在最初的實(shí)現(xiàn)中,將這些聲明刪去了,導(dǎo)致json_to_bin api出錯(cuò),這里后續(xù)會(huì)修正這個(gè)問(wèn)題。
對(duì)于這些合約,在上面我們指出是在exec_one中處理的,實(shí)際的注冊(cè)在這里:
voidset_apply_handler( account_name receiver, account_name contract, action_name action, apply_handler v ){ apply_handlers[receiver][make_pair(contract,action)] = v; } ...#defineSET_APP_HANDLER( receiver, contract, action) \ set_apply_handler( #receiver, #contract, #action, &BOOST_PP_CAT(apply_, BOOST_PP_CAT(contract, BOOST_PP_CAT(_,action) ) ) )SET_APP_HANDLER( eosio, eosio, newaccount ); SET_APP_HANDLER( eosio, eosio, setcode ); SET_APP_HANDLER( eosio, eosio, setabi ); SET_APP_HANDLER( eosio, eosio, updateauth ); SET_APP_HANDLER( eosio, eosio, deleteauth ); SET_APP_HANDLER( eosio, eosio, linkauth ); SET_APP_HANDLER( eosio, eosio, unlinkauth );/*
SET_APP_HANDLER( eosio, eosio, postrecovery );
SET_APP_HANDLER( eosio, eosio, passrecovery );
SET_APP_HANDLER( eosio, eosio, vetorecovery );
*/SET_APP_HANDLER( eosio, eosio, canceldelay );
!> Eosforce不同 : 在eosforce中有些合約被屏蔽了。
這里不好查找的一點(diǎn)是,在宏定義中拼接了函數(shù)名,所以實(shí)際對(duì)應(yīng)的是apply_eosio_×的函數(shù),如newaccount對(duì)應(yīng)的是apply_eosio_newaccount。
我們這里專門分析下apply_eosio_newaccount,apply_eosio_setcode和apply_eosio_setabi,后續(xù)會(huì)有文檔專門分析所有系統(tǒng)合約。
5.1 apply_eosio_newaccount
新建用戶沒(méi)有什么特別之處,這里的寫法和合約中類似:
voidapply_eosio_newaccount(apply_context& context){// 獲得數(shù)據(jù)autocreate = context.act.data_as();try{// 各種檢查context.require_authorization(create.creator);// context.require_write_lock( config::eosio_auth_scope );auto& authorization = context.control.get_mutable_authorization_manager(); EOS_ASSERT( validate(create.owner), action_validate_exception,"Invalid owner authority"); EOS_ASSERT( validate(create.active), action_validate_exception,"Invalid active authority");auto& db = context.db;autoname_str = name(create.name).to_string(); EOS_ASSERT( !create.name.empty(), action_validate_exception,"account name cannot be empty"); EOS_ASSERT( name_str.size() <=12, action_validate_exception,"account names can only be 12 chars long");// Check if the creator is privilegedconstauto&creator = db.get(create.creator);if( !creator.privileged ) {// EOS中eosio.的賬戶都是系統(tǒng)賬戶,Eosforce中沒(méi)有指定保留賬戶EOS_ASSERT( name_str.find("eosio.") !=0, action_validate_exception,"only privileged accounts can have names that start with 'eosio.'"); }// 檢查賬戶重名autoexisting_account = db.find(create.name); EOS_ASSERT(existing_account ==nullptr, account_name_exists_exception,"Cannot create account named ${name}, as that name is already taken", ("name", create.name));// 創(chuàng)建賬戶constauto& new_account = db.create([&](auto& a) { a.name = create.name; a.creation_date = context.control.pending_block_time(); }); db.create([&](auto& a) { a.name = create.name; });for(constauto& auth : { create.owner, create.active } ){ validate_authority_precondition( context, auth ); }constauto& owner_permission = authorization.create_permission( create.name, config::owner_name,0,std::move(create.owner) );constauto& active_permission = authorization.create_permission( create.name, config::active_name, owner_permission.id,std::move(create.active) );// 初始化賬戶資源context.control.get_mutable_resource_limits_manager().initialize_account(create.name);int64_tram_delta = config::overhead_per_account_ram_bytes; ram_delta =2*config::billable_size_v; ram_delta = owner_permission.auth.get_billable_size(); ram_delta = active_permission.auth.get_billable_size(); context.trx_context.add_ram_usage(create.name, ram_delta);} FC_CAPTURE_AND_RETHROW( (create) ) }
5.2 apply_eosio_setcode和apply_eosio_setabi
apply_eosio_setcode和apply_eosio_setabi用來(lái)提交合約,實(shí)現(xiàn)上也沒(méi)有特別之處, 唯一注意的是之前談過(guò),apply_eosio_setcode既是系統(tǒng)合約,又帶code,這里的code作為參數(shù)
voidapply_eosio_setcode(apply_context& context){constauto& cfg = context.control.get_global_properties().configuration;// 獲取數(shù)據(jù)auto& db = context.db;autoact = context.act.data_as();// 權(quán)限context.require_authorization(act.account); EOS_ASSERT( act.vmtype ==0, invalid_contract_vm_type,"code should be 0"); EOS_ASSERT( act.vmversion ==0, invalid_contract_vm_version,"version should be 0"); fc::sha256 code_id;/// default ID == 0if( act.code.size() >0) { code_id = fc::sha256::hash( act.code.data(), (uint32_t)act.code.size() ); wasm_interface::validate(context.control, act.code); }constauto& account = db.get(act.account);int64_tcode_size = (int64_t)act.code.size();int64_told_size = (int64_t)account.code.size() * config::setcode_ram_bytes_multiplier;int64_tnew_size = code_size * config::setcode_ram_bytes_multiplier; EOS_ASSERT( account.code_version != code_id, set_exact_code,"contract is already running this version of code"); db.modify( account, [&](auto& a ) {/**TODO:consider whether a microsecond level local timestamp is sufficient to detect code version changes*///TODO:update setcode message to include the hash, then validate it in validatea.last_code_update = context.control.pending_block_time(); a.code_version = code_id; a.code.resize( code_size );if( code_size >0)memcpy( a.code.data(), act.code.data(), code_size ); });constauto& account_sequence = db.get(act.account); db.modify( account_sequence, [&](auto& aso ) { aso.code_sequence =1; });// 更新資源消耗if(new_size != old_size) { context.trx_context.add_ram_usage( act.account, new_size - old_size ); }}voidapply_eosio_setabi(apply_context& context){auto& db = context.db;autoact = context.act.data_as(); context.require_authorization(act.account);constauto& account = db.get(act.account);int64_tabi_size = act.abi.size();int64_told_size = (int64_t)account.abi.size();int64_tnew_size = abi_size; db.modify( account, [&](auto& a ) { a.abi.resize( abi_size );if( abi_size >0)memcpy( a.abi.data(), act.abi.data(), abi_size ); });constauto& account_sequence = db.get(act.account); db.modify( account_sequence, [&](auto& aso ) { aso.abi_sequence =1; });// 更新資源消耗if(new_size != old_size) { context.trx_context.add_ram_usage( act.account, new_size - old_size ); }}
6. 需要留意的問(wèn)題
1.TMT觀察網(wǎng)遵循行業(yè)規(guī)范,任何轉(zhuǎn)載的稿件都會(huì)明確標(biāo)注作者和來(lái)源;
2.TMT觀察網(wǎng)的原創(chuàng)文章,請(qǐng)轉(zhuǎn)載時(shí)務(wù)必注明文章作者和"來(lái)源:TMT觀察網(wǎng)",不尊重原創(chuàng)的行為TMT觀察網(wǎng)或?qū)⒆肪控?zé)任;
3.作者投稿可能會(huì)經(jīng)TMT觀察網(wǎng)編輯修改或補(bǔ)充。