Gas 是什么以及为什么 EVM 中的费用会变化
1. 概述
如果你曾经在 Ethereum 上发送过 transaction,并看到费用时低时高相差 10 倍,你就接触到了 gas 的概念。本文解释 gas 是什么、EVM 在 EIP-1559 前后如何计算费用、为什么 baseFee 会随每个块而变化。读完后,你将能够用代码自己计算交易费用,并知道如何合理设置 maxFeePerGas 来避免 transaction 被卡或多付钱。
2. Gas 是什么
Gas 是一个单位,用来度量 EVM(Ethereum Virtual Machine)执行 transaction 所需的 计算工作量。每个 opcode(虚拟机指令)都有固定的 gas 成本 — 整数加法(ADD)便宜,向 storage 写入数据(SSTORE)昂贵数百倍。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public value; // 存储槽
function increment() public {
value = value + 1; // ADD (~3 gas) + SSTORE (~5000-20000 gas)
}
}
行 value = value + 1 实际上是两个步骤:数学计算(便宜)和写入到 storage(昂贵)。这就是为什么优化 smart contract 通常专注于减少 storage 读写次数,而不是减少计算次数。
注意: Gas 不是钱。Gas 是 “工作量”。真实费用 = 消耗的 gas × 每个 gas 的价格(以 ETH 计)。
3. Gas limit、gas price 和 gas used
三个容易混淆的概念:
- Gas limit:你允许 transaction 消耗的最大 gas 数量。设置过低 → transaction 因
out of gas错误而回滚。 - Gas used:实际消耗的 gas 数量,始终 ≤ gas limit。
- Gas price:每个 gas 的价格,以
gwei计算(1 gwei = 10⁻⁹ ETH)。
交易费用(EIP-1559 之前)= gasUsed × gasPrice
例如简单的 ETH 转账消耗 21,000 gas,如果 gasPrice = 30 gwei:
费用 = 21,000 × 30 gwei = 630,000 gwei = 0.00063 ETH
4. 旧机制:legacy gas price
在 EIP-1559 之前(London fork 之前,2021 年),用户自行设置 gasPrice。价格高则矿工优先将 transaction 打包进 block。这种机制像盲拍:你不知道对手出价多少,容易出价过高或过低后还要等待。
5. EIP-1559:base fee + priority fee
EIP-1559 将费用分为两部分:
baseFee:由 network 根据前一个 block 的满度计算,被销毁(不进入 validator 手中)。priorityFee(tip):你额外支付给 validator 以获得优先处理。
用户设置两个值:
maxFeePerGas:你接受支付的每个 gas 的最高价格。maxPriorityFeePerGas:你接受支付的最高 tip。
5.1. 计算实际费用的公式
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
实际费用 = gasUsed × effectiveGasPrice
5.2. 用 Java 计算费用
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
package dev.nonla.evm;
import java.math.BigInteger;
public class FeeCalculator {
public BigInteger effectiveGasPrice(
BigInteger baseFee,
BigInteger maxPriorityFeePerGas,
BigInteger maxFeePerGas) {
BigInteger desired = baseFee.add(maxPriorityFeePerGas);
return desired.min(maxFeePerGas); // 永远不会超过 maxFeePerGas
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPrice取(baseFee + 期望 tip)和你设定的上限maxFeePerGas中的较小值。totalFee乘以实际消耗的 gas 得出需要支付的 ETH 数额。
package dev.nonla.evm;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import static org.assertj.core.api.Assertions.assertThat;
class FeeCalculatorTest {
private final FeeCalculator calculator = new FeeCalculator();
@Test
void 当基础费加提示小于最大时_应充分使用提示() {
BigInteger baseFee = BigInteger.valueOf(20); // gwei
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(BigInteger.valueOf(22));
}
@Test
void 当基础费激增超过最大时_则被限制在最大费用() {
BigInteger baseFee = BigInteger.valueOf(60); // 大幅上升
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // 限制在 50,不超过
}
}
第二个测试说明了为什么你的 transaction 在网络拥堵时会 被卡(pending):baseFee 超过了你设定的 maxFeePerGas,validator 没有动力接受因为实际 tip 太低。
6. 为什么 base fee 按块变化
baseFee 不是固定的 — 它根据前一个 block 的满度自动调整,基于大致公式:
baseFee_下一个 = baseFee_当前 × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
其中 gasTarget 是 block 的 50% gasLimit(Ethereum 的每个 block 的 gas limit 约 30 百万,目标约 15 百万)。
- Block 比目标更满 →
baseFee最多每块上升 12.5%。 - Block 比目标更空 →
baseFee最多每块下降 12.5%。
package dev.nonla.evm;
import java.math.BigInteger;
public class BaseFeeAdjuster {
private static final BigInteger EIGHT = BigInteger.valueOf(8);
public BigInteger nextBaseFee(BigInteger currentBaseFee, long gasUsed, long gasTarget) {
long delta = gasUsed - gasTarget;
BigInteger change = currentBaseFee
.multiply(BigInteger.valueOf(delta))
.divide(BigInteger.valueOf(gasTarget))
.divide(EIGHT);
BigInteger next = currentBaseFee.add(change);
// baseFee 永远不会变负
return next.max(BigInteger.ONE);
}
}
@Test
void 当块比目标更满时_基础费上升() {
BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
BigInteger current = BigInteger.valueOf(100); // gwei
BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);
// gasUsed = 2 倍目标 -> 上升接近 12.5%
assertThat(next).isGreaterThan(current);
}
这种自动调整机制解释了为什么你每隔约 12 秒(一个 block 的时间)看到 baseFee 跳跃:network 在自动平衡 block 空间的供给和需求,而不是 miner/validator 自行决定价格。
7. 通过 JSON-RPC 观察 base fee 和 gas price
你可以直接从 Ethereum node 获取 baseFeePerGas:
curl -s -X POST https://rpc.ankr.com/eth \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \
| jq '.result.baseFeePerGas'
"0x9502f9000"
返回值是十六进制(wei),你可以通过除以 10⁹ 转换为 gwei。
8. 发送 transaction 前用 Web3j 估算 gas
与其猜测 gasLimit,不如让 node 用 eth_estimateGas 事先计算 — 避免设置过低(回滚)或过高(暂时锁定额外 ETH)。
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.12.2</version>
</dependency>
package dev.nonla.evm;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthEstimateGas;
import org.web3j.protocol.core.methods.request.Transaction;
import java.math.BigInteger;
public class GasEstimator {
private final Web3j web3j;
public GasEstimator(Web3j web3j) {
this.web3j = web3j;
}
public BigInteger estimate(String from, String to, String data) throws Exception {
Transaction tx = Transaction.createFunctionCallTransaction(
from, null, null, null, to, data);
EthEstimateGas response = web3j.ethEstimateGas(tx).send();
if (response.hasError()) {
throw new IllegalStateException("估算失败:" + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGas在当前 state 上模拟 transaction 并返回需要的 gas。- 实际上应该在这个结果上再加 10–20% 的缓冲再设置
gasLimit,因为在估算和实际执行之间 state 可能改变。
9. 常见错误
- 设置
gasLimit过接近估算值:如果 storage 布局轻微变化(例如 slot 从 0 → 非零的 gas 成本不同),transaction 容易因out of gas而回滚。始终加上缓冲。 - 在网络拥堵时设置
maxFeePerGas过低:transaction 无限期留在 mempool,没有自动超时 — 你必须通过发送相同 nonce 但费用更高的 transaction(replace-by-fee)来手动取消。 - 混淆
gasPrice(legacy)和maxFeePerGas(EIP-1559):在已支持 1559 的网络上发送旧式 transaction 仍然有效,但你失去了单独控制 tip 的能力 — 钱包可能相对 baseFee 实际多付。 - 不计算足够的
priorityFee:如果 tip 接近 0 且 block 满的情况下,即使maxFeePerGas很高,validator 也没有动力快速打包你的 transaction。
注意: 不要在生产代码中硬编码固定的
gasPrice。始终调用eth_feeHistory或eth_gasPrice来根据当时行情获取价格,然后应用第 5 节的公式。
10. 总结
Gas 是 EVM 中计算工作量的度量单位;实际费用 = 消耗的 gas × 每个 gas 的价格。通过 EIP-1559,价格分为自动调整(根据 block 满度)的 baseFee 和你选择以获得优先权的 priorityFee。深入理解这个公式帮助你合理设置 gasLimit/maxFeePerGas,避免 transaction 被卡或被多收费 — 如果需要,你完全可以用 Java 编写自己的 fee 计算逻辑(如上例所示)来集成到后端,用于钱包监控或交易仪表板。