Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查阅读约 14 分钟

替换交易定价过低:修复卡住的 EVM 交易

通过理解内存池替换规则并执行正确的费用提升或取消操作,诊断并解决“replacement transaction underpriced”错误。

TL;DR

“replacement transaction underpriced”错误意味着你的节点拒绝了一笔第二笔交易,因为它使用了与内存池中已有交易相同的 nonce,但费用增加幅度未达到客户端要求的最低提升阈值。这是费用问题,不是 nonce 问题。要解决它,请使用 eth_getTransactionByHash 检查卡住的交易,使用 eth_getTransactionCount 检查待处理 nonce,然后选择提升费用(相同 nonce,更高的 maxFeePerGas 和 maxPriorityFeePerGas)、取消交易(相同 nonce,0 价值自转账并提高费用),或者如果费用已经足够则等待。始终根据当前基础费用和优先费用估算来计算替换费用,而不是猜测。

“Replacement Transaction Underpriced” 到底意味着什么

当你使用 eth_sendRawTransaction 发送交易时,节点会检查其内存池中是否已存在来自同一发送者且具有相同 nonce 的另一笔交易。如果存在,节点会将你的新交易视为替换交易并应用替换策略。当新交易的费用未超过现有交易费用达到客户端要求的最低提升百分比时,就会返回“replacement transaction underpriced”错误。这是费用拒绝,不是 nonce 拒绝。nonce 是正确的;价格不够激进。

以太坊 JSON-RPC 规范将 eth_sendRawTransaction 定义为向网络提交已签名交易。它没有定义替换规则;这些规则因客户端而异。例如,Geth 文档中记录了默认价格提升阈值(通常小费和费用上限均为 10%),但该值可配置,并因客户端和版本而异。始终将确切阈值视为“有文档说明/因客户端而异”,并对照你的节点配置或文档进行验证。有关参考,请参阅 Geth 交易池文档

由于替换被拒绝,你的原始交易仍留在池中。如果原始交易因费用过低而卡住,你现在陷入循环:你需要替换它,但你的替换交易定价必须足够高,既能超过旧交易,又能满足客户端的提升规则。

  • 相同发送者 + 相同 nonce = 替换尝试。
  • 拒绝原因:新费用未超过旧费用达到客户端的最低提升幅度。
  • 原始交易留在内存池中;不会自动取消任何内容。

诊断顺序:确认交易卡住并读取其费用参数

在替换任何内容之前,请确认交易确实卡住且尚未被打包。使用 eth_getTransactionByHash 并传入交易哈希。如果结果为 null,交易可能已完全从内存池中丢弃。如果结果包含 blockNumber,则交易已被打包,无需替换。如果 blockNumber 为 null 且交易存在,则它正在池中待处理。有关参考,请参阅 以太坊 JSON-RPC API 文档

接下来,使用 eth_getTransactionCount(address, 'pending') 读取账户的待处理 nonce。这告诉你网络期望该账户使用的下一个 nonce,包括待处理交易。将其与卡住交易的 nonce 进行比较。如果待处理 nonce 大于卡住的 nonce,则可能存在缺口或卡住交易后面有交易队列。如果待处理 nonce 等于卡住的 nonce,则卡住的交易是队列头部,必须首先解决。

同时读取卡住交易的 maxFeePerGas 和 maxPriorityFeePerGas(对于 EIP-1559 交易)或 gasPrice(对于传统交易)。这些值是基线。你的替换交易必须超过它们达到客户端的提升阈值,并且还要与当前市场具有竞争力。要深入了解费用估算,请参阅 eth_feeHistory 与费用估算

  • eth_getTransactionByHash:检查 blockNumber(null = 待处理,非 null = 已打包)。
  • eth_getTransactionCount(address, 'pending'):获取下一个可用 nonce。
  • 从卡住的交易中读取 maxFeePerGas 和 maxPriorityFeePerGas。

交易为何卡住:费用市场动态与 EIP-1559 上限

