1.3 StorageTransient的gas计算
1.3 Storage&Transient的gas计算
1.3.1 简介:两种存储
在 EVM 中,智能合约有两种主要的数据存储区域:
持久化存储 (Storage)
这是我们通常所说的“状态”。它是一个键值对映射(key-value store),将 32 字节的键映射到 32 字节的值。
- 持久性: 数据永久保存在以太坊区块链上,除非被合约逻辑修改。
- 高成本: 读写操作非常昂贵,因为它们会改变区块链的状态,需要所有节点达成共识。
- 指令:
SLOAD和SSTORE
瞬时存储 (Transient Storage)
这是由 EIP-1153 引入的新概念。它也是一个键值对映射,但其生命周期仅限于单次交易。
- 瞬时性: 数据在交易开始时为空,在交易结束时被丢弃。它不会永久改变区块链状态。
- 低成本: 读写操作比持久化存储便宜得多,因为它不涉及永久的状态变更。
- 用途: 主要用于在同一次交易内的不同合约调用之间传递状态,而无需昂贵地使用持久化存储。例如,在复杂的 DeFi 协议中用于“重入锁”或传递手续费信息。
- 指令:
TLOAD和TSTORE
1.3.2 SLOAD: 从持久化存储中读取
SLOAD 指令用于从合约的持久化存储中加载一个值到堆栈上。
# STACK 从堆栈 (Stack) 弹出一个 32 字节的 key。key = pop(evm.stack).to_be_bytes32()# GAS 计算 Gas 费用if (evm.message.current_target, key) in evm.accessed_storage_keys:charge_gas(evm, GAS_WARM_ACCESS)else:evm.accessed_storage_keys.add((evm.message.current_target, key))charge_gas(evm, GAS_COLD_SLOAD)# OPERATION 从当前合约的存储中,读取 key 对应的 valuevalue = get_storage(evm.message.block_env.state, evm.message.current_target, key)
Gas 成本:冷热访问 (Cold vs. Warm Access)
为了防止拒绝服务攻击 (DoS),EVM 对首次访问某个存储槽(slot)的操作收取更高的费用。
冷访问 (Cold Access): 在一笔交易中,第一次访问某个账户的某个存储 key。
- Gas 成本:
GAS_COLD_SLOAD(2100 Gas) - 代码中,通过检查 evm.accessed_storage_keys 集合来判断。如果 (address, key) 不在该集合中,就是冷访问。访问后,该键会被添加到集合中。
热访问 (Warm Access): 在同一笔交易中,对一个已经访问过的存储 key 进行再次访问。
- Gas 成本:
GAS_WARM_ACCESS(100 Gas) - 因为节点已经加载过这个数据,所以后续访问的成本要低得多。
1.3.3 SSTORE: 写入持久化存储
SSTORE 是最复杂的操作之一,因为它直接修改区块链状态,并且有复杂的 Gas 计费和退款机制。
if (evm.message.current_target, key) not in evm.accessed_storage_keys:evm.accessed_storage_keys.add((evm.message.current_target, key))gas_cost += GAS_COLD_SLOADif original_value == current_value and current_value != new_value:if original_value == 0:gas_cost += GAS_STORAGE_SETelse:gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD
else:gas_cost += GAS_WARM_ACCESS
# Refund Counter Calculation
if current_value != new_value:if original_value != 0 and current_value != 0 and new_value == 0:# Storage is cleared for the first time in the transactionevm.refund_counter += int(GAS_STORAGE_CLEAR_REFUND)if original_value != 0 and current_value == 0:# Gas refund issued earlier to be reversedevm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND)if original_value == new_value:# Storage slot being restored to its original valueif original_value == 0:# Slot was originally empty and was SET earlierevm.refund_counter += int(GAS_STORAGE_SET - GAS_WARM_ACCESS)else:# Slot was originally non-empty and was UPDATED earlierevm.refund_counter += int(GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS)
Gas 成本:复杂的计费与退款机制
SSTORE 的 Gas 成本取决于多种因素:
访问成本:
- 冷访问: 2100 Gas (
GAS_COLD_SLOAD) - 热访问: 100 Gas (
GAS_WARM_ACCESS)
写入成本:
-
创建 (Set): 当一个值为 0 的存储槽被设置为非 0 值时。
- 成本: 20,000 Gas (
GAS_STORAGE_SET)
- 成本: 20,000 Gas (
-
更新 (Update): 当一个非 0值的存储槽被设置为另一个非 0 值时。
- 成本: 2,900 Gas (
GAS_STORAGE_UPDATE-GAS_COLD_SLOAD)
也就是说,SSTORE 的写入成本已经包含了一次热访问的成本,如果你已经为冷访问付了 2100 gas,就不应该再全额收取 5000 gas,否则会重复计费。
Gas 退款 (Refund): - 成本: 2,900 Gas (
-
为了鼓励良好的状态管理(清理不再使用的存储),EVM 会提供 Gas 退款。
-
清理 (Clear): 当一个非 0 值的存储槽被设置为 0 时,会获得 4,800 Gas 的退款 (
GAS_STORAGE_CLEAR_REFUND)。 -
撤销退款(非0 → 0 → 非0),如果 slot 原来是非0,现在是0(说明之前已经清理过),但你又把它写回非0,然后又写回0。效果:撤销之前的退款,防止你通过反复写0骗取多次退款。
-
退款在交易结束时统一结算,并且最多只能退还当次交易所用 Gas 的一部分。如果你把 slot 的值改回了交易开始时的原值(即“恢复原状”)。效果:EVM 会返还你之前写入时多收的 gas,因为最终状态没变,相当于没做无用功。
你在同一笔交易内,对同一个 slot 进行了多次 SSTORE 操作。最终,这个 slot 的值又被“还原”为交易开始时的 original_value。EVM 规范规定:如果 slot 的最终值和 original_value 一样,应该退还之前多收的写入 gas(因为最终状态没变,相当于没做无用功)。
退款时只退“写入部分”,不退“访问部分”- 如果原来是0,现在又变回0,返还 GAS_STORAGE_SET - GAS_WARM_ACCESS。
- 如果原来是非0,现在又变回原来的非0,返还 GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS。
1.3.4 TLOAD: 从瞬时存储中读取
TLOAD 用于从瞬时存储中加载一个值到堆栈。
# STACK 从堆栈弹出一个 32 字节的 key。key = pop(evm.stack).to_be_bytes32()# GAS 收取固定的 Gas 费用。charge_gas(evm, GAS_WARM_ACCESS)# OPERATION 从当前交易的瞬时存储中读取 key 对应的 valuevalue = get_transient_storage(evm.message.tx_env.transient_storage, evm.message.current_target, key)push(evm.stack, value)
Gas 成本
TLOAD 的成本非常简单,它不区分冷热访问。
- Gas 成本:
GAS_WARM_ACCESS(100 Gas)
1.3.5 TSTORE: 写入瞬时存储
TSTORE 用于向瞬时存储写入一个值。
# STACK 从堆栈弹出 key 和 new_value。key = pop(evm.stack).to_be_bytes32()new_value = pop(evm.stack)# GAS 收取固定的 Gas 费用。charge_gas(evm, GAS_WARM_ACCESS)if evm.message.is_static:raise WriteInStaticContextset_transient_storage(evm.message.tx_env.transient_storage,evm.message.current_target,key,new_value,)
Gas 成本
与 TLOAD 类似,TSTORE 的成本也很简单,没有退款机制。
- Gas 成本:
GAS_WARM_ACCESS(100 Gas)
1.3.6 总结
| 指令 | 存储类型 | 主要功能 | Gas 成本 | 关键特性 |
|---|---|---|---|---|
| SLOAD | 持久化 | 读取数据 | 2100 (冷) / 100 (热) | 区分冷热访问 |
| SSTORE | 持久化 | 写入数据 | 复杂,依赖值的变化 | 有复杂的 Gas 计费和退款机制 |
| TLOAD | 瞬时 | 读取数据 | 100 (固定) | 成本低,数据仅在交易内有效 |
| TSTORE | 瞬时 | 写入数据 | 100 (固定) | 成本低,无退款,数据仅在交易内有效 |
