概述
以太坊虛擬機 EVM 是智能合約的運行環境。它不僅是沙盒封裝的,而且是完全隔離的,也就是說在 EVM 中運行代碼是無法訪問網絡、文件系統和其他進程的。智能合約與其他智能合約也是只有有限接觸。
賬戶
以太坊中有兩類賬戶(它們共用同一個地址空間): 外部賬戶 由公鑰-私鑰對控制; 合約賬戶 由存儲在賬戶中的代碼控制。
外部賬戶的地址是由公鑰決定的,而合約賬戶的地址是在創建該合約時確定的(這個地址通過合約創建者的地址和從該地址發出過的交易數量計算得到的,也就是所謂的“nonce”)
無論帳戶是否存儲代碼,這兩類賬戶對 EVM 來說是一樣的。
每個賬戶都有一個鍵值對形式的持久化存儲。其中key和value的長度都是256比特,我們稱之為 存儲。
此外,每個賬戶有一個以太幣余額( balance )(單位是“Wei”),余額會因為發送包含以太幣的交易而改變。
交易
交易可以看作是從一個帳戶發送到另一個帳戶的消息(這里的賬戶,可能是相同的或特殊的零帳戶,請參閱下文)。它能包含一個二進制數據(合約負載)和以太幣。
如果目標賬戶含有代碼,此代碼會被執行,并以 payload 作為入參。
如果目標賬戶是零賬戶(賬戶地址為 0 ),此交易將創建一個 新合約 。 如前文所述,合約的地址不是零地址,而是通過合約創建者的地址和從該地址發出過的交易數量計算得到的(所謂的“nonce”)。 這個用來創建合約的交易的payload會被轉換為EVM字節碼并執行。執行的輸出將作為合約代碼被永久存儲。這意味著,為創建一個合約,你不需要向合約發送真正的合約代碼,而是發送能夠產生真正代碼的代碼。
Gas
一經創建,每筆交易都收取一定數量的 gas,目的是限制執行交易所需要的工作量和為交易支付手續費。EVM 執行交易時,gas 將按特定規則逐漸耗盡。
gas price 是被交易發送者設置的一個數值,發送者賬戶需要預付的手續費= gas_price * gas 。如果交易執行后還有剩余, gas 會原路返還。
無論執行到什么位置,一旦 gas 被耗盡(比如降為負值),將會觸發一個 out-of-gas 異常。當前調用幀所做的所有狀態修改都將被回滾。
存儲,內存和棧
每個賬戶有一塊持久化內存區被稱為 存儲。 存儲是一個 key-value 的鍵值對 ,其存儲著一個由256位的鍵到256位的值的映射. 在合約中,不能枚舉戶中的存儲,且存儲的讀操作相對開銷高,修改存儲開銷更高。一個合約只能對它自己的存儲進行讀寫。
第二個內存區稱為 內存,合約會試圖為每一次消息調用獲取一塊被重新擦拭干凈的內存實例。 內存是線性的,可按字節級尋址,但讀的長度被限制為256位,而寫的長度可以是8位或256位。當訪問(無論是讀還是寫)之前從未訪問過的內存字(word)時(無論是偏移到該字內的任何位置),內存將按字進行擴展(每個字是256 bit)。擴容也將消耗一定的gas。 內存越大,費用就越高(平方級別)。
EVM 不是基于寄存器的,而是基于棧的,因此所有的計算都在一個被稱為 stack 的區域執行。 棧最大有1024個元素,每個元素長度是一個字(256 bit)。對棧的訪問只限于其頂端,限制方式為:允許拷貝最頂端的16個元素中的一個到棧頂,或者是交換棧頂元素和下面16個元素中的一個。所有其他操作都只能取最頂的兩個(或一個,或更多,取決于具體的操作)元素,運算后,把結果壓入棧頂。當然可以把棧上的元素放到存儲或內存中。但是無法只訪問棧上指定深度的那個元素,除非先從棧頂移除其他元素。
指令集
EVM的指令集量應盡量少,以最大限度地避免可能導致共識問題的錯誤實現。所有的指令都是針對"256位的字(word)"這個基本的數據類型來進行操作。具備常用的算術、位、邏輯和比較操作。也可以做到有條件和無條件跳轉。此外,合約可以訪問當前區塊的相關屬性,比如它的編號和時間戳。
消息調用
合約可以通過消息調用的方式來調用其它合約或者發送以太幣到非合約賬戶。消息調用和交易非常類似,它們都有一個源、目標、數據、以太幣、gas和返回數據。事實上每個交易都由一個頂層消息調用組成,這個消息調用又可創建更多的消息調用。
合約可以決定在其內部的消息調用中,對于剩余的 gas,應發送和保留多少。如果在內部消息調用時發生了out-of-gas異常(或其他任何異常),這將由一個被壓入棧頂的錯誤值所指明。此時,只有與該內部消息調用一起發送的gas會被消耗掉。并且,Solidity中,發起調用的合約默認會觸發一個手工的異常,以便異常可以從調用棧里“冒泡出來”。 如前文所述,被調用的合約(可以和調用者是同一個合約)會獲得一塊剛剛清空過的內存,并可以訪問調用的payload——由被稱為 calldata 的獨立區域所提供的數據。調用執行結束后,返回數據將被存放在調用方預先分配好的一塊內存中。 調用深度被 限制 為 1024 ,因此對于更加復雜的操作,我們應使用循環而不是遞歸。
委托調用/代碼調用和庫
有一種特殊類型的消息調用,被稱為 委托調用(delegatecall)。它和一般的消息調用的區別在于,目標地址的代碼將在發起調用的合約的上下文中執行,并且 msg.sender 和 msg.value 不變。 這意味著一個合約可以在運行時從另外一個地址動態加載代碼。存儲、當前地址和余額都指向發起調用的合約,只有代碼是從被調用地址獲取的。 這使得 Solidity 可以實現”庫“能力:可復用的代碼庫可以放在一個合約的存儲上,如用來實現復雜的數據結構的庫。
日志
有一種特殊的可索引的數據結構,其存儲的數據可以一路映射直到區塊層級。這個特性被稱為 日志(logs),Solidity用它來實現 事件(events)。合約創建之后就無法訪問日志數據,但是這些數據可以從區塊鏈外高效的訪問。因為部分日志數據被存儲在 布隆過濾器(Bloom filter) 中,我們可以高效并且加密安全地搜索日志,所以那些沒有下載整個區塊鏈的網絡節點(輕客戶端)也可以找到這些日志。
創建
合約甚至可以通過一個特殊的指令來創建其他合約(不是簡單的調用零地址)。創建合約的調用 create calls 和普通消息調用的唯一區別在于,負載會被執行,執行的結果被存儲為合約代碼,調用者/創建者在棧上得到新合約的地址。
自毀
合約代碼從區塊鏈上移除的唯一方式是合約在合約地址上的執行自毀操作 selfdestruct 。合約賬戶上剩余的以太幣會發送給指定的目標,然后其存儲和代碼從狀態中被移除。