EVM 交易执行过程和 Opcodes 简介
一. EVM 的交易执行流程
在以太坊虚拟机(EVM)上,执行一笔交易是一个多步骤的过程,包括从用户提交交易到交易在区块链上被确认。以下是详细的交易执行流程:
1.交易创建与签名
💡交易创建:用户创建交易,其中包含以下关键信息:
💡💡接收地址(To Address):交易的目标地址,可以是一个外部账户或智能合约地址。
💡💡发送者地址(From Address):交易的发送者地址。
💡💡交易金额(Value):要发送的以太币数量。
💡💡Gas 价格(Gas Price):用户愿意支付的每单位 Gas 的价格。
💡💡Gas 限制(Gas Limit):用户愿意为交易支付的最大 Gas 数量。
💡💡交易序号(Nonce):发送账户的交易计数器,用于防止重放攻击。
💡💡数据(Data):可选的附加数据,例如智能合约函数调用和参数。
此处如果是 EIP1559 格式的交易的话,交易数据结构将有所变化,请参考以太坊交易类型一文。
💡交易签名:用户使用其私钥对交易进行签名,确保交易的真实性和完整性。
2.交易传播与验证
💡交易传播:签名后的交易通过对等网络(P2P)传播到其他节点。每个节点都会验证交易的合法性。
💡交易验证:节点会进行以下验证:
💡💡签名验证:验证交易的签名是否正确。
💡💡Nonce 验证:验证交易的随机数是否正确,以确保交易顺序。
💡💡账户余额验证:验证发送者账户是否有足够的余额支付交易金额和 Gas 费用。
3.交易打包与区块创建
- 交易排序:矿工节点会根据 Gas 价格对待处理的交易进行排序,优先选择 Gas 价格较高的交易。
- 交易打包:矿工节点将经过排序的交易打包到一个新的区块中。每个区块的 Gas 限制决定了可以包含的最大交易数。
- 区块挖矿:矿工节点通过工作量证明(PoW)算法进行挖矿,找到符合条件的区块哈希,成功后将新区块添加到区块链中。
4.交易执行
- EVM 初始化:当区块被挖出并添加到区块链时,EVM 会初始化交易的执行环境,包括设置账户状态、交易上下文等。
- Gas 计数:EVM 开始执行交易,并对每一步操作计算消耗的 Gas。如果 Gas 消耗超过用户设置的 Gas 限制,交易会中止,所有状态变更会回滚,但已消耗的 Gas 不会返还。
- 执行操作码:EVM 按顺序执行字节码中的每个操作码(Opcode),这些操作码可以是算术运算、逻辑运算、数据存储、合约调用等。
- 状态更新:交易执行过程中,EVM 会根据操作码的执行结果更新合约状态(存储)和内存。
5.交易结果与日志
💡交易结果:交易执行完毕后,EVM 会生成执行结果,包括:
💡💡返回值:如果是合约调用,返回值可能是函数的返回结果。
💡💡状态变化:包括账户余额变化、存储变量变化等。
💡💡事件日志:合约执行过程中生成的事件日志(Logs),可用于记录重要信息。
💡交易收据:节点生成交易收据,包含交易的基本信息、Gas 使用情况、事件日志等。
6.状态持久化
- 状态持久化:交易执行完毕并生成结果后,EVM 会将最终的状态变化持久化到区块链状态数据库中。包括账户余额、存储变量等。
- 区块链同步:其他节点会同步新区块和交易,确保区块链全网的一致性。
7.交易确认
- 确认机制:区块被添加到区块链后,交易被认为是已确认。随着后续区块的不断添加,交易确认数(确认深度)会增加,确认深度越大,交易被篡改的可能性越小。
- 用户通知:用户可以通过区块浏览器或钱包应用查询交易状态和确认情况。
8.示例流程
假设用户A向用户B发送1 ETH,Gas价格为20 Gwei,Gas限制为21000:
- 用户A创建并签署交易。
- 交易通过P2P网络传播并被矿工节点接收。
- 矿工节点验证交易并将其打包到区块中。
- 矿工节点挖出新区块,区块被添加到区块链中。
- EVM 执行交易,更新用户A和用户B的账户余额。
- 生成交易收据,包含交易详细信息和Gas使用情况。
- 交易被多个区块确认,用户A和用户B可在钱包或区块浏览器中看到交易状态。
通过上述步骤,交易在EVM上的执行流程涵盖了从创建、传播、验证、执行到确认的全过程。
交易执行过过程中,比较重要的一步就是 EVM 按顺序执行交易中的字节码中的每个操作码(Opcode),在以太坊虚拟机(EVM)中,每一笔交易的执行过程实际上是对智能合约字节码中操作码(Opcodes)的执行过程。每个操作码代表一个特定的操作,如计算、数据存储、逻辑运算等
9.字节码执行流程
- 初始化:EVM加载合约字节码,初始化执行环境(如堆栈、内存、存储、Gas计数器等)。
- 逐步执行:EVM逐条读取并执行操作码,按照指令修改堆栈、内存和存储状态。
- Gas消耗:每条操作码的执行都会消耗一定数量的Gas,确保计算资源的合理使用。
- 结束:程序正常结束、Gas耗尽或遇到错误时停止执行,返回执行结果或错误信息。
二.Opcodes 简介
EVM(以太坊虚拟机)的操作码(Opcode)是智能合约字节码的基本指令集。这些操作码指示 EVM 执行特定的操作。Solidity 编写的智能合约在部署到以太坊区块链之前,会被编译成字节码,这些字节码是由一系列 EVM 操作码(Opcodes)组成的。EVM 在执行合约时,会逐条解释和执行这些操作码,完成智能合约的逻辑。理解 EVM 操作码是开发和调试智能合约的重要技能。通过熟悉这些操作码及其功能,可以更好地理解和优化智能合约的执行过程。
下面是一些常见的操作
💡基础操作码
💡💡STOP (0x00): 停止执行。
💡💡ADD (0x01): 弹出堆栈顶的两个元素,相加后将结果压入堆栈。
💡💡MUL (0x02): 弹出堆栈顶的两个元素,相乘后将结果压入堆栈。
💡💡SUB (0x03): 弹出堆栈顶的两个元素,相减后将结果压入堆栈。
💡💡DIV (0x04): 弹出堆栈顶的两个元素,相除后将结果压入堆栈。
💡堆栈操作
💡💡PUSH1 (0x60): 将一个字节的数据压入堆栈。PUSH1 0x01将 0x01 压入堆栈。
💡💡POP (0x50): 弹出堆栈顶的元素。
💡💡DUP1 (0x80): 复制堆栈顶的元素并压入堆栈。
💡💡SWAP1 (0x90): 交换堆栈顶的元素与第二个元素的位置。
💡内存和存储操作
💡💡MLOAD (0x51): 从内存中加载数据。地址从堆栈顶弹出。
💡💡MSTORE (0x52): 将数据存储到内存中。地址和值从堆栈顶弹出。
💡💡SLOAD (0x54): 从存储中加载数据。键从堆栈顶弹出。
💡💡SSTORE (0x55): 将数据存储到存储中。键和值从堆栈顶弹出。
💡比较和逻辑操作
💡💡LT (0x10): 检查两个元素的大小,如果第一个小于第二个,则压入 1,否则压入 0。
💡💡GT (0x11): 检查两个元素的大小,如果第一个大于第二个,则压入 1,否则压入 0。
💡💡EQ (0x14): 检查两个元素是否相等,相等则压入 1,否则压入 0。
💡💡AND (0x16): 对两个元素进行按位与操作,并将结果压入堆栈。
💡💡OR (0x17): 对两个元素进行按位或操作,并将结果压入堆栈。
💡合约调用和控制流
💡💡CALL (0xF1): 调用另一个合约,传递一定的 gas 和输入数据。
💡💡DELEGATECALL (0xF4): 使用调用合约的上下文(包括存储)调用另一个合约。
💡💡RETURN (0xF3): 返回合约执行结果。
💡💡REVERT (0xFD): 失败并回退所有更改。
💡环境信息
💡💡ADDRESS (0x30): 获取当前合约地址并压入堆栈。
💡💡BALANCE (0x31): 获取给定地址的余额并压入堆栈。
💡💡ORIGIN (0x32): 获取交易的发起者地址并压入堆栈。
💡💡CALLER (0x33): 获取调用者地址并压入堆栈。
💡其他
💡💡LOG0, LOG1, LOG2, LOG3, LOG4 (0xA0-0xA4): 创建日志记录。
💡💡CREATE (0xF0): 创建新合约。
三. EVM 详细介绍
以太坊虚拟机(EVM)是以太坊区块链的核心组件之一,负责执行智能合约的字节码。
1. EVM 的概述
- 定义与作用:以太坊虚拟机(Ethereum Virtual Machine,EVM)是以太坊网络上运行智能合约的虚拟机,类似Java 的 JVM。它是一个基于堆栈的虚拟机,设计用于在去中心化计算机上执行智能合约。
- 状态转换:EVM负责处理以太坊账户之间的状态转换。每当在区块链上发送交易或调用智能合约时,EVM都会执行相应的操作来更新账户状态。
- 字节码执行:智能合约使用Solidity等高级语言编写,编译后生成EVM可以执行的字节码。EVM的指令集(Opcodes)定义了可以执行的操作,例如数学运算、内存操作、存储访问等。
2.EVM 的工作原理
- 堆栈架构:EVM是一个基于堆栈的虚拟机,每条指令都会在堆栈上执行操作。它使用固定大小的堆栈来处理操作数,操作数可以是整数、地址等数据类型。
- 内存和存储:EVM具有自己的内存和存储空间。内存用于临时存储数据,例如大整数运算时的中间结果。存储则用于永久保存合约的状态变量。
- 油费机制:为了防止 DDoS 攻击和确保网络资源被合理使用,以太坊引入了燃料(Gas)和油费(Gas Fee)概念。每个操作码都有一个固定的油费成本,执行合约需要支付足够的油费才能完成。
3.EVM 的组成部分
- 栈(Stack):用于存储操作数和中间结果。EVM 的操作是基于栈的,例如数据的压入(push)、弹出(pop)等。
- 内存(Memory):用于存储临时数据,例如复杂的运算过程中使用的临时变量。内存以字节为单位扩展,但每次扩展有固定的油费成本。
- 存储(Storage):永久保存在区块链上的合约状态变量。与内存不同,存储变量的访问有相对高昂的油费成本,因为它们会影响全局状态。
- 堆(Heap):虽然 EVM 本身没有堆的概念,但是智能合约可以通过复杂的内存管理实现类似于堆的动态分配。
4.EVM 的安全性和优化
- 安全性:EVM被设计为安全执行环境,但合约开发者需要注意编写安全的合约代码,避免可能导致资金丢失或漏洞的操作。
- 优化:智能合约的优化可以通过减少存储访问、合理使用内存和油费、避免过于复杂的操作等方式实现。合约的优化可以减少油费成本并提高执行效率。
5.EVM 的发展和未来
- EVM 与 Ewasm:以太坊正在探索采用以太坊WebAssembly(Ewasm)替代 EVM,以提高性能和安全性。Ewasm 支持更多的编程语言,同时保留与现有智能合约的兼容性。
- 并行 EVM: 很多公司和项目方也在研究并行的 EVM 解决 EVM 性能问题。
四. EVM 的几个核心概念
以太坊虚拟机(EVM)中的堆栈(Stack)是其核心部分之一,用于处理操作数和中间结果。以下是关于EVM堆栈的详细介绍:
1.堆栈
1.1.堆栈的基本概念
- 作用:EVM 堆栈是一个基于后进先出(LIFO)原则的内存区域,用于存储和管理合约执行过程中的操作数和临时变量。
- 大小:EVM 堆栈的大小是固定的,通常为 1024 个单元。每个单元的大小为 32 字节(256位),因此堆栈的总大小为 32 KB。
1.2.堆栈操作
EVM 定义了一组堆栈操作指令,允许合约在堆栈上进行以下操作:
- 压入(Push):将数据压入堆栈顶部。
- 弹出(Pop):从堆栈顶部弹出数据。
- 复制(Dup):复制堆栈中的数据,将其放置在堆栈的更深位置。
- 交换(Swap):交换堆栈中的两个元素的位置。
1.3.堆栈指令集
以下是 EVM 中常见的堆栈操作指令:
- PUSH 指令:将数据压入堆栈。 示例:PUSH1 0x20 将字节码 0x20 压入堆栈顶部。
- POP 指令:从堆栈顶部弹出数据。 示例:POP 弹出堆栈顶部的数据。
- DUP 指令:复制堆栈中的数据。 示例:DUP2 复制堆栈中第2个位置的数据,并将其放置在堆栈顶部。
- SWAP 指令:交换堆栈中的两个元素。 示例:SWAP1 交换堆栈顶部的两个元素。
1.4.堆栈的限制和注意事项
- 深度限制:EVM对堆栈的深度有限制,通常为1024个单元。如果堆栈溢出或超过了最大深度,将导致合约执行失败。
- 数据类型:堆栈操作通常是基于32字节数据块(256位),因此操作数通常需要在合约编写时考虑到这种限制。
- 油费成本:每个堆栈操作都有相应的油费成本,这是为了确保合约执行不会过于复杂或滥用网络资源。
1.5.堆栈在智能合约中的应用
- 运算处理:堆栈在执行加减乘除等算术运算时起到关键作用,通过堆栈操作来处理操作数和中间结果。
- 函数调用:在调用合约中的其他函数时,EVM会使用堆栈来管理函数调用的参数和返回值。
- 状态管理:虽然EVM堆栈主要用于处理临时数据和操作数,但它也可以在某些情况下用于简单的状态管理,例如通过局部变量实现简单的状态机制。
2.内存(Memory)
以太坊虚拟机(EVM)中的内存(Memory)是一个临时存储区域,用于智能合约执行过程中存放数据。内存的特点是易变的,也就是说,它仅在合约执行期间有效,当执行结束后,内存中的数据会被清除。
2.1.内存的基本概念
- 定义:内存是EVM用于存储临时数据的区域。与堆栈不同,内存是线性地址空间,可以随机访问。
- 大小:内存大小是动态的,可以根据需要进行扩展。内存的初始大小为0字节,随着存储需求的增加,内存可以按需扩展。
- 单位:内存按字节(Byte)进行寻址,每个地址指向一个字节的数据。
2.2.内存操作
EVM提供了一组内存操作指令,允许智能合约在内存中读写数据。这些指令包括:
- 加载(MLOAD):从内存中读取数据。
- 存储(MSTORE):向内存中写入数据。
- 扩展(MSTORE8):向内存中写入一个字节的数据。
2.3.内存指令集
以下是EVM中常见的内存操作指令:
- MLOAD 指令:从内存中读取32字节的数据。 示例:MLOAD 将读取内存中的一个32字节块,并将其放入堆栈顶部。
- MSTORE 指令:将32字节的数据存储到内存中。 示例:MSTORE 将堆栈顶部的32字节数据存储到内存的指定位置。
- MSTORE8 指令:将一个字节的数据存储到内存中。 示例:MSTORE8 将堆栈顶部的一个字节数据存储到内存的指定位置。
2.4.内存扩展和成本
- 内存扩展:内存是按需扩展的,初始为0字节。当需要写入数据到超出当前内存大小的位置时,内存会自动扩展。内存扩展会消耗油费(Gas)。
- 油费成本:内存操作的油费成本取决于操作类型和内存大小。每32字节的内存扩展会有额外的油费成本,以确保合约执行的合理性。
2.5.内存在智能合约中的应用
- 参数传递:在合约调用和函数调用中,参数通常通过内存进行传递。尤其是涉及到较大数据块时,内存的使用更加频繁。
- 临时存储:在计算过程中,内存用于存储临时变量和中间结果,便于后续操作和结果处理。
- 数据处理:内存适用于处理较大和复杂的数据结构,例如数组和字符串,提供灵活的读写操作。
2.6.示例代码
以下是一个使用内存的简单Solidity智能合约示例:
pragma solidity ^0.8.0;
contract MemoryExample {
function useMemory(uint256 value) public pure returns (uint256) {
uint256[1] memory data;
data[0] = value;
uint256 result = data[0];
return result;
}
}
在这个示例中:
- 使用内存存储和读取传入的值。
- 通过MSTORE和MLOAD指令实现内存操作。
3.存储 Storage
以太坊虚拟机(EVM)中的存储(Storage)是一个持久化的存储区域,用于智能合约的状态变量。这些存储数据在合约生命周期内保持不变,除非被显式修改。
3.1.存储的基本概念
- 定义:存储是 EVM 用于保存合约状态变量的持久化存储区域。与内存不同,存储的数据在区块链上持久保存,合约之间的状态数据通过存储来保持。
- 结构:存储是一个键值对(key-value)存储,每个键和值都是 32 字节(256位)长。这意味着存储的数据可以表示为一个大数组,其中每个单元的大小为 32 字节。
3.2.存储操作
EVM提供了一组操作指令,用于智能合约在存储中读写数据。这些指令包括:
- 存储加载(SLOAD):从存储中读取数据。
- 存储存储(SSTORE):向存储中写入数据。
3.3.存储指令集
以下是EVM中常见的存储操作指令:
- SLOAD 指令:从存储中读取32字节的数据。 示例:SLOAD 将从存储中指定位置读取32字节数据,并将其放入堆栈顶部。
- SSTORE 指令:将32字节的数据存储到存储中。 示例:SSTORE 将堆栈顶部的32字节数据存储到存储的指定位置。
3.4.存储的成本
💡油费成本:存储操作的油费成本非常高,以确保网络资源的合理使用。存储写入操作(SSTORE)的成本远高于读取操作(SLOAD)。这是因为存储写入会对区块链状态进行永久性修改,而读取操作只是从已有的数据中提取信息。
💡💡存储写入(SSTORE):大约20000 Gas(如果存储位置从0变为非0),而修改非零存储位置的成本为5000 Gas。
💡💡存储读取(SLOAD):大约800 Gas。
3.5.存储在智能合约中的应用
- 状态变量:合约中的状态变量默认存储在存储区域。例如,合约中的整数、映射、数组等。
- 数据持久化:存储用于保存合约的长期数据,这些数据在合约调用之间保持不变。
3.6.存储布局
EVM存储是线性地址空间,智能合约中的状态变量按照定义的顺序存储在存储空间中。映射和动态数组的存储稍微复杂一些,它们的存储位置是通过哈希计算得出的。
3.7.示例代码
以下是一个使用存储的简单Solidity智能合约示例:
pragma solidity ^0.8.0;
contract StorageExample {
uint256 public storedValue;
function setStoredValue(uint256 value) public {
storedValue = value; // 这将使用 SSTORE 操作
}
function getStoredValue() public view returns (uint256) {
return storedValue; // 这将使用 SLOAD 操作
}
}
在这个示例中:
- storedValue 是一个状态变量,存储在EVM的存储区域中。
- setStoredValue 函数使用 SSTORE 操作将新的值写入存储。
- getStoredValue 函数使用 SLOAD 操作从存储中读取当前的值。
3.8.存储布局示例
假设合约中有以下状态变量:
contract Example {
uint256 public value1;
uint256 public value2;
mapping(uint256 => uint256) public map;
}
- value1 存储在存储位置 0x00。
- value2 存储在存储位置 0x01。
- map 的值存储在 keccak256(keccak256(key) + storage_slot_of_map) 计算得到的位置。
3.9.总结
EVM存储是智能合约中至关重要的一部分,用于保存合约的长期状态。通过了解存储的工作原理和使用方法,开发者可以有效地管理合约的状态变量,确保数据的持久性和安全性。存储操作的高油费成本提醒我们在设计合约时需要优化存储的使用,以减少不必要的存储写入操作,从而降低交易成本。
4.Gas 模型
在以太坊网络中,Gas 是衡量执行交易或智能合约操作所需计算资源的单位。Gas 模型的运行机制旨在防止滥用网络资源,确保以太坊网络的安全性和稳定性。
4.1.Gas 的基本概念
- 定义:Gas 是用来衡量以太坊网络中计算和存储操作成本的单位。每个操作(例如发送交易、执行智能合约函数)都需要消耗一定数量的 Gas。
- 目的:Gas 模型的主要目的是防止滥用网络资源,确保矿工获得报酬,并激励用户优化他们的代码以减少不必要的操作。
4.2.Gas 费用的计算
- 基础费用(Base Fee):每个区块都有一个基础费用,这是所有交易必须支付的最低费用。这一费用是动态调整的,取决于区块的使用情况。
- 优先费(Tip):用户可以在基础费用之外支付额外的小费(也称为优先费)来激励矿工优先处理他们的交易。
- 总费用(Total Fee):总费用是基础费用和优先费的总和。
4.3.Gas 模型的运行机制
- 交易提交:用户在提交交易时,会指定一个 Gas 价格(Gas Price),表示他们愿意支付的每单位 Gas 的费用。用户还会设置一个 Gas 限制(Gas Limit),表示他们愿意为交易支付的最大 Gas 数量。
- 交易执行:当矿工打包交易时,他们会检查交易的 Gas 价格和 Gas 限制。矿工会优先选择 Gas 价格较高的交易来打包,从而最大化他们的收益。
- Gas 消耗:在执行交易时,EVM 会逐条执行字节码操作,每个操作都会消耗一定数量的 Gas。如果交易的总 Gas 消耗超过了用户设置的 Gas 限制,交易将会失败,但已消耗的 Gas 不会退还。
- 交易完成:如果交易执行成功,未消耗的 Gas 会返还给用户。如果交易失败,所有已消耗的 Gas 仍然需要支付给矿工。
4.4.Gas 模型的详细结构
- Gas 单位:Gas 是一个抽象单位,没有直接的货币价值。它与实际的 ETH 通过 Gas 价格(Gas Price)转换。
- Gas 价格:用户可以自行设置他们愿意支付的 Gas 价格(通常以 Gwei 为单位,1 ETH = 10^9 Gwei)。矿工通常会优先处理 Gas 价格较高的交易。
- Gas 限制:每个区块都有一个最大 Gas 限制,当前的区块 Gas 限制通常为 30,000,000 Gas。这限制了一个区块中能包含的最大交易数。
4.5.Gas 成本示例
不同的操作在 EVM 中有不同的 Gas 成本。例如:
- 简单转账(ETH 转账):21000 Gas
- 合约创建:32000 Gas
- 存储操作(SSTORE):20000 Gas(存储一个新值)或 5000 Gas(更新一个现有值)
4.6.实际示例
假设我们要执行一个简单的智能合约函数调用:
pragma solidity ^0.8.0;
contract GasExample {
uint256 public value;
function setValue(uint256 newValue) public {
value = newValue;
}
}
在这个示例中:
- 部署合约的 Gas 成本包括合约代码的存储成本。
- 调用 setValue 函数的 Gas 成本包括 SLOAD 和 SSTORE 操作的成本。
4.7.EIP-1559 改进的 Gas 模型
EIP-1559 是以太坊改进提案之一,引入了一种新的 Gas 模型,旨在提高交易费用的预测性和网络的可扩展性。
- 基础费用(Base Fee):动态调整的基础费用,根据区块的拥堵情况自动调整。
- 小费(Tip):用户支付给矿工的额外费用,以激励矿工优先处理他们的交易。
- 费用燃烧(Fee Burn):基础费用的一部分会被“燃烧”,即从流通中永久移除。
4.8.总结
Gas 模型是以太坊网络中至关重要的一部分,它通过限制每个交易和智能合约操作的计算和存储资源,确保网络的安全性和稳定性。通过了解 Gas 的工作原理和优化合约代码,开发者可以有效地管理交易成本,确保合约在以太坊网络上的高效执行。
五.总结
EVM字节码是以太坊智能合约的核心,它定义了合约在EVM上的执行逻辑。通过理解字节码的结构和操作码,开发者可以编写和优化智能合约,确保其高效、安全地运行在以太坊网络上。了解Gas模型和字节码优化方法,对于控制交易成本和提高合约性能至关重要。