最近在處理智能合約的事務上鏈問題,發現其中仍舊有知識盲點。原有的認識是一個事務請求會從客戶端設備打包簽名,然后通過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函數。這樣,事務在區塊鏈中就走完了完整過程。
本文僅代表作者觀點,有疏漏部分歡迎討論,經討論正確的會自行更正。