Gas là gì và vì sao phí thay đổi trong EVM
1. Tổng quan
Nếu bạn từng gửi transaction trên Ethereum và thấy phí lúc rẻ lúc đắt gấp 10 lần, bạn đang chạm vào khái niệm gas. Bài này giải thích gas là gì, cách EVM tính phí trước và sau EIP-1559, vì sao baseFee thay đổi theo từng block. Đọc xong, bạn sẽ tự tính được phí giao dịch bằng code và biết đặt maxFeePerGas hợp lý để tránh transaction bị treo hoặc trả dư tiền.
2. Gas là gì
Gas là đơn vị đo công việc tính toán mà EVM (Ethereum Virtual Machine) phải thực hiện để chạy một transaction. Mỗi opcode (lệnh máy ảo) có một chi phí gas cố định — cộng số nguyên (ADD) rẻ, ghi dữ liệu vào storage (SSTORE) đắt gấp hàng trăm lần.
// 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)
}
}
Dòng value = value + 1 thực chất là hai bước: tính toán số học (rẻ) và ghi lại vào storage (đắt). Đây là lý do tối ưu smart contract thường tập trung vào giảm số lần đọc/ghi storage, không phải giảm phép tính.
Lưu ý: Gas không phải là tiền. Gas là “lượng công việc”. Phí thật = gas dùng × giá mỗi gas (tính bằng ETH).
3. Gas limit, gas price và gas used
Ba khái niệm hay bị nhầm:
- Gas limit: số gas tối đa bạn cho phép transaction tiêu. Đặt quá thấp → transaction revert với lỗi
out of gas. - Gas used: số gas thực tế đã tiêu, luôn ≤ gas limit.
- Gas price: giá mỗi đơn vị gas, tính bằng
gwei(1 gwei = 10⁻⁹ ETH).
Phí giao dịch (trước EIP-1559) = gasUsed × gasPrice
Ví dụ transfer ETH đơn giản tốn 21.000 gas, nếu gasPrice = 30 gwei:
Phí = 21.000 × 30 gwei = 630.000 gwei = 0.00063 ETH
4. Cơ chế cũ: legacy gas price
Trước EIP-1559 (trước fork London, 2021), người dùng tự đặt gasPrice. Giá cao thì miner ưu tiên đưa transaction vào block trước. Cơ chế này giống đấu giá mù: bạn không biết đối thủ trả bao nhiêu, dễ trả quá cao hoặc quá thấp rồi phải chờ.
5. EIP-1559: base fee + priority fee
EIP-1559 tách phí thành hai phần:
baseFee: do network tự tính dựa trên độ đầy của block trước, bị burn (đốt, không vào tay validator).priorityFee(tip): bạn trả thêm cho validator để được ưu tiên.
Người dùng đặt hai giá trị:
maxFeePerGas: giá tối đa bạn chấp nhận trả mỗi gas.maxPriorityFeePerGas: tip tối đa bạn chấp nhận trả.
5.1. Công thức tính phí thực tế
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
phí thực tế = gasUsed × effectiveGasPrice
5.2. Tính fee bằng 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); // không bao giờ vượt maxFeePerGas
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPricelấy giá trị nhỏ hơn giữa (baseFee + tip mong muốn) và mức trầnmaxFeePerGasbạn đặt.totalFeenhân với gas đã dùng thực tế để ra số ETH phải trả.
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); // tăng mạnh
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // ghim ở 50, không trả quá
}
}
Test thứ hai minh họa đúng lý do transaction của bạn bị treo (pending) khi network tắc: baseFee tăng vượt maxFeePerGas bạn đặt, validator không còn động lực nhận vì tip thực nhận quá thấp.
6. Vì sao base fee thay đổi theo từng block
baseFee không cố định — nó tự điều chỉnh theo độ đầy block trước, dựa trên công thức xấp xỉ:
baseFee_tiếp = baseFee_hiện_tại × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
Trong đó gasTarget là 50% gasLimit của block (mỗi block Ethereum có gas limit khoảng 30 triệu, target khoảng 15 triệu).
- Block đầy hơn target →
baseFeetăng tối đa 12.5% mỗi block. - Block trống hơn target →
baseFeegiảm tối đa 12.5% mỗi 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 không bao giờ âm
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 target -> tăng gần 12.5%
assertThat(next).isGreaterThan(current);
}
Cơ chế tự điều chỉnh này giải thích tại sao bạn thấy baseFee nhảy liên tục mỗi ~12 giây (thời gian một block): network đang tự cân bằng cung-cầu không gian block, không phải miner/validator tự đặt giá tùy ý.
7. Quan sát base fee và gas price qua JSON-RPC
Bạn có thể lấy baseFeePerGas trực tiếp từ node Ethereum:
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"
Giá trị trả về là hex (wei), bạn chuyển sang gwei bằng cách chia 10⁹.
8. Ước lượng gas trước khi gửi transaction với Web3j
Thay vì đoán gasLimit, hãy để node tính trước bằng eth_estimateGas — tránh đặt quá thấp (revert) hoặc quá cao (khóa dư ETH tạm thời).
<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 thất bại: " + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGasmô phỏng transaction trên state hiện tại và trả về gas cần dùng.- Thực tế nên cộng thêm buffer 10–20% vào kết quả này trước khi đặt
gasLimit, vì state có thể thay đổi giữa lúc estimate và lúc transaction thật sự chạy.
9. Lỗi thường gặp
- Đặt
gasLimitsát đúng số estimate: nếu storage layout thay đổi nhẹ (ví dụ slot từ 0 → non-zero tốn gas khác), transaction dễ revert vìout of gas. Luôn cộng buffer. - Đặt
maxFeePerGasquá thấp trong lúc network tắc: transaction nằm trong mempool vô thời hạn, không timeout tự động — bạn phải tự hủy bằng cách gửi transaction cùng nonce với fee cao hơn (replace-by-fee). - Nhầm
gasPrice(legacy) vớimaxFeePerGas(EIP-1559): gửi transaction kiểu cũ trên mạng đã hỗ trợ 1559 vẫn hoạt động, nhưng bạn mất quyền kiểm soát tip riêng — ví có thể trả dư so với baseFee thực tế. - Không tính
priorityFeequá thấp: nếu tip gần 0 trong lúc block đầy, validator không có động lực đưa transaction vào block sớm dùmaxFeePerGascao.
Lưu ý: Đừng hard-code
gasPricecố định trong code production. Luôn gọieth_feeHistoryhoặceth_gasPriceđể lấy giá trị thị trường theo thời điểm, rồi áp công thức ở mục 5.
10. Kết luận
Gas là đơn vị đo công việc tính toán trong EVM; phí thực tế = gas dùng × giá mỗi gas. Với EIP-1559, giá đó tách thành baseFee tự điều chỉnh theo độ đầy block và priorityFee do bạn chọn để ưu tiên. Hiểu rõ công thức này giúp bạn đặt gasLimit/maxFeePerGas hợp lý, tránh transaction bị treo hoặc trả phí oan — và nếu cần, bạn hoàn toàn có thể tự viết logic tính fee bằng Java như ví dụ trên để tích hợp vào backend theo dõi ví hoặc dashboard giao dịch.