EOS源碼分析:transaction的一生
- 2019 年 10 月 3 日
- 筆記
最近在處理智慧合約的事務上鏈問題,發現其中仍舊有知識盲點。原有的認識是一個事務請求會從客戶端設備打包簽名,然後通過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方法的第一個參數是否包含異常。這些異常情況包括:
- 事務超時過期,通過將事務過期時間與當前最新區塊時間對比即可,若小於最新區塊時間則判定事務過期。
- 事務重複,在當前節點的db中尋找是否有相同事務id的存在,若存在則說明事務重複。
- 事務執行時出錯:
- 全節點配置為只讀模式的,不可以處理推送事務。
- 不允許忽略檢查以及延遲事務。
- 內部執行錯誤,例如許可權問題,資源問題,事務進入合約內部校驗錯誤等,詳細內容看下面對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函數。這樣,事務在區塊鏈中就走完了完整過程。
本文僅代表作者觀點,有疏漏部分歡迎討論,經討論正確的會自行更正。