EOS源碼分析:transaction的一生


最近在處理智能合約的事務上鏈問題,發現其中仍舊有知識盲點。原有的認識是一個事務請求會從客戶端設備打包簽名,然后通過RPC傳到非出塊節點,廣播給超級節點,校驗打包到可逆區塊,共識確認最后變為不可逆區塊。在執行事務完畢以后給客戶端一個“executed”的狀態響應。基於這個認識,本文將通過最新EOS代碼詳細分析驗證。

關鍵字:EOS,區塊鏈,eosjs,transaction,簽名,節點,出塊節點,事務校驗,事務廣播

客戶端的處理:打包與簽名

客戶端設備可以通過eosjs完成本地的事務體構建。下面以調用hello智能合約為例。

注意:eosio.cdt的hello合約中hi方法的參數名為nm,而不是user,我們下面采用與cdt相一致的方式。

方便起見,可以首先使用eosjs-api提供的transact方法,它可以幫助我們直接將事務體打包簽名並推送出去。

(async () => {
    const result = await api.transact({
        actions: [{
            account: 'useraaaaaaaa', // 合約部署者,是一個EOS賬戶
            name: 'hi',              // 調用方法名,hello合約的一個方法。
            authorization: [{        // 該方法需要的權限,默認為合約部署者權限
                actor: 'useraaaaaaaa',
                permission: 'active',
            }],
            data: {                 // 方法參數
                nm: 'water'
            },
        }]
    }, {
        blocksBehind: 3,            // 頂部區塊之前的某區塊信息作為引用數據,這是TAPoS的概念。
        expireSeconds: 30,          // 過期時間設置,自動計算當前區塊時間加上過期時間,得到截止時間。
    });
})();

然后我們可以進入transact方法中查看,仿照其實現邏輯,自行編寫一個完整流程的版本。

“打包”在EOS中與“壓縮”,“序列化”,“轉hex”等是相同的,因此所有之前提到過的壓縮,轉化等概念都是指同一件事。例如compression:none屬性,之前也提到過zlib的方式;cleos中convert命令;rpc中的abi_json_to_bin等。

①打包Actions

actions的結構與前面是相同的。

// actions結構與上面相同,這是我們與鏈交互的“個性化參數”
let actions = [{ 
    account: 'useraaaaaaaa',
    name: 'hi',
    authorization: [
        {
            actor: 'useraaaaaaaa',
            permission: 'active'
        }
    ],
    data: {
        nm: 'seawater'
    }
}];
// 打包Actions
let sActions = await api.serializeActions(actions);

eosjs中通過serializeActions方法將Actions對象序列化,序列化會把data的值壓縮(可理解為密文傳輸參數以及參數的值),最終變為:

[{
	account: 'useraaaaaaaa',
	name: 'hi',
	authorization: [{
		actor: 'useraaaaaaaa',
		permission: 'active'
	}],
	data: '0000005765C38DC2'
}]

②打包Transaction

首先設置事務Transactions的屬性字段。

let expireSeconds = 3;                                          // 設置過期時間為3秒
let blocktime = new Date(block.timestamp).getTime();            // 獲得引用區塊的時間:1566263146500
let timezone = new Date(blocktime + 8*60*60*1000).getTime();    // 獲得+8時區時間:1566291946500
let expired = new Date(timezone + expireSeconds * 1000);        // 獲得過期時間:2019-08-20T09:05:49.500Z
let expiration = expired.toISOString().split('.')[0];           // 轉換一下,得到合適的值:2019-08-20T09:05:49
    expiration: expiration,                     // 根據延遲時間與引用區塊的時間計算得到的截止時間
    ref_block_num: block.block_num,             // 引用區塊號,來自於查詢到的引用區塊的屬性值
    ref_block_prefix: block.ref_block_prefix,   // 引用區塊前綴,來自於查詢到的引用區塊的屬性值
    max_net_usage_words: 0,                     // 設置該事務的最大net使用量,實際執行時評估超過這個值則自動退回,0為不設限制
    max_cpu_usage_ms: 0,                        // 設置該事務的最大cpu使用量,實際執行時評估超過這個值則自動退回,0為不設限制
    compression: 'none',                        // 事務壓縮格式,默認為none,除此之外還有zlib等。
    delay_sec: 0,                               // 設置延遲事務的延遲時間,一般不使用。
    context_free_actions: [],                   
    actions: sActions,                          // 將前面處理好的Actions對象傳入。
    transaction_extensions: [],                 // 事務擴展字段,一般為空。
};
let sTransaction = await api.serializeTransaction(transaction); // 打包事務

