以太坊智慧合約虛擬機器(EVM)原理與實現
以太坊底層通過EVM模組支援合約的執行與呼叫,呼叫時根據合約地址獲取到程式碼,生成環境後載入到EVM中執行。通常智慧合約的開發流程是用solidlity編寫邏輯程式碼,再通過編譯器編譯元資料,最後再發布到以太坊上。
程式碼結構
.
├── analysis.go //跳轉目標判定
├── common.go
├── contract.go //合約資料結構
├── contracts.go //預編譯好的合約
├── errors.go
├── evm.go //執行器 對外提供一些外部介面
├── gas.go //call gas花費計算 一級指令耗費gas級別
├── gas_table.go //指令耗費計算函式表
├── gen_structlog.go
├── instructions.go //指令操作
├── interface.go
├── interpreter.go //直譯器 呼叫核心
├── intpool.go //int值池
├── int_pool_verifier_empty.go
├── int_pool_verifier.go
├── jump_table.go //指令和指令操作(操作,花費,驗證)對應表
├── logger.go //狀態日誌
├── memory.go //EVM 記憶體
├── memory_table.go //EVM 記憶體操作表 主要衡量操作所需記憶體大小
├── noop.go
├── opcodes.go //Op指令 以及一些對應關係
├── runtime
│ ├── env.go //執行環境
│ ├── fuzz.go
│ └── runtime.go //執行介面 測試使用
├── stack.go //棧
└── stack_table.go //棧驗證
指令
OpCode
檔案opcodes.go中定義了所有的OpCode,該值是一個byte,合約編譯出來的bytecode中,一個OpCode就是上面的一位。opcodes按功能分為9組(運算相關,塊操作,加密相關等)。
//算數相關
const (
// 0x0 range - arithmetic ops
STOP OpCode = iota
ADD
MUL
SUB
DIV
SDIV
MOD
SMOD
ADDMOD
MULMOD
EXP
SIGNEXTEND
)
Instruction
檔案jump.table.go定義了四種指令集合,每個集合實質上是個256長度的陣列,名字翻譯過來是(荒地,農莊,拜占庭,君士坦丁堡)估計是對應了EVM的四個發展階段。指令集向前相容。
frontierInstructionSet = NewFrontierInstructionSet()
homesteadInstructionSet = NewHomesteadInstructionSet()
byzantiumInstructionSet = NewByzantiumInstructionSet()
constantinopleInstructionSet = NewConstantinopleInstructionSet()
具體每條指令結構如下,欄位意思見註釋。
typeoperation struct {
//對應的操作函式
execute executionFunc
// 操作對應的gas消耗
gasCost gasFunc
// 棧深度驗證
validateStack stackValidationFunc
// 操作所需空間
memorySize memorySizeFunc
halts bool // 運算中止
jumps bool // 跳轉(for)
writes bool // 是否寫入
valid bool // 操作是否有效
reverts bool // 出錯回滾
returns bool // 返回
}
按下面的ADD指令為例
定義
ADD: {
execute: opAdd,
gasCost: constGasFunc(GasFastestStep),
validateStack: makeStackFunc(2, 1),
valid: true,
},
操作
不同的操作有所不同,操作物件根據指令不同可能影響棧,記憶體,statedb。
funcopAdd(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
//彈出一個值,取出一個值(這個值依舊儲存在棧上面,運算結束後這個值就改變成結果值)
x, y := stack.pop(), stack.peek()
//加運算
math.U256(y.Add(x, y))
//數值快取
evm.interpreter.intPool.put(x)
return nil, nil
}
gas花費
不同的運算有不同的初始值和對應的運算方法,具體的方法都定義在gas_table裡面。 按加法的為例,一次加操作固定耗費為3。
//固定耗費
funcconstGasFunc(gas uint64)gasFunc {
return func(gt params.GasTable, evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64)(uint64, error) {
return gas, nil
}
}
除此之外還有兩個定義會影響gas的計算,通常作為量化的一個單位。
//file go-ethereum/core/vm/gas.go
const (
GasQuickStep uint64 = 2
GasFastestStep uint64 = 3
GasFastStep uint64 = 5
GasMidStep uint64 = 8
GasSlowStep uint64 = 10
GasExtStep uint64 = 20
GasReturn uint64 = 0
GasStop uint64 = 0
GasContractByte uint64 = 200
)
//file go-ethereum/params/gas_table.go
type GasTable struct {
ExtcodeSize uint64
ExtcodeCopy uint64
Balance uint64
SLoad uint64
Calls uint64
Suicide uint64
ExpByte uint64
// CreateBySuicide occurs when the
// refunded account is one that does
// not exist. This logic is similar
// to call. May be left nil. Nil means
// not charged.
CreateBySuicide uint64
}
memorySize
因為加操作不需要申請記憶體因而memorySize為預設值0。
棧驗證
先驗證棧上的運算元夠不夠,再驗證棧是否超出最大限制,加法在這裡僅需驗證其引數夠不夠,運算之後棧是要減一的。
funcmakeStackFunc(pop, push int)stackValidationFunc {
return func(stack *Stack)error {
//深度驗證
if err := stack.require(pop); err != nil {
return err
}
//最大值驗證
//StackLimit uint64 = 1024
if stack.len()+push-pop > int(params.StackLimit) {
return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit)
}
return nil
}
}
智慧合約
合約是EVM智慧合約的儲存單位也是直譯器執行的基本單位,包含了程式碼,呼叫人,所有人,gas相關的資訊.
typeContract struct {
// CallerAddressistheresultofthecallerwhichinitialisedthis
// contract. Howeverwhenthe "callmethod" isdelegatedthisvalue
// needstobeinitialisedtothatofthecaller'scaller.
CallerAddresscommon.AddresscallerContractRefselfContractRefjumpdestsdestinations // resultofJUMPDESTanalysis.
Code []byteCodeHashcommon.HashCodeAddr *common.AddressInput []byteGasuint64value *big.IntArgs []byteDelegateCallbool
}
EVM原生預編譯了一批合約,定義在contracts.go裡面。主要用於加密操作。
// PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum
// contracts used in the Byzantium release.
var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256Add{},
common.BytesToAddress([]byte{7}): &bn256ScalarMul{},
common.BytesToAddress([]byte{8}): &bn256Pairing{},
}
執行機
棧
EVM中棧用於儲存運算元,每個運算元的型別是big.int,這就是網上很多人說EVM是256位虛擬機器的原因。執行opcode的時候,從上往下彈出運算元,作為操作的引數。
type Stack struct {
data []*big.Int
}
func(st *Stack)push(d *big.Int) {
// NOTE push limit (1024) is checked in baseCheck
//stackItem := new(big.Int).Set(d)
//st.data = append(st.data, stackItem)
st.data = append(st.data, d)
}
func(st *Stack)peek() *big.Int {
return st.data[st.len()-1]
}
func(st *Stack)pop()(ret *big.Int) {
ret = st.data[len(st.data)-1]
st.data = st.data[:len(st.data)-1]
return
}
記憶體
記憶體用於一些記憶體操作(MLOAD,MSTORE,MSTORE8)及合約呼叫的引數拷貝(CALL,CALLCODE)。
記憶體資料結構,維護了一個byte陣列,MLOAD,MSTORE讀取存入的時候都要指定位置及長度才能準確的讀寫。
type Memory struct {
store []byte
lastGasCost uint64
}
// Set sets offset + size to value
func(m *Memory)Set(offset, size uint64, value []byte) {
// length of store may never be less than offset + size.
// The store should be resized PRIOR to setting the memory
if size > uint64(len(m.store)) {
panic("INVALID memory: store empty")
}
// It's possible the offset is greater than 0 and size equals 0. This is because
// the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP)
if size > 0 {
copy(m.store[offset:offset+size], value)
}
}
func(self *Memory)Get(offset, size int64)(cpy []byte) {
if size == 0 {
return nil
}
if len(self.store) > int(offset) {
cpy = make([]byte, size)
copy(cpy, self.store[offset:offset+size])
return
}
return
}
記憶體操作
funcopMload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
offset := stack.pop()
val := evm.interpreter.intPool.get().SetBytes(memory.Get(offset.Int64(), 32))
stack.push(val)
evm.interpreter.intPool.put(offset)
return nil, nil
}
funcopMstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
// pop value of the stack
mStart, val := stack.pop(), stack.pop()
memory.Set(mStart.Uint64(), 32, math.PaddedBigBytes(val, 32))
evm.interpreter.intPool.put(mStart, val)
return nil, nil
}
funcopMstore8(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
off, val := stack.pop().Int64(), stack.pop().Int64()
memory.store[off] = byte(val & 0xff)
return nil, nil
}
stateDb
合約本身不儲存資料,那麼合約的資料是儲存在哪裡呢?合約及其呼叫類似於資料庫的日誌,儲存了合約定義以及對他的一系列操作,只要將這些操作執行一遍就能獲取當前的結果,但是如果每次都要去執行就太慢了,因而這部分資料是會持久化到stateDb裡面的。code中定義了兩條指令SSTORE SLOAD用於從db中讀寫合約當前的狀態。
func opSload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
loc := common.BigToHash(stack.pop())
val := evm.StateDB.GetState(contract.Address(), loc).Big()
stack.push(val)
return nil, nil
}
func opSstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
loc := common.BigToHash(stack.pop())
val := stack.pop()
evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(val))
evm.interpreter.intPool.put(val)
return nil, nil
}
執行過程
執行入口定義在evm.go中,功能就是組裝執行環境(程式碼,執行人關係,引數等)。
func(evm *EVM)Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int)(ret []byte, leftOverGas uint64, err error) {
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, gas, nil
}
// 合約呼叫深度檢查
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// balance 檢查
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
var (
to = AccountRef(addr)
//儲存當前狀態,如果出錯,就回滾到這個狀態
snapshot = evm.StateDB.Snapshot()
)
if !evm.StateDB.Exist(addr) {
//建立呼叫物件的stateObject
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
//呼叫別人合約可能需要花錢
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
//建立合約環境
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
start := time.Now()
// Capture the tracer start/end events in debug mode
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
defer func()