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 计算逻辑(如上例所示)来集成到后端,用于钱包监控或交易仪表板。

evmblockchainethereumgaseip-1559