注釋中沒有對context_free_actions進行說明,是因為這個字段在《區塊鏈 + 大數據:EOS存儲》中有詳解。
eosjs中通過serializeTransaction方法將Transaction對象序列化,得到一個Uint8Array類型的數組,這就是事務壓縮完成的值。

Uint8Array[198, 164, 91, 93, 21, 141, 3, 236, 69, 55, 0, 0, 0, 0, 1, 96, 140, 49, 198, 24, 115, 21, 214, 0, 0, 0, 0, 0, 0, 128, 107, 1, 96, 140, 49, 198, 24, 115, 21, 214, 0, 0, 0, 0, 168, 237, 50, 50, 8, 0, 0, 0, 87, 101, 195, 141, 194, 0]

③准備密鑰

密鑰的准備分兩步:首先通過已處理完畢的事務體獲得所需密鑰requiredKeys,然后在本地密鑰庫中查看可用密鑰availableKeys,比對找到對應密鑰。

signatureProvider.getAvailableKeys().then(function (avKeys) { // 獲得本地可用密鑰
    // 查詢事務必須密鑰
    rpc.getRequiredKeys({transaction: transaction, availableKeys: avKeys}).then(function (reKeys) {
        // 匹配成功:本地可用密鑰庫中包含事務必須密鑰
        console.log(reKeys);
    });
});

由於執行結果存在先后的依賴關系,因此要采用回調嵌套的方式調用。最后成功獲得匹配的密鑰:

[ 'PUB_K1_69X3383RzBZj41k73CSjUNXM5MYGpnDxyPnWUKPEtYQmVzqTY7' ]

小插曲:關於block.timestamp 與 expiration的處理在第②步的代碼注釋中分析到了,expiration的正確取值直接影響到了rpc的getRequiredKeys方法的調用,否則會報錯:“Invalid Transaction”,這是由於事務體屬性字段出錯導致。另外時區的問題也要注意,new Date得到的是UTC時間,客戶端一般可根據自己所在時區自動調整。

④本地簽名

signatureProvider.sign({ // 本地簽名。
    chainId: chainId,
    requiredKeys: reKeys,
    serializedTransaction: sTransaction
}).then(function (signedTrx) {
    console.log(signedTrx);
});

注意,這部分代碼要代替第③步中的console.log(reKeys);,以達到回調順序依賴的效果。得到的簽名事務的結果如下:

{
	signatures: ['SIG_K1_Khut1qkaDDeL26VVT4nEqa6vzHf2wgy5uk3dwNF1Fei9GM1c8JvonZswMdc3W5pZmvNnQeEeLLgoCwqaYMtstV3h5YyesV'],
	serializedTransaction: Uint8Array[117, 185, 91, 93, 114, 182, 131, 21, 248, 224, 0, 0, 0, 0, 1, 96, 140, 49, 198, 24, 115, 21, 214, 0, 0, 0, 0, 0, 0, 128, 107, 1, 96, 140, 49, 198, 24, 115, 21, 214, 0, 0, 0, 0, 168, 237, 50, 50, 8, 0, 0, 0, 87, 101, 195, 141, 194, 0]
}

注意是由signatures和serializedTransaction兩個屬性構成的。

⑤推送事務

push_transaction方法的參數與第④步得到的結果結構是一致的,因此該對象可以直接被推送。

rpc.push_transaction(signedTrx).then(function (result) {
    console.log(result);
})

注意,這部分代碼要代替第④步中的console.log(signedTrx);,以達到回調順序依賴的效果。得到推送結果為:

{
	transaction_id: '4bc089165103879c4fcfc5331c8b03402e8206f8030c0c53374d31f5a1b35688',
	processed: {
		id: '4bc089165103879c4fcfc5331c8b03402e8206f8030c0c53374d31f5a1b35688',
		block_num: 47078,
		block_time: '2019-08-20T09:15:24.000',
		producer_block_id: null,
		receipt: {
			status: 'executed',
			cpu_usage_us: 800,
			net_usage_words: 13
		},
		elapsed: 800,
		net_usage: 104,
		scheduled: false,
		action_traces: [
			[Object]
		],
		except: null
	}
}

注意receipt響應值中包含了status: 'executed的內容,這個屬性將是下文着重提及的。

源碼位置

小結

事務的打包與簽名是在客戶端通過eosjs等工具完成的。從應用角度來看,直接使用api提供的transact是最簡單的方法,但如果要理解其中的邏輯,可以自行編寫一遍,但沒必要重新做封裝,畢竟transact已經有了。

節點的處理:校驗、執行和廣播

經過上一節,請求從客戶端發出來到達了RPC供應商。RPC服務的提供者包括出塊節點和非出塊節點,一般來講是非出塊節點。非出塊節點也會通過EOSIO/eos搭建一個nodeos服務,可以配置選擇自己同步的數據區域,不具備出塊能力。非出塊節點如果想具備釋放RPC服務的能力,需要配置chain_api_plugin,http_plugin。這部分內容可以轉到《EOS行為核心:解析插件chain_plugin》詳述。

push_transaction的返回結構體與上一節的響應數據體是一致的。

struct push_transaction_results {
  chain::transaction_id_type  transaction_id;
  fc::variant                 processed;
};

記住這兩個字段,然后向上滑動一點點,觀察具體的響應數據內容。

關於RPC的push_transaction方法的論述鏈接。繼承這篇文章的內容,下面進行補充。

transaction_async

事務的同步是通過transaction_async方法完成的,調用關系是chain_plugin插件通過method機制跳轉到producer_plugin中。
此時事務停留在非出塊節點的chain_plugin.cpp的void read_write::push_transaction方法中。除了傳入的事務體對象參數外,還有作為回調接收響應的push_transaction_results結構的實例next。進入函數體,首先針對傳入的參數對象params(具體內容參見上一節④本地簽名最后的簽名事務),轉為transaction_metadata的實例ptrx。接下來調用

app().get_method<incoming::methods::transaction_async>()

這是method模板的語法,方法后緊跟傳入等待同步的參數ptrx等以及一個result接收結果的對象(result由非出塊節點接收,這部分將在下一小節展開)。transaction_async作為method的Key值,被聲明在incoming::methods::transaction_async命名空間下。app應用實例的method集合中曾經注冊過該Key值,注冊的方式是關聯一個handle provider。這段注冊的代碼位於producer_plugin.cpp,

incoming::methods::transaction_async::method_type::handle _incoming_transaction_async_provider;

該provider內容實際上是調用了producer_plugin.cpp的on_incoming_transaction_async方法,正在同步進來的事務。接下來調用process_incoming_transaction_async方法,處理正在進入的事務同步。這個方法首先會判斷當前節點是否正在出塊,如果未出塊則進入_pending_incoming_transactions容器,這是一個雙隊列結構。

這些等待中的事務將會在出塊節點開始出塊時通過start_block方法觸發重新回到process_incoming_transaction_async方法進行打包。

transaction_ack

當接收全節點同步過來的事務的出塊節點處於當值輪次時,會將接收的事務立即向其他節點(包括非出塊節點)進行廣播,主要通過channel機制跳轉到net_plugin中。
目前事務停留在當值出塊節點的producer_plugin的process_incoming_transaction_async方法中。transaction_ack作為channel號被聲明在producer插件的compat::channels::transaction_ack命名空間下。這個channel是由net_plugin訂閱。

channels::transaction_ack::channel_type::handle  incoming_transaction_ack_subscription;

這個頻道的訂閱器是net插件確認正在進來的事務。訂閱器的實現方法綁定在net_plugin_impl::transaction_ack方法上。

my->incoming_transaction_ack_subscription = app().get_channel<channels::transaction_ack>().subscribe(boost::bind(&net_plugin_impl::transaction_ack, my.get(), _1));

進入net_plugin_impl::transaction_ack方法。

/**
 * @brief 出塊節點確認事務
 * 
 * @param results 二元組pair類型,第一個元素為異常信息,第二個元素為事務數據。
 */
void net_plugin_impl::transaction_ack(const std::pair<fc::exception_ptr, transaction_metadata_ptr>& results) {
  const auto& id = results.second->id; // 從事務體中得到事務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);
  }
}

成功確認以后,調用bcast_transaction方法繼續廣播該事務。

/**
 * @brief 事務廣播給其他節點
 * 
 * @param ptrx 事務體
 */
