日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[以太坊源代码分析] I.区块和交易,合约和虚拟机

發(fā)布時(shí)間:2025/3/15 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [以太坊源代码分析] I.区块和交易,合约和虚拟机 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

最近在看以太坊(Ethereum)的源代碼, 初初看出點(diǎn)眉目。 區(qū)塊鏈?zhǔn)墙隉狳c(diǎn)之一,面向大眾讀者介紹概念的文章無數(shù),有興趣的朋友可自行搜索。我會(huì)從源代碼實(shí)現(xiàn)入手,較系統(tǒng)的介紹一下以太坊的系統(tǒng)設(shè)計(jì)和協(xié)議實(shí)現(xiàn)等,希望能提供有一定深度的內(nèi)容,歡迎有興趣的朋友多多討論。

注:1.源代碼在github上, 分C++和Golang兩個(gè)版本,這里我選擇的是Go語言版(github.com/ethereum/go-ethereum),以下文中提到的Ethereum 代碼部分,如無特別說明,均指go-ethereum; 2.github 主干代碼還在持續(xù)更新中,所以此文中摘錄的代碼將來可能會(huì)跟讀者的本地版本有所不同,如有差異我會(huì)作相應(yīng)修改。

1. 基本概念

1.1 SHA-3哈希加密,RLP編碼

Ethereum 代碼里哈希(hash)無處不在,許許多多的類型對(duì)象通過給定的哈希算法,可以得到一個(gè)哈希值。注意,算法中所使用的哈希函數(shù)是不可逆的,即對(duì)于h = hash(x), 僅僅通過哈希運(yùn)算的結(jié)果h 無法作逆運(yùn)算得到輸入x。哈希值在數(shù)學(xué)上的唯一性使得它可以用作某個(gè)對(duì)象的全局唯一標(biāo)識(shí)符。

Ethereum 中用到的哈希函數(shù)全部采用SHA-3(Secure Hash Algorithm 3,wikipedia)。SHA-3在2015年8月由美國標(biāo)準(zhǔn)技術(shù)協(xié)會(huì)(NIST)正式發(fā)布,作為Secure Hash Algorithm家族的最新一代標(biāo)準(zhǔn),它相比于SHA-2和SHA-1,采用了完全不同的設(shè)計(jì)思路,性能也比較好。需要注意的是,SHA-2目前并沒有出現(xiàn)被成功攻克的案例,SHA-3也沒有要立即取代SHA-2的趨勢(shì),NIST只是考慮到SHA-1有過被攻克的案例,未雨綢繆的征選了采用全新結(jié)構(gòu)和思路的SHA-3來作為一種最新的SHA方案。

RLP(Recursive Length Prefix)編碼,其定義可見wiki,它可以將一個(gè)任意嵌套的字節(jié)數(shù)組([]byte),編碼成一個(gè)“展平”無嵌套的[]byte。1 byte取值范圍0x00 ~ 0xff,可以表示任意字符,所以[]byte可以線性的表示任意的數(shù)據(jù)。最簡單比如一個(gè)字符串,如果每個(gè)字符用ASCII碼的二進(jìn)制表示,整個(gè)字符串就變成一個(gè)[]byte。 RLP 編碼其實(shí)提供了一種序列化的編碼方法,無論輸入是何種嵌套形式的元素或數(shù)組,編碼輸出形式都是[]byte。RLP是可逆的,它提供了互逆的編碼、解碼方法。

Ethereum 中具體使用的哈希算法,就是對(duì)某個(gè)類型對(duì)象的RLP編碼值做了SHA3哈希運(yùn)算,可稱為RLP Hash。 Ethereum 在底層存儲(chǔ)中特意選擇了專門存儲(chǔ)和讀取[k, v] 鍵值對(duì)的第三方數(shù)據(jù)庫,[k, v] 中的v 就是某個(gè)結(jié)構(gòu)體對(duì)象的RLP編碼值([]byte),k大多數(shù)情況就是v的RLP編碼后的SHA-3哈希值

1.2 常用數(shù)據(jù)類型 哈希值和地址

兩個(gè)最常用的自定義數(shù)據(jù)類型common.Hash用來表示哈希值,common.Address表示地址

