Gas – was es ist und warum sich Gebühren in der EVM ändern
1. Übersicht
Falls du schon mal eine Transaktion auf Ethereum gesendet hast und gesehen hast, dass die Gebühr manchmal um das 10-Fache günstiger oder teurer ist, hast du das Konzept von Gas kennengelernt. Dieser Artikel erklärt, was Gas ist, wie die EVM Gebühren vor und nach EIP-1559 berechnet, und warum sich baseFee von Block zu Block ändert. Nach dem Lesen kannst du die Transaktionsgebühren selbst mit Code berechnen und maxFeePerGas sinnvoll setzen, um hängende Transaktionen zu vermeiden oder kein Geld zu verschwenden.
2. Was ist Gas
Gas ist eine Maßeinheit für die Rechentätigkeiten, die die EVM (Ethereum Virtual Machine) ausführen muss, um eine Transaktion zu verarbeiten. Jeder Opcode (Virtual-Machine-Befehl) hat feste Gas-Kosten — Addition von Ganzzahlen (ADD) ist billig, das Schreiben von Daten in Storage (SSTORE) ist hundertfach teurer.
// 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)
}
}
Die Zeile value = value + 1 ist eigentlich zwei Schritte: arithmetische Berechnung (billig) und Schreiben in Storage (teuer). Deshalb konzentriert sich die Smart-Contract-Optimierung üblicherweise auf die Reduzierung von Lese-/Schreibvorgängen im Storage, nicht auf weniger Berechnungen.
Hinweis: Gas ist nicht Geld. Gas ist „die Menge an Arbeit”. Echte Gebühr = verwendetes Gas × Gaspreis (gemessen in ETH).
3. Gas-Limit, Gas-Preis und verbrauchtes Gas
Drei Konzepte, die oft verwechselt werden:
- Gas-Limit: das maximale Gas, das du die Transaktion verbrauchen lässt. Zu niedrig setzen → Transaktion wird zurückgewiesen mit Fehler
out of gas. - Verbrauchtes Gas: das tatsächlich verwendete Gas, immer ≤ Gas-Limit.
- Gas-Preis: der Preis pro Gas-Einheit, gemessen in
gwei(1 gwei = 10⁻⁹ ETH).
Transaktionsgebühr (vor EIP-1559) = gasUsed × gasPrice
Beispiel: Eine einfache ETH-Überweisung kostet 21.000 Gas, wenn gasPrice = 30 gwei:
Gebühr = 21.000 × 30 gwei = 630.000 gwei = 0,00063 ETH
4. Alter Mechanismus: Legacy-Gas-Preis
Vor EIP-1559 (vor dem London-Fork, 2021) legten Benutzer selbst den gasPrice fest. Ein höherer Preis bedeutete, dass Miner die Transaktion bevorzugt in den nächsten Block aufnahmen. Dieser Mechanismus war wie eine verdeckte Auktion: du wusstest nicht, was andere zahlten, sodass man leicht zu viel oder zu wenig bezahlen konnte und dann warten musste.
5. EIP-1559: Base Fee + Priority Fee
EIP-1559 teilt die Gebühr in zwei Teile:
baseFee: vom Netzwerk automatisch berechnet, basierend auf der Auslastung des vorherigen Blocks, und wird verbrannt (gebrannt, geht nicht an den Validator).priorityFee(Tip): du zahlst extra an den Validator für Vorrang.
Benutzer legen zwei Werte fest:
maxFeePerGas: der maximale Preis, den du pro Gas zahlst.maxPriorityFeePerGas: der maximale Tip, den du zahlst.
5.1. Formel für die tatsächliche Gebühr
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
tatsächliche Gebühr = gasUsed × effectiveGasPrice
5.2. Gebühren mit Java berechnen
<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); // nie über maxFeePerGas hinaus
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPricenimmt den kleineren Wert zwischen (baseFee + gewünschter Tip) und der ObergrenzemaxFeePerGas, die du setzt.totalFeemultipliziert mit dem tatsächlich verwendeten Gas, um die zu zahlende ETH-Menge zu erhalten.
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 khiBaseFeeCongTipNhoHonMax_thiDungTipDayDu() {
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 khiBaseFeeTangVuotMax_thiBiGhimOMaxFeePerGas() {
BigInteger baseFee = BigInteger.valueOf(60); // starker Anstieg
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // bei 50 begrenzt, nicht überschritten
}
}
Der zweite Test illustriert genau, warum deine Transaktion hängend bleibt (pending), wenn das Netzwerk überlastet ist: baseFee steigt über dein maxFeePerGas, und der Validator hat keinen Anreiz, es zu akzeptieren, da der tatsächliche Tip zu niedrig ist.
6. Warum sich die Base Fee von Block zu Block ändert
baseFee ist nicht fest — sie passt sich automatisch an die Auslastung des vorherigen Blocks an, basierend auf einer ungefähren Formel:
baseFee_nächster = baseFee_aktuell × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
Dabei ist gasTarget 50% des gasLimit des Blocks (jeder Ethereum-Block hat ein Gas-Limit von etwa 30 Millionen, ein Ziel von etwa 15 Millionen).
- Block voller als das Ziel →
baseFeesteigt maximal 12,5% pro Block. - Block leerer als das Ziel →
baseFeefällt maximal 12,5% pro Block.
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 ist nie negativ
return next.max(BigInteger.ONE);
}
}
@Test
void khiBlockDayHonTarget_thiBaseFeeTang() {
BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
BigInteger current = BigInteger.valueOf(100); // gwei
BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);
// gasUsed = 2x Ziel -> steigt um ~12,5%
assertThat(next).isGreaterThan(current);
}
Dieser selbstanpassende Mechanismus erklärt, warum du siehst, dass sich baseFee ständig alle ~12 Sekunden ändert (die Zeit für einen Block): das Netzwerk balanciert automatisch Angebot und Nachfrage nach Blockraum aus, anstatt dass Miner/Validator die Preise willkürlich festlegen.
7. Beobachte Base Fee und Gas Price über JSON-RPC
Du kannst baseFeePerGas direkt von einem Ethereum-Node abrufen:
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"
Der zurückgegebene Wert ist hexadezimal (Wei), du konvertierst zu gwei, indem du durch 10⁹ teilst.
8. Gas vor dem Senden einer Transaktion mit Web3j schätzen
Statt gasLimit zu raten, lass den Node es mit eth_estimateGas berechnen — vermeide, es zu niedrig (Rückschlag) oder zu hoch (temporär gesperrtes ETH) zu setzen.
<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("Schätzung fehlgeschlagen: " + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGassimuliert die Transaktion auf dem aktuellen State und gibt das benötigte Gas zurück.- In der Praxis solltest du einen Buffer von 10–20% zu diesem Ergebnis addieren, bevor du
gasLimitsetzt, da sich der State zwischen Schätzung und tatsächlicher Ausführung der Transaktion ändern kann.
9. Häufige Fehler
gasLimitgenau auf die Schätzung setzen: wenn sich das Storage-Layout leicht ändert (z. B. Slot von 0 → nicht-null kostet unterschiedlich Gas), kann die Transaktion leicht mitout of gaszurückgewiesen werden. Immer einen Buffer addieren.maxFeePerGaszu niedrig setzen, wenn das Netzwerk überlastet ist: Transaktion bleibt unbegrenzt im Mempool, kein automatisches Timeout — du musst sie selbst stornieren, indem du eine Transaktion mit der gleichen Nonce und höherer Gebühr sendest (Replace-by-Fee).gasPrice(Legacy) mitmaxFeePerGas(EIP-1559) verwechseln: Das Senden von Transaktionen im alten Stil auf einem Netzwerk, das 1559 bereits unterstützt, funktioniert immer noch, aber du verlierst die Kontrolle über den separaten Tip — die Geldbörse kann über dem tatsächlichen baseFee zu viel zahlen.priorityFeezu niedrig nicht berücksichtigen: wenn der Tip nahe 0 ist, während Blöcke voll sind, hat der Validator keinen Anreiz, die Transaktion frühzeitig in den Block aufzunehmen, auch wennmaxFeePerGashoch ist.
Hinweis: Codiere
gasPricenicht hart in Production-Code. Rufe immereth_feeHistoryodereth_gasPriceauf, um Marktpreise nach Zeit zu erhalten, und wende dann die Formel aus Punkt 5 an.
10. Fazit
Gas ist eine Maßeinheit für die Rechentätigkeiten in der EVM; die tatsächliche Gebühr = verwendetes Gas × Gaspreis. Mit EIP-1559 wird dieser Preis in baseFee, die sich automatisch an die Blockauslastung anpasst, und priorityFee, den du für Vorrang wählst, aufgeteilt. Wenn du diese Formel verstehst, kannst du gasLimit/maxFeePerGas sinnvoll setzen, um hängende Transaktionen oder unnötige Gebühren zu vermeiden — und wenn nötig, kannst du die Gebührenlogik mit Java wie im obigen Beispiel selbst schreiben und in ein Wallet-Tracking-Backend oder Transaktions-Dashboard integrieren.