【源码阅读】evmⅠ

2024-03-19 13:12
文章标签 源码 阅读 evm

本文主要是介绍【源码阅读】evmⅠ,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

代码位置如下:
代码
参考link
以太坊中有一个很重要的用途是智能合约,而其中evm模块是实现了执行智能合约的虚拟机。evm可以逐条解析执行智能合约的指令。
evm中的核心对象是EVM,代表一个以太坊虚拟机。其内部主要依赖:解释器Interoreter(循环解释执行给定的合约指令,直接遇到退出指令)、配置和状态数据库StateDB(用来提供数据的永久存储和查询)

1、创建EVM

前提是在core/state_processor.go中使用ApplyTransaction函数来处理交易。在这个函数里面,需要创建一个EVM来执行交易中的数据。
使用NewEVM函数来实现创建。

func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM

主要包括:

  1. ctx: 提供访问当前区块链数据和挖矿环境的函数和数据
  2. statedb: 以太坊状态数据库对象
  3. chainConfig: 当前节点的区块链配置信息
  4. vmConfig: 虚拟机配置信息
  5. 解释器
    而解释器的创建是通过函数NewEVMInterpreter实现的。该函数首先要根据以太坊的版本不同,为jumpTable字段进行不同的填充。

2、创建合约

使用EVM.Create函数来创建合约。通过当前合约的创建者地址和其账户中的 Nonce 值,计算出来一个地址值,作为合约的地址。然后将这个地址和其它参数传给 EVM.create 方法。同一份合约代码,每次创建合约时得到的合约地址都是不一样的,因为合约是通过发送交易创建,而每发送一次交易 Nonce 值都会改变。

func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
}

而EVM.create函数才是主要内容。

func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error)

在函数的第一部分,需要进行判断,包括合约的递归调用次数depth是否在限制之内、以及创建者是否有足够的以太币。每次创建完成就需要将创建者的nonce值加一,这样每一次创建合约的地址才会不一样。

	if evm.depth > int(params.CallCreateDepth) {return nil, common.Address{}, gas, ErrDepth}if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {return nil, common.Address{}, gas, ErrInsufficientBalance}nonce := evm.StateDB.GetNonce(caller.Address())if nonce+1 < nonce {return nil, common.Address{}, gas, ErrNonceUintOverflow}evm.StateDB.SetNonce(caller.Address(), nonce+1)

在完成一系列的判断之后,需要创建合约账户,将交易中规定的金额转到账户中,如果账户已经存在就返回错误。

	if evm.chainRules.IsBerlin {evm.StateDB.AddAddressToAccessList(address)}// Ensure there's no existing contract already at the designated addresscontractHash := evm.StateDB.GetCodeHash(address)if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) {return nil, common.Address{}, 0, ErrContractAddressCollision}// Create a new account on the statesnapshot := evm.StateDB.Snapshot()evm.StateDB.CreateAccount(address)if evm.chainRules.IsEIP158 {evm.StateDB.SetNonce(address, 1)}evm.Context.Transfer(evm.StateDB, caller.Address(), address, value)

在第三部分中需要创建一个contract对象,其中包括合约在执行过程中的必要信息,比如合约的创建者、合约的地址等。具体函数在contract.go文件里面。在合约创建之后,需要调用run函数来运行合约。如果合约运行成功,并且代码长度没有超过限制MaxCodeSize,就会将合约代码存储到stateDB中的合约账户中并且消耗一定的gas。因为在编译器阶段,会向源代码中插入其它代码,所以存储的是运行后的返回码。

//判断长度
if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize {err = ErrMaxCodeSizeExceeded}// Reject code starting with 0xEF if EIP-3541 is enabled.if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {err = ErrInvalidCode}//没有错误就写入数据库if err == nil {createDataGas := uint64(len(ret)) * params.CreateDataGasif contract.UseGas(createDataGas) {evm.StateDB.SetCode(address, ret)} else {err = ErrCodeStoreOutOfGas}}

2.1执行合约创建

主要实现在interpreter中的Run函数。首先迭代次数depth的增加和减少是在解释器中实现的,使用到了defer延迟函数。
之后我们判断readOnly和in.readOnly,只有当readOnly为真(即我们需要设置为只读)并且in.readOnly为假(即当前不处于只读状态)时,才会执行大括号内的代码。

	if readOnly && !in.readOnly {in.readOnly = truedefer func() { in.readOnly = false }()}

以太坊虚拟机的工作流程:
由solidity语言编写的智能合约,通过编译器编译成bytecode,之后发到以太坊上,以太坊底层通过evm模块支持合约的执行和调用,调用时根据合约获取代码,即合约的字节码,生成环境后载入到 EVM 执行。
所以需要opcode操作码和mem存储以及为了应对高并发情况下的栈资源问题,代码中创建了stack来保存一些被创造但未使用的栈空间。
主要函数内容以及解释如下:

	for {//如果处于调试模式,那么会记录执行前的状态信息,包括程序计数器(pc)、剩余的gas和操作的成本。if debug {// Capture pre-execution values for tracing.logged, pcCopy, gasCopy = false, pc, contract.Gas}// Get the operation from the jump table and validate the stack to ensure there are// enough stack items available to perform the operation.//从合约中获取当前程序计数器指向的操作op = contract.GetOp(pc)//从操作表中获取对应的操作operation := in.table[op]//获取操作的固定gas成本cost = operation.constantGas // For tracing// Validate stack 对stack进行验证if sLen := stack.len(); sLen < operation.minStack {return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack}} else if sLen > operation.maxStack {return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}}//如果剩余的gas不足以执行这个操作,那么返回错误if !contract.UseGas(cost) {return nil, ErrOutOfGas}//如果操作有动态的gas成本,那么计算新的内存大小并扩展内存以适应操作if operation.dynamicGas != nil {// All ops with a dynamic memory usage also has a dynamic gas cost.var memorySize uint64if operation.memorySize != nil {memSize, overflow := operation.memorySize(stack)if overflow {return nil, ErrGasUintOverflow}if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {return nil, ErrGasUintOverflow}}var dynamicCost uint64//计算动态的gas成本dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize)cost += dynamicCost // for tracing//如果计算过程中出现错误或者剩余的gas不足以支付动态的gas成本,那么返回错误if err != nil || !contract.UseGas(dynamicCost) {return nil, ErrOutOfGas}//如果处于调试模式,那么记录执行后的状态信息if debug {in.evm.Config.Tracer.CaptureState(pc, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err)logged = true}if memorySize > 0 {mem.Resize(memorySize)}} else if debug {in.evm.Config.Tracer.CaptureState(pc, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err)logged = true}// 执行操作res, err = operation.execute(&pc, in, callContext)if err != nil {break}//将程序计数器加一,准备执行下一个操作pc++}

这篇关于【源码阅读】evmⅠ的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/826103

相关文章

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

软件架构模式:5 分钟阅读

原文: https://orkhanscience.medium.com/software-architecture-patterns-5-mins-read-e9e3c8eb47d2 软件架构模式:5 分钟阅读 当有人潜入软件工程世界时,有一天他需要学习软件架构模式的基础知识。当我刚接触编码时,我不知道从哪里获得简要介绍现有架构模式的资源,这样它就不会太详细和混乱,而是非常抽象和易