最近在看以太坊(Ethereum)的源代碼, 初初看出點眉目。 區塊鏈是近年熱點之一,面向大衆讀者介紹概念的文章無數,有興趣的朋友可自行搜索。我會從源代碼實現入手,較系統的介紹一下以太坊的系統設計和協議實現等,但願能提供有必定深度的內容,歡迎有興趣的朋友多多討論。git
注:1.源代碼在github上, 分C++和Golang兩個版本,這裏我選擇的是Go語言版(github.com/ethereum/go-ethereum),如下文中提到的Ethereum 代碼部分,如無特別說明,均指go-ethereum; 2.github 主幹代碼還在持續更新中,因此此文中摘錄的代碼未來可能會跟讀者的本地版本有所不一樣,若有差別我會做相應修改。github
Ethereum 代碼裏哈希(hash)無處不在,許許多多的類型對象經過給定的哈希算法,能夠獲得一個哈希值。注意,算法中所使用的哈希函數是不可逆的,即對於h = hash(x), 僅僅經過哈希運算的結果h 沒法做逆運算獲得輸入x。哈希值在數學上的惟一性使得它能夠用做某個對象的全局惟一標識符。golang
Ethereum 中用到的哈希函數所有采用SHA-3(Secure Hash Algorithm 3,wikipedia)。SHA-3在2015年8月由美國標準技術協會(NIST)正式發佈,做爲Secure Hash Algorithm家族的最新一代標準,它相比於SHA-2和SHA-1,採用了徹底不一樣的設計思路,性能也比較好。須要注意的是,SHA-2目前並無出現被成功攻克的案例,SHA-3也沒有要當即取代SHA-2的趨勢,NIST只是考慮到SHA-1有過被攻克的案例,未雨綢繆的徵選了採用全新結構和思路的SHA-3來做爲一種最新的SHA方案。算法
RLP(Recursive Length Prefix)編碼,其定義可見wiki,它能夠將一個任意嵌套的字節數組([]byte),編碼成一個「展平」無嵌套的[]byte。1 byte取值範圍0x00 ~ 0xff,能夠表示任意字符,因此[]byte能夠線性的表示任意的數據。最簡單好比一個字符串,若是每一個字符用ASCII碼的二進制表示,整個字符串就變成一個[]byte。 RLP 編碼其實提供了一種序列化的編碼方法,不管輸入是何種嵌套形式的元素或數組,編碼輸出形式都是[]byte。RLP是可逆的,它提供了互逆的編碼、解碼方法。spring
Ethereum 中具體使用的哈希算法,就是對某個類型對象的RLP編碼值作了SHA3哈希運算,可稱爲RLP Hash。 Ethereum 在底層存儲中特地選擇了專門存儲和讀取[k, v] 鍵值對的第三方數據庫,[k, v] 中的v 就是某個結構體對象的RLP編碼值([]byte),k大多數狀況就是v的RLP編碼後的SHA-3哈希值。數據庫
兩個最經常使用的自定義數據類型common.Hash用來表示哈希值,common.Address表示地址數組
在Ethereum 代碼裏,全部用到的哈希值,都使用該Hash類型,長度爲32bytes,即256 bits;Ethereum 中全部跟賬號(Account)相關的信息,好比交易轉賬的轉出賬號(地址)和轉入賬號(地址),都會用該Address類型表示,長度20bytes。緩存
big.Int是golang提供的數據類型,用來處理比較大的整型數,固然它也能夠處理諸如64bit,32bit的經常使用整數。安全
big.Int是一個結構體(struct),至關於C++中的class,因此每次新建big.Int時能夠用 x := new(big.Int), 返回一個指針。注意對Int的算術操做,要使用該對象的成員函數,好比Add():數據結構
Ethereum 代碼中, 不少整型變量的類型都選用big.Int,好比Gas和Ether。
Gas, 是Ethereum裏對全部活動進行消耗資源計量的單位。這裏的活動是泛化的概念,包括但不限於:轉賬,合約的建立,合約指令的執行,執行中內存的擴展等等。因此Gas能夠想象成現實中的汽油或者燃氣。
Ether, 是Ethereum世界中使用的數字貨幣,也就是常說的以太幣。若是某個賬號,Address A想要發起一個交易,好比一次簡單的轉賬,即向 Address B 發送一筆金額H,那麼Address A 自己擁有的Ether,除了轉賬的數額H以外,還要有額外一筆金額用以支付交易所耗費的Gas。
若是能夠實現Gas和Ether之間的換算,那麼Ethereum系統裏全部的活動,均可以用Ether來計量。這樣,Ether就有了點通常等價物,也就是貨幣的樣子。
區塊(Block)是Ethereum的核心結構體之一。在整個區塊鏈(BlockChain)中,一個個Block是以單向鏈表的形式相互關聯起來的。Block中帶有一個Header(指針), Header結構體帶有Block的全部屬性信息,其中的ParentHash 表示該區塊的父區塊哈希值, 亦即Block之間關聯起來的前向指針。只不過要想獲得父區塊(parentBlock)對象,直接解析這個ParentHash是不夠的, 而是要將ParentHash同其餘字符串([]byte)組合成合適的key([]byte), 去kv數據庫裏查詢相應的value才能解析獲得。 Block和Header的部分紅員變量定義以下:
Header的整型成員Number表示該區塊在整個區塊鏈(BlockChain)中所處的位置,每個區塊相對於它的父區塊,其Number值是+1。這樣,整個區塊鏈會存在一個原始區塊,即創世塊(GenesisBlock), 它的Number是0,由系統天然生成而沒必要去額外挖掘(mine)。Block和BlockChain的實現細節,以後會有更詳細的討論。
Block中還有一個Tranction(指針)數組,這是咱們這裏關注的。Transaction(簡稱tx),是Ethereum裏標示一次交易的結構體, 它的成員變量包括轉賬金額,轉入方地址等等信息。Transaction的完整聲明以下:
每一個tx都聲明瞭本身的(Gas)Price 和 GasLimit。 Price指的是單位Gas消耗所折抵的Ether多少,它的高低意味着執行這個tx有多麼昂貴。GasLimit 是該tx執行過程當中所容許消耗資源的總上限,經過這個值,咱們能夠防止某個tx執行中出現惡意佔用資源的問題,這也是Ethereum中有關安全保護的策略之一。擁有獨立的Price和GasLimit, 也意味着每一個tx之間都是相互獨立的。
轉賬轉入方地址Recipient可能爲空(nil),這時在後續執行tx過程當中,Ethereum 須要建立一個地址來完成這筆轉賬。Payload是重要的數據成員,它既能夠做爲所建立合約的指令數組,其中每個byte做爲一個單獨的虛擬機指令;也能夠做爲數據數組,由合約指令進行操做。合約由以太坊虛擬機(Ethereum Virtual Machine, EVM)建立並執行。
細心的朋友在這裏會有個疑問,爲什麼交易的定義裏沒有聲明轉賬的轉出方地址? 問的好,tx 的轉賬轉出方地址確實沒有如轉入方同樣被顯式的聲明出來,而是被加密隱藏起來了,在Ethereum裏這個轉出方地址是機密,不能直接暴露。這個對tx加密的環節,在Ethereum裏被稱爲簽名(sign), 關於它的實現細節容後再述。
Block 類型的基本目的之一,就是爲了執行交易。狹義的交易可能僅僅是一筆轉賬,而廣義的交易同時還會支持許多其餘的意圖。Ethereum 中採用的是廣義交易概念。按照其架構設計,交易的執行可大體分爲內外兩層結構:第一層是虛擬機外,包括執行前將Transaction類型轉化成Message,建立虛擬機(EVM)對象,計算一些Gas消耗,以及執行交易完畢後建立收據(Receipt)對象並返回等;第二層是虛擬機內,包括執行轉賬,和建立合約並執行合約的指令數組。
執行tx的入口函數是StateProcessor的Process()函數,其實現代碼以下:
GasPool 類型其實就是big.Int。在一個Block的處理過程(即其全部tx的執行過程)中,GasPool 的值可以告訴你,剩下還有多少Gas可使用。在每個tx執行過程當中,Ethereum 還設計了償退(refund)環節,所償退的Gas數量也會加到這個GasPool裏。
Process()函數的核心是一個for循環,它將Block裏的全部tx逐個遍歷執行。具體的執行函數叫ApplyTransaction(),它每次執行tx, 會返回一個收據(Receipt)對象。Receipt結構體的聲明以下:
Receipt 中有一個Log類型的數組,其中每個Log對象記錄了Tx中一小步的操做。因此,每個tx的執行結果,由一個Receipt對象來表示;更詳細的內容,由一組Log對象來記錄。這個Log數組很重要,好比在不一樣Ethereum節點(Node)的相互同步過程當中,待同步區塊的Log數組有助於驗證同步中收到的block是否正確和完整,因此會被單獨同步(傳輸)。
Receipt的PostState保存了建立該Receipt對象時,整個Block內全部「賬戶」的當時狀態。Ethereum 裏用stateObject來表示一個帳戶Account,這個帳戶可轉賬(transfer value), 可執行tx, 它的惟一標示符是一個Address類型變量。 這個Receipt.PostState 就是當時所在Block裏全部stateObject對象的RLP Hash值。
Bloom類型是一個Ethereum內部實現的一個256bit長Bloom Filter。 Bloom Filter概念定義可見wikipedia,它可用來快速驗證一個新收到的對象是否處於一個已知的大量對象集合之中。這裏Receipt的Bloom,被用以驗證某個給定的Log是否處於Receipt已有的Log數組中。
咱們來看下StateProcessor.ApplyTransaction()的具體實現,它的基本流程以下圖:
ApplyTransaction()首先根據輸入參數分別封裝出一個Message對象和一個EVM對象,而後加上一個傳入的GasPool類型變量,由TransitionDb()函數完成tx的執行,待TransitionDb()返回以後,建立一個收據Receipt對象,最後返回該Recetip對象,以及整個tx執行過程所消耗Gas數量。
GasPool對象是在一個Block執行開始時建立,並在該Block內全部tx的執行過程當中共享,對於一個tx的執行可視爲「全局」存儲對象; Message由這次待執行的tx對象轉化而來,並攜帶了解析出的tx的(轉賬)轉出方地址,屬於待處理的數據對象;EVM 做爲Ethereum世界裏的虛擬機(Virtual Machine),做爲這次tx的實際執行者,完成轉賬和合約(Contract)的相關操做。
咱們來細看下TransitioinDb()的執行過程(/core/state_transition.go)。假設有StateTransition對象st, 其成員變量initialGas表示初始可用Gas數量,gas表示即時可用Gas數量,初始值均爲0,因而st.TransitionDb() 可由如下步驟展開:
由上可見,除了步驟3中EVM 函數的執行,其餘每一個步驟都在圍繞着Gas消耗量做文章(EVM 虛擬機的運行原理容後再述)。到這裏,你們能夠對Gas在以太坊系統裏的做用有個初步概念,Gas就是Ethereum系統中的血液。
步驟5的償退機制頗有意思,設立它的目的何在?目前爲止我只能理解它能夠避免交易執行過程當中過快消耗Gas,至於對其全面準確的理解尚需時日。
步驟6就更有趣了,正是這個獎勵機制的存在,纔會吸引社會上的礦工(miner)去賣力「挖礦」(mining)。越大的運算能力帶來越多的的區塊(交易)產出,礦工也就能經過該獎勵機制賺取越多的以太幣。
Ethereum 中每一個交易(transaction,tx)對象在被放進block時,都是通過數字簽名的,這樣能夠在後續傳輸和處理中隨時驗證tx是否通過篡改。Ethereum 採用的數字簽名是橢圓曲線數字簽名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比於基於大質數分解的RSA數字簽名算法,能夠在提供相同安全級別(in bits)的同時,僅需更短的公鑰(public key)。關於ECDSA的算法理論和實現細節,本系列會有另一篇文章專門加以介紹。這裏須要特別留意的是,tx的轉賬轉出方地址,就是對該tx對象做ECDSA簽名計算時所用的公鑰publicKey。
Ethereum中的數字簽名計算過程所生成的簽名(signature), 是一個長度爲65bytes的字節數組,它被截成三段放進tx中,前32bytes賦值給成員變量R, 再32bytes賦值給S,末1byte賦給V,固然因爲R、S、V聲明的類型都是*big.Int, 上述賦值存在[]byte -> big.Int的類型轉換。
當須要恢復出tx對象的轉賬轉出方地址時(好比在須要執行該交易時),Ethereum 會先從tx的signature中恢復出公鑰,再將公鑰轉化成一個common.Address類型的地址,signature由tx對象的三個成員變量R,S,V轉化成字節數組[]byte後拼接獲得。
Ethereum 對此定義了一個接口Signer, 用來執行掛載簽名,恢復公鑰,對tx對象作哈希等操做。
生成數字簽名的函數叫SignTx(),它會先調用其餘函數生成signature, 而後調用tx.WithSignature()將signature分段賦值給tx的成員變量R,S,V。
恢復出轉出方地址的函數叫Sender(), 參數包括一個Signer, 一個Transaction,代碼以下:
Sender()函數體中,signer.Sender()會從本次數字簽名的簽名字符串(signature)中恢復出公鑰,並轉化爲tx的(轉賬)轉出方地址。
在上文提到的ApplyTransaction()實現中,Transaction對象須要首先被轉化成Message接口,用到的AsMessage()函數即調用了此處的Sender()。
在Transaction對象tx的轉賬轉出方地址被解析出之後,tx 就被徹底轉換成了Message類型,能夠提供給虛擬機EVM執行了。
每一個交易(Transaction)帶有兩部份內容須要執行:1. 轉賬,由轉出方地址向轉入方地址轉賬一筆以太幣Ether; 2. 攜帶的[]byte類型成員變量Payload,其每個byte都對應了一個單獨虛擬機指令。這些內容都是由EVM(Ethereum Virtual Machine)對象來完成的。EVM 結構體是Ethereum虛擬機機制的核心,它與協同類的UML關係圖以下:
其中Context結構體分別攜帶了Transaction的信息(GasPrice, GasLimit),Block的信息(Number, Difficulty),以及轉賬函數等,提供給EVM;StateDB 接口是針對state.StateDB 結構體設計的本地行爲接口,可爲EVM提供statedb的相關操做; Interpreter結構體做爲解釋器,用來解釋執行EVM中合約(Contract)的指令(Code)。
注意,EVM 中定義的成員變量Context和StateDB, 僅僅聲明瞭變量名而無類型,而變量名同時又是其類型名,在Golang中,這種方式意味着宗主結構體能夠直接調用該成員變量的全部方法和成員變量,好比EVM調用Context中的Transfer()。
交易的轉賬操做由Context對象中的TransferFunc類型函數來實現,相似的函數類型,還有CanTransferFunc, 和GetHashFunc。
這三個類型的函數變量CanTransfer, Transfer, GetHash,在Context初始化時從外部傳入,目前使用的均是一個本地實現:
可見目前的轉賬函數Transfer()的邏輯很是簡單,轉賬的轉出帳戶減掉一筆以太幣,轉入帳戶加上一筆以太幣。因爲EVM調用的Transfer()函數實現徹底由Context提供,因此,假設若是基於Ethereum平臺開發,須要設計一種全新的「轉賬」模式,那麼只需寫一個新的Transfer()函數實現,在Context初始化時賦值便可。
有朋友或許會問,這裏Transfer()函數中對轉出和轉入帳戶的操做會當即生效麼?萬一兩步操做之間有錯誤發生怎麼辦?答案是不會當即生效。StateDB 並非真正的數據庫,只是一行爲相似數據庫的結構體。它在內部以Trie的數據結構來管理各個基於地址的帳戶,能夠理解成一個cache;當該帳戶的信息有變化時,變化先存儲在Trie中。僅當整個Block要被插入到BlockChain時,StateDB 裏緩存的全部帳戶的全部改動,纔會被真正的提交到底層數據庫。
合約(Contract)是EVM用來執行(虛擬機)指令的結構體。先來看下Contract的定義:
在這些成員變量裏,caller是轉賬轉出方地址(帳戶),self是轉入方地址,不過它們的類型都用接口ContractRef來表示;Code是指令數組,其中每個byte都對應於一個預約義的虛擬機指令;CodeHash 是Code的RLP哈希值;Input是數據數組,是指令所操做的數據集合;Args 是參數。
有意思的是self這個變量,爲何轉入方地址要被命名成self呢? Contract實現了ContractRef接口,返回的偏偏就是這個self地址。
因此當Contract對象做爲一個ContractRef接口出現時,它返回的地址就是它的self地址。那何時Contract會被類型轉換成ContractRef呢?當Contract A調用另外一個Contract B時,A就會做爲B的caller成員變量出現。Contract能夠調用Contract,這就爲系統在業務上的潛在擴展,提供了空間。
建立一個Contract對象時,重點關注對self的初始化,以及對Code, CodeAddr 和Input的賦值。
另外,StateDB 提供方法SetCode(),能夠將指令數組Code存儲在某個stateObject對象中; 方法GetCode(),能夠從某個stateObject對象中讀取已有的指令數組Code。
stateObject 是Ethereum裏用來管理一個帳戶全部信息修改的結構體,它以一個Address類型變量爲惟一標示符。StateDB 在內部用一個巨大的map結構來管理這些stateObject對象。全部帳戶信息-包括Ether餘額,指令數組Code, 該帳戶發起合約次數nonce等-它們發生的全部變化,會首先緩存到StateDB裏的某個stateObject裏,而後在合適的時候,被StateDB一塊兒提交到底層數據庫。注意,一個Contract所對應的stateObject的地址,是Contract的self地址,也就是轉賬的轉入方地址。
EVM 目前有五個函數能夠建立並執行Contract,按照做用和調用方式,能夠分紅兩類:
考慮到與執行交易的相關性,這裏着重探討Create()和Call()。先來看Call(),它用來處理(轉賬)轉入方地址不爲空的狀況:
Call()函數的邏輯能夠簡單分爲以上6步。其中步驟(3)調用了轉賬函數Transfer(),轉入帳戶caller, 轉出帳戶addr;步驟(4)建立一個Contract對象,並初始化其成員變量caller, self(addr), value和gas; 步驟(5)賦值Contract對象的Code, CodeHash, CodeAddr成員變量;步驟(6) 調用run()函數執行該合約的指令,最後Call()函數返回。相關代碼可見:
由於此時(轉賬)轉入地址不爲空,因此直接將入參addr初始化Contract對象的self地址,並可從StateDB中(實際上是以addr標識的帳戶stateObject對象)讀取出相關的Code和CodeHash並賦值給contract的成員變量。注意,此時轉入方地址參數addr同時亦被賦值予contract.CodeAddr。
再來看看EVM.Create(),它用來處理(轉賬)轉入方地址爲空的狀況。
與Call()相比,Create()由於沒有Address類型的入參addr,其流程有幾處明顯不一樣:
還有一點隱藏的比較深,Call()有一個入參input類型爲[]byte,而Create()有一個入參code類型一樣爲[]byte,沒有入參input,它們之間有無關係?其實,它們來源都是Transaction對象tx的成員變量Payload!調用EVM.Create()或Call()的入口在StateTransition.TransitionDb()中,當tx.Recipent爲空時,tx.data.Payload 被看成所建立Contract的Code;當tx.Recipient 不爲空時,tx.data.Payload 被看成Contract的Input。
EVM中執行合約(指令)的函數是run(),其實現代碼以下:
可見若是待執行的Contract對象剛好屬於一組預編譯的合約集合-此時以指令地址CodeAddr爲匹配項-那麼它能夠直接運行;沒有通過預編譯的Contract,纔會由Interpreter解釋執行。這裏的"預編譯",可理解爲不須要編譯(解釋)指令(Code)。預編譯的合約,其邏輯所有固定且已知,因此執行中再也不須要Code,僅需Input便可。
在代碼實現中,預編譯合約只需實現兩個方法Required()和Run()便可,這兩方法僅需一個入參input。
目前,Ethereuem 代碼中已經加入了多個預編譯合約,功能覆蓋了包括橢圓曲線密鑰恢復,SHA-3(256bits)哈希算法,RIPEMD-160加密算法等等。相信基於自身業務的需求,二次開發者徹底能夠加入本身的預編譯合約,大大加快合約的執行速度。
解釋器Interpreter用來執行(非預編譯的)合約指令。它的結構體UML關係圖以下所示:
Interpreter結構體經過一個Config類型的成員變量,間接持有一個包括256個operation對象在內的數組JumpTable。operation是作什麼的呢?每一個operation對象正對應一個已定義的虛擬機指令,它所含有的四個函數變量execute, gasCost, validateStack, memorySize 提供了這個虛擬機指令所表明的全部操做。每一個指令長度1byte,Contract對象的成員變量Code類型爲[]byte,就是這些虛擬機指令的任意集合。operation對象的函數操做,主要會用到Stack,Memory, IntPool 這幾個自定義的數據結構。
這樣一來,Interpreter的Run()函數就很好理解了,其核心流程就是逐個byte遍歷入參Contract對象的Code變量,將其解釋爲一個已知的operation,而後依次調用該operation對象的四個函數,流程示意圖以下:
operation在操做過程當中,會須要幾個數據結構: Stack,實現了標準容器 -棧的行爲;Memory,一個字節數組,可表示線性排列的任意數據;還有一個intPool,提供對big.Int數據的存儲和讀取。
已定義的operation,種類很豐富,包括:
須要特別注意的是LOGn指令操做,它用來建立n個Log對象,這裏n最大是4。還記得Log在什麼時候被用到麼?每一個交易(Transaction,tx)執行完成後,會建立一個Receipt對象用來記錄這個交易的執行結果。Receipt攜帶一個Log數組,用來記錄tx操做過程當中的全部變更細節,而這些Log,正是經過合適的LOGn指令-即合約指令數組(Contract.Code)中的單個byte,在其對應的operation裏被建立出來的。每一個新建立的Log對象被緩存在StateDB中的相對應的stateObject裏,待須要時從StateDB中讀取。
以太坊的出現大大晚於比特幣,雖然明顯受到比特幣系統的啓發,但在整個功能定位和設計架構上卻作了不少更廣更深的思考和嘗試。以太坊更像是一個經濟活動平臺,而並不侷限一種去中心化數字代幣的產生,分發和流轉。本文從交易執行的角度切入以太坊的系統實現,但願能提供一點管中窺豹的做用。