天天看點

EVM原理及其功能擴充EVM原理及其功能擴充

EVM原理及其功能擴充

EVM運作機制概述

EVM即以太坊虛拟機,用于執行智能合約。智能合約可用進階開發語言Solidity進行開發,合約源代碼經過編譯得到可在EVM中運作的位元組碼。在部署合約、與合約互動的時候,位元組碼都是以16進制字元串形式傳遞和展現。

EVM運作過程中,其本身并不是一個獨立的協程、線程更不是程序,它隻是交易處理的一部分,在交易處理過程中以函數方式被調用。

調用路徑為:StateProcessor.Process --> core.ApplyTransaction(初始化evm對象) --> StateTransition.TransitionDb() --> 根據交易類型執行 evm.Create 或 evm.Call。

在evm.Create中會執行相關驗證、轉賬、初始化Contract對象并調用 run 函數開始執行合約代碼,執行成功後獲得傳回值也就是要存儲到鍊上的合約代碼,将傳回值存儲到鍊上。

而evm.Call既是調用入口,同時它本身也是一個可遞歸的函數,在合約位元組碼中指令 0xf1 即代表CALL操作,在CALL操作中會遞歸調用evm.Call。在evm.Call中會執行驗證及轉賬、從資料庫擷取合約代碼初始化Contract對象、并調用 run 函數開始執行合約代碼這些處理。

重要:上述"從資料庫擷取合約代碼初始化Contract對象"意味着,給合約賬戶進行轉賬時,合約賬戶所關聯的合約代碼将會被執行。
在合約代碼中,會固定包含檢查轉賬金額的邏輯,如果金額大于,則會執行合約的fallback函數。
如果合約沒有fallback函數,或者fallback函數沒有payable修飾符,則代碼執行會抛異常,進而交易失敗。
           

EVM是基于棧的虛拟機,另外會有一個記憶體空間用于臨時存儲資料,而最終大部分的指令都是對棧中的資料進行操作。

EVM資料存儲概述

合約狀态值(狀态變量、狀态常量,即需要持久化的内容)在底層資料庫中的存儲方式,可用幾條規則概括:

  1. 參照錄音帶存儲原理,可以認為一個合約對應了一條無限長的錄音帶,錄音帶上以32位元組為機關,擁有無數個存儲槽;每個存儲槽的位置就是它的key,也是用32位元組表示。
  2. 對于簡單的,大小在32位元組以内的變量,以定義變量的順序作為它的key來存儲變量值。即第一個變量的key為0,第二個變量的key為1,……
  3. 結構體和定長數組也是順序存儲(隻要每個值都是32位元組以内的),比如結構體變量定義在位置1,結構體内部要兩個成員,則這兩個成員的key依序為 1和2。數組類似,隻是在處理數組時編譯器會多加一些邊界檢查的代碼。
  4. 連續的若幹個小的值,可能被優化為存儲的同一個位置,比如:合約中前四個狀态變量都是uint64類型的,則四個狀态變量的值會被打包成一個32位元組的值存儲在0位置。
  5. map中内容的存儲,如果map中的value在32位元組以内,則會按以下公式得到資料庫中的key:keccak256(bytes32(map中的key)+bytes32(map變量的位置)); 例如,一個map變量在合約中最先定義,map中一個key為"abc",則其在資料庫中的存儲位置為:keccak256(bytes32("abc")+bytes32(0))
  6. 如果map中的value是一個複雜類型,存儲需求超過32位元組,則會按上述公式得到第一個存儲位置,然後依序加1得到後續資料的存儲位置;
  7. 可變長度數組,與map類似,但更複雜點:以數組變量所在位置為key,存儲數組的長度。然後從keccak256(bytes32(position))開始存儲數組中的元素;
  8. 可變長度位元組數組和字元串一樣:如果長度小于等于31位元組,則直接在變量位置處存儲字元串值,并用值的最後一個位元組存儲字元串的編碼長度。編碼長度 = 字元數 * 2 。比如,"abc"的存儲值為 "0x6162630000000000000000000000000000000000000000000000000000000006"
  9. 當可變長度位元組數組或字元串長度大于31位元組時,變量位置存儲的是 編碼長度,而此時編碼長度公式變為 編碼長度 = 字元數 * 2 + 1 。 然後,從 keccak256(bytes32(position))位置,使用連續的若幹個存儲槽存儲字元串值。 

    進而,對于字元串,如果編碼長度是奇數,則代表的是長字元串,如果是偶數則代表不超過31位元組的字元串。

EVM代碼結構

evm的代碼都在core包裡面,除了入口相關的一些代碼,具體運作合約的代碼都在core/vm包下

代碼檔案或結構體 說明
evm.go 定義了EVM運作環境結構體,并實作 轉賬處理 這些比較進階的,跟交易本身有關的功能
vm/evm.go 定義了EVM結構體,提供Create和Call方法,作為虛拟機的入口,分别對應建立合約和執行合約代碼
vm/interpreter.go 虛拟機的排程器,開始真正的解析執行合約代碼
vm/opcodes.go 定義了虛拟機指令碼(操作碼)
vm/instructions.go 絕大部分的操作碼對應的實作都在這裡
vm/gas_table.go 絕大部分操作碼所需的gas都在這裡計算
vm/jump_table.go 定義了operation,就是将opcode和gas計算函數、具體實作函數等關聯起來
vm/stack.go evm所需要的棧
vm/memory.go evm的記憶體結構
vm/intpool.go *big.Int的池子,主要是性能上的考慮,跟業務邏輯無關

