Ethereum 源码分析之EVM

Ethereum_evm

以太坊虚拟机规定了从一个区块到另一个区块计算新的有效状态的规则. 以太坊的状态是一个大型的数据结构,它不仅保存所有账户和余额.而且还保存一个机器状态,它可以根据预定义的一组规则在不同的区块之间进行更改,并且可以执行任意的机器代码.

1. 以太坊的基本概念

外部用户对ETH的访问: 可以发送创建合约的交易,消息调用的交易或者查询世界状态的值.
在这里插入图片描述

交易的顺序:

交易必须要满足原子性(要么全部完成,要么就是部分完成). 且用户propose transactions存放到pending transaction pool中,然后miner通过某种方式获得记账权,决定了哪些交易被打包到区块中且区块中的交易执行顺序.然而区块之间的顺序(决定哪个区块先上链)是由共识算法决定的.

在这里插入图片描述

2. Ethereum虚拟机的基本概念
2.1. 以太坊虚拟机层:

在这里插入图片描述

以太坊虚拟机代码的生成方式:

在这里插入图片描述

包含有Solidity source、Viper source以及LLL source相关.

2.1 以太坊虚拟机的组成:

分为两个部分虚拟机状态以及存储状态.

  1. 虚拟机状态: 程序计数器(PC),内存,堆栈,可用燃料.

  2. 存储状态: 以太坊虚拟机代码,账户存储.

在这里插入图片描述

2.2 以太坊状态转换函数:

EVM的行为就像一个数学函数:在给定输入的情况,它会产生确定性的输出.因此,将以太坊更正式地描述为具有状态转换函数非常有帮助.

Y(S,T)=S'
给定一个旧的有效状态(S),和一组新的有效交易(T),以太坊状态转换函数Y(S,T),
产生新的有效输出状态S'
2.3 交易:

交易可以分为消息调用交易(sender:外部账户或者合约账户)和创建合约账户交易(sender:外部账户).

账户类型分为:外部账户(是由私钥控制的)和合约账户(保存有EVM代码和存储树).

外部账户的地址可以是由私钥生成公钥,然后对公钥求hash,然后生成160位的字节.

合约账户的地址可以是由创建合约的sender address+nonce进行RLP编码,然后kec求hash后取右边160位作为地址.
在这里插入图片描述

EVM说明:

EVM作为一个堆栈机运行,其栈的深度为1024个项。每个项目都是256位字.为了便于使用,使用256位加密技术(如Keccak-256哈希或secp256k1签名).

内存: 在执行期间,EVM会维护一个瞬态内存 (作为字可寻址的字节数组),该内存不会在交易之间持久存在.

2.4 EVM的操作码:

已经编译的智能合约字节码作为以EVM 操作码为单位执行标准的堆栈操作.如XOR,AND,ADD,SUB等等.

在这里插入图片描述

在以太坊中所有可编程计算都需要付费.单位是以gas为例. 上述可以知道gas的消耗来自:操作码的执行,从世界状态中读取数据到stack中以及消息调用(message call). Eth的操作是对栈直接操作的,不会访问状态树和存储树的.

EVM的操作码具体实现(go-ethereum\core\vm\contract.go): GetOp的实现实际上是读取Contract.Code[n],然后返回.那么Code中的每个字节都代表一个操作码.操作码的高四位表示操作码的类型,操作码的低四位表示操作码的该类型的具体操作.

在这里插入图片描述

2.5 Stack,Memory,Account Storage

栈是由1024个元素组成,每个元素是256位.

在这里插入图片描述

  1. :每个操作只能对在栈顶的的16个大小为256位的元素操作. 具体的方式:

  2. 对栈的访问只限于其顶端的16个元素(16*256bits).限制方式:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面的16元素中的一个.所有其他对栈的操作都限制在取栈顶的两个或者1个元素,运算后,把结果压入栈顶.当然可以把栈中的元素放到存储(状态树)或者内存中. 但是无法访问栈上指定深度的那个元素,除非先从栈顶移除其他元素.

  3. EVM不是基于寄存器的,而是基于栈的,因此所有的计算都在一个称为栈的区域执行.所有的操作主要是对栈进行读取和修改的,以及消息调用.

内存: evm会为每次消息调用获取一块被重新擦拭干净的内存实例.内存是线性的,可以按照字节寻址,但读的长度被限制为256位,而写的长度可以是8位或者是256位.

  1. 每次从栈中读取和修改的以256位为单位.每次从memory中读取的单位为256位,从状态树中读取的单位也是256位

  2. 访问栈的指令有: PUSH,POP,COPY,SWAP.

访问内存的指令有: MSTORE,MSTORE8,MLOAD.

访问存储树的指令有: SSTORE/SLOAD.

在这里插入图片描述

  1. 内存字节序(Endian for Memory):

采用的是大端存储,那么意味着内存高字节放在栈的低地址. 下述介绍了内存和栈之间的数据交换:

在这里插入图片描述

内存的高位字节放在stack中放在低位上.

  1. 输入数据的字节序(Endian for input data)

采用的是大端存储,那么意味着输入数据的高字节放在内存或栈的地址处. 下述介绍了输入数据与内存/栈的数据交换:

在这里插入图片描述

  1. 在EVM中对storage trie和state trie的访问仍然使用的是go-ethereum\core\state\statedb.go中的相关接口
