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方法的第一個參數是否包含異常。這些異常情況包括:

  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函數。這樣,事務在區塊鏈中就走完了完整過程。

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

更多文章請轉到一面千人的部落格園