GasとはなにかEVMで手数料が変動する理由

1. 概要

Ethereum でトランザクションを送信した際、ある時は安く、ある時は 10 倍高い手数料を見たことがあれば、それが gas という概念です。この記事では、gas とは何か、EVM が EIP-1559 前後でどのように手数料を計算するか、そして baseFee がブロックごとに変動する理由を説明します。読み終わったら、コードで自分自身取引手数料を計算でき、トランザクションが停止したり余分な代金を払うのを避けるため、合理的な maxFeePerGas を設定できるようになります。

2. Gas とは

Gas は、トランザクションを実行するために EVM (Ethereum Virtual Machine) が実行する必要がある計算作業を測定する単位です。各 opcode (仮想マシン命令) には固定の gas コストがあります。整数の加算 (ADD) は安く、ストレージへのデータ書き込み (SSTORE) は数百倍高い。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint256 public value; // slot storage

    function increment() public {
        value = value + 1; // ADD (~3 gas) + SSTORE (~5000-20000 gas)
    }
}

value = value + 1 の行は実質的には 2 つのステップです:算術計算(安い)とストレージへの書き込み(高い)。これが、スマートコントラクトの最適化が、計算を減らすのではなく、ストレージの読み取り/書き込みの回数を減らすことに焦点を当てる理由です。

注意: Gas は通貨ではありません。Gas は「仕事量」です。実際の手数料 = 使用 gas × 1 gas あたりの価格 (ETH で計算)。

3. Gas limit、gas price、および gas used

混同されやすい 3 つの概念:

  • Gas limit: トランザクションが消費することを許可する最大 gas 数。設定が低すぎると → トランザクションは out of gas エラーでリバートされます。
  • Gas used: 実際に消費された gas 数。常に ≤ gas limit。
  • Gas price: 1 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. 古いメカニズム: レガシー gas price

EIP-1559 以前 (London フォーク前の 2021 年)、ユーザーは自分で gasPrice を設定していました。価格が高いほど、マイナーはトランザクションをブロックに優先的に含めます。このメカニズムはブラインドオークションのようです:競争相手がいくら払っているかわからず、多く払いすぎたり、少なく払いすぎたり、待つ必要があったりします。

5. EIP-1559: base fee + priority fee

EIP-1559 は手数料を 2 つの部分に分割します:

  • baseFee: ネットワークが前のブロックの満杯度に基づいて自動的に計算され、burn (焼却) されます (バリデーターには入りません)。
  • priorityFee (tip): 優先度の高い処理を受けるためにバリデーターに追加で支払う金額。

ユーザーは 2 つの値を設定します:

  • maxFeePerGas: 1 gas あたりの最大支払い価格。
  • maxPriorityFeePerGas: 最大チップ金額。

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 + 希望するチップ) と設定した上限 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 whenBaseFeePlusTipLessThanMax_thenUseFullTip() {
        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 whenBaseFeeExceedsMaxThenPinnedToMaxFeePerGas() {
        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 に固定、超過分なし
    }
}

2 番目のテストは、ネットワークが混雑している時にトランザクションが停止 (pending) する理由を示しています:baseFee が設定した maxFeePerGas を超えると、バリデーターはチップが低いため取り込むインセンティブがなくなるからです。

6. baseFee がブロックごとに変動する理由

baseFee は固定ではなく、前のブロックの満杯度に基づいて自動的に調整されます。以下の公式に従います:

baseFee_次 = baseFee_現在 × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)

ここで gasTarget はブロックの gasLimit の 50% です (各 Ethereum ブロックには約 30 百万のガスリミットがあり、ターゲットは約 15 百万です)。

  • ブロックがターゲットより満杯 → baseFee は最大 12.5% ずつ増加します。
  • ブロックがターゲットより空き → 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 whenBlockFullerThanTargetThenBaseFeeIncreases() {
    BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
    BigInteger current = BigInteger.valueOf(100); // gwei

    BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);

    // gasUsed = 2x target -> 約 12.5% 増加
    assertThat(next).isGreaterThan(current);
}

このメカニズムにより、12 秒ごと (1 ブロックの時間) に baseFee が変動する理由が説明されます。ネットワークが需給に基づいてブロック空間を自動的に調整しており、マイナー/バリデーターが恣意的に価格を設定しているわけではありません。

7. JSON-RPC を通じた baseFee と gas price の確認

Ethereum ノードから直接 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"

返される値は 16 進数 (wei) であり、10⁹ で割ることで gwei に変換できます。

8. Web3j を使用したトランザクション送信前の gas 推定

gasLimit を推測する代わりに、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("Estimate 失敗: " + response.getError().getMessage());
        }
        return response.getAmountUsed();
    }
}
  • ethEstimateGas は、現在の状態でトランザクションをシミュレートし、必要な gas を返します。
  • 実際には、gasLimit を設定する前にこの結果に 10~20% のバッファを追加する必要があります。これは、推定時とトランザクション実行時の間に状態が変わる可能性があるためです。

9. よくあるエラー

  • gasLimit を見積値に合わせて設定する: ストレージレイアウトがわずかに変わった場合 (例:スロット 0 → 非ゼロは異なる gas を消費)、トランザクションは out of gas のためにリバートしやすくなります。常にバッファを追加します。
  • ネットワークが混雑している時に maxFeePerGas を設定しすぎる: トランザクションはメモリプール内に無期限に留まり、自動的にタイムアウトしません。より高い手数料を持つ同じ nonce のトランザクションを送信して自分でキャンセルする必要があります (replace-by-fee)。
  • gasPrice (レガシー) と maxFeePerGas (EIP-1559) を混同する: 1559 をサポートするネットワークで古いタイプのトランザクションを送信することはできますが、チップを個別に制御する権利を失い、ウォレットは実際の baseFee より過剰に支払う可能性があります。
  • priorityFee が低すぎる: ブロックが満杯の場合、チップが 0 に近い場合、maxFeePerGas が高くても、バリデーターはトランザクションを早期にブロックに含める動機がありません。

注意: 本番環境のコードで gasPrice をハードコードしないでください。常に eth_feeHistory または eth_gasPrice を呼び出して現在の市場価値を取得し、セクション 5 の公式を適用してください。

10. まとめ

Gas は EVM での計算作業を測定する単位です。実際の手数料 = 使用 gas × 1 gas あたりの価格。EIP-1559 では、この価格はブロックの満杯度に基づいて自動的に調整される baseFee と、優先度を選択する priorityFee に分割されます。この公式をしっかり理解することで、gasLimit/maxFeePerGas を合理的に設定でき、トランザクションが停止したり不当な手数料を払うことを避けられます。必要に応じて、上記の例のように Java でフィー計算ロジックを自分で書き、ウォレット監視バックエンドまたは取引ダッシュボードに統合することもできます。

evmblockchainethereumgaseip-1559