深入浅出:以太坊虚拟机(EVM)存储模型设计与权衡

核心思想:分层存储

EVM采用分层存储架构,基本原则是:数据的持久性与访问成本成正比。即越持久的数据,操作它所需的Gas(费用)越高。这种设计是为了让开发者谨慎使用区块链的全局状态。

在以太坊虚拟机(EVM)中,数据存储根据可变性生命周期 分为几个不同的区域,上图将其分为了 "易变数据区""不易变数据区"

一、易变数据区

这部分数据在交易执行期间存在,交易结束后被清除,不永久保存到区块链状态。

  1. stack(栈)
    • 大小为 32 字节 × 256 位(即 256 个 32 字节的槽位)。
    • 用于 EVM 指令执行时的临时数据存储,比如局部变量、计算中间值。
    • 访问速度极快,但大小有限,只能通过 push/pop 方式操作。
    • 功能 :EVM是一个栈式虚拟机 ,它的指令(如ADD, MUL, SWAP)主要从栈顶获取操作数并压入结果。它用于存储局部变量、函数参数和计算中间值
    • 特点
      • 速度极快,Gas成本几乎为零
      • 大小严格限制为1024个元素(256位深 x 32字节宽)。这是为了防止无限递归调用等攻击,也是许多"栈溢出"漏洞的根源。
      • 只能通过PUSHPOPDUPSWAP等特定指令访问,无法随机访问。
    • 类比:CPU的L1缓存。容量最小,但速度最快。
  2. calldata(调用数据区)
    • 只读区域,存储本次交易或调用的输入数据(例如函数参数)。
    • 在外部调用时传入,比如用户调用合约函数时附带的 data 字段。
    • 功能 :一个只读 的字节区域,存储触发本次合约执行的原始输入数据 。例如,当你调用一个函数transfer(address to, uint amount)时,函数选择器和参数就编码在这里。
    • 特点
      • 完全只读,无法修改。
      • Gas成本模型特殊:在普通调用中,读取calldata不消耗Gas;但在合约创建(CREATE)时,calldata是合约的初始化代码,其字节数会影响部署成本。
      • 对于external函数的数组结构体参数,显式声明为calldata类型可以节省大量Gas,因为它避免了将数据复制到内存的开销。
    • 类比:只读的消息报文或网络请求体。
  3. memory(内存)
    • 临时、可读写、线性的字节数组,以 32 字节或 1 字节为单位存取。
    • 用于存储函数内的复杂数据结构(如数组、字符串),生命周期仅限于本次合约执行。
    • 功能 :一个可动态增长的字节数组 ,用于存储函数内的复杂临时数据,如数组、字符串、结构体,或作为调用外部合约前的数据准备区。
    • 特点易失性 ,交易结束即释放。可读可写 ,支持以字节或字(32字节)为单位进行精细访问。Gas成本适中。首次扩展(扩容)时需一次性支付Gas,后续访问成本很低。每次调用都从零开始,是线性的、连续的空间。
    • 类比:电脑的RAM。程序运行时的主工作内存。

二、不易变数据区

这部分数据会持久化保存到区块链状态中,在交易之间保持存在。

  1. code(代码区)
    • 只读区域,存储合约的字节码,部署后不可更改(除非使用 delegatecall 调用其他合约代码)。
    • 通过 EXTCODECOPY 等指令可读取其他合约的代码。
    • 功能 :存储合约的运行时字节码。这是合约部署后,其代码的只读副本。
    • 特点
      • 完全只读,不可变 (通过CREATE2和自毁可以变相实现代码更新,但原地址代码不变)。
      • 可以通过EXTCODECOPY等指令读取其他合约的代码,用于实现代理模式或合约验证。
      • 部署时,代码大小直接影响一次性部署成本。
    • 类比:存储在ROM中的可执行程序文件。
  2. storage(存储区)
    • 持久化的键值存储(256 位 → 256 位),合约状态变量存放于此。
    • 读写成本高(gas 费昂贵),因为会修改区块链全局状态。
    • 功能 :合约的永久存储 ,所有状态变量 (非常量、常量)都存储在这里。它是一个巨大的(2²⁵⁶ 槽位)mapping,每个槽位为32字节。
    • 特点
      • 持久化:写入storage即修改了区块链的全球状态。
      • Gas极其昂贵
        • SSTORE(写入)首次写入一个非零值约20,000 Gas,修改现有值约5,000 Gas。
        • SLOAD(读取)约800 Gas。
      • 优化Storage使用是合约Gas优化的重中之重(如使用更小的数据类型打包、使用映射代替数组)。
    • 类比:硬盘数据库。每一次写入都像在做一个永久的、不可篡改的数据库提交。

