Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
集成与开发阅读约 13 分钟

eth_newFilter 生命周期:过滤器 ID、getFilterChanges 与过期机制

深入解析以太坊有状态过滤器 API:过滤器 ID 如何工作、为什么 getFilterChanges 是破坏性游标,以及如何应对服务端过期。

TL;DR

eth_newFilter 在特定节点上创建一个服务端过滤器对象,并返回一个十六进制数量 ID。该 ID 是节点本地状态的句柄,而非可移植的查询。eth_getFilterChanges 是一种破坏性读取:它仅返回自上次轮询以来累积的项目,并推进内部游标,因此调用两次会丢失第一批数据。过滤器在一段不活动时间后过期(如 Geth 等客户端有文档说明),之后 getFilterChanges 会返回过滤器未找到错误;eth_uninstallFilter 可显式释放过滤器。由于过滤器 ID 不会在节点间迁移,负载均衡端点会破坏简单的过滤器使用。本文涵盖完整生命周期、可运行的 Node.js 轮询循环,以及可针对您自己的端点填写的测量表。

过滤器是什么:以十六进制 ID 为键的服务端状态

过滤器不是您重新发送的查询。它是由 eth_newFilter(用于日志)、eth_newBlockFilter(用于新区块哈希)或 eth_newPendingTransactionFilter(用于待处理交易哈希)在特定节点上创建的对象。节点存储过滤器的条件和内部游标,然后返回一个十六进制数量 ID,例如 0x1 或 0x7f3a。该 ID 是您后续调用中唯一需要发送的内容。以太坊 JSON-RPC 规范定义了这些方法及其返回结构;JSON-RPC 2.0 规范定义了请求/响应封装以及当 ID 未知时返回的错误对象。

关键的心智模型是:过滤器存在于节点的内存中,而非您的请求中。这立即带来两个后果。第一,该 ID 对任何其他节点都无意义。第二,节点可以在不通知您的情况下丢弃过滤器,这就是为什么生命周期比初始调用更重要。如果您仍在比较这种拉取模型与推送订阅,请参阅 eth_subscribe 订阅与轮询过滤器对比

过滤器方法的契约由 以太坊 JSON-RPC 规范中的过滤器方法 定义,针对未知或过期过滤器 ID 返回的错误封装遵循 JSON-RPC 2.0 规范。服务端过滤器的不活动超时行为由 Geth 等客户端记录,这就是为什么长时间静默的轮询循环必须重新创建过滤器,而不能假设它会持续存在。

  • eth_newFilter:根据过滤器对象(fromBlock、toBlock、address、topics)创建日志过滤器。
  • eth_newBlockFilter:创建一个累积新区块哈希的过滤器。
  • eth_newPendingTransactionFilter:创建一个累积待处理交易哈希的过滤器。
  • 三者均返回一个十六进制数量 ID;该 ID 是节点本地状态。

三种过滤器类型及 getFilterChanges 的返回内容

过滤器类型决定了 eth_getFilterChanges 返回的元素类型。日志过滤器返回日志对象数组,每个对象包含 address、topics、data、blockNumber、transactionHash 及相关字段。区块过滤器返回区块哈希的十六进制字符串数组。待处理交易过滤器返回交易哈希数组。当自上次轮询以来没有新内容到达时,数组为空。

这种类型差异是集成错误的常见来源:为日志过滤器编写的代码假设返回对象,当指向返回字符串的区块过滤器时就会出错。如果您的目标是读取已知范围内的完整日志,而非跟踪新日志,那么无状态的 eth_getLogs 事件主题过滤 路径通常更简单,且无需管理 ID。

  • 日志过滤器 -> 日志对象数组。
  • 区块过滤器 -> 区块哈希数组。
  • 待处理交易过滤器 -> 交易哈希数组。
  • 空数组表示自上次轮询以来没有新项目,而非错误。

游标语义:getFilterChanges 与 getFilterLogs 对比