void dispatch_manager::bcast_transaction(const transaction_metadata_ptr& ptrx) {
   std::set<connection_ptr> skips; // 相當於連接黑名單,從連接集合中跳過廣播。
   const auto& id = ptrx->id; // 獲取事務id

   auto range = received_transactions.equal_range(id); // 已接收事務集是接收其他節點廣播的事務,而不是自己發起廣播的事務
   for (auto org = range.first; org != range.second; ++org) {
      skips.insert(org->second); // 如果找到該事務,說明該事務已被其他節點優先廣播,則自己不必額外處理。將事務連接插入skips集合。
   }
   received_transactions.erase(range.first, range.second); // 刪除已接收事務集中該事務,邏輯清空。
   // 在本地事務集local_txns中查詢,若找到則直接退出,說明該事務已完成廣播共識。
   if( my_impl->local_txns.get<by_id>().find( id ) != my_impl->local_txns.end() ) {
      fc_dlog(logger, "found trxid in local_trxs" );
      return;
   }
   // 將事務插入到本地事務集local_txns
   time_point_sec trx_expiration = ptrx->packed_trx->expiration();
   const packed_transaction& trx = *ptrx->packed_trx;

   auto buff = create_send_buffer( trx );

   node_transaction_state nts = {id, trx_expiration, 0, buff};
   my_impl->local_txns.insert(std::move(nts));
   // 符合廣播條件,開始廣播。
   my_impl->send_transaction_to_all( buff, [&id, &skips, trx_expiration](const connection_ptr& c) -> bool {
      if( skips.find(c) != skips.end() || c->syncing ) {
         return false; // 若該事務已被其他節點優先廣播,則自己不做處理。
         }
         const auto& bs = c->trx_state.find(id);
         bool unknown = bs == c->trx_state.end();
         if( unknown ) { // trx_state未找到事務,則插入。
            c->trx_state.insert(transaction_state({id,0,trx_expiration}));
            fc_dlog(logger, "sending trx to ${n}", ("n",c->peer_name() ) );
         }
         return unknown;
   });
}

繼續,進入send_transaction_to_all方法,查看廣播的具體實現。net插件維護了一個connections集合,該集合動態維護了全網節點的p2p連接情況。

/**
 * @brief 模板方法:發送事務給全體成員
 * 
 * @tparam VerifierFunc 模板類
 * @param send_buffer 事務數據
 * @param verify 模板類實例
 */
template<typename VerifierFunc>
void net_plugin_impl::send_transaction_to_all(const std::shared_ptr<std::vector<char>>& send_buffer, VerifierFunc verify) {
   for( auto &c : connections) {
      if( c->current() && verify( c )) { // 在上面的使用中,就是檢查是否在skips集合中。
         // 進入連接隊列,建立連接,發送消息。
         c->enqueue_buffer( send_buffer, true, priority::low, no_reason ); // enqueue_buffer->queue_write->do_queue_write->boost::asio::async_write
      }
   }
}

最終的建立socket連接並發送數據的過程在注釋中已體現:enqueue_buffer -> queue_write -> do_queue_write -> boost::asio::async_write,不再深入源碼詳細討論。

process_incoming_transaction_async

void net_plugin_impl::transaction_ack方法中的參數二元組對象results是由process_incoming_transaction_async方法體中對transaction_ack頻道發布的數據。上一小節詳細分析了transaction_ack頻道的訂閱處理,這一小節回到process_incoming_transaction_async方法分析transaction_ack頻道的信息發布。該方法體內部首先定義了一個send_response方法。

