[以太坊源代码分析] I.区块和交易,合约和虚拟机
最近在看以太坊(Ethereum)的源代碼, 初初看出點(diǎn)眉目。 區(qū)塊鏈?zhǔn)墙隉狳c(diǎn)之一,面向大眾讀者介紹概念的文章無數(shù),有興趣的朋友可自行搜索。我會從源代碼實現(xiàn)入手,較系統(tǒng)的介紹一下以太坊的系統(tǒng)設(shè)計和協(xié)議實現(xiàn)等,希望能提供有一定深度的內(nèi)容,歡迎有興趣的朋友多多討論。
注:1.源代碼在github上, 分C++和Golang兩個版本,這里我選擇的是Go語言版(github.com/ethereum/go-ethereum),以下文中提到的Ethereum 代碼部分,如無特別說明,均指go-ethereum; 2.github 主干代碼還在持續(xù)更新中,所以此文中摘錄的代碼將來可能會跟讀者的本地版本有所不同,如有差異我會作相應(yīng)修改。
1. 基本概念
1.1 SHA-3哈希加密,RLP編碼
Ethereum 代碼里哈希(hash)無處不在,許許多多的類型對象通過給定的哈希算法,可以得到一個哈希值。注意,算法中所使用的哈希函數(shù)是不可逆的,即對于h = hash(x), 僅僅通過哈希運(yùn)算的結(jié)果h 無法作逆運(yùn)算得到輸入x。哈希值在數(shù)學(xué)上的唯一性使得它可以用作某個對象的全局唯一標(biāo)識符。
Ethereum 中用到的哈希函數(shù)全部采用SHA-3(Secure Hash Algorithm 3,wikipedia)。SHA-3在2015年8月由美國標(biāo)準(zhǔn)技術(shù)協(xié)會(NIST)正式發(fā)布,作為Secure Hash Algorithm家族的最新一代標(biāo)準(zhǔn),它相比于SHA-2和SHA-1,采用了完全不同的設(shè)計思路,性能也比較好。需要注意的是,SHA-2目前并沒有出現(xiàn)被成功攻克的案例,SHA-3也沒有要立即取代SHA-2的趨勢,NIST只是考慮到SHA-1有過被攻克的案例,未雨綢繆的征選了采用全新結(jié)構(gòu)和思路的SHA-3來作為一種最新的SHA方案。
RLP(Recursive Length Prefix)編碼,其定義可見wiki,它可以將一個任意嵌套的字節(jié)數(shù)組([]byte),編碼成一個“展平”無嵌套的[]byte。1 byte取值范圍0x00 ~ 0xff,可以表示任意字符,所以[]byte可以線性的表示任意的數(shù)據(jù)。最簡單比如一個字符串,如果每個字符用ASCII碼的二進(jìn)制表示,整個字符串就變成一個[]byte。 RLP 編碼其實提供了一種序列化的編碼方法,無論輸入是何種嵌套形式的元素或數(shù)組,編碼輸出形式都是[]byte。RLP是可逆的,它提供了互逆的編碼、解碼方法。
Ethereum 中具體使用的哈希算法,就是對某個類型對象的RLP編碼值做了SHA3哈希運(yùn)算,可稱為RLP Hash。 Ethereum 在底層存儲中特意選擇了專門存儲和讀取[k, v] 鍵值對的第三方數(shù)據(jù)庫,[k, v] 中的v 就是某個結(jié)構(gòu)體對象的RLP編碼值([]byte),k大多數(shù)情況就是v的RLP編碼后的SHA-3哈希值。
1.2 常用數(shù)據(jù)類型 哈希值和地址
兩個最常用的自定義數(shù)據(jù)類型common.Hash用來表示哈希值,common.Address表示地址
[plain]?view plain?copy在Ethereum 代碼里,所有用到的哈希值,都使用該Hash類型,長度為32bytes,即256 bits;Ethereum 中所有跟帳號(Account)相關(guān)的信息,比如交易轉(zhuǎn)帳的轉(zhuǎn)出帳號(地址)和轉(zhuǎn)入帳號(地址),都會用該Address類型表示,長度20bytes。
big.Int是golang提供的數(shù)據(jù)類型,用來處理比較大的整型數(shù),當(dāng)然它也可以處理諸如64bit,32bit的常用整數(shù)。
big.Int是一個結(jié)構(gòu)體(struct),相當(dāng)于C++中的class,所以每次新建big.Int時可以用 x := new(big.Int), 返回一個指針。注意對Int的算術(shù)操作,要使用該對象的成員函數(shù),比如Add():
[plain]?view plain?copy?Ethereum 代碼中, 很多整型變量的類型都選用big.Int,比如Gas和Ether。
1.3 汽油(Gas)和以太幣(Ether)
Gas, 是Ethereum里對所有活動進(jìn)行消耗資源計量的單位。這里的活動是泛化的概念,包括但不限于:轉(zhuǎn)帳,合約的創(chuàng)建,合約指令的執(zhí)行,執(zhí)行中內(nèi)存的擴(kuò)展等等。所以Gas可以想象成現(xiàn)實中的汽油或者燃?xì)狻?/p>
Ether, 是Ethereum世界中使用的數(shù)字貨幣,也就是常說的以太幣。如果某個帳號,Address A想要發(fā)起一個交易,比如一次簡單的轉(zhuǎn)帳,即向 Address B 發(fā)送一筆金額H,那么Address A 本身擁有的Ether,除了轉(zhuǎn)帳的數(shù)額H之外,還要有額外一筆金額用以支付交易所耗費(fèi)的Gas。
如果可以實現(xiàn)Gas和Ether之間的換算,那么Ethereum系統(tǒng)里所有的活動,都可以用Ether來計量。這樣,Ether就有了點(diǎn)一般等價物,也就是貨幣的樣子。
1.4 區(qū)塊是交易的集合
區(qū)塊(Block)是Ethereum的核心結(jié)構(gòu)體之一。在整個區(qū)塊鏈(BlockChain)中,一個個Block是以單向鏈表的形式相互關(guān)聯(lián)起來的。Block中帶有一個Header(指針), Header結(jié)構(gòu)體帶有Block的所有屬性信息,其中的ParentHash 表示該區(qū)塊的父區(qū)塊哈希值, 亦即Block之間關(guān)聯(lián)起來的前向指針。只不過要想得到父區(qū)塊(parentBlock)對象,直接解析這個ParentHash是不夠的, 而是要將ParentHash同其他字符串([]byte)組合成合適的key([]byte), 去kv數(shù)據(jù)庫里查詢相應(yīng)的value才能解析得到。 Block和Header的部分成員變量定義如下:
[plain]?view plain?copyHeader的整型成員Number表示該區(qū)塊在整個區(qū)塊鏈(BlockChain)中所處的位置,每一個區(qū)塊相對于它的父區(qū)塊,其Number值是+1。這樣,整個區(qū)塊鏈會存在一個原始區(qū)塊,即創(chuàng)世塊(GenesisBlock), 它的Number是0,由系統(tǒng)自然生成而不必去額外挖掘(mine)。Block和BlockChain的實現(xiàn)細(xì)節(jié),之后會有更詳細(xì)的討論。
Block中還有一個Tranction(指針)數(shù)組,這是我們這里關(guān)注的。Transaction(簡稱tx),是Ethereum里標(biāo)示一次交易的結(jié)構(gòu)體, 它的成員變量包括轉(zhuǎn)帳金額,轉(zhuǎn)入方地址等等信息。Transaction的完整聲明如下:
[plain]?view plain?copy轉(zhuǎn)帳轉(zhuǎn)入方地址Recipient可能為空(nil),這時在后續(xù)執(zhí)行tx過程中,Ethereum 需要創(chuàng)建一個地址來完成這筆轉(zhuǎn)帳。Payload是重要的數(shù)據(jù)成員,它既可以作為所創(chuàng)建合約的指令數(shù)組,其中每一個byte作為一個單獨(dú)的虛擬機(jī)指令;也可以作為數(shù)據(jù)數(shù)組,由合約指令進(jìn)行操作。合約由以太坊虛擬機(jī)(Ethereum Virtual Machine, EVM)創(chuàng)建并執(zhí)行。
細(xì)心的朋友在這里會有個疑問,為何交易的定義里沒有聲明轉(zhuǎn)帳的轉(zhuǎn)出方地址? 問的好,tx 的轉(zhuǎn)帳轉(zhuǎn)出方地址確實沒有如轉(zhuǎn)入方一樣被顯式的聲明出來,而是被加密隱藏起來了,在Ethereum里這個轉(zhuǎn)出方地址是機(jī)密,不能直接暴露。這個對tx加密的環(huán)節(jié),在Ethereum里被稱為簽名(sign), 關(guān)于它的實現(xiàn)細(xì)節(jié)容后再述。
2. 交易的執(zhí)行
Block 類型的基本目的之一,就是為了執(zhí)行交易。狹義的交易可能僅僅是一筆轉(zhuǎn)帳,而廣義的交易同時還會支持許多其他的意圖。Ethereum 中采用的是廣義交易概念。按照其架構(gòu)設(shè)計,交易的執(zhí)行可大致分為內(nèi)外兩層結(jié)構(gòu):第一層是虛擬機(jī)外,包括執(zhí)行前將Transaction類型轉(zhuǎn)化成Message,創(chuàng)建虛擬機(jī)(EVM)對象,計算一些Gas消耗,以及執(zhí)行交易完畢后創(chuàng)建收據(jù)(Receipt)對象并返回等;第二層是虛擬機(jī)內(nèi),包括執(zhí)行轉(zhuǎn)帳,和創(chuàng)建合約并執(zhí)行合約的指令數(shù)組。
2.1 虛擬機(jī)外
2.1.1 入口和返回值
執(zhí)行tx的入口函數(shù)是StateProcessor的Process()函數(shù),其實現(xiàn)代碼如下:
[plain]?view plain?copyGasPool 類型其實就是big.Int。在一個Block的處理過程(即其所有tx的執(zhí)行過程)中,GasPool 的值能夠告訴你,剩下還有多少Gas可以使用。在每一個tx執(zhí)行過程中,Ethereum 還設(shè)計了償退(refund)環(huán)節(jié),所償退的Gas數(shù)量也會加到這個GasPool里。
Process()函數(shù)的核心是一個for循環(huán),它將Block里的所有tx逐個遍歷執(zhí)行。具體的執(zhí)行函數(shù)叫ApplyTransaction(),它每次執(zhí)行tx, 會返回一個收據(jù)(Receipt)對象。Receipt結(jié)構(gòu)體的聲明如下:
Receipt 中有一個Log類型的數(shù)組,其中每一個Log對象記錄了Tx中一小步的操作。所以,每一個tx的執(zhí)行結(jié)果,由一個Receipt對象來表示;更詳細(xì)的內(nèi)容,由一組Log對象來記錄。這個Log數(shù)組很重要,比如在不同Ethereum節(jié)點(diǎn)(Node)的相互同步過程中,待同步區(qū)塊的Log數(shù)組有助于驗證同步中收到的block是否正確和完整,所以會被單獨(dú)同步(傳輸)。
Receipt的PostState保存了創(chuàng)建該Receipt對象時,整個Block內(nèi)所有“帳戶”的當(dāng)時狀態(tài)。Ethereum 里用stateObject來表示一個賬戶Account,這個賬戶可轉(zhuǎn)帳(transfer value), 可執(zhí)行tx, 它的唯一標(biāo)示符是一個Address類型變量。 這個Receipt.PostState 就是當(dāng)時所在Block里所有stateObject對象的RLP Hash值。
Bloom類型是一個Ethereum內(nèi)部實現(xiàn)的一個256bit長Bloom Filter。?Bloom Filter概念定義可見wikipedia,它可用來快速驗證一個新收到的對象是否處于一個已知的大量對象集合之中。這里Receipt的Bloom,被用以驗證某個給定的Log是否處于Receipt已有的Log數(shù)組中。
2.1.2 消耗Gas,亦獎勵Gas
我們來看下StateProcessor.ApplyTransaction()的具體實現(xiàn),它的基本流程如下圖:
ApplyTransaction()首先根據(jù)輸入?yún)?shù)分別封裝出一個Message對象和一個EVM對象,然后加上一個傳入的GasPool類型變量,由TransitionDb()函數(shù)完成tx的執(zhí)行,待TransitionDb()返回之后,創(chuàng)建一個收據(jù)Receipt對象,最后返回該Recetip對象,以及整個tx執(zhí)行過程所消耗Gas數(shù)量。
GasPool對象是在一個Block執(zhí)行開始時創(chuàng)建,并在該Block內(nèi)所有tx的執(zhí)行過程中共享,對于一個tx的執(zhí)行可視為“全局”存儲對象; Message由此次待執(zhí)行的tx對象轉(zhuǎn)化而來,并攜帶了解析出的tx的(轉(zhuǎn)帳)轉(zhuǎn)出方地址,屬于待處理的數(shù)據(jù)對象;EVM 作為Ethereum世界里的虛擬機(jī)(Virtual Machine),作為此次tx的實際執(zhí)行者,完成轉(zhuǎn)帳和合約(Contract)的相關(guān)操作。
我們來細(xì)看下TransitioinDb()的執(zhí)行過程(/core/state_transition.go)。假設(shè)有StateTransition對象st,? 其成員變量initialGas表示初始可用Gas數(shù)量,gas表示即時可用Gas數(shù)量,初始值均為0,于是st.TransitionDb() 可由以下步驟展開:
由上可見,除了步驟3中EVM 函數(shù)的執(zhí)行,其他每個步驟都在圍繞著Gas消耗量作文章(EVM 虛擬機(jī)的運(yùn)行原理容后再述)。到這里,大家可以對Gas在以太坊系統(tǒng)里的作用有個初步概念,Gas就是Ethereum系統(tǒng)中的血液。
步驟5的償退機(jī)制很有意思,設(shè)立它的目的何在?目前為止我只能理解它可以避免交易執(zhí)行過程中過快消耗Gas,至于對其全面準(zhǔn)確的理解尚需時日。
步驟6就更有趣了,正是這個獎勵機(jī)制的存在,才會吸引社會上的礦工(miner)去賣力“挖礦”(mining)。越大的運(yùn)算能力帶來越多的的區(qū)塊(交易)產(chǎn)出,礦工也就能通過該獎勵機(jī)制賺取越多的以太幣。
2.1.3 交易的數(shù)字簽名
Ethereum 中每個交易(transaction,tx)對象在被放進(jìn)block時,都是經(jīng)過數(shù)字簽名的,這樣可以在后續(xù)傳輸和處理中隨時驗證tx是否經(jīng)過篡改。Ethereum 采用的數(shù)字簽名是橢圓曲線數(shù)字簽名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比于基于大質(zhì)數(shù)分解的RSA數(shù)字簽名算法,可以在提供相同安全級別(in bits)的同時,僅需更短的公鑰(public key)。關(guān)于ECDSA的算法理論和實現(xiàn)細(xì)節(jié),本系列會有另外一篇文章專門加以介紹。這里需要特別留意的是,tx的轉(zhuǎn)帳轉(zhuǎn)出方地址,就是對該tx對象作ECDSA簽名計算時所用的公鑰publicKey。
Ethereum中的數(shù)字簽名計算過程所生成的簽名(signature), 是一個長度為65bytes的字節(jié)數(shù)組,它被截成三段放進(jìn)tx中,前32bytes賦值給成員變量R, 再32bytes賦值給S,末1byte賦給V,當(dāng)然由于R、S、V聲明的類型都是*big.Int, 上述賦值存在[]byte -> big.Int的類型轉(zhuǎn)換。
當(dāng)需要恢復(fù)出tx對象的轉(zhuǎn)帳轉(zhuǎn)出方地址時(比如在需要執(zhí)行該交易時),Ethereum 會先從tx的signature中恢復(fù)出公鑰,再將公鑰轉(zhuǎn)化成一個common.Address類型的地址,signature由tx對象的三個成員變量R,S,V轉(zhuǎn)化成字節(jié)數(shù)組[]byte后拼接得到。
Ethereum 對此定義了一個接口Signer, 用來執(zhí)行掛載簽名,恢復(fù)公鑰,對tx對象做哈希等操作。
[plain]?view plain?copy生成數(shù)字簽名的函數(shù)叫SignTx(),它會先調(diào)用其他函數(shù)生成signature, 然后調(diào)用tx.WithSignature()將signature分段賦值給tx的成員變量R,S,V。
[plain]?view plain?copy恢復(fù)出轉(zhuǎn)出方地址的函數(shù)叫Sender(), 參數(shù)包括一個Signer, 一個Transaction,代碼如下:
[plain]?view plain?copy在上文提到的ApplyTransaction()實現(xiàn)中,Transaction對象需要首先被轉(zhuǎn)化成Message接口,用到的AsMessage()函數(shù)即調(diào)用了此處的Sender()。
[plain]?view plain?copy在Transaction對象tx的轉(zhuǎn)帳轉(zhuǎn)出方地址被解析出以后,tx 就被完全轉(zhuǎn)換成了Message類型,可以提供給虛擬機(jī)EVM執(zhí)行了。
2.2 虛擬機(jī)內(nèi)
每個交易(Transaction)帶有兩部分內(nèi)容需要執(zhí)行:1. 轉(zhuǎn)帳,由轉(zhuǎn)出方地址向轉(zhuǎn)入方地址轉(zhuǎn)帳一筆以太幣Ether; 2. 攜帶的[]byte類型成員變量Payload,其每一個byte都對應(yīng)了一個單獨(dú)虛擬機(jī)指令。這些內(nèi)容都是由EVM(Ethereum Virtual Machine)對象來完成的。EVM 結(jié)構(gòu)體是Ethereum虛擬機(jī)機(jī)制的核心,它與協(xié)同類的UML關(guān)系圖如下:
其中Context結(jié)構(gòu)體分別攜帶了Transaction的信息(GasPrice, GasLimit),Block的信息(Number, Difficulty),以及轉(zhuǎn)帳函數(shù)等,提供給EVM;StateDB 接口是針對state.StateDB 結(jié)構(gòu)體設(shè)計的本地行為接口,可為EVM提供statedb的相關(guān)操作; Interpreter結(jié)構(gòu)體作為解釋器,用來解釋執(zhí)行EVM中合約(Contract)的指令(Code)。
注意,EVM 中定義的成員變量Context和StateDB, 僅僅聲明了變量名而無類型,而變量名同時又是其類型名,在Golang中,這種方式意味著宗主結(jié)構(gòu)體可以直接調(diào)用該成員變量的所有方法和成員變量,比如EVM調(diào)用Context中的Transfer()。
2.2.1 完成轉(zhuǎn)帳
交易的轉(zhuǎn)帳操作由Context對象中的TransferFunc類型函數(shù)來實現(xiàn),類似的函數(shù)類型,還有CanTransferFunc, 和GetHashFunc。
[plain]?view plain?copy這三個類型的函數(shù)變量CanTransfer, Transfer, GetHash,在Context初始化時從外部傳入,目前使用的均是一個本地實現(xiàn):
有朋友或許會問,這里Transfer()函數(shù)中對轉(zhuǎn)出和轉(zhuǎn)入賬戶的操作會立即生效么?萬一兩步操作之間有錯誤發(fā)生怎么辦?答案是不會立即生效。StateDB 并不是真正的數(shù)據(jù)庫,只是一行為類似數(shù)據(jù)庫的結(jié)構(gòu)體。它在內(nèi)部以Trie的數(shù)據(jù)結(jié)構(gòu)來管理各個基于地址的賬戶,可以理解成一個cache;當(dāng)該賬戶的信息有變化時,變化先存儲在Trie中。僅當(dāng)整個Block要被插入到BlockChain時,StateDB 里緩存的所有賬戶的所有改動,才會被真正的提交到底層數(shù)據(jù)庫。
2.2.2 合約的創(chuàng)建和賦值
合約(Contract)是EVM用來執(zhí)行(虛擬機(jī))指令的結(jié)構(gòu)體。先來看下Contract的定義:
[plain]?view plain?copy有意思的是self這個變量,為什么轉(zhuǎn)入方地址要被命名成self呢? Contract實現(xiàn)了ContractRef接口,返回的恰恰就是這個self地址。
[plain]?view plain?copy所以當(dāng)Contract對象作為一個ContractRef接口出現(xiàn)時,它返回的地址就是它的self地址。那什么時候Contract會被類型轉(zhuǎn)換成ContractRef呢?當(dāng)Contract A調(diào)用另一個Contract B時,A就會作為B的caller成員變量出現(xiàn)。Contract可以調(diào)用Contract,這就為系統(tǒng)在業(yè)務(wù)上的潛在擴(kuò)展,提供了空間。
創(chuàng)建一個Contract對象時,重點(diǎn)關(guān)注對self的初始化,以及對Code, CodeAddr 和Input的賦值。?
另外,StateDB 提供方法SetCode(),可以將指令數(shù)組Code存儲在某個stateObject對象中; 方法GetCode(),可以從某個stateObject對象中讀取已有的指令數(shù)組Code。
stateObject 是Ethereum里用來管理一個賬戶所有信息修改的結(jié)構(gòu)體,它以一個Address類型變量為唯一標(biāo)示符。StateDB 在內(nèi)部用一個巨大的map結(jié)構(gòu)來管理這些stateObject對象。所有賬戶信息-包括Ether余額,指令數(shù)組Code, 該賬戶發(fā)起合約次數(shù)nonce等-它們發(fā)生的所有變化,會首先緩存到StateDB里的某個stateObject里,然后在合適的時候,被StateDB一起提交到底層數(shù)據(jù)庫。注意,一個Contract所對應(yīng)的stateObject的地址,是Contract的self地址,也就是轉(zhuǎn)帳的轉(zhuǎn)入方地址。
EVM 目前有五個函數(shù)可以創(chuàng)建并執(zhí)行Contract,按照作用和調(diào)用方式,可以分成兩類:
- Create(), Call(): 二者均在StateProcessor的ApplyTransaction()被調(diào)用以執(zhí)行單個交易,并且都有調(diào)用轉(zhuǎn)帳函數(shù)完成轉(zhuǎn)帳。
- CallCode(), DelegateCall(), StaticCall():三者由于分別對應(yīng)于不同的虛擬機(jī)指令(1 byte)操作,不會用以執(zhí)行單個交易,也都不能處理轉(zhuǎn)帳。
考慮到與執(zhí)行交易的相關(guān)性,這里著重探討Create()和Call()。先來看Call(),它用來處理(轉(zhuǎn)帳)轉(zhuǎn)入方地址不為空的情況:
Call()函數(shù)的邏輯可以簡單分為以上6步。其中步驟(3)調(diào)用了轉(zhuǎn)帳函數(shù)Transfer(),轉(zhuǎn)入賬戶caller, 轉(zhuǎn)出賬戶addr;步驟(4)創(chuàng)建一個Contract對象,并初始化其成員變量caller, self(addr), value和gas; 步驟(5)賦值Contract對象的Code, CodeHash, CodeAddr成員變量;步驟(6) 調(diào)用run()函數(shù)執(zhí)行該合約的指令,最后Call()函數(shù)返回。相關(guān)代碼可見:
再來看看EVM.Create(),它用來處理(轉(zhuǎn)帳)轉(zhuǎn)入方地址為空的情況。
與Call()相比,Create()因為沒有Address類型的入?yún)ddr,其流程有幾處明顯不同:
- 步驟(3)中創(chuàng)建一個新地址contractAddr,作為(轉(zhuǎn)帳)轉(zhuǎn)入方地址,亦作為Contract的self地址;
- 步驟(6)由于contracrAddr剛剛新建,db中尚無與該地址相關(guān)的Code信息,所以會將類型為[]byte的入?yún)ode,賦值予Contract對象的Code成員;
- 步驟(8)將本次執(zhí)行合約的返回結(jié)果,作為contractAddr所對應(yīng)賬戶(stateObject對象)的Code儲存起來,以備下次調(diào)用。
還有一點(diǎn)隱藏的比較深,Call()有一個入?yún)nput類型為[]byte,而Create()有一個入?yún)ode類型同樣為[]byte,沒有入?yún)nput,它們之間有無關(guān)系?其實,它們來源都是Transaction對象tx的成員變量Payload!調(diào)用EVM.Create()或Call()的入口在StateTransition.TransitionDb()中,當(dāng)tx.Recipent為空時,tx.data.Payload 被當(dāng)作所創(chuàng)建Contract的Code;當(dāng)tx.Recipient 不為空時,tx.data.Payload 被當(dāng)作Contract的Input。
2.2.3 預(yù)編譯的合約
EVM中執(zhí)行合約(指令)的函數(shù)是run(),其實現(xiàn)代碼如下:
[plain]?view plain?copy可見如果待執(zhí)行的Contract對象恰好屬于一組預(yù)編譯的合約集合-此時以指令地址CodeAddr為匹配項-那么它可以直接運(yùn)行;沒有經(jīng)過預(yù)編譯的Contract,才會由Interpreter解釋執(zhí)行。這里的"預(yù)編譯",可理解為不需要編譯(解釋)指令(Code)。預(yù)編譯的合約,其邏輯全部固定且已知,所以執(zhí)行中不再需要Code,僅需Input即可。
在代碼實現(xiàn)中,預(yù)編譯合約只需實現(xiàn)兩個方法Required()和Run()即可,這兩方法僅需一個入?yún)nput。
[plain]?view plain?copy2.2.4 解釋器執(zhí)行合約的指令
解釋器Interpreter用來執(zhí)行(非預(yù)編譯的)合約指令。它的結(jié)構(gòu)體UML關(guān)系圖如下所示:
Interpreter結(jié)構(gòu)體通過一個Config類型的成員變量,間接持有一個包括256個operation對象在內(nèi)的數(shù)組JumpTable。operation是做什么的呢?每個operation對象正對應(yīng)一個已定義的虛擬機(jī)指令,它所含有的四個函數(shù)變量execute, gasCost, validateStack, memorySize 提供了這個虛擬機(jī)指令所代表的所有操作。每個指令長度1byte,Contract對象的成員變量Code類型為[]byte,就是這些虛擬機(jī)指令的任意集合。operation對象的函數(shù)操作,主要會用到Stack,Memory, IntPool 這幾個自定義的數(shù)據(jù)結(jié)構(gòu)。
這樣一來,Interpreter的Run()函數(shù)就很好理解了,其核心流程就是逐個byte遍歷入?yún)ontract對象的Code變量,將其解釋為一個已知的operation,然后依次調(diào)用該operation對象的四個函數(shù),流程示意圖如下:
operation在操作過程中,會需要幾個數(shù)據(jù)結(jié)構(gòu): Stack,實現(xiàn)了標(biāo)準(zhǔn)容器 -棧的行為;Memory,一個字節(jié)數(shù)組,可表示線性排列的任意數(shù)據(jù);還有一個intPool,提供對big.Int數(shù)據(jù)的存儲和讀取。
已定義的operation,種類很豐富,包括:
- 算術(shù)運(yùn)算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP...;
- 邏輯運(yùn)算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT...;
- 業(yè)務(wù)功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2...等等
需要特別注意的是LOGn指令操作,它用來創(chuàng)建n個Log對象,這里n最大是4。還記得Log在何時被用到么?每個交易(Transaction,tx)執(zhí)行完成后,會創(chuàng)建一個Receipt對象用來記錄這個交易的執(zhí)行結(jié)果。Receipt攜帶一個Log數(shù)組,用來記錄tx操作過程中的所有變動細(xì)節(jié),而這些Log,正是通過合適的LOGn指令-即合約指令數(shù)組(Contract.Code)中的單個byte,在其對應(yīng)的operation里被創(chuàng)建出來的。每個新創(chuàng)建的Log對象被緩存在StateDB中的相對應(yīng)的stateObject里,待需要時從StateDB中讀取。
3. 小結(jié)
以太坊的出現(xiàn)大大晚于比特幣,雖然明顯受到比特幣系統(tǒng)的啟發(fā),但在整個功能定位和設(shè)計架構(gòu)上卻做了很多更廣更深的思考和嘗試。以太坊更像是一個經(jīng)濟(jì)活動平臺,而并不局限一種去中心化數(shù)字代幣的產(chǎn)生,分發(fā)和流轉(zhuǎn)。本文從交易執(zhí)行的角度切入以太坊的系統(tǒng)實現(xiàn),希望能提供一點(diǎn)管中窺豹的作用。
- Gas是Ethereum系統(tǒng)的血液。一切資源,活動,交互的開銷,都以Gas為計量單元。如果定義了一個GasPrice,那么所有的Gas消耗亦可等價于以太幣Ether。
- Block是Transaction的集合。Block在插入BlockChain前,需要將所有Transaction逐個執(zhí)行。Transaction的執(zhí)行會消耗發(fā)起方的Ether,但系統(tǒng)在其執(zhí)行完成時,會給予其作者(挖掘出這個Block的賬戶)一筆補(bǔ)償,這筆補(bǔ)償是“礦工”賺取收入的來源之一。
- Ethereum 定義了自己的虛擬機(jī)EVM, 它與合約(Contract)機(jī)制相結(jié)合,能夠在提供非常豐富的操作的同時,又能很好的控制存儲空間和運(yùn)行速度。Contract由Transaction轉(zhuǎn)化得到。
- Ethereum 里的哈希函數(shù),用的是SHA-3,256 bits;數(shù)據(jù)(數(shù)組)的序列化,用的是RLP編碼,所以所有對象,數(shù)組的哈希算法,實際用的RLP + SHA-3。數(shù)字簽名算法,使用了橢圓曲線數(shù)字簽名算法(ECDSA)。
總結(jié)
以上是生活随笔為你收集整理的[以太坊源代码分析] I.区块和交易,合约和虚拟机的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: go-ethereum-code-ana
- 下一篇: [以太坊源代码分析] II. 数据的呈现