当交易费用低于市场愿意支付的价格时,交易就会卡住。在 EIP-1559 下,交易指定 maxFeePerGas 和 maxPriorityFeePerGas。实际支付的费用为 min(maxFeePerGas, baseFee + maxPriorityFeePerGas)。如果基础费用上升到超过你的 maxFeePerGas 减去优先费用,你的交易将无法被打包,因为协议不允许你支付超过上限的费用。它会留在池中,直到基础费用下降或你替换它。有关参考,请参阅 EIP-1559 规范

另一种情况是交易被丢弃。如果内存池已满或交易因费用过低而被驱逐,eth_getTransactionByHash 可能返回 null。在这种情况下,nonce 仍然空闲,你可以简单地发送一笔具有相同 nonce 和更高费用的新交易。严格来说这不是替换,因为没有什么可替换的,但 nonce 管理是相同的。

理解内存池有助于你决定是等待还是替换。读取以太坊内存池 指南解释了如何检查池内容和待处理交易。

  • 卡住:费用低于市场清算价格,或 maxFeePerGas 对当前基础费用来说太低。
  • 丢弃:交易被驱逐;nonce 再次空闲。
  • EIP-1559 上限:有效费用不能超过 maxFeePerGas。

三种恢复路径:提升、取消或等待

正确的操作取决于交易是否仍在池中、nonce 是否仍然空闲,以及当前费用市场是否值得等待。使用下面的决策表将你的情况映射到正确的路径。

提升:如果交易仍在待处理,并且你希望它执行,请使用相同的 nonce 重新发送,并显著提高 maxPriorityFeePerGas 和 maxFeePerGas。新费用必须超过旧费用达到客户端的提升阈值,并且还要与当前市场具有竞争力。这是最常见的修复方法。

取消:如果你不再希望交易执行,请使用相同的 nonce 重新发送一笔 0 价值自转账(发送到你自己的地址),并采用相同的提升费用。这笔替换交易将被打包并消耗 nonce,从而有效地取消原始交易。一旦替换交易被打包,原始交易将从池中丢弃。

等待:如果交易费用对当前条件确实足够,而池只是很深,等待可能是最佳选择。用更高的费用替换可能会不必要地多付。监控基础费用和你的交易在池中的位置。

  • 仍在池中,nonce 空闲:使用相同 nonce 进行提升或取消。
  • 已从池中消失,nonce 空闲:使用相同 nonce 和更高费用发送新交易。
  • nonce 已被消耗:交易已打包;无需操作。
  • nonce 仍空闲但交易待处理:需要替换。

决策表:症状到操作

使用此表快速确定正确的恢复路径。关键输入是 eth_getTransactionByHash 的结果(存在或 null)以及卡住交易的 nonce 与 eth_getTransactionCount 返回的待处理 nonce 之间的比较。

如果交易存在且其 nonce 等于待处理 nonce,则它是队列头部。你必须替换它以解除后续交易的阻塞。如果其 nonce 小于待处理 nonce,则它前面可能还有其他交易,但如果你使用相同的 nonce,替换它仍然有效。

如果交易为 null 且待处理 nonce 等于卡住的 nonce,则 nonce 空闲。你可以使用该 nonce 和更高费用发送新交易。如果待处理 nonce 大于卡住的 nonce,则 nonce 已被另一笔交易消耗,卡住的交易无关紧要。

  • 存在 + nonce == 待处理 nonce:使用相同 nonce、更高费用替换。
  • 存在 + nonce < 待处理 nonce:使用相同 nonce、更高费用替换;检查缺口。
  • Null + nonce == 待处理 nonce:使用相同 nonce、更高费用发送新交易。
  • Null + nonce < 待处理 nonce:nonce 已被消耗;无需操作。

计算有效的替换费用

不要猜测替换费用。从网络读取当前基础费用和优先费用估算。你可以使用 eth_feeHistory 或 eth_maxPriorityFeePerGas。然后设置替换交易的 maxPriorityFeePerGas 和 maxFeePerGas,严格高于旧交易的值(至少达到客户端的提升阈值)以及当前市场估算。

