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。
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。