三、关系总结

区域 可变性 生命周期 Gas 成本
stack 可变 指令执行期间 免费(几乎)
calldata 只读 本次交易 较低(创建交易时付费)
memory 可变 本次合约执行 动态(扩展时收费)
code 只读 合约生命周期 仅部署时付费
storage 可变 永久(除非修改) 很高

四、交互流程与Gas影响分析

  1. 一次典型的函数调用
    • 用户交易携带 calldata 进入EVM。
    • EVM从 code 区加载合约字节码并开始执行。
    • 执行指令在 stack 上进行算术和逻辑运算。
    • 如果需要复杂临时数据(如解析calldata中的数组),则在 memory 中分配空间进行操作。
    • 如果函数需要更新合约状态(如更新余额),则执行昂贵的 storage 写入操作。
  2. Gas优化的关键启发
    • 最小化Storage操作 :这是节省Gas的黄金法则。使用事件(event)记录而非存储,使用内存变量进行计算,最后再一次性写入Storage。
    • 善用Calldata和Memory :对于外部函数,大数组应使用calldata传入,避免复制到memory
    • 警惕栈深度:复杂的函数调用链和递归可能导致栈溢出(超过1024深度),使交易失败。
    • 代码大小:庞大的合约代码会增加部署成本和调用成本(因为需要加载更多代码)。

安全视角

  • 栈溢出:恶意递归调用可能耗光栈空间。
  • Storage冲突:在代理合约或可升级合约中,Storage布局的不兼容会导致严重漏洞。
  • 内存扩展攻击:攻击者可能诱导合约操作一个巨大的内存数组,导致Gas消耗激增而拒绝服务(DoS)。

总结

你提供的这张图精炼地概括了EVM的存储层次结构

  • 栈和内存"工作车间" ,快但临时。
  • Calldata"输入原料" ,只读。
  • Storage"成品仓库" ,改造成本高。
  • Code"生产线图纸" ,一旦部署,不易更改。
相关推荐
谈笑也风生1 小时前
浅谈:被称为新基建的区块链(五)
区块链
傻小胖1 小时前
4.BTC-协议-北大肖臻老师客堂笔记
区块链
Ynchen. ~1 小时前
[深度解析] 信任的重构:从盲签名到区块链的不可篡改哲
区块链·隐语
找不到、了3 小时前
栈帧四要素:JVM 方法执行的完整上下文
java·jvm
技术不打烊3 小时前
Solidity 是什么?区块链智能合约开发入门指南 下
web3·solidity
TroubleBoy丶3 小时前
Docker可用镜像
java·linux·jvm·docker
Zzzzzxl_3 小时前
互联网大厂Java/Agent面试实战:Spring Boot、JVM、微服务与AI Agent/RAG场景问答
java·jvm·spring boot·ai·agent·rag·microservices
未若君雅裁3 小时前
JVM高级篇总结笔记
java·jvm·笔记
Zzzzzxl_4 小时前
互联网大厂Java/Agent面试实战:JVM、Spring Boot、微服务与RAG全栈问答
java·jvm·springboot·agent·rag·microservices·vectordb