The Ethereum Virtual Machine(EVM)簡介

WesleyWang97發表於2018-05-18

概述

EVM是以太坊智慧合約的執行時環境.它不僅僅是個沙盒,而是完全隔離的.這意味著程式碼在EVM中執行時沒有辦法連線網路,檔案系統或者其他程式,甚至一個智慧合約沒有辦法訪問另一個智慧合約.
官方文件地址:https://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html

賬號

以太坊有兩種不同型別的賬號共享著相同的地址空間:

  • 外部賬號(External accounts),受公鑰私鑰(public-private key),也就是人,控制
  • 合約賬號(contract accounts),受儲存在賬號中的程式碼控制

外部賬號的地址是由公鑰決定的.而一個合約賬號的地址是在合約建立時決定的(這個地址通過合約建立者的地址和從該地址發出過的交易數量計算得到的,也就是所謂的“nonce”)

無論帳戶是否儲存程式碼,這兩類賬戶對 EVM 來說是一樣的。

每個賬戶都有一個鍵值對形式的持久化儲存。其中 key 和 value 的長度都是256位,我們稱之為儲存(storage)

此外,每個賬戶有一個以太幣餘額( balance )(單位是“Wei”),餘額會因為傳送包含以太幣的交易而改變。

交易

交易可以看作是從一個帳戶傳送到另一個帳戶的訊息(這裡的賬戶,可能是相同的或特殊的零帳戶,請參閱下文)。它能包含一個二進位制資料(合約負載)和以太幣。

如果目標賬戶含有程式碼,此程式碼會被執行,並以合約負載(payload)作為入參。

如果目標賬戶是零賬戶(賬戶地址為 0 ),此交易將建立一個新合約。 如前文所述,合約的地址不是零地址,而是通過合約建立者的地址和從該地址發出過的交易數量計算得到的(所謂的“nonce”)。這個用來建立合約的交易的 payload 會被轉換為 EVM 位元組碼並執行。執行的輸出將作為合約程式碼被永久儲存。這意味著,為建立一個合約,你不需要向合約傳送真正的合約程式碼,而是傳送能夠產生真正程式碼的程式碼。

Gas

一經建立,每筆交易都收取一定數量的 gas ,目的是限制執行交易所需要的工作量和為交易支付手續費。EVM 執行交易時,gas 將按特定規則逐漸耗盡。

gas price 是交易傳送者設定的一個值,傳送者賬戶需要預付的手續費 = gas_price * gas 。如果交易執行後還有剩餘, gas 會原路返還。

無論執行到什麼位置,一旦 gas 被耗盡(比如降為負值),將會觸發一個 out-of-gas 異常。當前呼叫幀(call frame)所做的所有狀態修改都將被回滾。

譯者注:呼叫幀(call frame),指的是下文講到的EVM的執行棧(stack)中當前操作所需要的若干元素。

儲存,記憶體和棧

每個賬戶有一塊持久化記憶體區稱為 儲存 。 儲存是將256位字對映到256位字的鍵值儲存區。 在合約中列舉儲存是不可能的,且讀儲存的相對開銷很高,修改儲存的開銷甚至更高。合約只能讀寫儲存區內屬於自己的部分。

第二個記憶體區稱為 記憶體 ,合約會試圖為每一次訊息呼叫獲取一塊被重新擦拭乾淨的記憶體例項。 記憶體是線性的,可按位元組級定址,但讀的長度被限制為256位,而寫的長度可以是8位或256位。當訪問(無論是讀還是寫)之前從未訪問過的記憶體字(word)時(無論是偏移到該字內的任何位置),記憶體將按字進行擴充套件(每個字是256位)。擴容也將消耗一定的gas。 隨著記憶體使用量的增長,其費用也會增高(以平方級別)。

