聽說你都買了 EOS,卻連代碼長什麼樣都不知道?

區塊鏈兄弟社區,區塊鏈技術專業問答先行者,中國區塊鏈技術愛好者聚集地

聽說你都買了 EOS,卻連代碼長什麼樣都不知道?

來源:簡書原文標題:深入解讀EOS源代碼之——區塊鏈內核

鏈接:https://www.jianshu.com/p/f42c1dd39ece

本文約10000字+,閱讀(觀看)需要1小時


最近發現很多人投資了 EOS,卻並不關心 EOS 目前的開發進度和技術細節,如果你投資了 EOS, 還有一定的技術基礎,那就更應該關心 EOS 的開發情況了,下面我們就從 EOS 的源代碼說起:EOS區塊鏈內核

為了理解EOS區塊鏈的基本原理,首先需要了解這它的數據結構和關鍵算法,數據結構和算法是構成整個骨架的基本元素,對於深入理解EOS至關重要。下文從一些基本概念開始,逐漸深入底層,一步步探究其中邏輯與思路。

基本概念

  • name

    name在EOS裡可以理解為標識符,這與一般意義的英文單詞name不同,name的長度是13個字節。源代碼中所有形式為xxx_name的名稱都可以理解為此類型,一般的,xxx表示其功能或者目的,_name表明這是一個標識符。

  • sha256

    sha256是一個長度為32字節的數據結構,通常用16進製表示,可以理解由32個表示16進制的字符隨機組成的字符串,這樣的數據結構通常用來表示校驗碼或者標識符(ID)。ID與name的區別在於name是人類可讀的,而ID只是一串隨機的字符序列。

  • scope

    scope實際是一個上層設計的概念,並不屬於底層實現的範疇,只是相關scope的說明並不多,在這裡略作解釋。scop可以理解為權限,也就是規定當前這個操作所要求的授權列表,scope可以包含零個,一個,或者多個授權要求。

  • merkle

    merkle是指默克爾樹,這是一種利用Hash算法總是輸出固定長度的數這一特性而設計的樹型數據結構,它的特點是消耗空間小,利用Hash的特點可以用很小的存儲空間來驗證大量數據的簽名,而不需要把數據全部保存下來,就可以保證數據的完整性和一致性。有關默克爾樹的詳細解釋,請參考維基百科(https://en.wikipedia.org/wiki/Merkle_tree)

  • digest

    digest中文含義是摘要,屬於密碼學範疇。對於EOS中的區塊來說,摘要就是對整個區塊數據做Hash運算得到的結果,由於Hash的結果長度固定,而且單向不可逆,完全符合摘要的要求。下面是源代碼中對digest函數的定義:

 digest_type block_header::digest()const { return digest_type::hash(*this); }
  • block_id vs block_num

    很多人把區塊id和區塊序號混為一談,其實它們是不一樣的概念,區塊id是一個與區塊內容有關的標識,而區塊序號僅僅是從0計數的區塊編號。鮮為人知的是,這兩個概念之間在底層實現上是有關係的,請看下面的代碼:

 block_id_type signed_block_header::id()const { block_id_type result = fc::sha256::hash(*this); result._hash[0] &= 0xffffffff00000000; result._hash[0] += fc::endian_reverse_u32(block_num()); // store the block num in the ID, 160 bits is plenty for the hash return result; }

不難看出id實際是由兩部分組成:對區塊內容做Hash運算,然後與區塊序號進行拼接而得到的。所以id實際包含了區塊序號,通過id可以計算得出序號。

  • action vs message

    值得指出的是,目前在EOS中message和action的概念是一樣的,實際上action是自3.0以後的新規範,message的概念已經可以拋棄,由於3.0版本發佈到目前為止只有幾天的年齡,互聯網上的大部分文字和材料依然使用message的概念,為了緩解讀者的困擾,在此特做說明,下文均採用action代替message。

區塊

區塊是EOS和其它所有區塊鏈技術的核心數據結構,要掌握任何一個區塊鏈技術,首先需要理解的就是區塊的設計。

區塊的目的

在EOS的源代碼中,設計者給出了區塊這個結構的主要目的:

 /** * The block_summary defines the set of transactions that were successfully applied as they * are organized into cycles and shards. A shard contains the set of transactions IDs which * are either user generated transactions or code-generated transactions. * * * The primary purpose of a block is to define the order in which messages are processed. * * The secodnary purpose of a block is certify that the messages are valid according to * a group of 3rd party validators (producers). * * The next purpose of a block is to enable light-weight proofs that a transaction occured * and was considered valid. * * The next purpose is to enable code to generate messages that are certified by the * producers to be authorized. * * A block is therefore defined by the ordered set of executed and generated transactions, * and the merkle proof is over set of messages delivered as a result of executing the * transactions. * * A message is defined by { receiver, code, function, permission, data }, the merkle * tree of a block should be generated over a set of message IDs rather than a set of * transaction ids. */ struct signed_block_summary : public signed_block_header { ... };

翻譯成中文就是:區塊的

  1. 主要目的是定義處理消息的順序。

  2. 次要目的是通過第三方生產者驗證消息的有效性。

  3. 第三個目的是提供輕量級的交易驗證機制。

  4. 第四個目的是授權給智能合約代碼,以產生新的消息。

區塊的結構

因此區塊是由按順序組織的交易來構成的。區塊像是一個容器,它自頂向下依次包含以下這些結構:

Block: 區塊 => regions: 區域 => cycles: 週期 => shards: 片區 => transactions: 交易 => actions: 操作

然而從區塊的直接設計來看,它的定義就是按順序構成的交易集合:

/** * This structure contains the set of signed transactions referenced by the * block summary. This inherits from block_summary/signed_block_header and is * what would be logged to disk to enable the regeneration of blockchain state. * * The transactions are grouped to mirror the cycles in block_summary, generated * transactions are not included.  */ struct signed_block : public signed_block_summary { digest_type calculate_transaction_merkle_root()const; vector input_transactions; /// this is loaded and indexed into map that is referenced by summary };

這與上面表述的結構不一致,如何理解呢?實際上,從signed_block的定義不難看出,這個區塊的定義“繼承”了signed_block_summary的定義,而signed_block_summary的定義就是上述的自上而下的包含結構,所以兩種區塊的不同表示並不衝突,可以理解為包含結構是底層結構的一種映射,實際的區塊僅僅需要定義交易的先後順序即可,而cycle,shard這些概念是為了有機的組織一定數量的交易而設計出來的,是交易組合的不同視圖和抽象,目的是為了更方便高效的執行運算,包括區塊的驗證和簽名等等。

區塊的存儲形式 - 區塊日誌

區塊日誌是存儲區塊的二進制文件,區塊日誌的特性是隻能從末尾追加(append only),區塊日誌包含兩類文件:

  • 區塊文件,結構如下:

 +---------+----------------+---------+----------------+-----+------------+-------------------+ | Block 1 | Pos of Block 1 | Block 2 | Pos of Block 2 | ... | Head Block | Pos of Head Block | +---------+----------------+---------+----------------+-----+------------+-------------------+ 

區塊文件包含區塊的內容已經每個區塊的位置信息。區塊位置信息是固定的8字節寬度,這樣便於在連續讀取區塊的時候,按照讀一個區塊,向後跳躍8個字節,讀一個區塊,向後跳躍8個字節的模式快速加載區塊內容。

  • 索引文件,結構如下:

 +----------------+----------------+-----+-------------------+ | Pos of Block 1 | Pos of Block 2 | ... | Pos of Head Block | +----------------+----------------+-----+-------------------+

區塊索引的目的在於提供一個基於區塊序號的快速隨機搜索方法,如果一直區塊序號,使用索引文件可以快速定位目標區塊在區塊文件中的具體位置。索引文件不是必須的,沒有索引文件區塊鏈仍然可以運行,索引文件的主要作用是通過少量空間換取速度提升。索引文件可以通過順序讀取區塊文件來重新構建。

如果讀者運行過eos全節點的eosiod程序,可以發現這個程序啟動時會創建上述兩個文件,如下圖所示

blockchain@syphml11:~/eos/build/programs/eosd/data-dir/blocks$ ls -lhtotal 137M-rw-rw-r-- 1 blockchain blockchain 6.9M Jan 29 18:29 blocks.index-rw-rw-r-- 1 blockchain blockchain 130M Jan 29 18:29 blocks.log

區塊中的關鍵算法

我們現在瞭解了區塊的結構,而結構的設計是為了運算的目的,下面我們來介紹與區塊相關的關鍵算法。

計算交易的默克爾樹根

這樣的表述或許有些過於術語化,對於這一步運算,讀者需要理解的是對於一個區塊的主要運算實際就是做Hash驗證,而默克爾樹就是為了提高驗證效率而設計的數據結構,這個樹型結構的根部是一個Hash值,這個Hash值前後是否一致就表明區塊數據的有效性,這裡需要強調的是由於交易的id已經包含了交易內容的Hash值,所以在計算默克爾樹的時候並不需要重新計算交易的Hash值,只需要通過交易的id計算默克爾樹的各個結點即可。源代碼如下:

 digest_type signed_block::calculate_transaction_merkle_root()const { vector ids; ids.reserve(input_transactions.size()); for( const auto& t : input_transactions ) ids.emplace_back( t.id() ); return merkle( std::move(ids) ); }

實際上,在不同視圖下的默克爾樹的計算原理都類似,下面分別是在shard和region這兩個視圖下的默克爾樹計算:

void shard_trace::calculate_root() { static const size_t GUESS_ACTS_PER_TX = 10; vector action_roots; action_roots.reserve(transaction_traces.size() * GUESS_ACTS_PER_TX); for (const auto& tx :transaction_traces) { for (const auto& at: tx.action_traces) { digest_type::encoder enc; fc::raw::pack(enc, at.receiver); fc::raw::pack(enc, at.act.account); fc::raw::pack(enc, at.act.name); fc::raw::pack(enc, at.act.data); fc::raw::pack(enc, at.region_id); fc::raw::pack(enc, at.cycle_index); fc::raw::pack(enc, at.data_access); action_roots.emplace_back(enc.result()); } } shard_root = merkle(action_roots); } digest_type block_trace::calculate_action_merkle_root()const { vector shard_roots; shard_roots.reserve(1024); for(const auto& rt: region_traces ) { for(const auto& ct: rt.cycle_traces ) { for(const auto& st: ct.shard_traces) { shard_roots.emplace_back(st.shard_root); } } } return merkle(shard_roots); }

上面這段代碼值得注意的是shard視圖下的默克爾樹構建,shard之下已經是交易了,這一層的運算EOS採取的是直接對交易內容中的每一個action進行打包,再構建默克爾樹,所以這一層面的計算不是基於id的,而是基於內容的。

交易

上文提到,區塊的基本組成是交易,交易的基本單位是操作。下面我們來介紹交易的數據結構。

交易的數據結構交易狀態

當一筆交易被某個區塊引用時,區塊生產者針對這筆交易會做出相應的操作,而操作的不同結果會導致這筆交易的不同狀態,交易一共有三種狀態:

  • 成功執行

  • 軟失敗:執行未成功,但是異常處理機制成功捕獲錯誤並且執行

  • 硬失敗:執行未成功,異常處理機制也執行失敗

交易數據結構視圖

 +-----------+--------------+------------+--------+----+----------+----------+----+-----------+ | block num | block prefix | expiration | region | id | action 1 | action 2 |... | signature | +-----------+--------------+------------+--------+----+----------+----------+----+-----------+ +------------------head ------------------------+--------------------body-------------------+

交易頭

交易頭的長度是固定的,可以避免解析時的動態內存分配,便於快速解析交易部分信息。值得指出的是,所有的交易都有一個期限,這個期限限定了一個交易必須在規定時間內被納入區塊鏈,如果我們發現一個交易的時限已經過去,就可以放心的放棄這個交易,因為所有生產者都不會將它納入任何區塊。

除了佔用4個字節的期限,交易頭還包含2個字節的區塊序號和4個字節的區塊前綴,以及2個字節的區域(region)標識。交易頭的總長度是12個字節。

交易體

交易體包含了3部分內容:

  • 交易ID

  • 交易操作

  • 交易簽名

交易ID是通過對交易內容本身經過Hash運算得出,所以每個交易的ID是與其內容一一對應的。交易的主體是由操作構成的,關於操作的細節,本文的後面部分會有詳細解讀。一個交易在納入區塊之前必須含有簽名,用以驗證交易的合法性,除此之外,交易體也包含了簽名的摘要,同樣用於交易的驗證機制。

延遲型交易

交易分為兩種類型:一種是賬戶發起的普通交易,一種是由代碼生成的自動交易,自動交易可以設置一個延遲時間,這樣的交易叫延遲型交易,這種交易不會立即被執行,而是等到設定時間到時才會被執行。

交易中的關鍵算法

在交易這一視圖層的算法相對簡單,不涉及複雜的運算和策略,其中值得一提的是從簽名中提取公鑰的計算。這個過程並不複雜,可以總結為下面三個環節:

  • 遍歷交易的所有簽名

  • 通過簽名和簽名摘要(digest)抽取公鑰

  • 把公鑰和簽名組成的配對放入緩存,以便下次快捷訪問

以下是這部分計算的源代碼:

flat_set signed_transaction::get_signature_keys( const chain_id_type& chain_id )const{ try { using boost::adaptors::transformed; constexpr size_t recovery_cache_size = 100000; static recovery_cache_type recovery_cache; const digest_type digest = sig_digest(chain_id); flat_set recovered_pub_keys; for(const signature_type& sig : signatures) { recovery_cache_type::index::type::iterator it = recovery_cache.get().find(sig); if(it == recovery_cache.get().end() || it->trx_id != id()) { public_key_type recov = public_key_type(sig, digest); recovery_cache.emplace_back( cached_pub_key{id(), recov, sig} ); //could fail on dup signatures; not a problem recovered_pub_keys.insert(recov); continue; } recovered_pub_keys.insert(it->pub_key); } while(recovery_cache.size() > recovery_cache_size) recovery_cache.erase(recovery_cache.begin()); return recovered_pub_keys;} FC_CAPTURE_AND_RETHROW() }

操作

EOS區塊鏈中的交易是由一個個操作組成的,操作可以理解成一個能夠更改區塊鏈全局狀態的方法,操作的順序是確定的,一個交易內的操作要麼全部執行成功,要麼都不執行,這與交易的本意(transaction)是一致的。操作是區塊鏈的最底層邏輯,相當於區塊鏈這個大腦的神經元,區塊鏈的智能最終也是通過一個個操作的組合來實現的。從EOS的源代碼中不難看出,作者對於操作的設計是下了一番功夫的。

操作的設計原則

在源代碼中,EOS的作者給出了以下幾大設計原則:

  • 獨立原則 操作本身須包含足以解釋操作含義的信息,而不需要依賴區塊鏈提供的上下文信息來幫助詮釋操作。所以,即便一個操作的當前狀態可以通過區塊鏈上的數據推導得出,我們也需要將狀態信息納入操作數據中,以便每個操作是容易理解的。這個原則體現的是區塊的可解釋性,這一點非常重要,這個底層的設計原則將影響整個區塊鏈的使用效率。

  • 餘額可計算原則 一個賬戶當前的餘額計算,僅僅依賴於與這個賬戶相關的信息便可得出,而不需要解析整個區塊鏈才能獲得。這個原則針對的是比特幣的設計,由於比特幣的餘額計算需要掃描區塊鏈中的所有交易才能精準的計算出一個賬戶的餘額,這使得一個非常基礎的計算落地起來都變得相當繁瑣,EOS的這個設計目的在於提升運算效率。

  • 明確費用原則 區塊鏈的交易費用隨時間變化而變化,所以,一個簽名過的交易須明確的認同這個交易所需要支付的費用,這個費用是在交易形成之前就已經設定並且明確好了的,這一點也非常重要,因為明確的費用協議才能保證餘額的正確計算。

  • 明確授權原則 每個操作須包含足夠的授權信息以標明是哪一個賬戶擁有授權這個操作的權力,這種明確授權的設計思路帶來幾個好處:

  • 便於集中管理

  • 便於並行處理

  • 關聯賬戶原則 每個操作須包含足夠的足夠關聯賬戶信息,以保證這個操作能夠遍歷所有相關聯的賬戶,也就是這個操作能夠影響的所有賬戶,這個原則的目的同樣是為了確保賬戶的餘額能夠得到及時和準確的運算。

操作的設計思路操作的來源

一個操作可以通過兩種途徑產生:

  1. 由一個賬號產生,通過簽名來授權,即顯性方式。

  2. 由代碼生成,即隱形方式。

底層邏輯

操作的設計遵循React Flux設計模式(https://github.com/facebook/flux/tree/master/examples/flux-concepts),簡單的說就是每一個操作將會被賦予一個名稱,然後被分發給一個或者多個handler。在EOS環境中,每個操作對應的handler是通過scope和name來定義的,默認的handler也可以再次將操作分發給下一級的多個handler。所以,每個EOS應用可以實現自己的handler,當操作被分發到這個應用時,相應的handler的代碼就會被執行。

操作的設計思路中另一重要概念是授權。每一個操作的執行都必須確保具備了指定賬戶的授權。授權通過許可(permission)的方式聲明,對於一個授權動作,賬戶可以要求任意數量的許可,許可的驗證是獨立於代碼執行的,只有所有規定的許可被成功驗證之後,對應的代碼才能夠被執行。我們能感受到這裡面設計者對安全性的追求是很極致的,設計者將安全特性深深的嵌入了區塊鏈的底層設計邏輯,同時又不讓安全機制成為性能和結構的累贅,讓它自成體系,獨立管理。

操作的數據結構

操作的數據結構定義如下:

struct action { account_name account; action_name name; vector authorization; bytes data; ... };

理解了操作的設計思路的讀者一定會驚呼:竟然如此簡潔!

一個操作僅由四個部分構成:

  1. 賬戶:操作的來源

  2. 名稱:操作的標識

  3. 數據:執行操作需要用到的信息

這樣簡潔的結構不需要過多解釋了。

操作的關鍵算法

在操作這個視圖層,比較複雜的算法邏輯是與授權相關的部分,這部分內容最好的資料是EOS的官方文檔,讀者可以查看有關這部分內容的中文翻譯:https://github.com/BlockchainTranslator/EOS/blob/master/wiki/Accounts-%26-Permissions.md

鏈控制器

讀者現在應該對EOS區塊鏈內核的底層結構和基本邏輯有了瞭解,上述內容是內核的基本組成部分,如同一個機器的零部件,然而,要讓機器運轉起來,還需要把這些零部件串起來的控制中心,而鏈控制器(chain_controller)就是這個控制中心。下面我們來詳細介紹EOS的鏈控制器。

鏈控制器的基本功能

首先,我們需要理解鏈控制器存在的主要目的是作為外部世界與區塊鏈內核之間的交互界面,所以它有著承上啟下的作用,承上為支撐區塊鏈與外部的交互,啟下為管理區塊鏈內部的狀態變更。所以,從理解的不同角度,鏈控制器可以被理解為以下兩個概念:

  1. 從外部視角,鏈控制器是一個數據庫,這與鏈外對區塊鏈的理解是一致的,從狹義的角度看,區塊鏈就像一個不可更改的數據庫,而鏈控制器可以看做這個數據庫的管家,外部世界不用直接與區塊鏈底層通信,而是通過這個管家來與區塊鏈交互。

  2. 從內部視角,鏈控制器是一個協調者,區塊鏈內部各個部件均獨立運作,這與上述的設計原則是一致的,這樣的話,各個部件之間如何調度與協調,就需要一個有全局視角的角色來統一管理,這就是鏈控制器的第二個作用。

用作者的原話來說,鏈控制器的目的是以一種可以擴展的方式來跟蹤區塊鏈的狀態變化

鏈控制器的基本要素

為了維護可靠的區塊鏈狀態,鏈控制器管理著兩個不用類型的數據庫:

  1. 區塊日誌 前文已經介紹過了。它的作用是存儲已經經過驗證的不可逆轉的區塊鏈主幹。

  2. 塊數據庫 這是一個結構化數據庫,關於塊數據庫的詳細解讀,將會在另外的文章裡面說明,在這裡讀者只需要明白這是一個帶索引的可以增刪改查的數據庫。它的作用是維護和管理未經驗證的區塊鏈主幹與分支兩個子數據庫,主幹數據庫存儲的是當前長度最長的一條鏈,而分支存儲的是因為不同分叉而造成的分支鏈,隨著時間的推移,主鏈是有可能被分支替代的,每當一個新的區塊要寫入時,EOS都會重新計算各分支和主幹的長度,並且決定要不要切換主幹。最後,執行錯誤或者驗證不通過的區塊最終會被拋棄,並從數據庫中清理出去。

除了存儲部分,鏈控制器還需精確維護一個消息機制,以保證對區塊鏈中的其他成員廣播最新的區塊狀態變更,這個消息機制是區塊鏈網絡能夠正常和自主運行的基礎。

事務處理機制

熟悉數據庫的讀者一定知道,對於數據庫的寫入操作,維持一個事務的原子性是很關鍵的,也就是說,對於一批寫入操作,要麼全都成功執行,要麼都不執行,一定不能出現只執行了其中一部分的情況,因為這樣將導致數據的不一致性,這樣的錯誤是致命的。EOS中對於塊數據庫的寫操作採用同樣的設計原則,事務的使用主要體現在兩個層面:

  1. 區塊 整個區塊的內容是一個整體,要麼全都寫入,要麼都不寫入

  2. 交易 一個交易是一個整體,其中的操作要麼都執行,要麼都不執行

當事務中的某個步驟出現錯誤時,整個事務就會啟動回滾機制,整個區塊鏈將會恢復到這個事務發生之前的狀態。

鏈控制器中的關鍵過程

鏈控制器有著統籌全局,協調資源的作用,它管理的過程中最重要的有三個:

鏈控制器的初始化

鏈控制器的初始化是區塊鏈的起點,它的核心過程如下所示:

/** initialize the controller*/chain_controller::chain_controller( const chain_controller::controller_config& cfg ):_db( cfg.shared_memory_dir, (cfg.read_only ? database::read_only : database::read_write), cfg.shared_memory_size),_block_log(cfg.block_log_dir){ _initialize_indexes(); for (auto& f : cfg.applied_irreversible_block_callbacks) applied_irreversible_block.connect(f); contracts::chain_initializer starter(cfg.genesis); starter.register_types(*this, _db); // Behave as though we are applying a block during chain initialization (it's the genesis block!) with_applying_block([&] { _initialize_chain(starter); }); _spinup_db(); _spinup_fork_db(); if (_block_log.read_head() && head_block_num() < _block_log.read_head()->block_num()) replay();} /// chain_controller::chain_controller

它包含了6個環節:

  1. 初始化數據庫索引

  2. 與需要廣播的對象建立連接,以便每次新區塊的加入可以被區塊鏈網絡收聽到

  3. 初始化創世區塊的配置,創世區塊指的是區塊鏈中的第一個區塊,是之後所有區塊的老祖宗,其名因此而來

  4. 初始化區塊鏈,詳見下文

  5. 啟動數據庫

  6. 如果區塊日誌中已經有內容,那麼重新播放所有歷史區塊

區塊鏈的初始化

上面提到的區塊鏈的初始化過程的核心代碼如下所示:

/** initialize the blockchain*/void chain_controller::_initialize_chain(contracts::chain_initializer& starter){ try { if (!_db.find()) { _db.with_write_lock([this, &starter] { auto initial_timestamp = starter.get_chain_start_time(); FC_ASSERT(initial_timestamp != time_point(), "Must initialize genesis timestamp." ); FC_ASSERT( block_timestamp_type(initial_timestamp) == initial_timestamp, "Genesis timestamp must be divisible by config::block_interval_ms" ); // Create global properties const auto& gp = _db.create([&starter](global_property_object& p) { p.configuration = starter.get_chain_start_configuration(); p.active_producers = starter.get_chain_start_producers(); }); _db.create([&](dynamic_global_property_object& p) { p.time = initial_timestamp; p.recent_slots_filled = uint64_t(-1); p.virtual_net_bandwidth = gp.configuration.max_block_size * (config::blocksize_average_window_ms / config::block_interval_ms ); p.virtual_act_bandwidth = gp.configuration.max_block_acts * (config::blocksize_average_window_ms / config::block_interval_ms ); }); // Initialize block summary index for (int i = 0; i < 0x10000; i++) _db.create([&](block_summary_object&) {}); auto acts = starter.prepare_database(*this, _db); // create a block for our genesis transaction to send to applied_irreversible_block below signed_block block{}; block.producer = config::system_account_name; block_trace btrace{block}; btrace.region_traces.emplace_back(); auto& rtrace = btrace.region_traces.back(); rtrace.cycle_traces.emplace_back(); auto& ctrace = rtrace.cycle_traces.back(); ctrace.shard_traces.emplace_back(); auto& strace = ctrace.shard_traces.back(); signed_transaction genesis_setup_transaction; // not actually signed, signature checking is skipped genesis_setup_transaction.actions = move(acts); block.input_transactions.emplace_back(genesis_setup_transaction); ilog( "applying genesis transaction" ); with_skip_flags(skip_scope_check | skip_transaction_signatures | skip_authority_check | received_block | genesis_setup, [&](){ transaction_metadata tmeta( genesis_setup_transaction ); transaction_trace ttrace = __apply_transaction( tmeta ); strace.append(ttrace); }); // TODO: Should we write this genesis block instead of faking it on startup? strace.calculate_root(); applied_block(btrace); applied_irreversible_block(block); ilog( "done applying genesis transaction" ); }); }} FC_CAPTURE_AND_RETHROW() }

上面這段代碼不難理解,它描述了這樣一個初始化過程:

  1. 確定區塊鏈創建時間

  2. 初始化全局變量

  3. 初始化區塊概要索引

  4. 創建創世區塊(genesis)

  5. 將創世區塊寫入區塊鏈

  6. 計算創世區塊的默克爾樹根

  7. 廣播新區塊(創世區塊)的加入

區塊的創造

區塊的創造過程相對比較繁瑣,下面是核心部分的源代碼:

 // verify that the block signer is in the current set of active producers. shared_ptr new_head = _fork_db.push_block(new_block); //If the head block from the longest chain does not build off of the current head, we need to switch forks. if (new_head->data.previous != head_block_id()) { //If the newly pushed block is the same height as head, we get head back in new_head //Only switch forks if new_head is actually higher than head if (new_head->data.block_num() > head_block_num()) { wlog("Switching to fork: ${id}", ("id",new_head->data.id())); auto branches = _fork_db.fetch_branch_from(new_head->data.id(), head_block_id()); // pop blocks until we hit the forked block while (head_block_id() != branches.second.back()->data.previous) pop_block(); // push all blocks on the new fork for (auto ritr = branches.first.rbegin(); ritr != branches.first.rend(); ++ritr) { ilog("pushing blocks from fork ${n} ${id}", ("n",(*ritr)->data.block_num())("id",(*ritr)->data.id())); optional<:exception> except; try { auto session = _db.start_undo_session(true); _apply_block((*ritr)->data, skip); session.push(); } catch (const fc::exception& e) { except = e; } if (except) { wlog("exception thrown while switching forks ${e}", ("e",except->to_detail_string())); // remove the rest of branches.first from the fork_db, those blocks are invalid while (ritr != branches.first.rend()) { _fork_db.remove((*ritr)->data.id()); ++ritr; } _fork_db.set_head(branches.second.front()); // pop all blocks from the bad fork while (head_block_id() != branches.second.back()->data.previous) pop_block(); // restore all blocks from the good fork for (auto ritr = branches.second.rbegin(); ritr != branches.second.rend(); ++ritr) { auto session = _db.start_undo_session(true); _apply_block((*ritr)->data, skip); session.push(); } throw *except; } } return true; //swithced fork } else return false; // didn't switch fork }

這個過程可以理解為:

  1. 檢查區塊生產者的合法性

  2. 如果當前主幹已經不是最長鏈,需要切換到最長的分叉鏈上切換過程如下

  1. 彈出一定數量的區塊,直到碰到分叉處

  2. 將分叉中的新區塊寫入主幹數據庫

  3. 切換後的分叉成為主幹

  4. 如果上述過程失敗,則恢復狀態

  1. 如果不需要切換分支,則往主幹數據庫中寫入區塊

  2. 返回結果,以表明這次寫入是否造成了分支切換

交易的寫入

交易的寫入過程相對簡單一些,源代碼如下所示:

// record a txtransaction_trace chain_controller::__apply_transaction( transaction_metadata& meta ) { transaction_trace result(meta.id); for (const auto &act : meta.trx.actions) { apply_context context(*this, _db, act, meta); context.exec(); fc::move_append(result.action_traces, std::move(context.results.applied_actions)); fc::move_append(result.deferred_transactions, std::move(context.results.generated_transactions)); fc::move_append(result.canceled_deferred, std::move(context.results.canceled_deferred)); } uint32_t act_usage = result.action_traces.size(); for (auto &at: result.action_traces) { at.region_id = meta.region_id; at.cycle_index = meta.cycle_index; if (at.receiver == config::system_account_name && at.act.account == config::system_account_name && at.act.na 

文章發佈只為分享區塊鏈技術內容,版權歸原作者所有,觀點僅代表作者本人,絕不代表區塊鏈兄弟贊同其觀點或證實其描述


分享到:


相關文章: