并非所有国家都是平等的
以太坊的状态并不均衡:少数合约占据了大部分存储空间,而大多数账户的生命周期都很短——有时甚至只有一个区块。研究状态访问模式可以揭示哪些状态保持活跃,哪些状态变得冷淡。如果我们针对实际使用情况进行优化,就可以提高执行层的运行速度。
分析
本研究按账户类型、字节码使用情况、部署器和工厂模式、代码大小以及插槽活动分析了状态使用情况。研究涵盖了从创世区块到Pectra 区块 22,431,083(2025 年 5 月)之前的所有区块。
EOA 与合同 — 哪一个持续时间更长?
活动跨度= 从特定状态的第一次访问到最后一次访问(读取和写入)的块距离。
0 活动跨度= 仅在单个块中访问状态。
假设1 个区块 = 12 秒。
合同 | 紧急行动 | |
---|---|---|
账户总数 | 50,119,846 | 243,161,178 |
中位活动跨度(区块) | 0 | 22,317(约3.1天) |
P75 活动跨度(块) | 966,579(约4.5个月) | 1,274,601(约5.8个月) |
P95 活动跨度(块) | 4,857,691(约1.85年) | 6,800,324(~2.59年) |
零活动跨度份额 | 55.17% | 4.50% |
EOA 显然比合同持续时间更长。
EOA 的中位寿命跨度不为零(约 3.1 天),而合同寿命跨度的中位值为零。EOA 的右尾在每个分位数(q75、q95)处都较重,表明其长寿群体较大。大多数合同在设计或意图上都是短暂的。
约55%的合同为零跨度合同。可能的驱动因素包括:- 工厂垃圾邮件和大规模铸造模板(例如,代币克隆、永远不会看到后续的配对合约)。
- 为单个交易创建的MEV/实用程序部署。
- 自毁模式(尤其是 EIP-6780 之前),其中创建和销毁发生在同一个区块/交易内。
“短暂的 EOA” 也很常见。
从首次观察到最后一次观察,平均 EOA 持续时间仅为约 3.1 天, 75% 的EOA 会在约 6 个月内结束。这可能包括一次性认领者/铸币者、交易所充值地址和机器人钱包。长期合同确实存在,但并不典型。
跨度多年的合约往往由标准驱动或基于基础设施:代币合约(ERC-20/721)、注册中心、路由器、多重签名/代理管理合约以及其他核心原语。许多应用程序团队会在升级时轮换地址,这会将活动分散到新部署中,并缩短每个地址的测量跨度。
合同有何不同?
一份合约 ≠ 一个独立的应用程序。事实上,许多合约会重用现有的字节码,或者使用相同的模板(通过工厂)进行部署。我们可以根据不同的类别来分析这些合约:
- 模板与唯一字节码
- 按部署者地址
- 按工厂地址
- 按代码大小
- 有状态(带槽) vs. 无状态(不带槽)
模板与唯一字节码
模板= 由多个合约地址部署的字节码哈希。
唯一= 字节码哈希只部署一次。
字节码计数 | 合约数量 | 平均中位活动跨度(块) | |
---|---|---|---|
模板 | 150,587 | 48,663,814 | 468,852(约2.1个月) |
独特的 | 1,456,032 | 1,456,032 | 905,439(~4.1个月) |
全部的 | 1,606,619 | 50,119,846 |
数据显示:
- 合约高度集中:约 15 万个模板字节码占所有合约的97% 。仅Top 1代码哈希就覆盖了14.4%的合约; Top 10 代码哈希覆盖了51% ; Top 100 代码哈希覆盖了81.8% 。这体现了严重的长尾模板重用,而非多样化的实现。
- 插槽不在克隆所在的地方:尽管只占合约的2.9% ,但唯一字节码拥有约 875.2M 个存储槽,而模板则拥有约 429.1M 个存储槽——约 67% 的状态位于一次性实现中。
独立合约的中位活动周期(约 4.1 个月)几乎是模板(约 2.1 个月)的两倍。这符合直觉:许多模板都是极简代理/克隆、代币工厂或临时部署(垃圾邮件/模因币、MEV 脚手架、EIP-6780 之前的自毁模式),这些部署寿命很短,或者在创建后从未被使用过。
独特的合约状态负载更重,这可能反映了管理更大规模持久数据的复杂系统(DEX、Staking、桥接、Rollup 基础设施)。另一方面,合约中大量的模板份额解释了零活动跨度部署的长尾现象——许多克隆合约在创建后从未真正执行过。
按部署者地址
Single = 部署者仅部署一个合约。
多个= 部署多个合约的部署者。
部署者数量 | 合同总数 | 每份合约的平均槽位 | 平均中位活动跨度(块) | |
---|---|---|---|---|
单身的 | 4,793,357 | 4,793,357 | 30.65 | 197,134(~0.9个月) |
多种的 | 850,064 | 45,326,489 | 193.28 | 687,037(约3.1个月) |
全部的 | 5,643,421 | 50,119,846 |
数据显示:
- 一小部分部署者几乎承担了所有的部署工作——仅15.1%的部署者就承担了90.4%的合约。集中度极高:仅排名前 500 位的部署者就承担了所有合约的57.4% ;而最大的单个部署者仅占3.15% 。
- 活跃的部署者平均会生成更多持久且占用更多 slot 的合约。来自“多个”部署者的合约寿命约为 3.5 倍(687k 个区块 vs 197k 个区块),每个合约占用的slot 数量约为 6 倍(193 个 vs 31 个)。这种模式适合那些在链上维护产品(并且取得成功)的团队/服务,而不是一次性的实验。
- 但最顶级的部署者倾向于短暂型部署。在部署量排名前十的合约中,它们的活跃期极短(平均数百个区块),存储空间极小——这与大规模生产的克隆、工厂垃圾和 MEV 脚手架的情况一致。换句话说:在“多部署”组中,分为构建者(持久、有状态部署)和喷雾者(高容量、短期部署)。
合约部署高度集中。大多数持久且状态密集型的合约来自重复部署者,而部署量最大的部署者则在区块链上部署了大量短期合约。
对于多个合约部署者来说,也可能是他们犯了一个错误,所以他们必须重新部署合约。
按工厂地址
非工厂= 由 EOA 直接创建(无中介合同)。
工厂 – 个人= 由一份生产一个孩子的合同创建。
工厂 – 多= 由生产多个孩子的合同创建。
数数 | 合同总数 | 每份合约的平均槽位 | 平均中位活动跨度(块) | |
---|---|---|---|---|
非工厂 | 808,188 | 5,470,844 | 313.14 | 768,567(约3.6个月) |
个人 | 66,900 | 66,900 | 196.58 | 431,153(约2个月) |
多 | 32,929 | 44,582,102 | 177.58 | 510,060(~2.36个月) |
全部的 | 908,017 | 50,119,846 |
数据显示:
- 大多数合约都是通过合约工厂铸造的克隆合约。约89.1%的合约来自多合约工厂(32,929 个地址),而只有10.9% 的合约是在没有合约工厂的情况下部署的。然而,非合约工厂合约的平均负载(约313 个存储槽/合约)比合约工厂子合约(约178-197 个存储槽/合约;中位数为431-510,000个区块)更重,活跃时间更长(活跃时间中位数约为 769,000 个区块)。
- 集中度极高。排名前五的工厂占所有部署的约43% ,排名前一百的工厂占所有部署的约89% 。一半的工厂部署的合同不超过5份, 99%的工厂部署的合同不超过4,978份。
工厂可扩展廉价、轻量级的代码。单个和多个工厂组中每个合约的插槽数量较少,平均活动跨度较短,这与代理模式、代币/交易对垃圾邮件、空投铸币以及 MEV/脚手架合约相符——这些合约易于量产,通常寿命较短,而且许多合约从未积累过有意义的插槽。
非工厂部署更倾向于基础设施。更大的存储空间和更长的跨度意味着定制系统(治理、保险库、路由器、网桥、汇总基础设施)由 EOA/多重签名直接部署,并持续保持活跃。
“独立工厂”≠ 持久。只生产一个子进程的工厂比非工厂部署的跨度更短,槽位也更少。许多工厂看起来像是工厂的引导程序或虚荣用途,而不是长期运行的应用程序。
按代码大小
年龄= 最新区块(22431083) - 首次看到该状态的区块。
尺寸类别 | 合约数量 | 零活动跨度(%) | 非零中值活动跨度(块) | 非零 P99 活动跨度(块) | 平均年龄(街区) |
---|---|---|---|---|---|
微小(<1KiB) | 44,298,982 | 57.9 | 2,192,443(约10个月) | 8,958,543(约3年) | 6,318,754(~2.4年) |
小(1-5KiB) | 4,333,667 | 41.4 | 205,882(约28天) | 11,690,310(~4年) | 11,044,783(~4.2年) |
中等(5-10KiB) | 592,936 | 23.4 | 58,613(约8天) | 11,417,402(~4年) | 7,283,986(~2.8年) |
大(10-20KiB) | 794,012 | 8.6 | 8,302(约1天) | 10,871,423(~4年) | 5,297,655(~2年) |
非常大(20-24KiB) | 100,249 | 11.1 | 273,516(约1个月) | 11,276,444(~4年) | 5,973,078(~2.3年) |
数据显示:
- Tiny 占主导地位,但这是两个不同的世界。Tiny 合约约占所有合约的 88% ,但超过一半(57.9%)是零跨度合约,这与大规模铸造的克隆合约和存根合约一致。然而,在那些活跃的合约中,Tiny 的跨度中位数是所有合约中最长的(约 219 万个区块,约 10 个月),这表明存在第二个长期使用的子群体。
- 活动跨度与规模的关系并非单调变化,而是呈 U 形。以非零活动量为条件,中值跨度从极小 → 小型 → 中型 →大型(最短,约 8.3k 个区块 ≈ 1 天)下降,然后反弹至非常大型(约 27.3k 个区块 ≈ 1 个月) 。因此,“大型代码运行时间更长”是错误的,因为中等规模的代码变动速度最快。
- 每个规模类别都有一个长尾。P99 集群跨越所有规模的约 1000 万到 1170 万个区块(约 3-4 年),这意味着每个区块中都有一小部分合约会持续存在数年。
微型合约通常充斥着代理和工厂克隆。大多数合约价格低廉、一次性使用且从未使用过。然而,真正重要且有用的合约运行时间更长,因此非零合约群体的跨度也更长。另一种可能性是,它们是用于升级的网关合约。
小型/中型/大型合约专注于定制化、炒作周期合约(例如代币/NFT 铸币/挖矿、一次性应用逻辑)。团队还会在升级时轮换地址,因此活动范围涵盖各个细分领域。
超大型合同通常预示着系统复杂。这类合同比中型合同更为罕见,且持续时间更长,但并非总是成功——一些大型部署从未被采用。
有状态合约 vs. 无状态合约
有状态= 至少有 1 个存储槽的合约
无状态= 合约存储槽为 0
类型 | 合同总数 | 零寿命(%) | 中位非零活动跨度(块) |
---|---|---|---|
有状态的 | 23,127,186 | 50.8 | 105,112(约14.6天) |
无国籍 | 26,992,660 | 58.91 | 3,185,332(约1.2年) |
数据显示:
- 大多数合约都是无状态的。53.9 % 的合约从未接触过存储;46.1% 的合约接触过存储。
- 零活动在无状态合约中更为常见。58.9 % vs. 有状态合约为 50.8%。
- 无状态合约的运行时间更长。它们的中位非零跨度比有状态合约长约30 倍(1.2 年 vs. 14.6 天)。
为什么这种情况可能发生
- 无状态 = 廉价耐用的实用程序。许多都是简单的转发器、代理或助手。它们易于批量部署(因此很多最终都无法实现跨度),但有用的程序可以运行数年,因为它们不保存数据,也不需要迁移。
- 有状态 = 升级或替换。代币、矿池、保险库以及存储数据的应用程序更有可能升级或更换为新版本。这将活动分散到新地址,并缩短每个旧地址的跨度。
存储槽的使用频率如何?
图 20:大多数存储槽位被写入一次后就不再使用,超过 60% 的存储槽位处于零活动跨度。图 21(洛伦兹曲线):存储使用情况极度倾斜,基尼系数为 0.973。数据显示:
- 超过一半的存储槽位(63.3%)是临时的。这些槽位一旦设置就永远不会被访问。
- 集中度极高。单个合约占据总插槽的约 6%(参见XEN Crypto );排名前 1000 的合约占据所有插槽的约 51%。
零活动槽常见于一次性标志、空投声明、初始化记录、废弃余额或快速替换的合同中的数据。
一小部分大型系统承载了以太坊大部分的活跃存储槽位。这就是为什么一小部分合约占据了总存储槽位。
需要注意的是,代理模式通常会将逻辑和存储拆分。逻辑合约通常呈现无状态且长期有效,而存储合约则保存状态并可能轮换。仅查看地址(而非项目)会发现有状态代理的跨度较短。
一些标记为“零活动”的 slot 可能仍会在链下读取(通过 RPC 调用)。这些读取操作不会出现在链上。
开放思想
状态到期
大多数状态很快就会冷却。一个务实的方向是定期修剪非活动状态,并采用复活方案:
- 窗口: 12-18 个月的活动窗口适合分布:许多州的中位非零跨度远低于一年,而长寿州则高于一年。
- 优点:保持热状态较小,减缓状态增长,减少磁盘 I/O,减少存储大小并提高块处理时间
首先,我们来考虑一下,通过移除零活动跨度账户和存储槽位,可以节省多少存储空间。我们在 Geth 的平面快照(键值“状态”表)上进行了测量,将原始快照与移除这些键后的快照(节点已同步到区块 22,431,083)进行了比较:
原始大小(GiB) | 零活动跨度大小 (GiB) | 删除后 (GiB) | 储蓄 | |
---|---|---|---|---|
账户 | 13.48 | 2.58 | 10.90 | 19.14% |
插槽 | 94.12 | 57.62 | 36.50 | 61.22% |
全部的 | 107.60 | 60.20 | 47.40 | 55.95% |
仅在快照上,删除零跨度状态就会减少约 56% 的存储空间——这是一个强烈的信号,表明状态到期可能会大大缩小热状态集。
更便宜地部署已存在的字节码
尽管客户端通常会通过代码哈希对磁盘上的字节码进行重复数据删除,但重复代码仍然会在部署过程中浪费 Gas。以下是一些可行的方案:
- 本地重复折扣(相同区块/时期) :如果字节码
codehash
在同一个区块(或短时期)中出现两次,则第二次部署支付的代码押金减少。- 优点
- 可以根据合约部署的顺序轻松地在区块中进行验证
- 工厂和 AA 钱包立即节省 gas 成本
- 由于折扣仅适用于区块内,因此可以限制垃圾邮件的影响
- 缺点
- 不公平:谁应该支付全部金额?
- EL 客户端实现的复杂性
- 优点
- 全局“codehash 注册表” :系统合约存储了
codehash → exists
在其存储中。所有合约部署都会首先检查字节码是否已存在。- 优点:
- AA 钱包、代理和工厂合约的 Gas 节省。
- 如果字节码已经存在,则可以使用委托合约的机制
- 缺点:
- 激励垃圾克隆
- 向注册表添加 DoS 表面——需要谨慎定价,以免折扣被垃圾邮件发送者滥用
- 随着存储 trie 大小的增加,字节码哈希可能会成为状态增长的问题
- ZKEVM 必须证明代码哈希值的存在
- 优点:
- 每个库对应一个合约:不再为同一个库创建重复合约,而是添加一个新的操作码
LIBCREATE
,用于部署一个仅使用代码哈希的“库合约”。这可能有助于删除重复库,但前提是进一步的分析表明这些库确实占用了合约的很大一部分。
每个地址的渐进式存储定价
如今,全球储能市场由少数几份合同主导。我们或许希望价格能够反映边际负担。
按地址渐进式存储定价: SSTORE
基本成本加上当slot_count(address)
超过阈值时产生的阶梯式附加费(需要协议内 slot 计数器,类似于 EIP-2027 风格的元数据)。保持成本可预测(阶梯式),同时推动大型合约将状态外部化成本内部化。
优点:
- 与地址的实际存储增长相关的成本。
- 与按时段重新定价相比,分级定价使成本规划更加容易。
- 鼓励更好的存储设计,避免滥用“无限插槽”。
缺点:
- 需要插槽计数元数据和规则;增加了复杂性。
- 当交易跨越层级中间交易时,气体估算会变得更加困难。
- 可能会促使开发人员将状态分散到多个合约中(更多调用、更多表面、更差的用户体验)。
- 有用的合约往往有很多空位,而我们却在惩罚它们的活跃度
临时存储模型
许多 slot 只需写入一次,便不会再被触及。为开发人员提供一个更便宜、合约可读的存储位置,用于存储可确定性地丢弃的短期数据。
工作原理(高级):
- 每个账户的临时存储根:将
tempStorageRoot
添加到每个合约账户,用于存储临时数据 - 全局时间段:所有写入都进入当前时间段(例如 6-12 个月)
- 确定性到期:一旦一个周期结束,其数据在读取时逻辑上为零,无论它是否已被 EL 客户端物理修剪
- 定价:使当前的
SSTORE
更昂贵,并且将数据存储在临时存储中比SSTORE
更便宜
优点:
- 将成本与预期寿命相匹配
- 简单的思维模型:“使用临时存储来存储大约 N 个月后可能会丢失的数据”
- 与未来状态到期工作正交(且兼容)
缺点:
- 由于新的帐户字段,需要迁移帐户
- 如果开发人员依赖存储重置之后的值,则工具风险
- 天然气时间表需要仔细调整以避免滥用
- 已部署合约中的
SSTORE
具有更高的 gas 成本,可能不公平
EL 客户端的性能改进
关于 EL 客户如何利用这些信息来提高其绩效的一些想法:
- 存储引擎中的冷/热状态隔离:将最近接触的帐户/插槽保留在“热”存储引擎中,然后以较低的频率批量压缩和快照冷帐户/插槽,以减少写入放大。
- 槽位计数驱动的提交:优先处理持有超大槽位份额的大型合约的 trie 路径的数据库提交。这将优先处理最严重的违规者。
- 缓存频繁出现的代码哈希并检查是否存在:对于合同创建,跳过重新保存重复的字节码,减少写入放大和压缩压力。
结束语
以太坊的问题不仅仅是状态增长——没有办法处理陈旧状态。
大多数账户都是短期账户。工厂和模板克隆占据了部署的主导地位,但独特的合约占据了大部分存储空间。一小部分合约占据了大部分插槽,而且许多插槽只编写一次,之后就再也没有被修改过。如果我们能够在不破坏可组合性的前提下,找到突破性的解决方案,以太坊就能更快地扩展。
并非所有状态都是平等的——也许协议不应该将其视为平等。
致谢
特别感谢 ethPandaOps 团队提供的出色的Xatu基础设施。
还要感谢 Guillaume、Carlos、Matan 和 Ignacio 审阅本文。
后续步骤
- 强度分析:将跨度与活动强度配对,以区分“安静长寿”和“短暂但激烈”的状态。还应考虑气体消耗。
- 按标签分组:使用标签(例如令牌、路由器、保险库、桥梁、工厂、代理)进行分析,以进一步巩固推理。
- EIP-6780 后自毁:调查有多少合约原本应该自毁并从状态树中移除,但由于 EIP6780 而未能成功。同时调查有多少合约执行了
SENDALL
操作码。 - 空余额合约:检查有多少合约被创建、持有多年,然后被清空。如果这些合约不再使用,或许我们可以删除它们,这样就没人关心了。
- 库:检查有多少合约是使用库部署的。
- 根据独特/模板划分工厂/非工厂合同的分配。
- 根据不同的属性(例如部署者、工厂、代码哈希、类别)以及它们在 EL 客户端中代表多少存储空间来识别集群。
附录
链接到分析代码库。