对于 EIP-1559,一个安全的公式是:newMaxPriorityFeePerGas = max(oldMaxPriorityFeePerGas * (1 + bump), currentPriorityFeeEstimate)。newMaxFeePerGas = max(oldMaxFeePerGas * (1 + bump), currentBaseFee * 2 + newMaxPriorityFeePerGas)。提升因子因客户端而异;常见默认值为 10%,但请与你的节点核实。始终向上取整,以避免刚好低于阈值。

如果你使用的是传统 gasPrice 交易,同样的逻辑适用:newGasPrice = max(oldGasPrice * (1 + bump), currentGasPriceEstimate)。

  • 读取当前基础费用和优先费用估算。
  • 对旧费用值应用提升因子。
  • 取提升后的旧费用和当前市场费用的最大值。

可运行的 Node.js 示例:检查、提升并提交

此示例使用 ethers.js v6 检查卡住的交易,计算替换费用,并使用相同的 nonce 提交提升。它假设你拥有卡住的交易哈希和发送者的私钥。将 RPC URL 替换为你的提供商端点。要获取可靠的端点,请参阅 RPC 端点指南(RPC Assistant)

代码首先获取卡住的交易和待处理 nonce。然后根据旧交易的费用和当前网络条件计算新的费用值。最后,它使用相同的 nonce 和提升后的费用发送新交易。如果替换被拒绝并显示“replacement transaction underpriced”,请增加提升因子并重试。

const { ethers } = require('ethers');

async function bumpStuckTransaction(rpcUrl, privateKey, stuckTxHash) {
  const provider = new ethers.JsonRpcProvider(rpcUrl);
  const wallet = new ethers.Wallet(privateKey, provider);

  // 1. Fetch the stuck transaction
  const stuckTx = await provider.getTransaction(stuckTxHash);
  if (!stuckTx) {
    console.log('Transaction not found in mempool. It may be dropped or mined.');
    return;
  }
  if (stuckTx.blockNumber) {
    console.log('Transaction already mined in block', stuckTx.blockNumber);
    return;
  }

  // 2. Get pending nonce
  const pendingNonce = await provider.getTransactionCount(wallet.address, 'pending');
  console.log('Stuck nonce:', stuckTx.nonce, 'Pending nonce:', pendingNonce);

  // 3. Compute replacement fees
  const feeData = await provider.getFeeData();
  const bumpFactor = 1.2; // 20% bump; adjust based on client threshold
  const oldMaxPriority = stuckTx.maxPriorityFeePerGas || 0n;
  const oldMaxFee = stuckTx.maxFeePerGas || stuckTx.gasPrice || 0n;

  const newMaxPriority = oldMaxPriority * BigInt(Math.floor(bumpFactor * 100)) / 100n;
  const newMaxFee = oldMaxFee * BigInt(Math.floor(bumpFactor * 100)) / 100n;

  const finalMaxPriority = newMaxPriority > (feeData.maxPriorityFeePerGas || 0n) ? newMaxPriority : feeData.maxPriorityFeePerGas;
  const finalMaxFee = newMaxFee > (feeData.maxFeePerGas || 0n) ? newMaxFee : feeData.maxFeePerGas;

  console.log('Replacement fees:', { maxPriorityFeePerGas: finalMaxPriority, maxFeePerGas: finalMaxFee });

  // 4. Send replacement with same nonce
  const tx = await wallet.sendTransaction({
    to: stuckTx.to,
    value: stuckTx.value,
    data: stuckTx.data,
    nonce: stuckTx.nonce,
    maxPriorityFeePerGas: finalMaxPriority,
    maxFeePerGas: finalMaxFee,
    gasLimit: stuckTx.gasLimit,
    chainId: (await provider.getNetwork()).chainId
  });

  console.log('Replacement sent:', tx.hash);
  await tx.wait();
  console.log('Replacement mined');
}

// Usage:
// bumpStuckTransaction('https://your-rpc-endpoint', '0x...', '0x...');

在重试循环中处理“Replacement Underpriced”错误

如果你的替换被拒绝并显示“replacement transaction underpriced”,这意味着你的提升不足。节点要求的提升阈值高于你的增加幅度,或者当前市场费用高于你的新费用。在这种情况下,请增加提升因子并重试。一种常见模式是从 10% 提升开始,然后 20%,然后 50%,直到替换被接受。

