Monad 在两条独立的轴上为交易定价:执行 Gas 价格和单独的数据(calldata)价格。以太坊 JSON-RPC 方法 eth_gasPrice 和 eth_estimateGas 各自只涉及其中一条轴,因此如果客户端将 gasUsed 乘以单一建议价格,就会错误估算数据密集型调用。正确的估算需要读取建议执行价格、读取当前数据价格、估算执行 Gas、计算 calldata 大小,并将它们与显式缓冲合并。本文解释其机制,展示可运行的 Node.js 估算器,并提供结果表,以便你针对自己的端点进行测量,而不是信任硬编码常量。
从操作角度理解 Monad 的双轴定价模型
Monad 关于 gas 定价 的文档指出,网络按两条独立的轴收费:执行 Gas 价格和单独的数据(calldata)价格。执行 Gas 覆盖交易消耗的计算量;数据价格覆盖交易携带的字节数。因此,交易的有效成本是所用 Gas 和 calldata 大小的函数,而不是单一的 gasUsed × gasPrice 乘积。
正是这一点打破了带着以太坊思维模型而来的读者的认知。在以太坊 L1 上,费用本质上是一维的:gasUsed × (baseFee + priorityFee)。在 Monad 上,如果读者天真地将 gasUsed 乘以一个建议价格,就会根据调用的数据密集程度而低估或高估。简单的价值转移和大型 calldata 合约调用可能消耗相似的执行 Gas,但携带的数据成本却大不相同。
从操作角度看,该模型告诉你每条轴惩罚交易的哪一部分。如果你的调用是计算密集型的(循环、存储写入),执行轴占主导。如果你的调用是数据密集型的(大型 ABI 编码参数、批量负载、calldata 密集型合约部署),数据轴占主导。同时读取两条轴,才能让你推理应该优化哪一条。
- 执行轴:按消耗的执行 Gas 单位定价。
- 数据轴:按交易携带的 calldata 字节定价。
- 有效成本:两者的组合,而不是单一的 gasUsed × gasPrice 乘积。
- 实际后果:数据密集型调用需要与计算密集型调用不同的估算方式。
eth_gasPrice 在 Monad 上返回什么,又省略了什么
以太坊 JSON-RPC 规范将 eth_gasPrice 定义为返回以 wei 计的建议 Gas 价格。在 Monad 上,应将其视为建议的执行 Gas 价格,而不是总成本。它只是双轴计算的一个输入,确切的响应字段有文档说明 / 因节点版本而异,因此请根据当前负载进行验证,而不是硬编码假设。
这种省略很重要。如果客户端将 eth_gasPrice 作为唯一的费用输入,就会错误估算任何成本主要由 calldata 主导的交易。该方法不编码数据价格,也不知道你的交易将携带多少字节。它回答的是一个更窄的问题:当前建议的执行价格是多少。
如果你通过托管端点连接,无论提供商是谁,方法语义都相同;不同的是节点返回的建议值以及提供商侧的缓存。关于端点选择和提供商特定行为,请参阅 Monad RPC 提供商指南 和 Monad 网络页面。
- 返回:以 wei 计的建议执行 Gas 价格。
- 不返回:数据价格或总交易成本。
- 风险:将其作为唯一费用输入会错误估算数据密集型调用。
- 验证:响应字段有文档说明 / 因节点版本而异。
为什么 eth_estimateGas 回答的是“能否装下”,而不是“成本”
规范将 eth_estimateGas 描述为返回交易执行所需 Gas 的估算值。这是一个以 Gas 单位表示的 Gas 上限,只涉及执行轴。它不返回价格,也不考虑数据轴。
将上限与价格混为一谈是一个常见错误。开发者读取 eth_estimateGas 返回的数字,乘以 eth_gasPrice 返回的数字,就以为自己得到了成本。他们得到的是执行轴估算。数据轴仍未定价,而在 calldata 密集型交易中,这个缺口可能是账单中更大的部分。
正确的解读是:eth_estimateGas 告诉你交易是否能装进 Gas 上限,以及大致消耗多少执行量。它是规模估算工具,不是定价工具。在向用户展示成本之前,请为每条轴分别读取价格。
- eth_estimateGas 返回 Gas 上限(执行轴),不是价格。
- 将其乘以 eth_gasPrice 得到的是仅执行轴的估算。
- 仅靠这一对方法,数据密集型交易仍未定价。
- 用它来估算规模,然后分别对两条轴定价。
eth_feeHistory 的定位,以及为什么低活动量会让它充满噪声
eth_feeHistory 方法返回历史 base fee 和 priority fee 数据,通常带有百分位区间。在费用波动性高的以太坊 L1 上,这些百分位区间很有信息量。在费用波动性较低的链上,这些区间可能很平坦,而平坦的区间几乎不能提供关于下一步该支付多少的信号。
在 Monad 上,应将 feeHistory 视为参考性信息。它有助于检查当前建议价格是否处于正常范围,但不应成为费用的唯一驱动因素。优先使用节点当前建议的执行价格加上显式缓冲,并单独读取数据价格。关于该方法语义的更深入讨论,请参阅 以太坊费用历史与优先费估算 和 eth_feeHistory 优先费百分位估算器。
一个实用模式是将 feeHistory 用作护栏:如果你选择的价格远在近期范围之外,就记录下来,让调用方决定。不要在低活动量链上静默地钳制到历史百分位,因为该百分位可能反映的是几乎没有费用压力的时期。
- 在 Monad 上,feeHistory 是参考性的,不是权威的。
- 低活动量链上的平坦百分位区间几乎不携带信号。
- 优先使用当前建议价格加上显式缓冲。
- 将 feeHistory 用作范围护栏,而不是硬性钳制。
实用的双轴估算器配方
该配方包含五次读取和一次组合。第一,读取建议执行价格。第二,读取当前数据价格。第三,用 eth_estimateGas 估算执行 Gas。第四,计算 calldata 的字节大小。第五,将两条轴组合成一个预期成本数值,并应用可设置的缓冲,以便调用方在成本和上链信心之间权衡。
组合是读者容易出错的部分。预期成本不是 gasUsed × gasPrice。它是 (executionGas × executionPrice) + (calldataBytes × dataPrice),并根据你的风险偏好对总额或每条轴应用缓冲。在输出中保持两个组成部分分离,这样你就能看到给定交易中哪条轴占主导。
由于数据价格不是客户端应该硬编码的常量,请在估算时读取它。如果你的节点没有暴露专用的数据价格方法,请查看当前的 Monad 文档和你的提供商的方法列表;确切的方法名和响应结构有文档说明 / 因节点版本而异。如果你正在选择端点,API 服务 和 RPC 定价 页面描述了托管访问的结构。
- 读取建议执行价格。
- 读取当前数据价格。
- 估算执行 Gas(eth_estimateGas)。
- 计算 calldata 字节大小。
- 组合:(gas × execPrice) + (bytes × dataPrice),然后应用缓冲。
针对 Monad RPC 端点的可运行 Node.js 估算器
下面的脚本查询 Monad RPC 端点以获取建议执行价格,为示例交易估算 Gas,计算 calldata 大小,并打印两条轴以及组合数值。它只使用标准 JSON-RPC 调用和 Node 内置的 fetch,因此在现代 Node 运行时上无需额外依赖即可运行。
将端点 URL 和示例交易替换为你自己的。数据价格读取显示为占位调用,因为确切的方法名有文档说明 / 因节点版本而异;请查看当前的 Monad 文档和你的提供商的方法列表,然后接入正确的方法。该脚本有意分别打印两条轴,以便你看到哪条轴占主导。
// monad-estimator.js — run with: node monad-estimator.js
const RPC_URL = process.env.MONAD_RPC_URL || 'https://your-monad-endpoint';
const BUFFER = 1.15; // 15% buffer, settable
async function rpc(method, params = []) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(method + ': ' + JSON.stringify(json.error));
return json.result;
}
async function main() {
// 1. Suggested execution gas price (wei)
const execPriceHex = await rpc('eth_gasPrice');
const execPrice = BigInt(execPriceHex);
// 2. Current data price (wei per byte). Method name varies by node version.
// Check current Monad docs / provider method list and wire it here.
let dataPrice = 0n;
try {
const dataPriceHex = await rpc('eth_dataPrice'); // placeholder name
dataPrice = BigInt(dataPriceHex);
} catch (e) {
console.warn('data price method unavailable on this endpoint:', e.message);
}
// 3. Sample transaction
const tx = {
from: '0x0000000000000000000000000000000000000001',
to: '0x0000000000000000000000000000000000000002',
value: '0x0',
data: '0x' + 'ab'.repeat(200) // 200 bytes of calldata
};
// 4. Estimate execution gas
const gasHex = await rpc('eth_estimateGas', [tx]);
const gas = BigInt(gasHex);
// 5. Size calldata
const calldataBytes = BigInt((tx.data.length - 2) / 2);
// 6. Combine both axes
const execCost = gas * execPrice;
const dataCost = calldataBytes * dataPrice;
const total = execCost + dataCost;
const buffered = (total * BigInt(Math.round(BUFFER * 100))) / 100n;
console.log('execution gas :', gas.toString());
console.log('exec price (wei) :', execPrice.toString());
console.log('calldata bytes :', calldataBytes.toString());
console.log('data price (wei/B) :', dataPrice.toString());
console.log('exec cost (wei) :', execCost.toString());
console.log('data cost (wei) :', dataCost.toString());
console.log('total (wei) :', total.toString());
console.log('with buffer (wei) :', buffered.toString());
}
main().catch(e => { console.error(e); process.exit(1); });在自己的输出中分别读取两条轴
单一的组合数字掩盖了机制。将执行成本和数据成本作为单独的字段打印出来,这样当用户问为什么交易很贵时,你可以指出负责的那条轴。这也让你的估算器可调试:如果组合数值看起来不对,你可以看到是执行读取还是数据读取异常。
同样的分离也有助于优化。如果数据轴占主导,考虑你的 calldata 是否可以压缩、以不同方式批处理或移到链下。如果执行轴占主导,检查合约逻辑。没有这种拆分,你就是在猜测。
保持缓冲显式且可配置。缓冲是一种策略选择,不是协议常量;将其暴露出来,以便调用方在成本和上链信心之间权衡。记录你发布的默认值及其原因。
- 将执行成本和数据成本作为单独的字段打印。
- 利用拆分来诊断哪条轴异常。
- 将缓冲暴露为可配置策略,而不是隐藏常量。
- 记录原始读取值,以便估算可复现。
// split-axes.js — print each pricing axis as its own field, then a combined total.
// Run with: node split-axes.js
const RPC_URL = process.env.MONAD_RPC_URL || 'https://your-monad-endpoint';
async function rpc(method, params = []) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(method + ': ' + JSON.stringify(json.error));
return json.result;
}
// Convert a wei value to a human-readable string without losing precision.
function wei(v) {
const s = v.toString().padStart(19, '0');
return s.slice(0, -18) + '.' + s.slice(-18);
}
async function report(tx) {
const execPrice = BigInt(await rpc('eth_gasPrice'));
const gas = BigInt(await rpc('eth_estimateGas', [tx]));
const calldataBytes = BigInt((tx.data.length - 2) / 2);
// The data axis is read separately; if the endpoint does not expose it,
// report the gap explicitly instead of assuming a zero price.
let dataPrice = null;
try { dataPrice = BigInt(await rpc('eth_dataPrice')); }
catch { console.warn('data-price method unavailable on this endpoint'); }
const execCost = gas * execPrice;
const dataCost = dataPrice === null ? null : calldataBytes * dataPrice;
console.log(JSON.stringify({
execution: { gas: gas.toString(), priceWei: execPrice.toString(), costWei: execCost.toString(), costMON: wei(execCost) },
data: { bytes: calldataBytes.toString(), priceWei: dataPrice === null ? null : dataPrice.toString(),
costWei: dataCost === null ? null : dataCost.toString() },
combinedWei: dataCost === null ? null : (execCost + dataCost).toString(),
note: dataCost === null ? 'data axis unavailable; execution-only estimate' : 'both axes priced'
}, null, 2));
}
report({
from: '0x0000000000000000000000000000000000000001',
to: '0x0000000000000000000000000000000000000002',
value: '0x0',
data: '0x' + 'ab'.repeat(200)
}).catch(e => { console.error(e.message); process.exit(1); });结果表:针对自己的端点进行测量
不要信任硬编码常量或来自别人端点的数字。使用上面的估算器脚本或你自己的客户端,针对你自己的 Monad RPC 端点填写下表。在一天中的不同时间以及针对不同交易形态运行它,以便看到两条轴如何变化。
记录端点、时间戳、建议执行价格、数据价格、估算 Gas、calldata 大小和组合数值。如果你的端点没有暴露数据价格方法,请将其记录为缺口,并在断定该轴为零之前查看当前的 Monad 文档和你的提供商的方法列表。
该表是一种测量方法,不是基准。它产生的值特定于你的端点和你的交易;将它们视为需要验证的观察结果,而不是通用数字。
- 列:端点、时间戳、执行价格、数据价格、Gas、calldata 字节数、组合成本、应用的缓冲。
- 行:每种交易形态一行(价值转移、小型调用、大型 calldata 调用、部署)。
- 重复:在多个时间运行以观察轴的变化。
- 记录缺口:记录你的端点未暴露的任何方法。
故障排除:当单轴思维模型以错误形式浮现
人们还搜索的查询“为什么 MetaMask 说 Gas 不足”正是单轴定价的症状。一个估算执行 Gas 并乘以单一建议价格的钱包可能会低估数据密集型交易,而缺口会以资金不足或 Gas 不足错误的形式出现,而不是清晰的定价消息。
诊断路径是分离两条轴。读取建议执行价格、读取数据价格、估算 Gas 并计算 calldata 大小。如果组合数值超过余额或钱包应用的费用上限,你就找到了缺口。如果端点没有暴露数据价格方法,这种缺失本身就是发现:你的估算器在单轴上运行。
其他常见症状也能清晰对应。交易能装进 Gas 上限但仍因成本失败,指向数据轴。交易因 Gas 上限失败,指向执行轴。交易成功但成本高于预期,通常意味着估算中未包含数据轴。关于执行时序症状,请参阅 Monad 交易生命周期与异步执行收据状态;关于余额语义,请参阅 Monad 储备余额与 eth_getBalance。
- Gas 不足错误通常意味着数据轴被省略。
- 因 Gas 上限失败 → 执行轴。
- 因成本失败但能装进上限 → 数据轴。
- 成功但成本高于预期 → 数据轴未被估算。
- 缺少数据价格方法 → 估算器在单轴上运行。
局限性、权衡,以及估算器无法承诺的事情
Monad 的定价参数由协议定义,可能会变化。数据价格不是客户端应该硬编码的常量,确切的方法名和响应字段有文档说明 / 因节点版本而异。你发布的任何估算器都应在估算时读取当前值,并在方法不可用时优雅降级。
估算器是估算,不是保证。执行 Gas 可能随状态变化,建议价格可能在估算和交易上链之间变动。缓冲会降低缺口出现的概率,但不会消除它。将组合数值视为规划数字,而不是合同。
准确性和复杂性之间也存在权衡。单轴估算更简单,对于 calldata 较小的计算密集型调用可能足够。双轴估算对数据密集型调用更准确,但需要读取额外的值并处理其缺失。根据你的应用实际发送的交易形态来选择,并记录该选择。
- 定价参数由协议定义,可能会变化。
- 数据价格不是可硬编码的常量。
- 方法名和字段有文档说明 / 因节点版本而异。
- 估算是规划数字,不是保证。
- 双轴准确性需要额外读取和回退路径。
后续步骤:将双轴定价接入你的技术栈
首先针对你的端点运行估算器,并为你实际发送的交易形态填写结果表。这会告诉你数据轴对你的工作负载是否重要。如果重要,将双轴组合接入你的费用显示和预检检查,以便用户看到反映两条轴的成本。
然后决定当数据价格读取不可用时如何处理。安全的默认做法是记录缺口,并在执行轴上回退到保守缓冲,同时标记该估算为单轴。不要静默地假设数据价格为零。
关于端点选择和提供商特定行为,请查看 Monad RPC 提供商指南、Monad 网络页面 和 API 服务 概览。关于更广泛的费用估算背景,OnFinality Learn 中心 汇集了相关的以太坊和 Monad 文章,RPC 定价 描述了托管访问的结构。
- 运行估算器,并为你的交易形态填写结果表。
- 将双轴组合接入费用显示和预检检查。
- 为缺失的数据价格读取定义回退方案;永远不要假设为零。
- 在选择端点之前查看提供商和网络页面。