【Web3技术分享系列专题】以太坊虚拟机(EVM)设计原理-状态转移源码分析
【摘要】 EVM地址、Optional AccessList、EVM数据存储位置、状态转移与Gas计算、合约调用、内建合约、合约执行、操作码执行、栈操作演示
EVM地址
普通账户地址:EOA(External Owner Account),可以持有私钥,可以发送交易的账户地址。通常使用钱包进行创建。
合约账户地址:由创建合约的EOA地址+交易的nonce值,计算哈希后取第12~31共20个字节。
通过钱包进行EOA生成
下面通过钱包派生一个EOA地址的函数,通过第代码片段的993行可以看到,其输入参数包含公钥信息,和派生路径信息。
从下面的代码片段可以看出,通过钱包进行EOA地址生成时,地址的获取,是和公钥直接相关的,对公钥进行哈希后,取了第12到31个字节的数据。
合约地址生成
下面的代码片段是节点接收到区块后,进行交易执行时操作逻辑。函数将区块中的每一笔交易信息转换成‘msg’这种结构。
从上面代码片段中的第82行进入函数‘applyTransaction’,该函数会对msg中的目的地址字段‘to’进行判断,如果目的地之字段为空,那么协议就认为给笔交易是一个合约创建交易。接下来就要调用合约地址创建函数。
从下面的代码片段我们可以看到,合约地址的生成是将交易的sender地址,以及此笔交易的nonce值,进行哈希后,取第12到31个字节。
Optional AccessList(EIP2930)
EVM执行区块中的交易,进行状态转移时,将transaction都转化为msg,msg的类型如下,其中有一个accessList成员变量。
AccessList说明
- 包含一个AccessList,指明一组address以及对应于每个address要访问的一组storage keys。这些地址和存储key被添加到 accessed_addresses 和 accessed_storage_keys 全局集合中(在 EIP-2929 中引入)。
- 通过AccessList声明要访问的数据,可以节省gas费用。
- AccessList之外的数据也可以访问,但是gas费用比较高。
- Address或者AccessList中的key目前是可以重复,如果重复了会重复收费,没有其他不同
类型AccessList的结构如下:
在发送交易时提前声明要访问的数据。规定了格式:每个地址对应多个key值。如下图所示:
由于新增了AccessList,交易的大小会比没有AccessList更大。也就会增加gas消耗,每个key值固定消耗1900wei,每个address固定消耗2400wei。
不过,当存储的读取可以预测时,处理交易更容易。因为clients可以预加载数据,并行读取数据。此外,在一些场景中AccessList难以实时构建,在交易生成和签名之间存在长时间滞后。目前,只有10%的折扣,将来会考虑提高list之外的key的费用。发送交易前,可以通过API进行进行创建AccessList,同时会返回该交易对应的gas消耗。
EVM数据存储位置
EVM的数据存储有以下几类:
- Memory(线性地址空间)
- Stack
- Trie (Merkle-Tree ,stateDB)
- Ancient存储(只能追加,不能修改)
- LevelDB
其中Memory、Stack、Trie和LevelDB都很好理解其作用。第4条Ancient存储不好理解,经过走读代码,发现起作用是进行冷存储的。将历史区块数据进行保存。
表示单链数据表(例如块)。它由一个数据文件(snappy 编码的任意数据 blob)和一个 索引入口 文件(指向数据文件的未压缩的 64 位索引)组成。从下面的代码片段可以看到,保存了区块哈希表、区块头表、区块body表、收据表还有一个困难度表。
状态转移与Gas计算
状态转移前的检查工作
检查交易信息是否满足共识规则:
- 检查nonce值是否正确(等于状态数据库中的nonce值,且+1后不大于2^64)
- 调用者有足够的余额(balance > gaslimit * gasprice)
- 当前区块可用Gas量可以供当前交易消耗的(当前交易的MaxFeePerGas > 当前区块的BaseFee)。
- 当前交易中支付的gas 大于 固有Gas费(data占用)
- 固有Gas不能溢出(max:2^64)
- 检查账户余额 > 转账金额(msg的value字段)
固有Gas费计算
- Initial gas=53000(创建合约) 或者 21000(其他)
- gas += data字段的 NoneZero字节数量 * 68(EIP2028:16)
- gas += data字段的 Zero字节数量 * 4
- gas += len(accessList) * 2400
- gas += len(accessList. StorageKeys) * 1900
合约调用
交易的类型分为转账、创建合约、调用合约。
合约执行函数逻辑如下图所示,关注点有
- 先检查合约嵌套调用的深度,不能超过1024。
- 判断是否是内建合约
- 执行实际的转账操作
- 内建合约的代码不需要从StateDB中读取,直接调用
- 合约执行后,即时出错了,gas的消耗不会退回,依然要背减掉。
内建合约
内建合约根据不同协议版本,有一点差别。大致有以下版本:
- PrecompiledContractsHomestead
- PrecompiledContractsByzantium
- PrecompiledContractsIstanbul
- PrecompiledContractsBerlin
- PrecompiledContractsBLS(测试使用)
其中,合约函数的功能描述如下:
- ecrecover: 返回ecdsa签名的公钥
- sha256hash: 计算哈希值
- ripemd160hash: 计算哈希值
- dataCopy:数据拷贝
- bigModExp:大整数指数模运算
- bn256AddByzantium:椭圆曲线点加
- bn256ScalarMulByzantium:椭圆曲线标量乘法
- bn256PairingByzantium:bn256 曲线实现配对
- blake2F:快速安全的哈希算法。BLAKE2 is specified in RFC 7693
合约执行
合约的执行就是将操作码转换成指令集中的指令,一条条执行的过程。不同版本的协议,指令集有所不同。不同指令集的版本如下:
下面的代码片段是合约执行的逻辑。程序计数器从0开始,不断的取出操作码opcode,然后根据opcode找到对应的操作operation。其中operation中就包括了gas消耗、需要的Stack大小、Memory大小、执行函数。每个操作的gas消耗包含固定gas消耗和动态gas消耗,动态gas消耗通过动态gas计算函数进行计算得出。
操作码执行举例
CALLDATALOAD:把交易的input字段中的第4字节之后的32字节入栈。Get inputdata in current environment
SLOAD:根据关键字key,加载StateDB中的值value
SSTORE:根据关键字,保存value到StateDB
栈操作演示
假设,i=2,num=5,合约函数如下:
编译后的操作码:
Stack的执行过程演示如下,栈顶是输入参数 i = 2,栈顶下面的元素忽略为x。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)