【Web3技术分享系列专题】以太坊虚拟机(EVM)设计原理-状态转移源码分析

举报
NineDay 发表于 2023/03/28 16:24:28 2023/03/28
【摘要】 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的数据存储有以下几类:
  1. Memory(线性地址空间)
  2. Stack
  3. Trie (Merkle-Tree ,stateDB)
  4. Ancient存储(只能追加,不能修改)
  5. LevelDB
其中Memory、Stack、Trie和LevelDB都很好理解其作用。第4条Ancient存储不好理解,经过走读代码,发现起作用是进行冷存储的。将历史区块数据进行保存。
表示单链数据表(例如块)。它由一个数据文件(snappy 编码的任意数据 blob)和一个 索引入口 文件(指向数据文件的未压缩的 64 位索引)组成。从下面的代码片段可以看到,保存了区块哈希表、区块头表、区块body表、收据表还有一个困难度表。

状态转移与Gas计算

状态转移前的检查工作

检查交易信息是否满足共识规则:
  1. 检查nonce值是否正确(等于状态数据库中的nonce值,且+1后不大于2^64)
  2. 调用者有足够的余额(balance > gaslimit * gasprice)
  3. 当前区块可用Gas量可以供当前交易消耗的(当前交易的MaxFeePerGas > 当前区块的BaseFee)。
  4. 当前交易中支付的gas 大于 固有Gas费(data占用)
  5. 固有Gas不能溢出(max:2^64)
  6. 检查账户余额 > 转账金额(msg的value字段)

固有Gas费计算

  1. Initial gas=53000(创建合约) 或者 21000(其他)
  2. gas += data字段的 NoneZero字节数量 * 68(EIP2028:16)
  3. gas += data字段的 Zero字节数量 * 4
  4. gas += len(accessList) * 2400
  5. 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:根据关键字,保存valueStateDB

栈操作演示

假设,i=2,num=5,合约函数如下:
编译后的操作码:
Stack的执行过程演示如下,栈顶是输入参数 i = 2,栈顶下面的元素忽略为x。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。