```go
StateDB.Empty(address)
StateDB.GetBalance(address)
StateDB.GetCode(addr)
StateDB.GetCodeHash(address)
StateDB.GetState(scope.Contract.Address(), hash)
StateDB.SetState(scope.Contract.Address(),loc.Bytes32(), val.Bytes32())
StateDB.GetBalance(scope.Contract.Address())
StateDB.AddBalance(beneficiary.Bytes20(), balance)
StateDB.Suicide(scope.Contract.Address())
evm.StateDB.AddLog(&types.Log{
            Address: scope.Contract.Address(),
            Topics:  topics,
            Data:    d,
            // This is a non-consensus field, but assigned here because
            // core/state doesn't know the current block number.
            BlockNumber: interpreter.evm.Context.BlockNumber.Uint64(),
})
 等等接口
```
2.6 消息调用(Message call)

EVM可以发送消息调用给其他账户(该账户可以是外部账户也可以是合约账户).

消息调用的发送方只能是合约账户.

  1. 在以太坊中,消息调用的深度被限制在少于1024层.

  2. 消息调用我们可以理解为EVM调用另一个EVM.

消息调用是由EVM内存中call 指令触发的,然后消息调用和返回值都是由共享内存的方式传递的.

在这里插入图片描述

其中包含了两个指令(Instruction): call instruction和return instruction.

2.7 异常(Exception):

异常的方式有: 跳转失败(invalid jump destination),指令不正确(invalid instruction),堆栈下溢(stack underflow)以及燃料用完(out-of-gas).

在这里插入图片描述

2.8 Gas的相关字段:
gasLimit:表示每笔交易所允许使用的最大燃气数量.
gasPrice:表示你愿意支付每单位燃气的费用.
gasFeeCap:表示支付给矿工的最大总费用限制.
gasTipCap:表示你愿意额外支付给矿工的最大费用.
3. Ethereum EVM源码分析:
3.1 EVM相关的各个实例:
type code []byte   //EVM code
type storage map[common.Hash]common.Hash //Account Storage
//core/vm/stack.go
type Stack struct{
    
     //stack
    data []*big.Int  //big.Int 256位
}
//core/vm/memory.go
type Memory struct{
    
    
    store []byte //Memory,按字节寻址
    lastGasCost uint64 
}
//core/vm/instruction.go,定义了些指令的实现.
//core/vm/gas.go 定义了gas燃料.

//core/vm/interpreter.go 
func (in *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err 
error)
//core/state_processor.go 定义交易的执行(ApplyTransaction)
func ApplyTransaction(config *params.ChainConfig, bc *BlockChain, author 
*common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx 
*types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, uint64, error)
定义消息的执行:
func ApplyMessage()
3.2 Bootstrap of EVM in Geth

根据下述的调用关系,我们可以知道一笔交易就会创建一个EVM.

处理区块的接口是:go-ethereum\core\state_processor.go Process方法其中会遍历block中的交易,为每个交易调用ApplyTransaction函数.

在go-ethereum\core\state_processor.go applyTrasanction前必须要先将交易转化为消息.其代码部分为:go-ethereum\core\types\transaction.go AsMessage 消息的数据结构在下述的代码块中.

在这里插入图片描述

根据上述代码中Run[core/vm/interpreter.go]的方法

func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) 

传入的是contract,因此为每一次智能合约的执行都会创建一个memory和stack.

//EVM的数据结构
type EVM struct {
    
    
    // Context provides axiliary blockchain related information
    Context BlockContext
    TxContext
    // StateDB gives access to the underlying state
    StateDB StateDB
    // Depth is the current call stack
    depth int

    // chainConfig contains information about the current chain
    chainConfig *params.ChainConfig
    // chain rules contains the chain rules for the current epoch
    chainRules params.Rules
    // virtual machine configuration options used to initialise the
    // evm.
    Config Config
    // global (to this context) ethereum virtual machine
    // used throughout the execution of the tx.
    interpreter *EVMInterpreter
    // abort is used to abort the EVM calling operations
    // NOTE: must be set atomically
    abort int32
    // callGasTemp holds the gas available for the current call. This is needed because the
    // available gas is calculated in gasCall* according to the 63/64 rule and later
    // applied in opCall*.
    callGasTemp uint64
}
//Message is a fully derived transaction and implements core.Message
type Message struct {
    
    
    to         *common.Address //消息调用交易的目的地址
    from       common.Address //消息调用交易的sender
    nonce      uint64 //消息调用交易的sender账户nonce
    amount     *big.Int //消息调用交易的sender账户的amount
    gasLimit   uint64 //此消息调用交易的所能花费的最大gas量(wei)
    gasPrice   *big.Int //此消息调用交易的每wei gas的价格
    gasFeeCap  *big.Int //此消息调用交易的愿意支付的最大eth
    gasTipCap  *big.Int //此消息调用所需要额外支付的eth
    data       []byte
    accessList AccessList //访问的列表
    isFake     bool
}

core/state_transaction.go:TransactionDb方法实现了什么?

func (st *StateTransition)TransitionDb()(*ExecutionResult,error)
 在这个接口中我们可以知道当msg.To为nil,该消息就是创建合约交易.
    //TODO the code module to process the msg
    if contractCreation {
    
    
        ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value)
    } else {
    
    
        // Increment the nonce for the next transaction
        st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
        ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value)
    }

上述接口主要做了6个方面的检查:

在这里插入图片描述

3.3 如何获得每笔交易访问和修改StateTrie的账户地址以及StorageTrie的Key
  1. 在EVM层面的修改: 在go-ethereum\core\vm\evm.go的EVM结构体加入两个字段(readSetIncludeStorage []state.SItems;writeSetIncludeStorage []state.SItems)

state.SItems的结构体组成为:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Blockchain210/article/details/134092679