evm的opcode,大體上可以粗略地分為兩類,一類是基礎操作,如壓棧、出棧、加減乘除等數學運算、邏輯比較、hash等等;另一類是跟交易業務密切相關的,可以稱為業務指令,比如BALANCE、ADDRESS、CALLER、CALL等等,這些指令有些對應了Solidity中的全局函數或屬性。

通過Solidity編寫的合約,需要進行編譯,編譯後變成可供虛拟機執行的二進制碼,這些二進制碼實際上在所有地方都按16進制位元組數組或字元串表示。

編譯後二進制碼的結構

以一個最簡單的無實際功能無構造器的合約為例,看看編譯後代碼的結構。 合約代碼:

pragma solidity ^0.4.11;
contract C {
}
           

編譯後得到這樣一串資料:

60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

 這串資料需要作為16進制格式顯示的位元組數組對待,這就是evm需要執行的代碼。

上面這串代碼可分為三部分,即:部署代碼、合約代碼、Auxdata。

  1. 部署代碼 在建立合約的時候,evm.Create會先建立合約賬戶,然後運作部署代碼,運作完成後,它會将 合約代碼+Auxdata 作為傳回值傳回,然後evm.Create函數中就會将傳回值跟合約賬戶關聯起來存儲在區塊鍊上,這樣就完成了合約的部署。 上述代碼中,部署代碼為前面的 

    60606040523415600e57600080fd5b5b603680601c6000396000f300

  2. 合約代碼 在這個合約中,合約代碼隻有11位元組:

    60606040525b600080fd00

     這部分的代碼就是存儲在鍊上,供後續調用的代碼。
  3. Auxdata 每個合約最後面的43位元組,就是Auxdata,會跟在合約代碼後面被存儲起來。即

    a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

     由于後續跟合約互動,是需要知道合約的ABI(應用二進制接口,也就是合約接口的描述資訊)的,而在鍊上卻沒有存儲ABI資訊, 是以,要麼就隻能在部署合約時,使用者自己儲存好ABI以及合約位址(以太坊錢包就有這樣的功能,幫我們儲存了這些資訊),但這樣的話就隻有自己能調用這個合約,别人不知道ABI就不能調用合約。 想要讓别人也能調用我們部署的合約,在以太坊中提供了兩種機制,一個就是使用者直接将相關資訊上傳到etherscan.io這個網站跟合約關聯起來,另一個就是以太坊的swarm網絡。 而這裡的Auxdata,就是給swarm網絡使用的,可以認為就是swarm網絡的位址,也就是以太坊希望後續自動将合約的相關資訊包含ABI存儲到swarm中,這樣任何一個人從區塊上查詢到合約代碼後,就可以通過auxdata到swarm網絡中下載下傳合約的資訊。 Auxdata的固定格式為:

    0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29

  4. 構造函數參數 如果合約有構造函數參數,則建立合約時還需要跟上編碼之後的參數,代碼就變成如下結構:部署代碼+合約代碼+Auxdata+構造參數 構造參數的編碼方式,就是将參數值按順序編碼成32位元組的資料,連接配接起來。不同的類型有不同的規則,具體見Solidity文檔。

功能擴充

evm是以太坊合約的執行部分,想要從合約程式設計語言層面擴充功能,就需要同時在Solidity上和evm上實作擴充。Solidity上實作功能擴充,最重要的就是弄清楚它的編譯過程。 Solidity是面向使用者的進階程式設計語言,其中的一句代碼,可能編譯後就對應了一堆位元組碼。 真正要擴充功能,主要涉及以下幾點:

  1. 增加opcode,進而與已有的opcode做功能上的區分,也就是擴充了指令
  2. 需要在Solidity語言中,提供供使用者調用的接口,比如對Solidity中的address對象,增加address.balanceOf(bytes10 symbol)函數;
  3. 在Solidity編譯器中,支援上述擴充的接口的識别,并編譯成一段正确的指令組合。這段指令組合從邏輯上可以認為由三部分組成:新擴充指令所需參數的準備階段+新擴充的指令+對結果的處理
  4. 在EVM中增加新擴充的指令的功能支援。

以下幾點,有助于了解如何進行功能擴充:

  1. evm是基于棧的,用一個位元組值表示指令。是以evm中指令的個數最多隻有0x0~0xff共256個。是以任何一個指令,都有如下一些屬性:(1)指令碼;(2)該指令需要消耗棧頂多少個元素;(3)該指令執行完後,會往棧裡壓入幾個元素;(4)其他一些屬性,如針對PUSH指令,它所額外需要的memory中的元素數量,以及它是否除了對棧的操作外,還産生其他的影響等。
  2. 需要定義清楚指令所需棧頂的若幹個元素的順序,棧頂值代表什麼,第二個值代表什麼,在執行到這個指令之前,所有資料需要在棧中就緒;也就是,編譯過程不能将進階語言的一條語句,對應成底層的一個指令,而應該是一堆指令。

在evm中擴充功能,相對比較簡單,具體就是:

  1. 在opcode中定義指令碼
  2. 在instructions中實作指令的處理,處理就是按照指令定義從棧頂取需要的資料,然後将結果壓入棧頂。
  3. 在gas_table中提供gas函數
  4. 在jump_table中增加由上述内容組合成的operation

參考

深入了解以太坊虛拟機

繼續閱讀