EVM 不是基於暫存器的,而是基於棧的,因此所有的計算都在一個被稱為 棧(stack) 的區域執行。棧最大有1024個元素,每個元素長度是一個字(256位)。對棧的訪問只限於其頂端,限制方式為:允許拷貝最頂端的16個元素中的一個到棧頂,或者是交換棧頂元素和下面16個元素中的一個。所有其他操作都只能取最頂的兩個(或一個,或更多,取決於具體的操作)元素,運算後,把結果壓入棧頂。當然可以把棧上的元素放到儲存或記憶體中。但是無法只訪問棧上指定深度的那個元素,除非先從棧頂移除其他元素。

指令集

EVM的指令集量應儘量少,以最大限度地避免可能導致共識問題的錯誤實現。所有的指令都是針對”256位的字(word)”這個基本的資料型別來進行操作。具備常用的算術、位、邏輯和比較操作。也可以做到有條件和無條件跳轉。此外,合約可以訪問當前區塊的相關屬性,比如它的編號和時間戳。

訊息呼叫

合約可以通過訊息呼叫的方式來呼叫其它合約或者傳送以太幣到非合約賬戶。訊息呼叫和交易非常類似,它們都有一個源、目標、資料、以太幣、gas和返回資料。事實上每個交易都由一個頂層訊息呼叫組成,這個訊息呼叫又可建立更多的訊息呼叫。

合約可以決定在其內部的訊息呼叫中,對於剩餘的 gas ,應傳送和保留多少。如果在內部訊息呼叫時發生了out-of-gas異常(或其他任何異常),這將由一個被壓入棧頂的錯誤值所指明。此時,只有與該內部訊息呼叫一起傳送的gas會被消耗掉。並且,Solidity中,發起呼叫的合約預設會觸發一個手工的異常,以便異常可以從呼叫棧裡“冒泡出來”。 如前文所述,被呼叫的合約(可以和呼叫者是同一個合約)會獲得一塊剛剛清空過的記憶體,並可以訪問呼叫的payload——由被稱為 calldata 的獨立區域所提供的資料。呼叫執行結束後,返回資料將被存放在呼叫方預先分配好的一塊記憶體中。 呼叫深度被 限制 為 1024 ,因此對於更加複雜的操作,我們應 使用迴圈而不是遞迴

委託呼叫/程式碼呼叫和庫

有一種特殊型別的訊息呼叫,被稱為 委託呼叫(delegatecall) 。它和一般的訊息呼叫的區別在於,目標地址的程式碼將在發起呼叫的合約的上下文中執行,並且 msg.sendermsg.value 不變。 這意味著一個合約可以在執行時從另外一個地址動態載入程式碼。儲存、當前地址和餘額都指向發起呼叫的合約,只有程式碼是從被呼叫地址獲取的。 這使得 Solidity 可以實現”庫“能力:可複用的程式碼庫可以放在一個合約的儲存上,如用來實現複雜的資料結構的庫。

日誌

有一種特殊的可索引的資料結構,其儲存的資料可以一路對映直到區塊層級。這個特性被稱為 日誌(logs) ,Solidity用它來實現 事件(events) 。合約建立之後就無法訪問日誌資料,但是這些資料可以從區塊鏈外高效的訪問。因為部分日誌資料被儲存在 布隆過濾器(Bloom filter) 中,我們可以高效並且加密安全地搜尋日誌,所以那些沒有下載整個區塊鏈的網路節點(輕客戶端)也可以找到這些日誌。

建立

合約甚至可以通過一個特殊的指令來建立其他合約(不是簡單的呼叫零地址)。建立合約的呼叫 create calls 和普通訊息呼叫的唯一區別在於,負載會被執行,執行的結果被儲存為合約程式碼,呼叫者/建立者在棧上得到新合約的地址。

自毀

合約程式碼從區塊鏈上移除的唯一方式是合約在合約地址上的執行自毀操作 selfdestruct 。合約賬戶上剩餘的以太幣會傳送給指定的目標,然後其儲存和程式碼從狀態中被移除。

儘管一個合約的程式碼中沒有顯式地呼叫 selfdestruct ,它仍然有可能通過 delegatecallcallcode 執行自毀操作。
目前, 外部賬戶 不能從狀態中移除。

相關文章