auto send_response = [this, &trx, &chain, &next](const fc::static_variant<fc::exception_ptr, transaction_trace_ptr>& response) {
   next(response); // 通過next方法將response傳回客戶端。
   if (response.contains<fc::exception_ptr>()) { // 響應內容中有異常情況出現,則發布數據中的第一個元素為異常對象,作為transaction_ack在net插件中的result.first數據。
      _transaction_ack_channel.publish(priority::low, std::pair<fc::exception_ptr, transaction_metadata_ptr>(response.get<fc::exception_ptr>(), trx));
      if (_pending_block_mode == pending_block_mode::producing) { // 如果當前節點正在出塊,則打印日志區塊拒絕該事務。
         fc_dlog(_trx_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is REJECTING tx: ${txid} : ${why} ",
               ("block_num", chain.head_block_num() + 1)
               ("prod", chain.pending_block_producer())
               ("txid", trx->id)
               ("why",response.get<fc::exception_ptr>()->what())); // why的值為拒絕該事務的原因,即打印出異常對象的可讀信息。
      } else { // 如果當前節點尚未出塊,則打印未出塊節點的推測執行:拒絕該事務。
         fc_dlog(_trx_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid} : ${why} ",
                  ("txid", trx->id)
                  ("why",response.get<fc::exception_ptr>()->what())); // 同樣打印異常
      }
   } else { // 如果響應內容中無異常,說明成功執行,則第一個元素為空。
      _transaction_ack_channel.publish(priority::low, std::pair<fc::exception_ptr, transaction_metadata_ptr>(nullptr, trx));
      if (_pending_block_mode == pending_block_mode::producing) { // 如果當前節點正在出塊,則打印日志區塊接收該事務。
         fc_dlog(_trx_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is ACCEPTING tx: ${txid}",
                  ("block_num", chain.head_block_num() + 1)
                  ("prod", chain.pending_block_producer())
                  ("txid", trx->id));
      } else { // 如果當前節點尚未出塊,則打印未出塊節點的推測執行:接收該事務。
         fc_dlog(_trx_trace_log, "[TRX_TRACE] Speculative execution is ACCEPTING tx: ${txid}",
                  ("txid", trx->id));
      }
   }
};

從send_response方法的定義可以看出,第二個參數永遠是事務體本身,這是不變的。而第一個參數是否包含異常信息是不確定的,取決於調用者的傳入情況。所以接下來實際上是對事務狀態的判斷,從而影響傳給send_response方法的第一個參數是否包含異常。這些異常情況包括:

  1. 事務超時過期,通過將事務過期時間與當前最新區塊時間對比即可,若小於最新區塊時間則判定事務過期。
  2. 事務重復,在當前節點的db中尋找是否有相同事務id的存在,若存在則說明事務重復。
  3. 事務執行時出錯:
    1. 全節點配置為只讀模式的,不可以處理推送事務。
    2. 不允許忽略檢查以及延遲事務。
    3. 內部執行錯誤,例如權限問題,資源問題,事務進入合約內部校驗錯誤等,詳細內容看下面對controller::push_transaction方法的分析。

controller::push_transaction

/**
 * @brief 這是新事務進入區塊狀態的進入點。將會檢查權限,是否立即執行或延遲執行。
 *        最后,將事務返回體插入到等待中的區塊。
 * 
 * @param trx 事務體
 * @param deadline 截止時間
 * @param billed_cpu_time_us CPU抵押時間
 * @param explicit_billed_cpu_time CPU抵押時間是否明確,一般是false,未顯式指定
 * 
 * @return transaction_trace_ptr 事務跟蹤,返回的結構體對象
 */
transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx,
                                          fc::time_point deadline,
                                          uint32_t billed_cpu_time_us,
                                          bool explicit_billed_cpu_time = false )
{
   EOS_ASSERT(deadline != fc::time_point(), transaction_exception, "deadline cannot be uninitialized"); // 截止時間的格式出現問題

   transaction_trace_ptr trace; // 定義事務跟蹤實例。
   try {
      auto start = fc::time_point::now();
      const bool check_auth = !self.skip_auth_check() && !trx->implicit; // implicit事務會忽略檢查也可以自己設置跳過auth檢查,則check_auth 為false。
      // 得到要使用的cpu的時間值。
      const fc::microseconds sig_cpu_usage = check_auth ? std::get<0>( trx->recover_keys( chain_id ) ) : fc::microseconds();
      // 得到權限的公鑰
      const flat_set<public_key_type>& recovered_keys = check_auth ? std::get<1>( trx->recover_keys( chain_id ) ) : flat_set<public_key_type>();
      if( !explicit_billed_cpu_time ) { // 未顯式指定CPU抵押時間。
         // 計算已消費CPU時間
         fc::microseconds already_consumed_time( EOS_PERCENT(sig_cpu_usage.count(), conf.sig_cpu_bill_pct) );
         if( start.time_since_epoch() <  already_consumed_time ) {
            start = fc::time_point();
         } else {
            start -= already_consumed_time;
         }
      }

      const signed_transaction& trn = trx->packed_trx->get_signed_transaction();
      transaction_context trx_context(self, trn, trx->id, 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;
      trace = trx_context.trace;
      try {
         if( trx->implicit ) { // 忽略檢查的事務的處理辦法
            trx_context.init_for_implicit_trx(); // 檢查事務資源(CPU和NET)可用性。
            trx_context.enforce_whiteblacklist = false;
         } else {
            bool skip_recording = replay_head_time && (time_point(trn.expiration) <= *replay_head_time);
            // 檢查事務資源(CPU和NET)可用性。
            trx_context.init_for_input_trx( trx->packed_trx->get_unprunable_size(),
                                             trx->packed_trx->get_prunable_size(),
                                             skip_recording);
         }
         trx_context.delay = fc::seconds(trn.delay_sec);
         if( check_auth ) {
            authorization.check_authorization( // 權限校驗
                     trn.actions,
                     recovered_keys,
                     {},
                     trx_context.delay,
                     [&trx_context](){ trx_context.checktime(); },
                     false
            );
         }
         trx_context.exec(); // 執行事務上下文,合約方法內部的校驗錯誤會在這里拋出,使事務行為在當前節點的鏈上生效。
         trx_context.finalize(); // 資源處理,四舍五入,自動扣除並更新賬戶的資源情況。

         auto restore = make_block_restore_point();

         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);
            pending->_block_stage.get<building_block>()._pending_trx_metas.emplace_back(trx);
         } else { // 以上代碼段都包含在try異常監控的作用域中,因此如果到此仍未發生異常而中斷,則判斷執行成功。
            transaction_receipt_header r;
            r.status = transaction_receipt::executed; // 注意:這就是客戶端接收到的那個非常重要的狀態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(pending->_block_stage.get<building_block>()._actions, move(trx_context.executed));

         if (!trx->accepted) {
            trx->accepted = true;
            emit( self.accepted_transaction, trx); // 發射接收事務的信號
         }

         emit(self.applied_transaction, std::tie(trace, trn));


         if ( read_mode != db_read_mode::SPECULATIVE && pending->_block_status == controller::block_status::incomplete ) {
            trx_context.undo(); // 析構器,undo撤銷操作。
         } else {
            restore.cancel();
            trx_context.squash(); // 上下文刷新
         }

         if (!trx->implicit) {
            unapplied_transactions.erase( trx->signed_id );
         }
         return trace;
      } catch( const disallowed_transaction_extensions_bad_block_exception& ) {
         throw;
      } catch( const protocol_feature_bad_block_exception& ) {
         throw;
      } catch (const fc::exception& e) {
         trace->error_code = controller::convert_exception_to_error_code( e );
         trace->except = e;
         trace->except_ptr = std::current_exception();
      }

      if (!failure_is_subjective(*trace->except)) {
         unapplied_transactions.erase( trx->signed_id );
      }

      emit( self.accepted_transaction, trx ); // 發射接收事務的信號,觸發controller相關信號操作
      emit( self.applied_transaction, std::tie(trace, trn) ); // 發射應用事務的信號,觸發controller相關信號操作

      return trace;
   } FC_CAPTURE_AND_RETHROW((trace))
} /// push_transaction

信號方面的內容請轉到controller的信號

小結

我們知道,非出塊節點和出塊節點使用的是同一套代碼部署的nodeos程序,然而非出塊節點可以配置是否要只讀模式,還是推測模式。所謂只讀模式,是不做數據上傳的,只能查詢,不能新增,它的數據結構只保留不可逆區塊的內容,十分簡單。而推測模式是可以處理並推送事務的,它的數據結構除了不可逆區塊的內容以外,還有可逆區塊的內容。所以非出塊節點是具備事務校驗、本地執行以及廣播的能力的,只是不具備區塊打包的能力,到了區塊層面的問題要到出塊節點來解決。事務的廣播和確認並不需要共識的存在,共識的發生是針對區塊的,而區塊打包是由出塊節點來負責,因此區塊共識只在出塊節點之間完成。而事務的廣播和確認只是單純的接收事務,散發事務而已,可以在所有節點中完成。

出塊節點的處理:打包區塊、共識、不可逆

本節請參考文章EOS生產區塊:解析插件producer_plugin

前面介紹了事務的產生、執行、散發的過程,而事務被打包進區塊的過程沒有說明,可以參照start_block函數。這樣,事務在區塊鏈中就走完了完整過程。

本文僅代表作者觀點,有疏漏部分歡迎討論,經討論正確的會自行更正。

更多文章請轉到一面千人的博客園


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM