Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
可靠性与一致性阅读约 14 分钟

使用 eth_newPendingTransactionFilter 监控待处理的以太坊交易

深入实践 eth_newPendingTransactionFilter:待处理池过滤器如何工作、为何脆弱,以及如何构建一个稳健的监控器。

TL;DR

eth_newPendingTransactionFilter 会创建一个服务端过滤器,返回进入节点待处理池的交易哈希。与返回日志的 eth_newFilter 和返回区块哈希的 eth_newBlockFilter 不同,该过滤器只返回交易哈希,因此使用者必须通过 eth_getTransactionByHash 或 eth_getTransactionReceipt 进一步查询才能了解交易信息。该过滤器通过 eth_getFilterChanges 轮询,该调用具有破坏性并会推进内部游标,而过滤器 id 是节点本地状态,在短暂空闲后就会过期。关键注意事项是:该方法完全依赖节点的 txpool 策略——禁用或裁剪公共待处理池的提供商会返回空结果或失真的子集,因此任何监控器都必须针对自己的端点进行验证。本文解释其机制,提供可运行的 Node.js 示例,并展示如何按端点测量待处理哈希的吞吐量和可解析率。

eth_newPendingTransactionFilter 实际做什么

eth_newPendingTransactionFilter 会在节点上创建一个服务端过滤器,返回进入节点待处理池的交易哈希。它是以太坊 JSON-RPC 规范中三个过滤器创建方法之一,另外两个是 eth_newFilter(返回匹配主题/地址过滤器的日志)和 eth_newBlockFilter(返回新区块哈希)。该方法不接受参数,返回一个十六进制字符串形式的过滤器 id。

返回值是过滤器 id,而不是数据流。节点维护一个待处理交易哈希的内部队列,只有在客户端使用该 id 调用 eth_getFilterChanges 时才将其暴露出来。这是一种拉取模型:客户端控制轮询节奏,节点在两次轮询之间累积哈希。以太坊 JSON-RPC 规范在 eth_newPendingTransactionFilter 中记录了该方法及其配套的轮询方法。

由于过滤器只返回哈希,它是一种发现机制,而不是数据源。要了解待处理交易的发送者、nonce、gas 价格、maxFeePerGas 或接收者,使用者必须对每个哈希发起后续的 eth_getTransactionByHash 调用。待处理交易监控器的大部分延迟和成本实际上都发生在第二次调用上。

  • eth_newPendingTransactionFilter:返回待处理交易哈希(无参数)。
  • eth_newFilter:返回在区块范围内匹配地址/主题的日志。
  • eth_newBlockFilter:返回新区块哈希。
  • 三者都返回一个过滤器 id,必须通过 eth_getFilterChanges 轮询。

eth_getFilterChanges 与 eth_getFilterLogs 的游标语义

eth_getFilterChanges 具有破坏性。每次调用只返回自上次轮询以来累积的项目,并推进内部游标,因此如果客户端在短时间内连续轮询两次,而期间没有新哈希到达,第二次调用会返回空数组。这是以太坊 JSON-RPC 规范在 eth_getFilterChanges 中记录的行为。游标是按过滤器 id 和按节点维护的;它不会在客户端或连接之间共享。

eth_getFilterLogs 不适用于待处理交易过滤器。该方法返回匹配日志过滤器的完整日志集,专用于 eth_newFilter。对待处理交易过滤器 id 调用它,根据客户端不同,行为未定义或返回错误。对于待处理交易,eth_getFilterChanges 是唯一适用的轮询方法。

破坏性游标有一个实际后果:如果你的轮询器崩溃或重启,自上次成功轮询以来累积的哈希就会丢失,除非节点仍将它们保留在过滤器队列中。稳健的监控器应将每次轮询视为一个检查点,并在做任何其他事情之前持久化或转发收到的哈希。

  • eth_getFilterChanges 只返回自上次轮询以来的新项目,并推进游标。
  • eth_getFilterLogs 用于日志过滤器,而非待处理交易过滤器。
  • 轮询器崩溃会丢失上次轮询与崩溃之间到达的哈希。

过滤器生命周期、过期与重建

过滤器 id 是节点本地状态。它们不能跨节点、跨重启或跨可能落到不同后端的负载均衡连接移植。在一个节点上创建的过滤器无法在另一个节点上轮询,节点重启前创建的过滤器也无法在重启后存活。这与 以太坊 eth_newFilter 与 getFilterChanges 生命周期 一文中描述的通用过滤器生命周期一致。

空闲过滤器会在短时间后被裁剪。确切的超时时间取决于客户端,规范并未固定;常见客户端会裁剪几分钟内未被轮询的过滤器。因此,长时间运行的监控器必须足够频繁地轮询以保持过滤器存活,并且必须准备好当轮询返回未知过滤器 id 错误时重建过滤器。

重建路径很直接:捕获错误,再次调用 eth_newPendingTransactionFilter,然后用新 id 恢复轮询。失败的轮询与新过滤器创建之间的间隔是一个可能丢失待处理哈希的窗口。事后无法从节点恢复这些哈希;唯一的缓解措施是让轮询间隔相对于过期超时保持足够短。

  • 过滤器 id 是节点本地的,无法在重启或切换节点后存活。
  • 空闲过滤器会在客户端相关的超时后被裁剪。
  • 遇到未知 id 错误时,重建过滤器并接受一个小缺口。

txpool 依赖:为什么结果因提供商而异

待处理交易过滤器从节点的交易池(txpool)读取。如果节点不维护公共待处理池,或者按费用或类型裁剪或阻止交易,过滤器将返回空结果或失真的子集。这是任何构建待处理交易监控器的人最重要的注意事项。该行为按客户端有文档记录,但实践中因提供商而异,必须视为“有文档记录 / 因提供商而异”。

一些提供商完全禁用公共待处理池,以减少内存压力并避免暴露未确认的交易数据。另一些则保留有界池,并优先驱逐低费用交易。针对一个端点有效的监控器在另一个端点上可能返回零哈希。以太坊交易池与 txpool 命名空间 一文介绍了 txpool 命名空间及其检查方法,这些方法有助于验证给定节点实际持有什么。

实际含义是:待处理交易监控器在被信任之前必须针对自己的端点进行验证。不要假设在本地开发节点上返回哈希的过滤器在托管 RPC 端点上行为相同。先测量,再决定该端点是否适合你的用例。

  • 过滤器从节点的 txpool 读取;没有池就没有哈希。
  • 基于费用或类型的裁剪会产生失真的子集。
  • 在生产中依赖监控器之前,先验证端点。

使用 eth_newPendingTransactionFilter 轮询:可运行的 Node.js 示例

下面的示例使用基于 fetch 的原始 JSON-RPC。它创建一个待处理交易过滤器,按间隔轮询 eth_getFilterChanges,用 eth_getTransactionByHash 解析每个哈希,并打印发送者、nonce、gasPrice 和 maxFeePerGas。当节点返回未知过滤器 id 错误时,它还会重建过滤器。

将 RPC_URL 替换为你的端点。轮询间隔设置为 2 秒,这足够短以保持大多数过滤器存活,但仍会留下可能错过突发交易的窗口。根据你在端点上观察到的过期行为进行调整。

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
let filterId = null;

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: Date.now(), method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

async function createFilter() {
  filterId = await rpc('eth_newPendingTransactionFilter');
  console.log('created filter', filterId);
}

async function poll() {
  if (!filterId) await createFilter();
  let hashes;
  try {
    hashes = await rpc('eth_getFilterChanges', [filterId]);
  } catch (err) {
    console.warn('filter lost, recreating:', err.message);
    filterId = null;
    return;
  }
  for (const hash of hashes) {
    try {
      const tx = await rpc('eth_getTransactionByHash', [hash]);
      if (!tx) continue;
      console.log({
        hash,
        from: tx.from,
        nonce: tx.nonce,
        gasPrice: tx.gasPrice,
        maxFeePerGas: tx.maxFeePerGas
      });
    } catch (err) {
      console.warn('resolve failed', hash, err.message);
    }
  }
}

(async () => {
  await createFilter();
  setInterval(poll, 2000);
})();

解析待处理哈希并检测卡住的交易

一旦你有了待处理哈希,如果节点仍持有该交易,eth_getTransactionByHash 会返回交易对象。如果交易已被打包,根据节点的保留策略,同一调用可能返回已打包的交易或 null。要确认,请使用 eth_getTransactionReceipt,它在交易待处理时返回 null。eth_getTransactionReceipt 返回 null 与待处理收据轮询 一文详细介绍了收据为 null 的情况。

通过将待处理哈希与 txpool 中稍后的 nonce 缺口关联,可以检测卡住的交易。如果你跟踪某个发送者见过的最高 nonce,然后观察到较低的 nonce 从未在后续轮询中出现,那么该 nonce 处的交易很可能卡住了。txpool 命名空间(txpool_content、txpool_inspect)可以在暴露它的节点上确认这一点,但许多托管端点并不暴露。

实用的监控器会维护每个发送者的已见 nonce 映射,并在观察到较高 nonce 而没有较低 nonce 时标记缺口。这是启发式的,并非权威:较低 nonce 的交易可能只是被从池中驱逐,或者节点根本没有看到它。将该信号视为调查的提示,而非证据。

  • eth_getTransactionByHash 将待处理哈希解析为交易对象。
  • eth_getTransactionReceipt 在交易待处理时返回 null。
  • txpool 中的 nonce 缺口是卡住交易的启发式信号。

轮询与 eth_subscribe('newPendingTransactions') 的对比

WebSocket 订阅 eth_subscribe('newPendingTransactions') 会在待处理哈希到达时推送给客户端,而不需要客户端轮询。这消除了轮询间隔作为丢失哈希来源的问题,也消除了过滤器过期问题,因为订阅绑定到连接而不是会被裁剪的过滤器 id。代价是客户端必须维护持久 WebSocket 连接并处理重连逻辑。

轮询方式在 HTTP 上更易于操作,可通过不支持长连接的代理和负载均衡器工作,并且对于批处理更容易推理。订阅方式更适合低延迟监控和能够容忍持久连接的工作负载。以太坊 eth_subscribe 日志与 WebSocket 轮询对比 一文比较了日志的两种模型;相同的权衡也适用于待处理交易。

两种方式都不会改变底层的 txpool 依赖。在没有公共待处理池的节点上订阅 newPendingTransactions 也会返回空结果。轮询与订阅之间的选择关乎传输和延迟,而非池的完整性。

  • eth_subscribe 推送哈希;eth_newPendingTransactionFilter 需要轮询。
  • 订阅避免过滤器过期,但需要持久连接。
  • 两者都依赖节点的 txpool 策略来决定实际可见的内容。

针对你的端点的可复现结果表

由于待处理池行为因提供商而异,了解端点行为的唯一可靠方法是测量它。下表是一个模板:使用相同的脚本和相同的观察窗口,针对你自己的端点填写。除非窗口和网络条件可比,否则不要跨端点比较数字。

在网络活动正常的时间段运行监控器至少十分钟。记录每分钟返回的待处理哈希数量、这些哈希中通过 eth_getTransactionByHash 解析为交易对象的比例、创建后第一次轮询是否返回任何内容,以及过滤器在不轮询的情况下能存活多久。最后一行通过创建一个过滤器并故意不轮询它直到发生错误来测量。

  • 每分钟待处理哈希数:在固定窗口内统计 eth_getFilterChanges 结果。
  • 可解析比例:返回非空 eth_getTransactionByHash 的哈希占比。
  • 创建后首次轮询行为:第一次轮询返回哈希还是空数组?
  • 过滤器过期间隔:从创建到不轮询时出现未知过滤器 id 错误的时间。
  • 池完整性:如果可能,与第二个端点或本地节点进行比较。

常见故障模式排查

最常见的故障是空结果集。如果 eth_getFilterChanges 持续返回空数组,首先要检查的是端点是否维护公共待处理池。在第二个端点上尝试 eth_newPendingTransactionFilter 并比较。如果两者都返回空,网络可能只是安静,或者两个端点都禁用了池。

第二个常见故障是未知过滤器 id 错误。这意味着过滤器被裁剪或节点重启了。修复方法是重建过滤器并恢复轮询。如果频繁发生,缩短轮询间隔或切换到 WebSocket 订阅。如果它在创建后立即发生,端点可能根本不支持该方法。

第三个故障是大量无法解析的哈希。当节点从池中驱逐交易的速度快于轮询器解析它们的速度,或者池有界且低费用交易被丢弃时,就会发生这种情况。缩短轮询间隔并并行解析哈希可能有所帮助,但根本原因是池策略。以太坊 RPC 节点指南(RPC Assistant) 涵盖了端点选择和能力检查,可帮助你选择合适的提供商。

  • 空结果:检查端点是否有公共待处理池。
  • 未知过滤器 id:重建过滤器并缩短轮询间隔。
  • 无法解析的哈希:池驱逐或有界池;缩短轮询间隔。

待处理交易监控的局限性与权衡

该方法不是流式的。哈希在轮询之间累积,而在单个轮询间隔内到达并被打包的突发交易可能永远不会被观察到。这是拉取模型固有的,无法通过缩短间隔完全消除,因为节点的过滤器队列是有界的,且游标具有破坏性。

过滤器 id 不可移植。它们无法跨节点、跨重启或跨可能落到不同后端的连接共享。假设在负载均衡池中过滤器 id 稳定的监控器会间歇性失败。安全的模式是将过滤器视为短暂的,并在任何错误时重建它。

公共待处理池不是全局待处理状态的可靠表示。它反映的是单个节点看到并选择保留的内容。不同节点看到不同的子集,提供商应用不同的策略。对于任何需要完整或权威待处理交易视图的应用,待处理池是错误的来源。将其用于发现和启发式,而非记账。

  • 非流式:轮询之间可能错过突发交易。
  • 过滤器 id 是节点本地的,不可移植。
  • 公共待处理池是部分的、依赖策略的视图。

后续步骤:选择端点并构建稳健的监控器

在构建生产监控器之前,先验证端点。运行上面的结果表,确认端点以有用的速率返回待处理哈希,并确认过滤器在你预期的轮询间隔内能存活。如果端点不维护公共待处理池,考虑 WebSocket 订阅或不同的提供商。以太坊 RPC 节点指南(RPC Assistant) 和 OnFinality Learn 中心 是比较端点能力的好起点。

对于生产环境,将监控器包装在监督程序中,在出错时重建过滤器、在解析哈希之前持久化它们,并发出待处理哈希速率、可解析性和过滤器重建的指标。将监控器视为尽力而为的发现层,而非真相来源。将其与你真正关心的交易的收据轮询配对使用。

如果你正在评估提供商,RPC 定价 和 API 服务 页面描述了可用的计划和端点。以太坊网络页面 列出了支持的以太坊网络。选择一个记录其待处理池行为的端点,并在承诺依赖它的设计之前自行测量。

  • 在构建之前用结果表验证端点。
  • 监督监控器:重建过滤器、持久化哈希、发出指标。
  • 将待处理监控视为尽力而为的发现,而非真相来源。

永远不用担心基础设施

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

开始