【以太坊剖析】以太坊虛擬機器(EVM)之基本定義

飛起航發表於2022-02-04

以太坊虛擬機器(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不足、棧溢位等問題導致的程式執行終止。
    • 正常停止: 按照程式碼邏輯完成指令的執行,並正常退出和輸出結果。
  • 狀態修改: 基於執行前的狀態資料,完成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,當前訊息呼叫或合約建立的深度(也就是當前已經被執行的 CALLCREATE 的數量)。
  • 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)指令相關

相關文章