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。
- 在设计围绕过滤器的方案之前,始终测量您自己的端点。