什么是交易 transaction
在前面我们讲什么是EOSIO的时候,说过EOSIO是一个依靠职能合约来执行任务的企业操作系统,EOSIO提供的是一个最基础的运行框架,合约负责的是真正的业务逻辑,外部业务,如DApp需要依靠自己的合约来完成自己的业务,那DApp是怎么和自己的职能合约进行交互呢?
答案是依靠EOSIO的交易消息transaction,所以简单来说transaction就是外部系统用来和在EOSIO中运行的职能合约进行交互的消息,是一种特殊的消息格式
Transaction的格式
Transaction相关的核心结构与变量如下
// 基类信息,记录了过期时间,引用块号,延迟执行等信息
struct transaction_header {
time_point_sec expiration; ///< the time at which a transaction expires
uint16_t ref_block_num = 0U; ///< specifies a block num in the last 2^16 blocks.
uint32_t ref_block_prefix = 0UL; ///< specifies the lower 32 bits of the blockid at get_ref_blocknum
fc::unsigned_int max_net_usage_words = 0UL; /// upper limit on total network bandwidth (in 8 byte words) billed for this transaction
uint8_t max_cpu_usage_ms = 0; /// upper limit on the total CPU time billed for this transaction
fc::unsigned_int delay_sec = 0UL; /// number of seconds to delay this transaction for during which it may be canceled.
};
// 继承 transaction_header,记录交互的真正数据,actions
struct transaction : public transaction_header {
vector<action> context_free_actions; // 不进行资源消耗的action
vector<action> actions; // 真正的交易执行数据
extensions_type transaction_extensions;
};
// 签过名的交易,投递到EOSIO中的交易必须要进行签名
struct signed_transaction : public transaction
{
vector<signature_type> signatures;
vector<bytes> context_free_data; ///< for each context-free action, there is an entry here
};
// 对签名交易的序列化打包,存储,传输都是使用的该结构
struct packed_transaction : fc::reflect_init {
vector<signature_type> signatures;
fc::enum_type<uint8_t, compression_type> compression;
bytes packed_context_free_data;
bytes packed_trx;
};
// action基类
struct action_base {
account_name account; //执行的账户名,也就是合约账户
action_name name; //执行的合约动作名称
vector<permission_level> authorization; //权限
};
struct action : public action_base {
bytes data; //动作参数,利用ABI打包出来的二进制参数
};
Transaction的投递过程
在前面讲解中,知道在EOSIO中,外部访问都是通过http接口来访问的,所以transaction的投递也不例外,具体的代码流程如下
// Http调用处理
// 这三个函数都可以投递交易,具体的调用可以参考在线文档
auto& _http_plugin = app().get_plugin<http_plugin>();
ro_api.set_shorten_abi_errors( !_http_plugin.verbose_errors() );
_http_plugin.add_api({
CHAIN_RW_CALL_ASYNC(push_transaction, chain_apis::read_write::push_transaction_results, 202, http_params_types::params_required),
CHAIN_RW_CALL_ASYNC(push_transactions, chain_apis::read_write::push_transactions_results, 202, http_params_types::params_required),
CHAIN_RW_CALL_ASYNC(send_transaction, chain_apis::read_write::send_transaction_results, 202, http_params_types::params_required)
});
// 以push_transaction为例,最终业务逻辑在chain_plugin插件的push_transaction函数中
void read_write::push_transaction(const read_write::push_transaction_params& params, next_function<read_write::push_transaction_results> next) {
try {
// 创建abi解析器,并将解析出来的交易保存进入变量 input_trx_v0
// 注意:在EOSIO后期版本中,解析交易的过程ABI完全没有用到,这里这样写,完全是为了兼容以前的版本
packed_transaction_v0 input_trx_v0;
auto resolver = make_resolver(this, abi_serializer::create_yield_function( abi_serializer_max_time ));
packed_transaction_ptr input_trx;
try {
abi_serializer::from_variant(params, input_trx_v0, std::move( resolver ), abi_serializer::create_yield_function( abi_serializer_max_time ));
input_trx = std::make_shared<packed_transaction>( std::move( input_trx_v0 ), true );
} EOS_RETHROW_EXCEPTIONS(chain::packed_transaction_type_exception, "Invalid packed transaction")
// 如果交易解析成功,则将交易投递给接下来的处理函数,业务逻辑在producer_plugin中
// 业务逻辑异步处理完成之后,将结果回调到result中,并且将最终的结果发送到客户端
app().get_method<incoming::methods::transaction_async>()(input_trx, true,
[this, token=trx_trace.get_token(), input_trx, next]
(const std::variant<fc::exception_ptr, transaction_trace_ptr>& result) -> void {
// 进行结果回调处理
} CATCH_AND_CALL(next);
}
});
} catch ( boost::interprocess::bad_alloc& ) {
chain_plugin::handle_db_exhaustion(); //异常处理
} catch ( const std::bad_alloc& ) {
chain_plugin::handle_bad_alloc(); //异常处理
} CATCH_AND_CALL(next); //异常处理
}
// 继续producer_plugin中的transaction投递
void on_incoming_transaction_async(const packed_transaction_ptr& trx, bool persist_until_expired, next_function<transaction_trace_ptr> next) {
chain::controller& chain = chain_plug->chain();
const auto max_trx_time_ms = _max_transaction_time_ms.load();
fc::microseconds max_trx_cpu_usage = max_trx_time_ms < 0 ? fc::microseconds::maximum() : fc::milliseconds( max_trx_time_ms );
// 利用线程池,进行签名解析,最核心的是要解出签名时用到的公钥key
auto future = transaction_metadata::start_recover_keys( trx, _thread_pool->get_executor(),
chain.get_chain_id(), fc::microseconds( max_trx_cpu_usage ), chain.configured_subjective_signature_length_limit() );
boost::asio::post(_thread_pool->get_executor(), [self = this, future{
std::move(future)}, persist_until_expired,
next{
std::move(next)}, trx]() mutable {
if( future.valid() ) {
future.wait();
// 签名解析完成时候,接下来将交易交给合约线程进行执行,注意这里合约执行实在单线程中执行的,为什么是单线程,后面我们再详细说明
// 合约执行最终的处理实在 process_incoming_transaction_async 函数中
app().post( priority::low, [self, future{
std::move(future)}, persist_until_expired, next{
std::move( next )}, trx{
std::move(trx)}]() mutable {
auto exception_handler = [&next, trx{
std::move(trx)}](fc::exception_ptr ex) {
fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid}, auth: ${a} : ${why} ",
("txid", trx->id())("a",trx->get_transaction().first_authorizer())("why",ex->what()));
next(ex);
};
try {
auto result = future.get();
if( !self->process_incoming_transaction_async( result, persist_until_expired, next ) ) {
if( self->_pending_block_mode == pending_block_mode::producing ) {
self->schedule_maybe_produce_block( true );
} else {
self->restart_speculative_block();
}
}
} CATCH_AND_CALL(exception_handler);
} );
}
});
}
在上面的流程中,是交易从api接口到执行的流程,依次经过
- 交易反序列化
- 投递给解签线程进行签名解析
- 解签完成之后最终投递给执行线程
- 执行完成之后返回结果给客户端
- 如果节点正在生产中,交易是需要暂存执行的
Transaction的执行过程
注意篇幅相关,我们只分析解释核心代码段的核心部分
bool process_incoming_transaction_async(const transaction_metadata_ptr& trx, bool persist_until_expired, next_function<transaction_trace_ptr> next) {
bool exhausted = false;
chain::controller& chain = chain_plug->chain();
try {
const auto& id = trx->id();
// 判断交易是否过期,重复
// 注意:如果当前系统正处于生产块的过程中,则投递过来的交易必须要暂存起来,等生产完成之后再执行,这个过程牵扯到块的生产与同步,后面我们再讲
if( !chain.is_building_block()) {
_unapplied_transactions.add_incoming( trx, persist_until_expired, next );
return true;
}
// 调用chain.push_transaction函数进行交易执行
auto trace = chain.push_transaction( trx, deadline, trx->billed_cpu_time_us, false, sub_bill );
if( trace->except ) {
// 如果交易执行失败了
// 注意:这里失败的判断,是要区分他为社么失败的,如果是执行超时空,并且是由于块生产引起的,是要重新运行的,这个也和块的同步有关
if( exception_is_exhausted( *trace->except, deadline_is_subjective )) {
_unapplied_transactions.add_incoming( trx, persist_until_expired, next );
if( _pending_block_mode == pending_block_mode::producing ) {
} else {
}
exhausted = block_is_exhausted();
} else {
_subjective_billing.subjective_bill_failure( first_auth, trace->elapsed, fc::time_point::now() );
auto e_ptr = trace->except->dynamic_copy_exception();
send_response( e_ptr );
}
} else {
//交易执行成功
//注意:这里如果交易是API投递过来的要暂存,这个和后面块同步有关,特别注意参数persist_until_expired
if( persist_until_expired && !_disable_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.
// No need to subjective bill since it will be re-applied
_unapplied_transactions.add_persisted( trx );
} else {
// if db_read_mode SPECULATIVE then trx is in the pending block and not immediately reverted
_subjective_billing.subjective_bill( trx->id(), expire, first_auth, trace->elapsed,
chain.get_read_mode() == chain::db_read_mode::SPECULATIVE );
}
// 消息广播,结果返回
send_response( trace );
}
} catch ( ... ) {
// 异常处理
} CATCH_AND_CALL(send_response);
return !exhausted;
}
transaction_trace_ptr push_transaction( ... )
{
transaction_trace_ptr trace;
try {
auto start = fc::time_point::now();
// 构建交易执行的上下文,并且设置相关的参数
transaction_checktime_timer trx_timer(timer);
transaction_context trx_context(self, *trx->packed_trx(), std::move(trx_timer), start);
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;
trx_context.subjective_cpu_bill_us = subjective_cpu_bill_us;
trace = trx_context.trace;
auto handle_exception =[&](const auto& e)
{
trace->error_code = controller::convert_exception_to_error_code( e );
trace->except = e;
trace->except_ptr = std::current_exception();
trace->elapsed = fc::time_point::now() - trx_context.start;
};
try {
const transaction& trn = trx->packed_trx()->get_transaction();
// 如果当前此笔交易需要鉴权,则要进行权限相关的鉴定操作
// 注意:一般来说外部投递过来的交易都是需要鉴权的,但是如果在重播行为中,交易是不需要重播的
if( check_auth ) {
authorization.check_authorization(
trn.actions,
trx->recovered_keys(),
{
},
trx_context.delay,
[&trx_context](){
trx_context.checktime(); },
false
);
}
// 调用exec函数,执行交易
trx_context.exec();
trx_context.finalize(); // Automatically rounds up network and CPU usage in trace and bills payers if successful
// 注意这里有个隐式交易的概念
// 外部投递过来的交易都是显示的
// onblock交易,还有合约中投递过来的交易等都是隐式的,隐式交易是不需要参与同步的,它一般只存在于内存中,onblock和块的同步,生产相关后面再讲
if (!trx->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);
trx->billed_cpu_time_us = trx_context.billed_cpu_time_us;
std::get<building_block>(pending->_block_stage)._pending_trx_metas.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::move_append( std::get<building_block>(pending->_block_stage)._action_receipt_digests,
std::move(trx_context.executed_action_receipt_digests) );
// 对于执行成功的交易
// 如果当前节点不是生产节点,是不能执行,需要回滚的
// 如果执行成功了,特别需要注意它执行了trx_context.squash(),这个函数类似git的提交,和交易回滚密切相关,最终执行chainbase的squash()
// 如果执行失败了,这里并没有调用trx_context.undo(),实际上它在chainbase中利用析构调用了,当然再次调用trx_context.undo()也是可以的,具体的流程可以去细读chainbase的代码
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 explicit
trx_context.undo();
} else {
restore.cancel();
trx_context.squash();
}
return trace;
} catch( ... ) {
// 进行异常处理
}
return trace;
} FC_CAPTURE_AND_RETHROW((trace))
} /// push_transaction
// 合约继续在exec中执行
// 从处理过程可以看出交易是按照action一个个执行,所以action是执行的最小单位
void transaction_context::exec() {
const transaction& trx = packed_trx.get_transaction();
if( apply_context_free ) {
// 派发context_free_actions
for( const auto& act : trx.context_free_actions ) {
schedule_action( act, act.account, true, 0, 0 );
}
}
if( delay == fc::microseconds() ) {
//如果交易不延迟执行,则立即派阀
for( const auto& act : trx.actions ) {
schedule_action( act, act.account, false, 0, 0 );
}
}
// 执行
auto& action_traces = trace->action_traces;
uint32_t num_original_actions_to_execute = action_traces.size();
for( uint32_t i = 1; i <= num_original_actions_to_execute; ++i ) {
execute_action( i, 0 );
}
// 如果需要延迟执行,暂存交易
if( delay != fc::microseconds() ) {
schedule_transaction();
}
}
// 按照单个action的顺序依次执行,最终会调用到如下函数
void apply_context::exec_one()
{
auto start = fc::time_point::now();
try {
try {
receiver_account = &db.get<account_metadata_object,by_name>( receiver );
if( !(context_free && control.skip_trx_checks()) ) {
privileged = receiver_account->is_privileged();
auto native = control.find_apply_handler( receiver, act->account, act->name );
// 如果是内部系统合约,则执行native操作
if( native ) {
if( trx_context.enforce_whiteblacklist && control.is_producing_block() ) {
control.check_contract_list( receiver );
control.check_action_list( act->account, act->name );
}
(*native)( *this );
}
// 如果是普通合约,则需要在合约wasm虚拟机中执行
if( ( receiver_account->code_hash != digest_type() ) &&
( !( act->account == config::system_account_name
&& act->name == "setcode"_n
&& receiver == config::system_account_name )
|| control.is_builtin_activated( builtin_protocol_feature_t::forward_setcode )
)
) {
// 判断黑白名单,是否可以执行
if( trx_context.enforce_whiteblacklist && control.is_producing_block() ) {
control.check_contract_list( receiver );
control.check_action_list( act->account, act->name );
}
// 调用虚拟机执行
try {
control.get_wasm_interface().apply( receiver_account->code_hash, receiver_account->vm_type, receiver_account->vm_version, *this );
} catch( const wasm_exit& ) {
}
}
}
} FC_RETHROW_EXCEPTIONS( warn, "pending console output: ${console}", ("console", _pending_console_output) )
if( control.is_builtin_activated( builtin_protocol_feature_t::action_return_value ) ) {
act_digest = generate_action_digest(
[this](const char* data, uint32_t datalen) {
return trx_context.hash_with_checktime<digest_type>(data, datalen);
},
*act,
action_return_value
);
} else {
act_digest = digest_type::hash(*act);
}
} catch ( const std::bad_alloc& ) {
... //异常处理
}
}
上面的代码,分析了在系统接收到具体的交易之后,接下来怎么执行,总结来说,经历过以下大的步骤
- 构建上下文执行环境
- 交易鉴权,分析该账户有没有权限来执行这笔交易
- 交易又分为隐式交易和非隐式交易
- 合约又分为系统合约和非系统合约
- 执行的过程如果发生了异常,数据库会发生回滚
- 最终具体的操作是按照action单元在wasm虚拟机中执行的
总结
交易的执行的流程非常复杂,大的过程分为虚拟机内执行和虚拟机之外执行,此文主要分析了在虚拟机之外执行,在虚拟机之外执行涉及了加密与解密,序列化与反序列化,块的生产与同步,chainbase的回滚与确认等相关结束点,同时看完之后我们又有了以下疑问
- 合约为什么在单线程中执行?能不能在多线程执行
- 交易是如何进行鉴权的,eosio的账号系统是个什么样子
- 节点在生产中,暂存起来的交易接下来会做如何处理
- chainbase是如何进行数据回滚的,是按照交易回滚的,还是按照什么原则进行回滚的
- 合约为什么会出现系统合约与非系统合约,这个系统合约是个什么回事
- onblock的具体流程是什么,块是如何进行生产的
- ABI的作用是什么?Action的参数是如何传递给虚拟机的
- 如果我们要给进行交易gas收费,如何改动代码
随着代码的深入,我们的疑问也就愈多,多读代码, 我们来一起学习提高,加油,努力