[plain]?view plain?copy
  • #?/commons/types.go??
  • const?(??
  • ????HashLength?=?32??
  • ????AddressLength?=?20??
  • )??
  • type?Hash?[HashLength]byte??
  • type?Address?[AddressLength]byte??
  • 在Ethereum 代碼里,所有用到的哈希值,都使用該Hash類型,長度為32bytes,即256 bits;Ethereum 中所有跟帳號(hào)(Account)相關(guān)的信息,比如交易轉(zhuǎn)帳的轉(zhuǎn)出帳號(hào)(地址)和轉(zhuǎn)入帳號(hào)(地址),都會(huì)用該Address類型表示,長度20bytes。

    big.Int是golang提供的數(shù)據(jù)類型,用來處理比較大的整型數(shù),當(dāng)然它也可以處理諸如64bit,32bit的常用整數(shù)。

    [plain]?view plain?copy
  • #?/go-1.x/src/math/big/int.go??
  • package?big??
  • type?Int?struct?{??
  • ????neg?bool??//?sign,?whether?negaive??
  • ????abs?nat???//?absolute?value?of?integer??
  • }??
  • big.Int是一個(gè)結(jié)構(gòu)體(struct),相當(dāng)于C++中的class,所以每次新建big.Int時(shí)可以用 x := new(big.Int), 返回一個(gè)指針。注意對(duì)Int的算術(shù)操作,要使用該對(duì)象的成員函數(shù),比如Add():

    [plain]?view plain?copy
  • func?(z?*Int)?Add(x,?y?*Int)?*Int???//?Add?sets?z?to?sum?x+y?and?returns?z??
  • ?Ethereum 代碼中, 很多整型變量的類型都選用big.Int,比如Gas和Ether。

    1.3 汽油(Gas)和以太幣(Ether)

    Gas, 是Ethereum里對(duì)所有活動(dòng)進(jìn)行消耗資源計(jì)量的單位。這里的活動(dòng)是泛化的概念,包括但不限于:轉(zhuǎn)帳,合約的創(chuàng)建,合約指令的執(zhí)行,執(zhí)行中內(nèi)存的擴(kuò)展等等。所以Gas可以想象成現(xiàn)實(shí)中的汽油或者燃?xì)狻?/p>

    Ether, 是Ethereum世界中使用的數(shù)字貨幣,也就是常說的以太幣。如果某個(gè)帳號(hào),Address A想要發(fā)起一個(gè)交易,比如一次簡單的轉(zhuǎn)帳,即向 Address B 發(fā)送一筆金額H,那么Address A 本身擁有的Ether,除了轉(zhuǎn)帳的數(shù)額H之外,還要有額外一筆金額用以支付交易所耗費(fèi)的Gas。

    如果可以實(shí)現(xiàn)Gas和Ether之間的換算,那么Ethereum系統(tǒng)里所有的活動(dòng),都可以用Ether來計(jì)量。這樣,Ether就有了點(diǎn)一般等價(jià)物,也就是貨幣的樣子。

    1.4 區(qū)塊是交易的集合

    區(qū)塊(Block)是Ethereum的核心結(jié)構(gòu)體之一。在整個(gè)區(qū)塊鏈(BlockChain)中,一個(gè)個(gè)Block是以單向鏈表的形式相互關(guān)聯(lián)起來的。Block中帶有一個(gè)Header(指針), Header結(jié)構(gòu)體帶有Block的所有屬性信息,其中的ParentHash 表示該區(qū)塊的父區(qū)塊哈希值, 亦即Block之間關(guān)聯(lián)起來的前向指針。只不過要想得到父區(qū)塊(parentBlock)對(duì)象,直接解析這個(gè)ParentHash是不夠的, 而是要將ParentHash同其他字符串([]byte)組合成合適的key([]byte), 去kv數(shù)據(jù)庫里查詢相應(yīng)的value才能解析得到。 Block和Header的部分成員變量定義如下:

    [plain]?view plain?copy
  • #?/core/types/block.go??
  • type?Block?struct?{??
  • ????header?*Header??
  • ????transactions?Transactions??//?type?Transactions?[]*Transaction??
  • ????...??
  • }??
  • type?Header?struct?{??
  • ????ParentHash?common.Hash??
  • ????Number?*big.Int??
  • ????...??
  • }???
  • Header的整型成員Number表示該區(qū)塊在整個(gè)區(qū)塊鏈(BlockChain)中所處的位置,每一個(gè)區(qū)塊相對(duì)于它的父區(qū)塊,其Number值是+1。這樣,整個(gè)區(qū)塊鏈會(huì)存在一個(gè)原始區(qū)塊,即創(chuàng)世塊(GenesisBlock), 它的Number是0,由系統(tǒng)自然生成而不必去額外挖掘(mine)。Block和BlockChain的實(shí)現(xiàn)細(xì)節(jié),之后會(huì)有更詳細(xì)的討論。

    Block中還有一個(gè)Tranction(指針)數(shù)組,這是我們這里關(guān)注的。Transaction(簡稱tx),是Ethereum里標(biāo)示一次交易的結(jié)構(gòu)體, 它的成員變量包括轉(zhuǎn)帳金額,轉(zhuǎn)入方地址等等信息。Transaction的完整聲明如下:

    [plain]?view plain?copy
  • #?/core/types/transaction.go??
  • type?Transaction?struct?{??
  • ????data?txdata??
  • ????hash,?size,?from?atomic.Value??//?for?cache??
  • }??
  • type?txdata?struct?{??
  • ????AccountNonce?uint64??
  • ????Price?*big.Int??
  • ????GasLimit?*big.Int??
  • ????Recipient?*common.Address??
  • ????Amount?*big.Int??
  • ????Payload?[]byte??
  • ????V,?R,?S?*big.Int???//?for?signature??
  • ????Hash?*common.Hash??//?for?marshaling??
  • }??
  • 每個(gè)tx都聲明了自己的(Gas)Price 和 GasLimit 。 Price指的是單位Gas消耗所折抵的Ether多少,它的高低意味著執(zhí)行這個(gè)tx有多么昂貴。GasLimit 是該tx執(zhí)行過程中所允許消耗資源的總上限,通過這個(gè)值,我們可以防止某個(gè)tx執(zhí)行中出現(xiàn)惡意占用資源的問題,這也是Ethereum中有關(guān)安全保護(hù)的策略之一。擁有獨(dú)立的Price和GasLimit, 也意味著每個(gè)tx之間都是相互獨(dú)立的。

    轉(zhuǎn)帳轉(zhuǎn)入方地址Recipient可能為空(nil),這時(shí)在后續(xù)執(zhí)行tx過程中,Ethereum 需要?jiǎng)?chuàng)建一個(gè)地址來完成這筆轉(zhuǎn)帳。Payload是重要的數(shù)據(jù)成員,它既可以作為所創(chuàng)建合約的指令數(shù)組,其中每一個(gè)byte作為一個(gè)單獨(dú)的虛擬機(jī)指令;也可以作為數(shù)據(jù)數(shù)組,由合約指令進(jìn)行操作。合約由以太坊虛擬機(jī)(Ethereum Virtual Machine, EVM)創(chuàng)建并執(zhí)行。

    細(xì)心的朋友在這里會(huì)有個(gè)疑問,為何交易的定義里沒有聲明轉(zhuǎn)帳的轉(zhuǎn)出方地址? 問的好,tx 的轉(zhuǎn)帳轉(zhuǎn)出方地址確實(shí)沒有如轉(zhuǎn)入方一樣被顯式的聲明出來,而是被加密隱藏起來了,在Ethereum里這個(gè)轉(zhuǎn)出方地址是機(jī)密,不能直接暴露。這個(gè)對(duì)tx加密的環(huán)節(jié),在Ethereum里被稱為簽名(sign), 關(guān)于它的實(shí)現(xiàn)細(xì)節(jié)容后再述。

    2. 交易的執(zhí)行

    Block 類型的基本目的之一,就是為了執(zhí)行交易。狹義的交易可能僅僅是一筆轉(zhuǎn)帳,而廣義的交易同時(shí)還會(huì)支持許多其他的意圖。Ethereum 中采用的是廣義交易概念。按照其架構(gòu)設(shè)計(jì),交易的執(zhí)行可大致分為內(nèi)外兩層結(jié)構(gòu)第一層是虛擬機(jī)外,包括執(zhí)行前將Transaction類型轉(zhuǎn)化成Message,創(chuàng)建虛擬機(jī)(EVM)對(duì)象,計(jì)算一些Gas消耗,以及執(zhí)行交易完畢后創(chuàng)建收據(jù)(Receipt)對(duì)象并返回等;第二層是虛擬機(jī)內(nèi),包括執(zhí)行轉(zhuǎn)帳,和創(chuàng)建合約并執(zhí)行合約的指令數(shù)組。

    2.1 虛擬機(jī)外

    2.1.1 入口和返回值

    執(zhí)行tx的入口函數(shù)是StateProcessor的Process()函數(shù),其實(shí)現(xiàn)代碼如下:

    [plain]?view plain?copy
  • #?/core/state_processor.go??
  • func?(p?*StateProcessor)?Process(block?*Block,?statedb?*StateDB,?cfg?vm.Config)?(types.Receipts,?[]*types.Log,?*big.Int,?error)?{??
  • ????var?{??
  • ????????receipts?????types.Receipts??
  • ????????totalUsedGas?=?big.NewInt(0)??
  • ????????header???????=?block.Header()??
  • ????????allLogs??????[]*types.Log??
  • ????????gp???????????=?new(GasPool).AddGas(block.GasLimit())??
  • ????}??
  • ????...??
  • ????for?i,?tx?:=?range?block.Transactions()?{??
  • ????????statedb.Prepare(tx.Hash(),?block.Hash(),?i)??
  • ????????receipt,?_,?err?:=?ApplyTransaction(p.config,?p.bc,?author:nil,?gp,?statedb,?header,?tx,?totalUsedGas,?cfg)??
  • ????????if?err?!=?nil?{?return?nil,?nil,?nil,?err}??
  • ????????receipts?=?append(receipts,?receipt)??
  • ????????allLogs?=?append(allLogs,?receipt.Logs...)??
  • ????}??
  • ????p.engine.Finalize(p.bc,?header,?statedb,?block.Transactions(),?block.Uncles(),?receipts)??
  • ????return?receipts,?allLogs,?totalUsedGas,?nil??
  • }??
  • GasPool 類型其實(shí)就是big.Int。在一個(gè)Block的處理過程(即其所有tx的執(zhí)行過程)中,GasPool 的值能夠告訴你,剩下還有多少Gas可以使用。在每一個(gè)tx執(zhí)行過程中,Ethereum 還設(shè)計(jì)了償退(refund)環(huán)節(jié),所償退的Gas數(shù)量也會(huì)加到這個(gè)GasPool里。

    Process()函數(shù)的核心是一個(gè)for循環(huán),它將Block里的所有tx逐個(gè)遍歷執(zhí)行。具體的執(zhí)行函數(shù)叫ApplyTransaction(),它每次執(zhí)行tx, 會(huì)返回一個(gè)收據(jù)(Receipt)對(duì)象。Receipt結(jié)構(gòu)體的聲明如下:


    Receipt 中有一個(gè)Log類型的數(shù)組,其中每一個(gè)Log對(duì)象記錄了Tx中一小步的操作。所以,每一個(gè)tx的執(zhí)行結(jié)果一個(gè)Receipt對(duì)象來表示;更詳細(xì)的內(nèi)容,由一組Log對(duì)象來記錄。這個(gè)Log數(shù)組很重要,比如在不同Ethereum節(jié)點(diǎn)(Node)的相互同步過程中,待同步區(qū)塊的Log數(shù)組有助于驗(yàn)證同步中收到的block是否正確和完整,所以會(huì)被單獨(dú)同步(傳輸)。

    Receipt的PostState保存了創(chuàng)建該Receipt對(duì)象時(shí),整個(gè)Block內(nèi)所有“帳戶”的當(dāng)時(shí)狀態(tài)。Ethereum 里用stateObject來表示一個(gè)賬戶Account,這個(gè)賬戶可轉(zhuǎn)帳(transfer value), 可執(zhí)行tx, 它的唯一標(biāo)示符是一個(gè)Address類型變量。 這個(gè)Receipt.PostState 就是當(dāng)時(shí)所在Block里所有stateObject對(duì)象的RLP Hash值。

    Bloom類型是一個(gè)Ethereum內(nèi)部實(shí)現(xiàn)的一個(gè)256bit長Bloom Filter。?Bloom Filter概念定義可見wikipedia,它可用來快速驗(yàn)證一個(gè)新收到的對(duì)象是否處于一個(gè)已知的大量對(duì)象集合之中。這里Receipt的Bloom,被用以驗(yàn)證某個(gè)給定的Log是否處于Receipt已有的Log數(shù)組中。

    2.1.2 消耗Gas,亦獎(jiǎng)勵(lì)Gas

    我們來看下StateProcessor.ApplyTransaction()的具體實(shí)現(xiàn),它的基本流程如下圖:


    ApplyTransaction()首先根據(jù)輸入?yún)?shù)分別封裝出一個(gè)Message對(duì)象和一個(gè)EVM對(duì)象,然后加上一個(gè)傳入的GasPool類型變量,由TransitionDb()函數(shù)完成tx的執(zhí)行,待TransitionDb()返回之后,創(chuàng)建一個(gè)收據(jù)Receipt對(duì)象,最后返回該Recetip對(duì)象,以及整個(gè)tx執(zhí)行過程所消耗Gas數(shù)量。

    GasPool對(duì)象是在一個(gè)Block執(zhí)行開始時(shí)創(chuàng)建,并在該Block內(nèi)所有tx的執(zhí)行過程中共享,對(duì)于一個(gè)tx的執(zhí)行可視為“全局”存儲(chǔ)對(duì)象; Message由此次待執(zhí)行的tx對(duì)象轉(zhuǎn)化而來,并攜帶了解析出的tx的(轉(zhuǎn)帳)轉(zhuǎn)出方地址,屬于待處理的數(shù)據(jù)對(duì)象;EVM 作為Ethereum世界里的虛擬機(jī)(Virtual Machine),作為此次tx的實(shí)際執(zhí)行者,完成轉(zhuǎn)帳和合約(Contract)的相關(guān)操作。

    我們來細(xì)看下TransitioinDb()的執(zhí)行過程(/core/state_transition.go)。假設(shè)有StateTransition對(duì)象st,? 其成員變量initialGas表示初始可用Gas數(shù)量,gas表示即時(shí)可用Gas數(shù)量,初始值均為0,于是st.TransitionDb() 可由以下步驟展開:

  • 購買Gas。首先從交易的(轉(zhuǎn)帳)轉(zhuǎn)出方賬戶扣除一筆Ether,費(fèi)用等于tx.data.GasLimit * tx.data.Price;同時(shí) st.initialGas = st.gas = tx.data.GasLimit;然后(GasPool) gp -= st.gas。
  • 計(jì)算tx的固有Gas消耗 - intrinsicGas。它分為兩個(gè)部分,每一個(gè)tx預(yù)設(shè)的消耗量,這個(gè)消耗量還因tx是否含有(轉(zhuǎn)帳)轉(zhuǎn)入方地址而略有不同;以及針對(duì)tx.data.Payload的Gas消耗,Payload類型是[]byte,關(guān)于它的固有消耗依賴于[]byte中非0字節(jié)和0字節(jié)的長度。最終,st.gas -= intrinsicGas
  • EVM執(zhí)行。如果交易的(轉(zhuǎn)帳)轉(zhuǎn)入方地址(tx.data.Recipient)為空,調(diào)用EVM的Create()函數(shù);否則,調(diào)用Call()函數(shù)。無論哪個(gè)函數(shù)返回后,更新st.gas。
  • 計(jì)算本次執(zhí)行交易的實(shí)際Gas消耗: requiredGas = st.initialGas - st.gas
  • 償退Gas。它包括兩個(gè)部分:首先將剩余st.gas 折算成Ether,歸還給交易的(轉(zhuǎn)帳)轉(zhuǎn)出方賬戶;然后,基于實(shí)際消耗量requiredGas,系統(tǒng)提供一定的補(bǔ)償,數(shù)量為refundGas。refundGas 所折算的Ether會(huì)被立即加在(轉(zhuǎn)帳)轉(zhuǎn)出方賬戶上,同時(shí)st.gas += refundGas,gp += st.gas,即剩余的Gas加上系統(tǒng)補(bǔ)償?shù)腉as,被一起歸并進(jìn)GasPool,供之后的交易執(zhí)行使用。
  • 獎(jiǎng)勵(lì)所屬區(qū)塊的挖掘者:系統(tǒng)給所屬區(qū)塊的作者,亦即挖掘者賬戶,增加一筆金額,數(shù)額等于 st.data,Price * (st.initialGas - st.gas)。注意,這里的st.gas在步驟5中被加上了refundGas, 所以這筆獎(jiǎng)勵(lì)金所對(duì)應(yīng)的Gas,其數(shù)量小于該交易實(shí)際消耗量requiredGas。
  • 由上可見,除了步驟3中EVM 函數(shù)的執(zhí)行,其他每個(gè)步驟都在圍繞著Gas消耗量作文章(EVM 虛擬機(jī)的運(yùn)行原理容后再述)。到這里,大家可以對(duì)Gas在以太坊系統(tǒng)里的作用有個(gè)初步概念,Gas就是Ethereum系統(tǒng)中的血液。

    步驟5的償退機(jī)制很有意思,設(shè)立它的目的何在?目前為止我只能理解它可以避免交易執(zhí)行過程中過快消耗Gas,至于對(duì)其全面準(zhǔn)確的理解尚需時(shí)日。

    步驟6就更有趣了,正是這個(gè)獎(jiǎng)勵(lì)機(jī)制的存在才會(huì)吸引社會(huì)上的礦工(miner)去賣力“挖礦”(mining)。越大的運(yùn)算能力帶來越多的的區(qū)塊(交易)產(chǎn)出,礦工也就能通過該獎(jiǎng)勵(lì)機(jī)制賺取越多的以太幣。

    2.1.3 交易的數(shù)字簽名

    Ethereum 中每個(gè)交易(transaction,tx)對(duì)象在被放進(jìn)block時(shí),都是經(jīng)過數(shù)字簽名的,這樣可以在后續(xù)傳輸和處理中隨時(shí)驗(yàn)證tx是否經(jīng)過篡改。Ethereum 采用的數(shù)字簽名是橢圓曲線數(shù)字簽名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比于基于大質(zhì)數(shù)分解的RSA數(shù)字簽名算法,可以在提供相同安全級(jí)別(in bits)的同時(shí),僅需更短的公鑰(public key)。關(guān)于ECDSA的算法理論和實(shí)現(xiàn)細(xì)節(jié),本系列會(huì)有另外一篇文章專門加以介紹。這里需要特別留意的是,tx的轉(zhuǎn)帳轉(zhuǎn)出方地址,就是對(duì)該tx對(duì)象作ECDSA簽名計(jì)算時(shí)所用的公鑰publicKey

    Ethereum中的數(shù)字簽名計(jì)算過程所生成的簽名(signature), 是一個(gè)長度為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對(duì)象的轉(zhuǎn)帳轉(zhuǎn)出方地址時(shí)(比如在需要執(zhí)行該交易時(shí)),Ethereum 會(huì)先從tx的signature中恢復(fù)出公鑰,再將公鑰轉(zhuǎn)化成一個(gè)common.Address類型的地址,signature由tx對(duì)象的三個(gè)成員變量R,S,V轉(zhuǎn)化成字節(jié)數(shù)組[]byte后拼接得到。

    Ethereum 對(duì)此定義了一個(gè)接口Signer, 用來執(zhí)行掛載簽名,恢復(fù)公鑰,對(duì)tx對(duì)象做哈希等操作。

    [plain]?view plain?copy
  • //?core/types/transaction_signing.go??
  • type?Signer?innterface?{??
  • ????Sender(tx?*Transaction)?(common.Address,?error)??
  • ????SignatureValues(tx?*Transaction,?sig?[]byte)?(r,?s,?v?*big.Int,?err?error)??
  • ????Hash(tx?*Transaction)?common.Hash??
  • ????Equal(Signer)?bool??
  • }??
  • 生成數(shù)字簽名的函數(shù)叫SignTx(),它會(huì)先調(diào)用其他函數(shù)生成signature, 然后調(diào)用tx.WithSignature()將signature分段賦值給tx的成員變量R,S,V。

    [plain]?view plain?copy
  • func?SignTx(tx?*Transaction,?s?Signer,?prv?*ecdsa.PrivateKey)?(*Transaction,?error)??
  • 恢復(fù)出轉(zhuǎn)出方地址的函數(shù)叫Sender(), 參數(shù)包括一個(gè)Signer, 一個(gè)Transaction,代碼如下:

    [plain]?view plain?copy
  • func?Sender(signer?Signer,?tx?*Transaction)?(common.Address,?error)?{??
  • ????if?sc?:=?tx.from().Load();?sc?!=?null?{??
  • ????????sigCache?:=?sc.(sigCache)//?cache?exists,??
  • ????????if?sigCache.signer.Equal(signer)?{??
  • ????????????return?sigCache.from,?nil??
  • ????????}???
  • ????}??
  • ????addr,?err?:=?signer.Sender(tx)??
  • ????if?err?!=?nil?{??
  • ????????return?common.Address{},?err??
  • ????}??
  • ????tx.from.Store(sigCache{signer:?signer,?from:?addr})?//?cache?it??
  • ????return?addr,?nil??
  • }??
  • Sender()函數(shù)體中,signer.Sender()會(huì)從本次數(shù)字簽名的簽名字符串(signature)中恢復(fù)出公鑰,并轉(zhuǎn)化為tx的(轉(zhuǎn)帳)轉(zhuǎn)出方地址。

    在上文提到的ApplyTransaction()實(shí)現(xiàn)中,Transaction對(duì)象需要首先被轉(zhuǎn)化成Message接口,用到的AsMessage()函數(shù)即調(diào)用了此處的Sender()。

    [plain]?view plain?copy
  • //?core/types/transaction.go??
  • func?(tx?*Transaction)?AsMessage(s?Signer)?(Message,error)?{??
  • ????msg?:=?Message{??
  • ????????price:?new(big.Int).Set(tx.data.price)??
  • ????????gasLimit:?new(big.Int).Set(tx.data.GasLimit)??
  • ????????...??
  • ????}??
  • ????var?err?error??
  • ????msg.from,?err?=?Sender(s,?tx)??
  • ????return?msg,?err??
  • }??
  • 在Transaction對(duì)象tx的轉(zhuǎn)帳轉(zhuǎn)出方地址被解析出以后,tx 就被完全轉(zhuǎn)換成了Message類型,可以提供給虛擬機(jī)EVM執(zhí)行了。

    2.2 虛擬機(jī)內(nèi)

    每個(gè)交易(Transaction)帶有兩部分內(nèi)容需要執(zhí)行:1. 轉(zhuǎn)帳,由轉(zhuǎn)出方地址向轉(zhuǎn)入方地址轉(zhuǎn)帳一筆以太幣Ether; 2. 攜帶的[]byte類型成員變量Payload,其每一個(gè)byte都對(duì)應(yīng)了一個(gè)單獨(dú)虛擬機(jī)指令。這些內(nèi)容都是由EVM(Ethereum Virtual Machine)對(duì)象來完成的。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 接口是針對(duì)state.StateDB 結(jié)構(gòu)體設(shè)計(jì)的本地行為接口,可為EVM提供statedb的相關(guān)操作; Interpreter結(jié)構(gòu)體作為解釋器,用來解釋執(zhí)行EVM中合約(Contract)的指令(Code)。

    注意,EVM 中定義的成員變量Context和StateDB, 僅僅聲明了變量名而無類型,而變量名同時(shí)又是其類型名,在Golang中,這種方式意味著宗主結(jié)構(gòu)體可以直接調(diào)用該成員變量的所有方法和成員變量,比如EVM調(diào)用Context中的Transfer()。

    2.2.1 完成轉(zhuǎn)帳

    交易的轉(zhuǎn)帳操作由Context對(duì)象中的TransferFunc類型函數(shù)來實(shí)現(xiàn),類似的函數(shù)類型,還有CanTransferFunc, 和GetHashFunc。

    [plain]?view plain?copy
  • //?core/vm/evm.go??
  • type?{??
  • ????CanTransferFunc?func(StateDB,?common.Address,?*big.Int)??
  • ????TransferFunc?func(StateDB,?common.Address,?common.Address,?*big.Int)??
  • ????GetHashFunc?func(uint64)?common.Hash??
  • }???
  • 這三個(gè)類型的函數(shù)變量CanTransfer, Transfer, GetHash,在Context初始化時(shí)從外部傳入,目前使用的均是一個(gè)本地實(shí)現(xiàn):

    [plain]?view plain?copy
  • //?core/evm.go??
  • func?NewEVMContext(msg?Message,?header?*Header,?chain?ChainContext,?author?*Address){??
  • ????return?vm.Context?{??
  • ????????CanTransfer:?CanTransfer,??
  • ????????Transfer:?Transfer,??
  • ????????GetHash:?GetHash(header,?chain),??
  • ????????...??
  • ????}??
  • }??
  • ??
  • func?CanTransfer(db?vm.StateDB,?addr?common.Address,?amount?*big.Int)?{??
  • ????return?db.GetBalance(addr).Cmp(amount)?>=?0??
  • }??
  • func?Transfer(db?vm.StateDB,?sender,?recipient?common.Address,?amount?*big.Int)?{??
  • ????db.SubBalance(sender,?amount)??
  • ????db.AddBalance(recipient,?amount)??
  • }??
  • 可見目前的轉(zhuǎn)帳函數(shù)Transfer()的邏輯非常簡單,轉(zhuǎn)帳的轉(zhuǎn)出賬戶減掉一筆以太幣,轉(zhuǎn)入賬戶加上一筆以太幣。由于EVM調(diào)用的Transfer()函數(shù)實(shí)現(xiàn)完全由Context提供,所以,假設(shè)如果基于Ethereum平臺(tái)開發(fā),需要設(shè)計(jì)一種全新的“轉(zhuǎn)帳”模式,那么只需寫一個(gè)新的Transfer()函數(shù)實(shí)現(xiàn),在Context初始化時(shí)賦值即可。

    有朋友或許會(huì)問,這里Transfer()函數(shù)中對(duì)轉(zhuǎn)出和轉(zhuǎn)入賬戶的操作會(huì)立即生效么?萬一兩步操作之間有錯(cuò)誤發(fā)生怎么辦?答案是不會(huì)立即生效。StateDB 并不是真正的數(shù)據(jù)庫,只是一行為類似數(shù)據(jù)庫的結(jié)構(gòu)體。它在內(nèi)部以Trie的數(shù)據(jù)結(jié)構(gòu)來管理各個(gè)基于地址的賬戶,可以理解成一個(gè)cache;當(dāng)該賬戶的信息有變化時(shí),變化先存儲(chǔ)在Trie中。僅當(dāng)整個(gè)Block要被插入到BlockChain時(shí),StateDB 里緩存的所有賬戶的所有改動(dòng),才會(huì)被真正的提交到底層數(shù)據(jù)庫。

    2.2.2 合約的創(chuàng)建和賦值

    合約(Contract)是EVM用來執(zhí)行(虛擬機(jī))指令的結(jié)構(gòu)體。先來看下Contract的定義:

    [plain]?view plain?copy
  • //?core/vm/contract.go??
  • type?ContractRef?interface?{??
  • ????Address()?common.Address??
  • }??
  • type?Contract?struct?{??
  • ????CallerAddress?common.Address??
  • ????caller?ContractRef??
  • ????self?ContractRef??
  • ??
  • ????jumpdests?destinations??
  • ????Code?[]byte??
  • ????CodeHash?common.Hash??
  • ????CodeAddr?*Address??
  • ????Input?[]byte??
  • ????Gas?uint64??
  • ????value?*big.Int??
  • ????Args?[]byte??
  • ????DelegateCall?bool??
  • }??
  • 在這些成員變量里,caller是轉(zhuǎn)帳轉(zhuǎn)出方地址(賬戶),self是轉(zhuǎn)入方地址,不過它們的類型都用接口ContractRef來表示;Code是 指令數(shù)組 ,其中每一個(gè)byte都對(duì)應(yīng)于一個(gè)預(yù)定義的虛擬機(jī)指令;CodeHash 是Code的RLP哈希值;Input是 數(shù)據(jù)數(shù)組, 是指令所操作的數(shù)據(jù)集合;Args 是參數(shù)。

    有意思的是self這個(gè)變量,為什么轉(zhuǎn)入方地址要被命名成self呢? Contract實(shí)現(xiàn)了ContractRef接口,返回的恰恰就是這個(gè)self地址。

    [plain]?view plain?copy
  • func?(c?*Contract)?Address()?common.Address?{??
  • ????return?c.self.Address()??
  • }??
  • 所以當(dāng)Contract對(duì)象作為一個(gè)ContractRef接口出現(xiàn)時(shí),它返回的地址就是它的self地址。那什么時(shí)候Contract會(huì)被類型轉(zhuǎn)換成ContractRef呢?當(dāng)Contract A調(diào)用另一個(gè)Contract B時(shí),A就會(huì)作為B的caller成員變量出現(xiàn)。Contract可以調(diào)用Contract,這就為系統(tǒng)在業(yè)務(wù)上的潛在擴(kuò)展,提供了空間。

    創(chuàng)建一個(gè)Contract對(duì)象時(shí),重點(diǎn)關(guān)注對(duì)self的初始化,以及對(duì)Code, CodeAddr 和Input的賦值。?

    另外,StateDB 提供方法SetCode(),可以將指令數(shù)組Code存儲(chǔ)在某個(gè)stateObject對(duì)象中; 方法GetCode(),可以從某個(gè)stateObject對(duì)象中讀取已有的指令數(shù)組Code。

    [plain]?view plain?copy
  • func?(self?*StateDB)?SetCode(addr?common.Address,?code?[]byte)??
  • func?(self?*StateDB)?GetCode(addr?common.Address)?code?[]byte??
  • stateObject 是Ethereum里用來管理一個(gè)賬戶所有信息修改的結(jié)構(gòu)體,它以一個(gè)Address類型變量為唯一標(biāo)示符。StateDB 在內(nèi)部用一個(gè)巨大的map結(jié)構(gòu)來管理這些stateObject對(duì)象。所有賬戶信息-包括Ether余額,指令數(shù)組Code, 該賬戶發(fā)起合約次數(shù)nonce等-它們發(fā)生的所有變化,會(huì)首先緩存到StateDB里的某個(gè)stateObject里,然后在合適的時(shí)候,被StateDB一起提交到底層數(shù)據(jù)庫。注意,一個(gè)Contract所對(duì)應(yīng)的stateObject的地址,是Contract的self地址,也就是轉(zhuǎn)帳的轉(zhuǎn)入方地址

    EVM 目前有五個(gè)函數(shù)可以創(chuàng)建并執(zhí)行Contract,按照作用和調(diào)用方式,可以分成兩類:

    • Create(), Call(): 二者均在StateProcessor的ApplyTransaction()被調(diào)用以執(zhí)行單個(gè)交易,并且都有調(diào)用轉(zhuǎn)帳函數(shù)完成轉(zhuǎn)帳
    • CallCode(), DelegateCall(), StaticCall():三者由于分別對(duì)應(yīng)于不同的虛擬機(jī)指令(1 byte)操作,不會(huì)用以執(zhí)行單個(gè)交易,也都不能處理轉(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)建一個(gè)Contract對(duì)象,并初始化其成員變量caller, self(addr), value和gas; 步驟(5)賦值Contract對(duì)象的Code, CodeHash, CodeAddr成員變量;步驟(6) 調(diào)用run()函數(shù)執(zhí)行該合約的指令,最后Call()函數(shù)返回。相關(guān)代碼可見:

    [plain]?view plain?copy
  • //?core/vm/evm.go??
  • func?(evm?*EVM)?Call(caller?ContractRef,?addr?common.Address,?input?[]byte,?gas?uint64,?value?*big.Int)?(ret?[]byte,?leftGas?*big.Int,?error){??
  • ????...??
  • ????var?snapshot?=?evm.StateDB.Snapshot()??
  • ????contract.SetCallCode(&addr,?evm.StateDB.GetCodeHash(addr),?evm.StateDB.GetCode(addr))??
  • ????ret,?err?=?run(evm,?snapshot,?contract,?input)??
  • ????return?ret,?contract.Gas,?err??
  • }??
  • 因?yàn)榇藭r(shí)(轉(zhuǎn)帳)轉(zhuǎn)入地址不為空,所以直接將入?yún)ddr初始化Contract對(duì)象的self地址,并可從StateDB中(其實(shí)是以addr標(biāo)識(shí)的賬戶stateObject對(duì)象)讀取出相關(guān)的Code和CodeHash并賦值給contract的成員變量。注意,此時(shí)轉(zhuǎn)入方地址參數(shù)addr同時(shí)亦被賦值予contract.CodeAddr。

    再來看看EVM.Create(),它用來處理(轉(zhuǎn)帳)轉(zhuǎn)入方地址為空的情況。


    與Call()相比,Create()因?yàn)闆]有Address類型的入?yún)ddr,其流程有幾處明顯不同:

    • 步驟(3)中創(chuàng)建一個(gè)新地址contractAddr,作為(轉(zhuǎn)帳)轉(zhuǎn)入方地址,亦作為Contract的self地址;
    • 步驟(6)由于contracrAddr剛剛新建,db中尚無與該地址相關(guān)的Code信息,所以會(huì)將類型為[]byte的入?yún)ode,賦值予Contract對(duì)象的Code成員;
    • 步驟(8)將本次執(zhí)行合約的返回結(jié)果,作為contractAddr所對(duì)應(yīng)賬戶(stateObject對(duì)象)的Code儲(chǔ)存起來,以備下次調(diào)用。

    還有一點(diǎn)隱藏的比較深,Call()有一個(gè)入?yún)nput類型為[]byte,而Create()有一個(gè)入?yún)ode類型同樣為[]byte,沒有入?yún)nput,它們之間有無關(guān)系?其實(shí),它們來源都是Transaction對(duì)象tx的成員變量Payload!調(diào)用EVM.Create()或Call()的入口在StateTransition.TransitionDb()中,當(dāng)tx.Recipent為空時(shí),tx.data.Payload 被當(dāng)作所創(chuàng)建Contract的Code;當(dāng)tx.Recipient 不為空時(shí),tx.data.Payload 被當(dāng)作Contract的Input。

    2.2.3 預(yù)編譯的合約

    EVM中執(zhí)行合約(指令)的函數(shù)是run(),其實(shí)現(xiàn)代碼如下:

    [plain]?view plain?copy
  • //?core/vm/evm.go??
  • func?run(evm?*EVM,?snapshot?int,?contract?*Contract,?input?[]byte)?([]byte,?error)?{??
  • ????if?contract.CodeAddr?!=?nil?{??
  • ????????precompiles?:=?PrecompiledContractsHomestead??
  • ????????...??
  • ????????if?p?:=?precompiles[*contract.CodeAddr];?p?!=?nil?{??
  • ????????????return?RunPrecompiledContract(p,?input,?contract)??
  • ????????}??
  • ????}??
  • ????return?evm.interpreter.Run(snapshot,?contract,?input)??
  • }??
  • 可見如果待執(zhí)行的Contract對(duì)象恰好屬于一組預(yù)編譯的合約集合-此時(shí)以指令地址CodeAddr為匹配項(xiàng)-那么它可以直接運(yùn)行;沒有經(jīng)過預(yù)編譯的Contract,才會(huì)由Interpreter解釋執(zhí)行。這里的"預(yù)編譯",可理解為不需要編譯(解釋)指令(Code)。預(yù)編譯的合約,其邏輯全部固定且已知,所以執(zhí)行中不再需要Code,僅需Input即可。

    在代碼實(shí)現(xiàn)中,預(yù)編譯合約只需實(shí)現(xiàn)兩個(gè)方法Required()和Run()即可,這兩方法僅需一個(gè)入?yún)nput。

    [plain]?view plain?copy
  • //?core/vm/contracts.go??
  • type?PrecompiledContract?interface?{??
  • ????RequiredGas(input?[]byte)?uint64??
  • ????Run(input?[]byte)?([]byte,?error)??
  • }??
  • func?RunPrecompiledContract(p?PrecompiledContract,?input?[]byte,?contract?*Contract)?(ret?[]byte,?err?error)?{??
  • ????gas?:=?p.RequiredGas(input)??
  • ????if?contract.UseGas(gas)?{??
  • ????????return?p.Run(input)??
  • ????}??
  • ????return?nil,?ErrOutOfGas??
  • }??
  • 目前,Ethereuem 代碼中已經(jīng)加入了多個(gè)預(yù)編譯合約,功能覆蓋了包括橢圓曲線密鑰恢復(fù),SHA-3(256bits)哈希算法,RIPEMD-160加密算法等等。相信基于自身業(yè)務(wù)的需求,二次開發(fā)者完全可以加入自己的預(yù)編譯合約,大大加快合約的執(zhí)行速度。

    2.2.4 解釋器執(zhí)行合約的指令

    解釋器Interpreter用來執(zhí)行(非預(yù)編譯的)合約指令。它的結(jié)構(gòu)體UML關(guān)系圖如下所示:


    Interpreter結(jié)構(gòu)體通過一個(gè)Config類型的成員變量,間接持有一個(gè)包括256個(gè)operation對(duì)象在內(nèi)的數(shù)組JumpTable。operation是做什么的呢?每個(gè)operation對(duì)象正對(duì)應(yīng)一個(gè)已定義的虛擬機(jī)指令,它所含有的四個(gè)函數(shù)變量execute, gasCost, validateStack, memorySize 提供了這個(gè)虛擬機(jī)指令所代表的所有操作。每個(gè)指令長度1byte,Contract對(duì)象的成員變量Code類型為[]byte,就是這些虛擬機(jī)指令的任意集合。operation對(duì)象的函數(shù)操作,主要會(huì)用到Stack,Memory, IntPool 這幾個(gè)自定義的數(shù)據(jù)結(jié)構(gòu)。

    這樣一來,Interpreter的Run()函數(shù)就很好理解了,其核心流程就是逐個(gè)byte遍歷入?yún)ontract對(duì)象的Code變量,將其解釋為一個(gè)已知的operation,然后依次調(diào)用該operation對(duì)象的四個(gè)函數(shù),流程示意圖如下:


    operation在操作過程中,會(huì)需要幾個(gè)數(shù)據(jù)結(jié)構(gòu): Stack,實(shí)現(xiàn)了標(biāo)準(zhǔn)容器 -棧的行為;Memory,一個(gè)字節(jié)數(shù)組,可表示線性排列的任意數(shù)據(jù);還有一個(gè)intPool,提供對(duì)big.Int數(shù)據(jù)的存儲(chǔ)和讀取。

    已定義的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個(gè)Log對(duì)象,這里n最大是4。還記得Log在何時(shí)被用到么?每個(gè)交易(Transaction,tx)執(zhí)行完成后,會(huì)創(chuàng)建一個(gè)Receipt對(duì)象用來記錄這個(gè)交易的執(zhí)行結(jié)果。Receipt攜帶一個(gè)Log數(shù)組,用來記錄tx操作過程中的所有變動(dòng)細(xì)節(jié),而這些Log,正是通過合適的LOGn指令-即合約指令數(shù)組(Contract.Code)中的單個(gè)byte,在其對(duì)應(yīng)的operation里被創(chuàng)建出來的。每個(gè)新創(chuàng)建的Log對(duì)象被緩存在StateDB中的相對(duì)應(yīng)的stateObject里,待需要時(shí)從StateDB中讀取。

    3. 小結(jié)

    以太坊的出現(xiàn)大大晚于比特幣,雖然明顯受到比特幣系統(tǒng)的啟發(fā),但在整個(gè)功能定位和設(shè)計(jì)架構(gòu)上卻做了很多更廣更深的思考和嘗試。以太坊更像是一個(gè)經(jīng)濟(jì)活動(dòng)平臺(tái),而并不局限一種去中心化數(shù)字代幣的產(chǎn)生,分發(fā)和流轉(zhuǎn)。本文從交易執(zhí)行的角度切入以太坊的系統(tǒng)實(shí)現(xiàn),希望能提供一點(diǎn)管中窺豹的作用。

    • Gas是Ethereum系統(tǒng)的血液。一切資源,活動(dòng),交互的開銷,都以Gas為計(jì)量單元。如果定義了一個(gè)GasPrice,那么所有的Gas消耗亦可等價(jià)于以太幣Ether。
    • Block是Transaction的集合。Block在插入BlockChain前,需要將所有Transaction逐個(gè)執(zhí)行。Transaction的執(zhí)行會(huì)消耗發(fā)起方的Ether,但系統(tǒng)在其執(zhí)行完成時(shí),會(huì)給予其作者(挖掘出這個(gè)Block的賬戶)一筆補(bǔ)償,這筆補(bǔ)償是“礦工”賺取收入的來源之一。
    • Ethereum 定義了自己的虛擬機(jī)EVM, 它與合約(Contract)機(jī)制相結(jié)合,能夠在提供非常豐富的操作的同時(shí),又能很好的控制存儲(chǔ)空間和運(yùn)行速度。Contract由Transaction轉(zhuǎn)化得到。
    • Ethereum 里的哈希函數(shù),用的是SHA-3,256 bits;數(shù)據(jù)(數(shù)組)的序列化,用的是RLP編碼,所以所有對(duì)象,數(shù)組的哈希算法,實(shí)際用的RLP + SHA-3。數(shù)字簽名算法,使用了橢圓曲線數(shù)字簽名算法(ECDSA)

    原文:?http://blog.csdn.net/teaspring/article/details/75389151

    總結(jié)

    以上是生活随笔為你收集整理的[以太坊源代码分析] I.区块和交易,合约和虚拟机的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。