EOS源码备忘-Push Transaction机制

这里我们讨论EOS Push Transaction 的逻辑,这块EOS与Eosforce实现有一些区别,我们会着重点出。 关于wasm相关的内容我们会有一片专门的文档分析。

我们这里通常将Transaction译做交易,其实这里应该是事务的意思。

1. Transaction与Action

在EOS中Transaction与Action是最重要的几个类型, 在EOS中,所有的链上行为都是Action,Transaction是一系列Action组成的事务。

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<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,这里指上下文无关的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<action>        context_free_actions;

      vector<action>        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<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 这篇文档里也有说明。

这里计算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<cf_action>();

  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_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" );

      // 以下是测试一些功能

      // 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打包,这样可以最大的节省空间,关于它的功能,会在下面使用的提到。

2. Transaction的接收和转发流程

了解Transaction类定义之后,我们先来看一下trx在EOS系统中的接收和转发流程,确定发起trx的入口, 在EOS中,大部分trx都是由用户所操纵的客户端发向同步节点,再通过同步网络发送给超级节点,超级节点会把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<channels::transaction_ack>().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 。

3. push_transaction代码分析

这里我们来分析下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<pair<chain_id_type, flat_set<public_key_type>>>  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<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,这个参数指示下面的逻辑是否要忽略对于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中区块内会包括一些出错的交易。

4. apply_context代码分析

这里我们来看看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:

*

*  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.// 上面几种情况下不需要做权限检查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(r);  t.trx_id = trx_context.id;  t.act = act;  t.console = _pending_console_output.str();// 放入以执行的列表中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;}

这里先看看对于加入_notified的账户的处理, 正常的逻辑中,执行的结果中会产生所有_notified(不包含最初的receiver)中账户对应的action_trace的列表, 这些会存入inline_traces中,这里其实是把通知账户的过程也当作了一种“inline action”。

这些trace信息会被其他插件利用,目前主要是history插件中的on_action_trace函数,这里会将所有action的执行信息和结果存入action_history_object供api调用,具体的过程这里不再消息描述。

以上就是整个apply_context执行合约的过程。

5. 几个内置的action

在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 );  }}

猜你喜欢

转载自blog.csdn.net/yhc166188/article/details/85339307