EOS源码备忘-Push Transaction机制
这里我们讨论EOS Push Transaction 的逻辑,这块EOS与Eosforce实现有一些区别,我们会着重点出。 关于wasm相关的内容我们会有一片专门的文档分析。
我们这里通常将Transaction译做交易,其实这里应该是事务的意思。
EOS中使用继承体系划分trx与action结构,关系图如下:
transaction_header <- transaction <- signed_transaction <- deferred_transaction
|
packed_transaction
1.1 Action
我们这里先看一下Action的声明:
// 权限结构structpermission_level{account_name actor; permission_name permission; }; …structaction{account_name account; action_name name;// 执行所需的权限vector authorization; bytes data; …// 打包成二进制templateTdata_as()const{ … } };
Action没有什么特别的内容,但要注意:
!> 在EOS中一个transaction中包含很多个action,而在Eosforce中一个trx只能包括一个action。
1.2 Transaction
下面我们分析一下transaction,这里简写为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超时时间uint16_tref_block_num =0U;// 包含trx的block num 注意这个值是后2^16个块中uint32_tref_block_prefix =0UL;// blockid的低32位fc::unsigned_int max_net_usage_words =0UL;// 网络资源上限uint8_tmax_cpu_usage_ms =0;// cpu资源上限fc::unsigned_int delay_sec =0UL;/// 延迟交易的延迟时间/**
* @return the absolute block number given the relative ref_block_num
* 计算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包含一个trx中固定长度的数据,这里之所以要单独提出来主要是为了优化。
transaction视为交易体数据,这里主要是存储这个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中一个交易中 action要么全部执行,要么都不执行
struct transaction : public transaction_header {
vector context_free_actions;
vector actions;
extensions_type transaction_extensions;
// 获取trx id
transaction_id_type id()const;
digest_type sig_digest( const chain_id_type& chain_id, const vector& cfd = vector() )const;
...
};
注意这里的context_free_actions,这里指上下文无关的Action,具体信息可以参见这里: https://medium.com/@bytemaster/eosio-development-update-272198df22c1 和 https://github.com/EOSIO/eos/issues/1387。 如果一个Action执行时只依赖与transaction的数据,而不依赖与链上的状态,这样的action可以并发的执行。
另外一个值得注意的是trx id:
transaction_id_type transaction::id()const{ digest_type::encoder enc; fc::raw::pack( enc, *this);returnenc.result();}
!> Eosforce不同
在Eosforce中为了添加手续费信息,trx与EOS结构不同,主要是增加了fee, 在transaction中:
struct transaction : public transaction_header {
vector context_free_actions;
vector actions;
extensions_type transaction_extensions;
asset fee; // EOSForce 增加的手续费,在客户端push trx时需要写入
transaction_id_type id()const;
digest_type sig_digest( const chain_id_type& chain_id, const vector& cfd = vector() )const;
flat_set get_signature_keys( const vector& signatures,
const chain_id_type& chain_id,
const vector& cfd = vector(),
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 这篇文档里也有说明。
这里计算trx id时完全使用trx的数据,这意味着,如果是两个trx数据完全一致,特别的他们在一个区块中,那么这两个trx的id就会是一样的。
1.3 signed_transaction
一个trx签名之后会得到一个signed_transaction,
structsigned_transaction:publictransaction { …vector signatures;// 签名vector context_free_data;// 上下文无关的action所使用的数据// 签名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包含签名数据和上下文无关的action所使用的数据,
这里要谈一下context_free_data,可以参见 https://github.com/EOSIO/eos/commit/a41b4d56b5cbfd0346de34b0e03819f72e834041 ,之前我们看过context_free_actions, 在上下文无关的action中可以去从context_free_data获取数据,可以参见在api_tests.cpp中的测试用例:
… {// 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的一个例子,在test_api.cpp中的合约会调用void test_action::test_cf_action()函数:
// 这个是测试context_free_action
的action
void test_action::test_cf_action() {
eosio::action act = eosio::get_action( 0, 0 );
cf_action cfa = act.data_as
if ( cfa.payload == 100 ) {
// verify read of get_context_free_data, also verifies system api access
// 测试在合约中通过 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) );
size = get_context_free_data( cfa.cfd_idx, &cfd[0], static_cast(size) );
eosio_assert(static_cast(size) == cfd.size(), "get_context_free_data failed" );
uint32_t v = eosio::unpack( &cfd[0], cfd.size() );
eosio_assert( v == cfa.payload, "invalid value" );
// 以下是测试一些功能
// verify crypto api access
checksum256 hash;
char test[] = "test";
...
// verify context_free_system_api
eosio_assert( true, "verify eosio_assert can be called" );
// 下面是测试一些在上下文无关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" );
}
}
接下来我们来看一看packed_transaction,通过这个类我们可以将trx打包,这样可以最大的节省空间,关于它的功能,会在下面使用的提到。
首先,关于客户端提交trx的流程,可以参见 https://eosforce.github.io/Documentation/#/zh-cn/eosforce_client_develop_guild , 我们这里从node的角度看是怎么处理收到的trx的。
对于一个节点,trx可能是其他节点同步过来的,也可能是客户端通过api请求的,我们先看看api:
EOS中通过http_plugin插件响应http请求,这里我们只看处理逻辑,在chain_api_plugin.cpp中注册的这两个:
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) });}
最终实际调用的是这里:
// 调用流程 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”)// 这里调用 incoming::methods::transaction_async 函数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 参数,我们在 EOS源码备忘-Block Produce机制 这篇文档中分析过。 incoming::methods::transaction_async注册的是on_incoming_transaction_async函数:
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();// 返回结果的回调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();// 超时时间检查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; }// 检查是否是已处理过的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; }// 看看是否超过最大的执行时间了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{// 这里直接调用push_transaction
来执行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);}
与之对应的是这个函数:
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中会调用到,记录trx已经被处理过了。
下面我们来看看send_response这个回调:
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)); } };
在执行之后会调用send_response,这里是将结果发送到_transaction_ack_channel中,对于_transaction_ack_channel, 这个实际对应的是下面这个类型:
namespacecompat {namespacechannels {usingtransaction_ack = channel_decl>; } }
在EOS中在net_plugin注册响应这个channel的函数:
my->incoming_transaction_ack_subscription =
app().get_channel().subscribe(
boost::bind(&net_plugin_impl::transaction_ack, my.get(), _1));
处理的函数如下:
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); } }
这里会将运行正常的广播给其他节点,这其中会发送给超级节点打包入块,打包过程可以参见 https://eosforce.github.io/Documentation/#/zh-cn/code/block_produce 。
这里我们来分析下push_transaction的过程,作为执行trx的入口,这个函数在EOS中非常重要,另一方面,这里EOS与Eosforce有一定区别,这里会具体介绍。
TODO 需要一个流程图,不过博客还不支持
3.1 transaction_metadata
我们先来看下push_transaction的transaction_metadata参数, 这个参数统一了各种不同类型,不同行为的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>> signing_keys;
bool accepted = false; // 标注是否调用了accepted信号,确保只调用一次
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(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(trx) );
signed_id = digest_type::hash(packed_trx);
}
const flat_set& 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
先看一下implicit,这个参数指示下面的逻辑是否要忽略对于trx的各种检查,一般用于系统内部的trx, 对于EOS,主要是处理on_block_transaction(可以参见出块文档),在start_block调用:
…autoonbtrx =std::make_shared( get_on_block_transaction() ); onbtrx->implicit =true;// on_block trx 会被无条件接受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变量达到不将trx写入区块,一些系统的trx没有必要写入区块。push_transaction( onbtrx, fc::time_point::maximum(), self.get_global_properties().configuration.min_transaction_cpu_usage,true);…
!> Eosforce不同之处
而对于EOSForce中,除了on_block action之外,onfee合约也是被设置为implicit==true的,onfee合约是eosforce的系统合约,设计用来收取交易的手续费。
3.2 push_transaction函数
下面我们逐行分析下代码,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执行时间的一个大上限,为了防止某些trx运行时间过长导致出块失败等问题,// 这里必须有一个严格的上限,一旦超过上限,交易会立即失败。EOS_ASSERT(deadline != fc::time_point(), transaction_exception,"deadline cannot be uninitialized"); transaction_trace_ptr trace;// trace主要用来保存执行中的一些错误信息。try{// trx_context是执行trx的上下文状态,下面会专门说明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; }// 设置数据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的就没有必要做下面的一些检查和记录,这里的检查主要是资源方面的trx_context.init_for_implicit_trx(); trx_context.can_subjectively_fail =false; }else{// 如果是重放并且不是重放过程中接到的新交易,则不去使用`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 ) {// 检测交易所需要的权限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); }// 执行,注意这时trx_context包括所有信息和状态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的交易,则需要进入区块。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类的交易是不会进入区块的,只会计入资源消耗// 因为这类的trx无条件运行,所以不需要另行记录。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; }// 这里会将执行过的action写入待出块状态的_actions之中fc::move_append(pending->_actions, move(trx_context.executed));// call the accept signal but only once for this transaction// 为这个交易调用accept信号,保证只调用一次if(!trx->accepted) { trx->accepted =true; emit( self.accepted_transaction, trx); }// 触发applied_transaction信号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压根没有在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
上面注释中阐述了大致的流程,下面仔细分析一下:
首先是trx_context,这个对象的类声明如下:
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;// 要执行的trxtransaction_id_type id; optional undo_session; transaction_trace_ptr trace;// 记录错误的tracefc::time_point start;// 起始时刻fc::time_point published;// publish的时刻vector executed;// 执行完成的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,// 这个是指trx打包后完整的大小uint64_tpacked_trx_prunable_size,// 这个指trx额外信息的大小uint32_tnum_signatures,// 这个参数没用上boolskip_recording )// 是否要跳过记录{// 根据cfg和trx初始化资源constauto& cfg = control.get_global_properties().configuration;// 利用packed_trx_unprunable_size和packed_trx_prunable_size 计算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;// 对于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);// 这里调用init函数, 在这个函数中会处理cpu资源和ram资源if(!skip_recording)// 将trx添加入记录中record_transaction( id, trx.expiration );/// checks for dupes}
这里会先计算net,再在init函数中处理其他资源:
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初始化过程,这里主要是处理资源消耗。
下面是exec函数,这个函数很简单:
voidtransaction_context::exec() { EOS_ASSERT( is_initialized, transaction_exception,“must first initialize”);// 调用dispatch_action
,这里并没有对上下文无关trx进行特别的操作,只是参数不同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{// 对于延迟交易,这里特别处理schedule_transaction(); } }
主要执行在dispatch_action中,这里会根据action不同分别触发对应的调用:
voidtransaction_context::dispatch_action( action_trace& trace,constaction& a, account_name receiver,boolcontext_free,uint32_trecurse_depth ) {// 构建apply_context执行action, apply_context的分析在下节进行apply_contextacontext( control, *this, a, recurse_depth ); acontext.context_free = context_free; acontext.receiver = receiver;try{ acontext.exec(); }catch( … ) { trace = move(acontext.trace);throw; }// 汇总结果到tracetrace = move(acontext.trace); }
对于延迟交易,执行schedule_transaction:
voidtransaction_context::schedule_transaction() {// 因为交易延迟执行,会消耗额外的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();// 将延迟交易写入节点运行时状态数据库中,到时会从这里查找出来执行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 ); });// 因为要写内存记录,所以也消耗了一定的ramadd_ram_usage( cgto.payer, (config::billable_size_v + trx_size) ); }
调用完exec之后会调用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}
接下来make_block_restore_point,这里添加了一个检查点:
// 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) ); }
而后对于不是implicit的交易会调用push_receipt,这里会将trx写入区块数据中,这也意味着implicit为true的交易虽然执行了,但不会在区块中。
/**
* 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时的逻辑重复,估计以后会重构。
接下来值得注意的是这里:
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
这里调用database::session对应的函数,
!> Eosforce不同之处
以上是EOS的流程,这里我们再来看看Eosforce的不同之处,Eosforce与EOS一个明显的不同是Eosforce采用了基于手续费的资源模型, 这种模型意味着,如果一个交易在超级节点打包进块时失败了,此时也要收取手续费,否则会造成潜在的攻击风险,所以Eosforce中,执行失败的交易也会写入区块中,这样每次执行时会调用对应onfee。 另一方面, Eosforce虽然使用手续费,但是还是区分cpu,net,ram资源,并且在大的限制上依然进行检查。 后续Eosforce会完成新的资源模型,这里会有所改动。
Eosforce中的push_transaction函数如下:
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暂时没有开放延迟交易和上下文无关交易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中,为了安全性,对于特定一些交易进行了额外的验证,主要是考虑到,系统会将执行错误的交易写入区块// 此时就要先验证下交易内容,特别是大小上有没有超出限制,否则将会带来安全问题。check_action(trx->trx.actions); transaction_trace_ptr trace;try{// 一样的代码 略去…try{// 一样的代码 略去…// 处理手续费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{// 这里会执行onfee合约,也是通过push_transaction
实现的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);// 这里如果执行失败直接抛出异常,不会执行下面的东西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,因为eos中出错的交易会被抛弃,所以eos的异常会被直接抛出到外层// 而在eosforce中出错的交易会进入区块// 但是要注意,这里如果这里并不是在超级节点出块时调用,虽然也会执行下面的逻辑,但是不会被转发给超级节点。try{if(explicit_billed_cpu_time && billed_cpu_time_us ==0){// 在eosforce中 因为超级节点打包区块时失败的交易也会被写入区块中,// 而很多交易失败的原因不是交易本身有问题,而是在执行交易时,资源上限被触发,导致交易被直接判定为失败,// 这时写入区块的交易的cpu消耗是0, 这里是需要失败的,否则重跑区块时会出现不同步的情况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) {// 这里不太好的地方是,对于出错的交易也被标为executed
(严格说也确实是executed),后续eosforce将会重构这里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
可以看出主要不同就是手续费导致的,这里必须要注意,就是eosforce中区块内会包括一些出错的交易。
这里我们来看看action的执行过程,上面在dispatch_action中创建apply_context执行action,我们这里分析这一块的代码。
apply_context结构比较大,主要是数据结构实现内容很多,这里我们只分析功能点,从这方面入手看结构,先从exec开始, 在上面push_trx最终调用的就是这个函数,执行actions:
voidapply_context::exec(){// 先添加receiver,关于_notified下面分析_notified.push_back(receiver);// 执行exec_one,这里是实际执行action的地方,下面单独分析trace = exec_one();// 下面处理inline action// 注意不是从0开始,会绕过上面添加的receiverfor(uint32_ti =1; i < _notified.size(); ++i ) { receiver = _notified[i];// 通知指定的账户 关于_notified下面分析trace.inline_traces.emplace_back( exec_one() ); }// 防止调用inline action过深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”); }// 先执行_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); }// 再执行_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允许在一个合约中触发另外一个合约的调用,需要注意的是这里与编程语言中的函数调用并不相同,从上面代码也可以看出,系统会先执行合约对应的action,再执行合约中的声明调用的inline action,注意recurse_depth,显然循环调用合约次数深度过高会引起错误。
为了更好的理解代码过程我们先来仔细看下 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会把action打包并调用下面的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)); }
可以看到这里调用的是内部的execute_inline函数:
/**
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:
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.// 上面几种情况下不需要做权限检查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?}// 这里只是把这个act放入_inline_actions列表中,并没有执行。_inline_actions.emplace_back( move(a) );}
注意上面代码中最后的_inline_actions,这里面放着执行action时所触发的所有action的数据,回到exec中:
// 防止调用inline action过深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”); }// 先执行_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); }// 再执行_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); }
这后半部分就是执行action,注意上面我们没有跟踪_cfa_inline_actions的流程,这里和_inline_actions的流程是一致的,区别是在合约中由send_context_free触发。
以上我们看了下inline action的处理,上面exec中没有提及的是_notified,下面来看看这个, 在合约中可以调用require_recipient:
// 把账户添加至通知账户列表中voidapply_context::require_recipient( account_name recipient ) {if( !has_recipient(recipient) ) { _notified.push_back(recipient); }}
在执行完action之后,执行inline action之前(严格上说inline action 不是action的一部分,所以在这之前)会通知所有在执行合约过程中添加入_notified的账户:
// 注意不是从0开始,会绕过上面添加的receiverfor(uint32_ti =1; i < _notified.size(); ++i ) { receiver = _notified[i]; trace.inline_traces.emplace_back( exec_one() ); }
这里可能有疑问的是为什么又执行了一次exec_one,下面分析exec_one时会说明。
以上我们分析了一下exec,这里主要是调用exec_one来执行合约,下面就来看看exec_one:
// 执行action,注意receiver
action_trace apply_context::exec_one(){autostart = fc::time_point::now();constauto& cfg = control.get_global_properties().configuration;try{// 这里是receiver是作为一个合约账户的情况constauto& a = control.get_account( receiver ); privileged = a.privileged;// 这里检查action是不是系统内部的合约,关于这方面下面会单独分析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 ); }// 这里会执行cpp中定义的代码(*native)( *this); }// 如果是合约账户的话,这里会执行if( a.code.size() >0// 这里对 setcode 单独处理了一下,这是因为setcode和其他合约都使用了code数据// 但是 setcode 是在cpp层调用的,code作为参数,所以这里就不会调用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黑白名单,不再细细说明control.check_action_list( act.account, act.name );// 这里主要是action黑名单,不再细细说明}try{// 这里就会调用虚拟机执行code,关于这方面,我们会单独写一篇分析文档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()))// 这里的代码分成了两部分,这里其实应该重构一下,下面的逻辑应该单独提出一个函数。// 上面对于_notified
其实就是从这里开始// 整理action_receipt数据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 ); }// 这里会生成一个action_trace结构直接用来标志action_tracet®; t.trx_id = trx_context.id; t.act = act; t.console = _pending_console_output.str();// 放入以执行的列表中trx_context.executed.emplace_back( move® );// 日志if( control.contracts_console() ) { print_debug(receiver, t); } reset_console(); t.elapsed = fc::time_point::now() - start;returnt;}
这里先看看对于加入_notified的账户的处理, 正常的逻辑中,执行的结果中会产生所有_notified(不包含最初的receiver)中账户对应的action_trace的列表, 这些会存入inline_traces中,这里其实是把通知账户的过程也当作了一种“inline action”。
这些trace信息会被其他插件利用,目前主要是history插件中的on_action_trace函数,这里会将所有action的执行信息和结果存入action_history_object供api调用,具体的过程这里不再消息描述。
以上就是整个apply_context执行合约的过程。
在EOS中有一些action的实现是在cpp层的,这里单独看下。
如果看合约中,会有这样几个只有定义而没有实现的合约:
/*
* 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项目的cpp中实现的,这里的声明是为了适配合约名相关的api, 这里Eosforce有个问题,就是在最初的实现中,将这些声明删去了,导致json_to_bin api出错,这里后续会修正这个问题。
对于这些合约,在上面我们指出是在exec_one中处理的,实际的注册在这里:
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中有些合约被屏蔽了。
这里不好查找的一点是,在宏定义中拼接了函数名,所以实际对应的是apply_eosio_×的函数,如newaccount对应的是apply_eosio_newaccount。
我们这里专门分析下apply_eosio_newaccount,apply_eosio_setcode和apply_eosio_setabi,后续会有文档专门分析所有系统合约。
5.1 apply_eosio_newaccount
新建用户没有什么特别之处,这里的写法和合约中类似:
voidapply_eosio_newaccount(apply_context& context){// 获得数据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.的账户都是系统账户,Eosforce中没有指定保留账户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));// 创建账户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用来提交合约,实现上也没有特别之处, 唯一注意的是之前谈过,apply_eosio_setcode既是系统合约,又带code,这里的code作为参数
voidapply_eosio_setcode(apply_context& context){constauto& cfg = context.control.get_global_properties().configuration;// 获取数据auto& db = context.db;autoact = context.act.data_as();// 权限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 ); }}