Skip to content

Commit 714a696

Browse files
committed
Add study notes for 2025-08-18
1 parent 69332df commit 714a696

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Sillyzhe.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,166 @@ timezone: UTC+8
1515
## Notes
1616

1717
<!-- Content_START -->
18+
# 2025-08-18
19+
20+
在以太坊里,Gas 是每个人必须理解的核心概念。本文主要讨论如何估算和优化 Gas,帮助开发者们能够写出更节能的区块链应用。
21+
22+
## Gas 是什么
23+
24+
Gas 是以太坊里用来衡量计算资源消耗的单位。在以太坊上执行写操作(例如转账)都得需要消耗一定数量的 Gas,读操作(例如查询余额)一般不需要消耗 Gas。每次写操作消耗的 Gas 费用为:Gas 数量 \* Gas 价格 = 交易费用。这种机制设计有两个核心目的:
25+
26+
1. **防止网络滥用**:通过为每个写操作设置成本,防止恶意行为者通过执行无限循环或资源密集型操作来攻击网络。
27+
2. **激励验证者**:为网络维护者提供经济激励,补偿他们验证交易和执行计算所花费的资源。
28+
29+
简而言之,Gas 是以太坊的"燃料",使整个网络能够安全、有序地运行。EVM 会追踪每个交易的总 Gas 消耗,确保不超过用户设置的 Gas 限制。如果交易执行过程中 Gas 用尽,交易将回滚(所有更改都会撤销),但已使用的 Gas 仍会被收取。
30+
31+
## 如何估算 Gas
32+
33+
通常来说 Gas 数量是能够预估的(模糊预估,一个大概值),而 Gas 价格是不能预估的。为什么呢?因为以太坊虚拟机(EVM)对每一条指令(如 ADD、SSTORE、CALL)都预先定义了固定的 Gas 消耗值。而 Gas 价格不能预估主要是因为价格由市场供需、网络拥堵、矿工选择和 EIP-1559 动态费用机制共同决定的,无法精准预测,这里不详细说了。
34+
35+
在预估 Gas 数量之前,我们先来看一下一些常见的 Gas 操作消耗的 Gas 数量:
36+
37+
**存储操作**:
38+
39+
* SLOAD(读取存储): ~2100 Gas (冷访问)/ 100 Gas(热访问)-- 第一次访问是冷访问,后续都是热访问
40+
* SSTORE(首次写入): ~20000 Gas
41+
* SSTORE(修改现有值): ~5000 Gas
42+
* SSTORE(清零): 可获得退款(但受EIP-3529限制)
43+
44+
**计算操作**:
45+
46+
* ADD/SUB: 3 Gas
47+
* MUL/DIV: 5 Gas
48+
* 比较运算: 3 Gas
49+
* OR: 3 Gas
50+
51+
**调用操作**:
52+
53+
* CALL(普通调用): 基础700 Gas + 变动成本
54+
* DELEGATECALL: 基础700 Gas + 变动成本
55+
* CREATE(合约创建): 32,000 Gas + 代码成本
56+
57+
**不同类型交易的基础费用**:
58+
59+
* 普通 ETH 转账:21000 Gas (这是以太坊协议规定的基础交易成本,用于支付交易签名验证和状态变更)
60+
* 合约调用:21000 Gas + 函数执行费用
61+
* 合约创建:21000 Gas + 32000 Gas + 代码存储费用
62+
63+
了解了一些常见操作消耗的 Gas 数量后,我们再来看看下面的示例。
64+
65+
### 示例:简单的代币转账函数
66+
67+
假设我们有一个 ERC-20 代币转账函数:
68+
69+
```ts
70+
function transfer(address recipient, uint256 amount) external override returns (bool) {
71+
if (_balances[msg.sender] < amount) {
72+
revert InsufficientBalance(_balances[msg.sender], amount);
73+
}
74+
75+
_balances[msg.sender] -= amount;
76+
_balances[recipient] += amount;
77+
emit Transfer(msg.sender, recipient, amount);
78+
return true;
79+
}
80+
81+
```
82+
83+
这个函数大概的 Gas 消耗如下所示:
84+
85+
1. 普通 ETH 转账交易基础费用:21000 Gas
86+
87+
2. 余额检查:
88+
89+
* SLOAD 读取 `_balances[msg.sender]` (冷访问): 2100 Gas
90+
* 比较操作 (<): 3 Gas
91+
3. 余额更新:
92+
93+
* SLOAD 读取 `_balances[msg.sender]` (热访问): 100 Gas
94+
* SSTORE 更新 `_balances[msg.sender]`: 5000 Gas
95+
* SLOAD 读取 `_balances[recipient]` (冷访问): 2100 Gas
96+
* SSTORE 更新 `_balances[recipient]` (假设首次写入): 20000 Gas
97+
4. 事件发送:
98+
99+
* LOG3 (Transfer 事件): ~1500 Gas
100+
5. 其他开销:
101+
102+
* 函数调用和返回: ~200 Gas
103+
* 参数编码/解码: ~200 Gas
104+
105+
**Gas 总消耗**:基础开销 + 函数操作 = 约 52203 Gas
106+
107+
当然,在实际执行时,根据具体状态(例如地址是否曾被访问过,存储位置是否已有值等)的不同,所消耗的 Gas 数量也会有所不同。
108+
109+
110+
111+
### 为什么Gas无法精确预估
112+
113+
虽然我们可以通过了解EVM操作码的基本Gas成本来估算函数的Gas消耗,但实际上无法精确预估具体交易的Gas用量,主要原因包括:
114+
115+
#### 1\. 状态依赖性
116+
117+
同一函数在不同状态下消耗的Gas会有所不同。例如,写入一个已有值的存储槽比写入空槽消耗少 15000 Gas,我们无法预先知道合约部署到主网后的精确状态。
118+
119+
#### 2\. 热/冷访问差异
120+
121+
基于EIP-2929,首次访问地址或存储槽(冷访问)比后续访问(热访问)贵得多。交易执行路径不同时,热/冷访问的模式也会变化,在复杂交易中,很难预测哪些访问是热的,哪些是冷的。
122+
123+
#### 3\. 执行上下文变化
124+
125+
Gas消耗受到区块链当前状态的影响,同一函数可能在不同区块链状态下有不同的执行路径,特别是依赖外部条件的函数(如时间戳、区块高度等)。
126+
127+
#### 4\. 退款机制的不确定性
128+
129+
存储清零操作可获得退款,但总退款受限于交易Gas消耗的1/5,复杂交易中退款上限可能会变化,难以准确计算。
130+
131+
#### 5\. 动态计算的不可预测性
132+
133+
* 循环次数、条件分支等在运行前无法确定
134+
* 输入参数大小(如数组长度、字符串长度)直接影响Gas消耗
135+
* 某些密码学操作(如keccak256)Gas消耗与输入数据内容相关
136+
137+
基于以上原因,我们很难准确的预估复杂的合约所消耗的 Gas 数量。
138+
139+
### 使用工具进行预估
140+
141+
即使 Gas 无法精确预估,但是我们还是可以借助工具来做一个大概的估算。例如我们可以使用 Hardhat 和 Foundry 来写测试代码进行 Gas 估算。
142+
143+
这次我们换一个复杂点的 `transfer` 函数来进行测试,合约代码如下:
144+
145+
```ts
146+
function _transfer(address sender, address recipient, uint256 amount) internal {
147+
if (sender == address(0) || recipient == address(0)) revert TransferToZeroAddress();
148+
if (_balances[sender] < amount) revert InsufficientBalance(_balances[sender], amount);
149+
150+
_balances[sender] -= amount;
151+
_balances[recipient] += amount;
152+
153+
lastTransferTime[sender] = block.timestamp;
154+
155+
emit Transfer(sender, recipient, amount);
156+
}
157+
158+
function transfer(
159+
address recipient,
160+
uint256 amount
161+
)
162+
external
163+
override
164+
whenNotPaused
165+
notBlacklisted(msg.sender, recipient)
166+
checkCooldown(msg.sender)
167+
nonReentrant
168+
returns (bool)
169+
{
170+
_transfer(msg.sender, recipient, amount);
171+
return true;
172+
}
173+
174+
```
175+
176+
这个 `transfer` 函数比之前的版本复杂了很多,上面使用了很多修饰符,而且还内部调用了 `_transfer` 函数,要是手动来算会比较费劲。所以这次打算使用 Hardhat 和 Foundry 来测试,这两个都是可以用来写 Solidity 测试的工具。
177+
18178
# 2025-08-15
19179

20180
### 7 抽象合约和接口

0 commit comments

Comments
 (0)