请注意,每次重试都必须使用相同的 nonce。如果你不小心使用了新的 nonce,就会在 nonce 序列中创建缺口,所有后续交易都会卡在缺口后面。这是一个常见的陷阱。发送前务必验证 nonce。

如果你使用 ethers.js,请注意如果你未指定费用字段,该库可能会自动填充。替换时,始终显式设置 maxFeePerGas 和 maxPriorityFeePerGas 为你计算的值,以避免库使用过时的估算。

  • 以更大的提升增量重试,直到被接受。
  • 替换时始终使用相同的 nonce。
  • 显式设置费用字段以避免过时估算。

陷阱与网络特定注意事项

使用新的 nonce 而不是相同的 nonce 是最具破坏性的错误。它会创建 nonce 缺口,网络不会打包任何具有更高 nonce 的交易,直到缺口被填补。这可能导致交易队列卡住。有关并发下 nonce 管理的详细说明,请参阅 并发下的 EVM nonce 管理

不刷新基础费用就提升是另一个陷阱。如果自你首次发送交易以来基础费用已经上升,你提升后的 maxFeePerGas 可能仍低于当前基础费用加优先费用,导致替换无法被打包。在计算替换之前,始终获取最新的费用数据。

在某些网络上,费用模型不同。Layer 2 排序器可能有不同的替换语义,一些链使用没有 EIP-1559 的传统 gasPrice 模型。在 BSC 风格的链上,gasPrice 是唯一的费用参数。始终验证特定网络的替换规则。对于以太坊主网,以太坊网络页面 提供了支持的 RPC 方法概述。

私有内存池和 MEV 中继提供了替代途径。通过私有中继发送交易可以绕过公共内存池并避免替换规则,但这是专门的工作流程,不是通用修复方法。

  • 新 nonce 会创建缺口并阻塞后续交易。
  • 过时的基础费用可能使提升后的交易无法被打包。
  • L2 和非 EIP-1559 链有不同的替换规则。
  • 私有内存池绕过公共池替换策略。

限制:重组、Nonce 缺口与排序器差异

重组会使替换复杂化。如果替换交易被打包,然后发生重组,原始交易可能会重新出现在池中。在这种情况下,你可能需要再次替换。替换后始终监控交易状态。

Nonce 缺口是持续存在的风险。如果你有缺口,任何具有更高 nonce 的交易都不会被打包,直到缺口被填补。你必须要么发送具有缺失 nonce 的交易,要么替换创建缺口的交易。像 eth_getTransactionCount 使用 'pending' 这样的工具有助于识别缺口。

在 L2 和其他基于排序器的网络上,替换语义可能不同。一些排序器按 FIFO 顺序处理交易,可能根本不支持基于费用的替换。始终检查网络的文档。有关一般 RPC 可靠性,请参阅 以太坊 RPC 超时处理

  • 重组可能使被替换的交易复活。
  • Nonce 缺口会阻塞所有更高 nonce 的交易。
  • L2 排序器可能不支持基于费用的替换。

后续步骤:监控、自动化与提供商选择

解决卡住的交易后,考虑设置监控,以便在交易待处理超过阈值时提醒你。这可以帮助你在费用市场变动过大之前采取行动。你可以在轮询循环中使用 eth_getTransactionByHash,或通过 WebSocket 订阅待处理交易。

对于生产系统,使用重试循环自动化提升逻辑,逐步增加费用直到替换被接受。始终设置最大费用上限以避免多付。在部署到主网之前,在测试网上测试你的逻辑。

选择可靠的 RPC 提供商对于及时提交和替换交易至关重要。OnFinality 提供 API 服务,包含以太坊和其他网络的端点。有关定价详情,请参阅 RPC 定价。要探索更多故障排查指南,请访问 OnFinality Learn 中心

  • 使用轮询或 WebSocket 监控待处理交易。
  • 使用有上限的重试循环自动化提升逻辑。
  • 使用可靠的 RPC 提供商以确保一致提交。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始