以太坊虛擬機器(EVM)
以太坊虛擬機器(Ethereum Virtual Machine,簡稱EVM)是一個基於棧的虛擬機器,基於特定的環境資料,執行一系列的位元組程式碼形式的指令,以修改系統狀態。EVM目前提供了11類,140個指令。
EVM是一個準圖靈機,這個“準”的限定來源於其中的運算是通過引數gas來限制的,也就是限定了可以執行的運算總量。EVM的具體邏輯定義為程式碼執行函式(Ξ):
\((\boldsymbol{\sigma}', g', A, \mathbf{o}) \equiv \Xi(\boldsymbol{\sigma}, g, I)\)
其中,函式的引數部分,σ表示狀態,g表示可用gas,I表示執行環境資料;函式的返回值部分,σ'表示計算後的狀態,g'表示剩餘gas,A表示累積子狀態,o表示結果輸出。
EVM的具體實現邏輯包含:
- 基本定義: 對環境、指令集、EVM狀態的定義
- 合約程式碼執行: 將合約位元組程式碼解析為一系列指令及其引數,並迴圈執行。
- 指令執行: 根據執行環境和EVM狀態,對單條指令進行解析、計費、執行
- 指令解析: 獲取將位元組碼解析為指令及引數
- GAS計費: 計算指令執行所要消耗的gas數量,並校驗gas是否充足
- 棧操作: 出入棧操作,及其校驗
- 記憶體操作: 讀寫記憶體操作,記憶體分配等
- 指令運算: 執行指令的具體邏輯
- 異常停止: 因Gas不足、棧溢位等問題導致的程式執行終止。
- 正常停止: 按照程式碼邏輯完成指令的執行,並正常退出和輸出結果。
- 指令執行: 根據執行環境和EVM狀態,對單條指令進行解析、計費、執行
- 狀態修改: 基於執行前的狀態資料,完成gas支付、空賬戶清除(及其補貼用於抵扣部分gas)等狀態修改操作。
基本定義
指令集
目前,EVM提供了140個指令。其中,以太坊黃皮書中定義了135個,包含特定的無效指令INVALID
;以太坊改進提案(Ethereum Improvement Proposals,下文中簡稱EIPs)中定義了5個。
指令定義
每個指令的定義資訊包括:
- 位元組碼 :或稱指令編號,數值範圍0x00 - 0xff。
- 助記符 :或稱指令名稱
- 出棧數量
- 入棧數量
- 指令含義 :指令的基本描述、Gas費用、運算邏輯,以及機器狀態(棧、記憶體)的操作
//OpCode.java:L618
private final byte opcode;//位元組碼
private final int require;//出棧數量
private final Tier tier;//gas費用級別
private final int ret;//入棧數量
private final EnumSet<CallFlags> callFlags;//訊息呼叫型別集合,用於標註CALL類指令,如是否靜態呼叫
//OpCode.java:L636
private OpCode(int op, int require, int ret, Tier tier, CallFlags ... callFlags) ...
指定定義示例
下面以加法算術指令ADD
指令為例,其指令定義為:
- 位元組碼:
0x01
- 出棧數量:
2
,即ADD
指令引數,被加數與加數 - 入棧數量:
1
,即ADD
指令運算結果,被加數與加數之和 - Gas費用級別:
VeryLowTier
,對應gas數量為3
//OpCode.java:L45
ADD(0x01, 2, 1, VeryLowTier),
指令類別
以太坊黃皮書中將指令劃分為11個類別,且每類指令的編號的數值範圍不同。比如,停止和算術運算類指令編號的陣列區間為0x00 - 0x0f(表示為0s)。
指令類別及其部分指令:
- 0s:停止和算術運算
- 停止指令:
STOP
- 算術類(部分):
ADD
(加)、SUB
(減)、MUL
(乘)、DIV
(除)、MOD
(取模)、EXP
(指數)
- 停止指令:
- 10s:比較和按位邏輯運算
- 比較邏輯運算(部分):
LT
(小於)、GT
(大於)、EQ
(等於)、ISZERO
(否,結合EQ指令實現不等於) - 按位邏輯運算:
AND
(與)、OR
(或)、NOT
(非)、XOR
(異或)
- 比較邏輯運算(部分):
- 20s:SHA3運算,
SHA3
(Keccak-256雜湊) - 30s:環境資訊(部分),
ADDRESS
(獲取Ia)、GASPRICE
(獲取Ip)、BALANCE
(獲取給定賬戶餘額) - 40s:區塊資訊
BLOCKHASH
:獲取指定高度的祖先區塊的頭部雜湊COINBASE
:獲取該區塊的礦工賬戶地址TIMESTAMP
:獲取該區塊的時間戳NUMBER
:獲取區塊的號碼DIFFICULTY
:獲得區塊的難度GASLIMIT
:獲得區塊的gas上限
- 50s:棧,記憶體,儲存和流程操作
- 棧:
POP
(出棧) - 記憶體:
MLOAD
(從記憶體中載入字)、MSTORE
(將字儲存到記憶體)、MSTORE8
(將位元組儲存到記憶體)、MSIZE
(獲取活動記憶體的大小) - 儲存:
SLOAD
(從儲存載入字)、SSTORE
(將字儲存到儲存) - 流程控制:
JUMP
(跳轉)、JUMPI
(有條件的跳轉)、JUMPDEST
(μpc加1)、PC
(獲取μpc)、GAS
(獲取μg)
- 棧:
- 60s & 70s: 入棧(Push)操作,
PUSH1
-PUSH32
- 80s: 複製(Duplicate)操作,
DUP1
-DUP16
- 90s: 替換(Exchange)操作,
SWAP1
-SWAP16
- a0s: 日誌操作,
LOG0
-LOG4
- f0s: 系統操作(部分)
CREATE
:建立合約CALL
:訊息呼叫合約CALLCODE
:使用指定帳戶的程式碼對當前帳戶進行訊息呼叫RETURN
:停止執行,返回輸出資料DELEGATECALL
:使用指定帳戶的程式碼對當前帳戶進行訊息呼叫,但保留sender和value屬性現有的值。INVALID
:保留的無效指令SELFDESTRUCT
:銷燬合約
此外,還在EIPs中定義瞭如下指令:
- EIP-145中定義了3個按位邏輯運算(10s)
SHL
:0x1b,左移 (shift left)SHR
:0x1c,邏輯右移 (logical shift right)SAR
:0x1d,算術右移 (arithmetic shift right)
- EIP-1014中定義了1個系統操作指令(f0s)
CREATE2
:0xf5,建立指定地址的合約賬戶
- EIP-1052中定義了1個環境資訊獲取指令(30s)
EXTCODEHASH
:0x3f,獲取合同程式碼的keccak256雜湊
執行環境
執行環境資料,表示為I
,包含屬性:
Ia
,擁有正在執行的程式碼的賬戶地址,一般是指交易的傳送方賬戶S(T)
,在合約呼叫合約的情況下則不同。Io
,觸發這次執行的初始交易的傳送者地址,初始交易是指由外部賬戶發起的交易S(T)
。Ip
,觸發這次執行的初始交易的 gas 價格。Id
,這次執行的輸入資料位元組陣列;如果執行代理是一個交易,這就是交易資料。Is
,觸發這次執行的賬戶地址;如果執行代理是一個交易,則為交易傳送者地址。Iv
,作為這次執行的一部分傳到當前賬戶的轉賬金額,以 Wei為單位;如果執行代理是一個交易, 這就是交易的轉賬金額。Ib
,所要執行的EVM位元組碼陣列。IH
,當前區塊的區塊頭。Ie
,當前訊息呼叫或合約建立的深度(也就是當前已經被執行的CALL
或CREATE
的數量)。Iw
,修改狀態的許可。
這裡我們來回顧一下交易資料與待執行程式碼的對照關係:
- 訊息呼叫交易
Ib
:即交易的被呼叫合約Tt
,儲存在世界狀態樹中的合約程式碼σ[Tt]c
Id
:交易的訊息呼叫輸入資料Td
,儲存在交易附言(Transaction.data)屬性。- 合約建立交易
Ib
:即交易的合約初始化程式碼Ti
,也儲存在交易附言(Transaction.data)屬性。Id
:此類交易無輸入資料
EVM狀態
EVM狀態,也稱機器狀態(Machine State),表示為μ
。
- 可用Gas
可用gas,表示為μg
,根據交易gas上限和已用gas數量計算:gasLimit - gasUsed。
//ProgramResult.java:L40
private long gasUsed;//已用gas數量
//Program.java:L745,計算剩餘可用gas,對應黃皮書的125公式
public long getGasLong() {
return invoke.getGasLong() - getResult().getGasUsed();
}
- 程式計數器
程式計數器,表示為μpc
,初始值為0。
//Program.java:L98
private int pc; //程式計數器,初始值為0,對應黃皮書的126公式
- 記憶體
記憶體,表示為μm
,記憶體模型是基於字定址(word-addressed)的位元組陣列,所有記憶體中的資料都會初始化為0。
//Program.java:L89
private Memory memory;
//Memory.java:L33
public class Memory ... {//記憶體模型是簡單地基於字定址(word-addressed) 的位元組陣列
private static final int CHUNK_SIZE = 1024;//記憶體塊大小,記憶體按塊進行擴充套件(1 chunk = 32 word = 1024 byte)
private static final int WORD_SIZE = 32;//字的大小(1 word = 32 byte),對應黃皮書的9.1章節中的約定
private List<byte[]> chunks = new LinkedList<>();//記憶體塊資料列表
...
}
- 記憶體已啟用字數
記憶體已啟用字數,表示為μi,初始值為0。
//Memory.java:L39
private int softSize; //μi,記憶體已啟用的字數,初始值為0,對應黃皮書
- 棧
棧,表示為μs
,其中棧中資料項的大小(即字長)是256位,最大深度為1024,初始值為空序列。
//Program.java:L88
private Stack stack;
//Stack.java:L24
public class Stack extends java.util.Stack<DataWord> ... //棧的字大小為256位,對應黃皮書9.1章節的約定
- 結果資料
主要是指訊息呼叫指令的結果輸出資料,表示為μo
,初始值為空序列。
//Program.java:L91
private byte[] returnDataBuffer;//μo,機器狀態中的指令輸出,對應黃皮書的130公式
交易子狀態
交易執行過程中會產生一些特定的資訊,記錄交易相關的賬戶、日誌等內容,也稱為交易子狀態或累積子狀態。
- 自銷燬賬戶集合
合約程式碼執行過程中,通過SELFDESTRUCT
指令銷燬賬戶的地址的集合,表示為As
,初始值為空集合。
//ProgramResult.java:L45
private Set<DataWord> deleteAccounts;//自毀賬戶集合(As):一組應該在交易完成後被刪除的賬戶
- 接觸賬戶集合
交易執行過程中所接觸賬戶的地址的集合,表示為At
,初始值為空集合。此集合的特徵是交易執行過程中賬戶狀態發生過變化,包含新建立的合約賬戶、訊息呼叫接收方賬戶、銷燬賬戶餘額的繼承賬戶等,交易執行完成時將刪除其中的空賬戶。
//ProgramResult.java:L46
private ByteArraySet touchedAccounts = new ByteArraySet(); //接觸賬戶集合(At),其中的空賬戶可以在交易結束時刪除
- 日誌集合
合約程式碼執行中,輸出的一系列帶有主題和資料內容的日誌,並且記錄了輸出日誌的賬戶地址,表示為Al
,初始值為空。其中,日誌的所屬賬戶地址和主題可以作為檢索條件,通過區塊頭部和交易收據中的Bloom過濾器,實現區塊、交易二級索引的資訊查詢。
合約開發者通過日誌,可以獲取合約執行的詳細狀態,方便合約的上層應用實現一些類似於非同步通知或回撥功能,為應用終端使用者提供更加友好的體驗。
//ProgramResult.java:L48
private List<LogInfo> logInfoList;//日誌集合(Al):這是一些列針對VM程式碼執行的歸檔的、可索引的‘檢查點’,允許以太坊世界的外部旁觀者(例如去中心化應用的前端)來簡單地跟蹤合約呼叫。
- 返還Gas數量
以太坊為了鼓勵合約開發者儘可能少的佔用計算資源,對於釋放已佔用資源的操作給予一定的gas補貼以抵扣部分交易費用,如通過SSTORE
指令釋放合約儲存資源,通過SELFDESTRUCT
指令清空指定賬戶的狀態資料。合約程式碼執行中,將根據約定計算返還gas數量,表示為Ar
,初始值為0。
//ProgramResult.java:L49
private long futureRefund = 0;//返還gas數量(Ar):與SSTORE、SUICIDE(黃皮書中為SELFDESTRUCT)指令相關