eth_getFilterChanges 是一种破坏性读取。它仅返回自上次调用以来累积的项目,并推进过滤器的内部游标越过它们。连续调用两次会返回第一批数据,然后返回空数组(或仅返回新到达的项目)。这就是为什么重复轮询是正常模式,也是为什么从两个代码路径意外调用它会丢失数据:第二个调用者消耗了第一个调用者本应看到的内容。

eth_getFilterLogs 是非破坏性的对应方法。它返回匹配过滤器条件的完整日志集,而不仅仅是增量。它可用作对账检查:在怀疑错过一批数据后,调用 getFilterLogs 查看完整匹配集,并与基于游标的循环记录进行对比。这两种方法回答不同的问题,在不理解游标的情况下混用它们,是导致日志重复或遗漏的已知原因。关于安全重试的相关讨论,请参阅 JSON-RPC 幂等性与重复请求安全

  • getFilterChanges:自上次轮询以来的增量,推进游标,破坏性。
  • getFilterLogs:完整匹配日志集,不推进游标。
  • 在循环中轮询 getFilterChanges 是预期行为;调用两次会丢弃第一个结果。
  • 使用 getFilterLogs 进行对账,而非在轮询循环中直接替换。

生命周期与过期:空闲过滤器会被丢弃

过滤器是具有不活动超时的服务端状态。Geth 的文档描述过滤器在一段时间未轮询后被移除,其他客户端也记录了类似行为。一旦过滤器被丢弃,针对该 ID 的 eth_getFilterChanges 会返回过滤器未找到错误,而非空数组。确切的超时窗口取决于客户端和版本,因此应将其视为因提供商而异的文档化行为,而非固定常量。

eth_uninstallFilter 显式释放过滤器,并返回一个布尔值,指示过滤器是否存在。因此,长时间静默的消费者必须处理丢弃:捕获过滤器未找到错误,重新创建过滤器,并恢复轮询。从最后处理的区块重新创建可避免缺口。无状态的 eth_getLogs 没有此类超时,因为它不持有服务端状态,这是某些团队偏好将其用于低频消费者的关键原因。

  • 空闲过滤器在客户端定义的不活动期后被丢弃。
  • 过期后,getFilterChanges 返回过滤器未找到错误,而非空数组。
  • eth_uninstallFilter 显式释放过滤器并返回布尔值。
  • 遇到过滤器未找到时重新创建,并从最后处理的区块恢复。

为什么过滤器 ID 不会跨节点迁移

由于过滤器是节点本地状态,针对一个端点创建的 ID 对另一个端点来说未知。如果您的客户端向节点 A 发送 eth_newFilter,然后通过轮询负载均衡器向节点 B 发送 eth_getFilterChanges,节点 B 从未见过该 ID,会返回过滤器未找到错误。这是过滤器 API 最常见的生产故障之一,且在单节点测试中不可见。

修复方法是将过滤器流量固定到创建过滤器的节点,或者完全避免有状态 API,改用无状态的 eth_getLogs 轮询。如果必须负载均衡,请使用粘性会话或为过滤器生命周期使用单一专用连接。提供商在此处的行为各不相同:一些托管端点记录了粘性路由,另一些则没有,因此请针对您自己的端点进行验证。关于端点选择指导,请参阅 以太坊 RPC 端点与提供商选择(RPC Assistant)

  • 过滤器 ID 是每节点状态,不可移植。
  • 轮询负载均衡会破坏简单的过滤器使用。
  • 将过滤器流量固定到创建节点,或使用无状态的 eth_getLogs。
  • 粘性路由支持因提供商而异;请通过实证验证。

可运行的 Node.js 示例:创建、轮询、对账、卸载

以下示例使用内置的 fetch(Node.js 18+),无外部依赖。它创建一个日志过滤器,按间隔轮询 eth_getFilterChanges,使用 eth_getFilterLogs 作为对账检查,在关闭时卸载过滤器,并在发生过滤器未找到错误时重新创建。请将端点 URL 和过滤器条件替换为您自己的值。

出错时重新创建的分支是大多数集成遗漏的部分。没有它,超过节点不活动超时的静默期会悄然结束您的流。对账调用是可选的,但在开发期间有助于确认游标行为符合预期。

const ENDPOINT = process.env.ETH_RPC_URL || 'https://your-endpoint.example';
const FILTER = { fromBlock: 'latest', address: null, topics: [] };

let filterId = null;
let running = true;

async function rpc(method, params) {
  const res = await fetch(ENDPOINT, {
    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) {
    const err = new Error(json.error.message);
    err.code = json.error.code;
    throw err;
  }
  return json.result;
}

async function createFilter() {
  filterId = await rpc('eth_newFilter', [FILTER]);
  console.log('created filter', filterId);
}

async function poll() {
  try {
    const changes = await rpc('eth_getFilterChanges', [filterId]);
    if (changes.length) console.log('new logs', changes.length);
  } catch (err) {
    if (err.code === -32000 || /filter not found/i.test(err.message)) {
      console.warn('filter expired, recreating');
      await createFilter();
    } else {
      throw err;
    }
  }
}

async function reconcile() {
  const all = await rpc('eth_getFilterLogs', [filterId]);
  console.log('full matching set', all.length);
}

async function shutdown() {
  running = false;
  if (filterId) {
    const ok = await rpc('eth_uninstallFilter', [filterId]);
    console.log('uninstalled', filterId, ok);
  }
}

(async () => {
  await createFilter();
  process.on('SIGINT', async () => { await shutdown(); process.exit(0); });
  while (running) {
    await poll();
    await new Promise(r => setTimeout(r, 5000));
  }
})();

结果表:针对您自己的端点测量过滤器行为

过滤器行为取决于客户端和提供商,因此唯一可靠的答案就是您自己测量的结果。针对您的端点运行以下探针,让过滤器空闲一段已知时间,然后轮询并记录发生的情况。将观察结果填入表中。不要假设其他提供商文档中的值适用于您。

该探针创建一个区块过滤器,记录 ID 格式,等待,然后尝试轮询。调整空闲期以覆盖疑似超时。带有代码和消息的 JSON-RPC 错误对象表示过滤器已被丢弃;空数组表示它仍然存活。

// probe.js - run with: node probe.js
const ENDPOINT = process.env.ETH_RPC_URL || 'https://your-endpoint.example';

async function rpc(method, params) {
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  return res.json();
}

(async () => {
  const created = await rpc('eth_newBlockFilter', []);
  console.log('id format:', created.result);
  const idleMs = Number(process.env.IDLE_MS || 300000);
  console.log('idling for', idleMs, 'ms');
  await new Promise(r => setTimeout(r, idleMs));
  const polled = await rpc('eth_getFilterChanges', [created.result]);
  console.log('after idle:', JSON.stringify(polled));
  const unknown = await rpc('eth_getFilterChanges', ['0xdeadbeef']);
  console.log('unknown id:', JSON.stringify(unknown));
})();

故障模式与故障排除

大多数过滤器事件分为四类。第一类是空闲期后过滤器未找到:消费者静默时间超过节点的不活动超时,下一次轮询出错。修复方法是捕获错误,并从最后处理的区块重新创建过滤器。第二类是在负载均衡端点上创建的过滤器,轮询到从未见过它的节点;修复方法是粘性路由或无状态轮询。

第三类是混用 getFilterChanges 和 getFilterLogs 导致日志重复或遗漏。由于 getFilterChanges 推进游标而 getFilterLogs 不推进,在同一循环中使用两者而不跟踪已处理的项目,会导致重复计数或缺口。第四类是过滤器状态膨胀:如果过滤器从未被卸载,节点会累积它们,直到过期或进程重启。始终在关闭时调用 eth_uninstallFilter。关于重组相关缺口,请参阅 通过 RPC 检测区块重组

  • 空闲后过滤器未找到:重新创建过滤器,并从最后处理的区块恢复。
  • 负载均衡端点:固定过滤器流量或切换到无状态的 eth_getLogs。
  • 混用游标方法:显式跟踪已处理项目,避免重复或缺口。
  • 从未卸载:过滤器会累积直到过期或重启;关闭时卸载。

运维指导:轮询节奏、幂等性与关闭

一旦理解了过滤器模型,运维细节就决定了轮询器是否可靠。以快于节点空闲超时的节奏轮询,使过滤器永不被丢弃,但也不要快到每次空的 getFilterChanges 调用都白白消耗一次往返;几秒的节奏是通常的折中,根据链的出块时间调整。由于 getFilterChanges 是破坏性读取,消费者必须在下次轮询前持久化对返回项目的处理结果,否则游标推进的瞬间项目就会丢失。

关闭时卸载过滤器可使节点的过滤器状态保持有界。未调用 eth_uninstallFilter 就崩溃的进程会让过滤器自行过期,这是可接受的,但每次重启都创建新过滤器而不卸载的进程会累积状态。将过滤器 ID 视为需要显式释放的资源,就像数据库游标一样,并在任何重启或过滤器未找到错误后重新创建它——绝不要恢复它。

  • 轮询快于空闲超时,但不要快于链产生新日志的速度。
  • 在下次 getFilterChanges 调用前持久化或转发每个项目;该读取是破坏性的。
  • 关闭时调用 eth_uninstallFilter,并在任何过期或重启后重新创建过滤器。
  • 绝不要跨端点变更缓存过滤器 ID:它是节点本地状态。

局限性与权衡:何时过滤器 API 是错误工具

有状态过滤器 API 对于轮询足够频繁以保持在非活动窗口内的高频消费者来说很方便,但它带来实际成本。它需要与一个节点保持持久连接,没有可移植的 ID,并且可能被静默丢弃。一些提供商已弃用过滤器方法,转而支持无状态的 eth_getLogs 轮询或基于推送的 eth_subscribe,可用的接口因提供商而异。在设计围绕过滤器的方案之前,请务必对照您端点的文档进行确认。

对于低频消费者、批量对账或任何必须在节点重启后存活的负载,无状态的 eth_getLogs 通常是更好的选择:无 ID、无游标、无过期。对于能够保持连接的高频、低延迟消费者,eth_subscribe 通常更可取。过滤器 API 介于两者之间,最好将其视为专用工具而非默认选择。如果您要批量读取收据,eth_getBlockReceipts 与单独收据读取对比 涵盖了相关的无状态模式。

  • 有状态:需要与一个节点保持持久连接。
  • 不可移植:ID 不会跨节点迁移。
  • 不活动后可能被静默丢弃。
  • 可用性因提供商而异;一些提供商弃用过滤器,转而支持 eth_getLogs 或 eth_subscribe。

后续步骤:在过滤器、getLogs 和订阅之间选择

根据轮询频率和连接稳定性来决定。如果您每隔几秒轮询一次,并且能够保持与一个节点的连接,那么只要处理过期和卸载,过滤器 API 就是可行的。如果您轮询不频繁或需要在重启后存活,请使用无状态的 eth_getLogs。如果您需要推送语义且提供商支持,请使用 eth_subscribe。OnFinality Learn 中心 有关于每种模式的配套文章。

在做出决定之前,针对您的端点运行上述探针并填写结果表。该测量结果,而非其他提供商的文档,才是您设计的基础。关于端点选项和提供商选择,请参阅 以太坊 RPC 端点与提供商选择(RPC Assistant),关于网络特定细节请参阅 OnFinality 上的以太坊。如果您正在规划容量,RPC 定价API 服务 页面描述了商业接口。

  • 高频 + 稳定连接:过滤器 API,并处理过期。
  • 低频或可容忍重启:无状态的 eth_getLogs。
  • 可用推送语义:eth_subscribe。
  • 在设计围绕过滤器的方案之前,始终测量您自己的端点。

永远不用担心基础设施

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

开始