Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 13 分钟

以太坊 eth_getLogs:地址与主题组合查询成本

区块范围宽度、地址过滤器和位置化主题数组如何各自独立地影响 eth_getLogs 的扫描成本,附决策矩阵与自测表格。

TL;DR

eth_getLogs 的成本由三个基本相互独立的维度决定:节点必须扫描多少个区块、过滤器匹配多少个地址,以及位置化 topics 数组如何构造。以太坊 JSON-RPC 规范将 topics 定义为按位置生效的 OR 过滤器,将地址数组定义为跨地址的 OR,但选择性并不会减少节点必须执行或从中提取日志的区块数量。窄区块范围配合宽泛的主题 OR 通常比宽区块范围配合高选择性过滤器更便宜,因为扫描本身才是主导成本。每请求的范围上限、超时和结果限制因服务商而异,因此唯一可靠的模型是针对你自己的端点实测出来的模型。本文区分了有文档记载的协议语义与服务商行为,并给出一个可复现、由你自己填写的测量表格。

eth_getLogs 的三个独立成本维度

一个 eth_getLogs 请求有三个参数,各自通过不同机制影响成本:fromBlock/toBlock 定义区块范围,address 定义匹配哪些合约地址,topics 定义位置化过滤数组。以太坊 JSON-RPC 规范将这些描述为过滤条件,而非成本模型,因此有必要把协议保证的内容与节点内部实际执行的操作区分开来。

区块范围决定节点必须访问多少个区块。日志是在执行之后从收据或区块体中提取的,因此节点通常无法仅因为过滤器具有选择性就跳过某个区块。这是最常被低估的维度,也是宽范围配合窄过滤器仍然可能很慢的原因。

address 和 topics 维度改变的是返回多少条日志以及每个区块做多少匹配工作,但不会改变扫描的区块数量。把这两者当成一个合并的“选择性”旋钮,是错误性能直觉最常见的来源。

  • 区块范围:节点必须访问并从中提取日志的区块数量。
  • 地址过滤器:单个地址或数组,按跨地址 OR 匹配。
  • 主题过滤器:位置化数组,每个位置为 OR,null 表示通配符。

有文档记载的主题语义:按位置 OR 与 null 通配符

以太坊 JSON-RPC 规范中关于 eth_getLogs 的部分将 topics 定义为一个 32 字节值的数组,其中顺序很重要。以太坊 JSON-RPC 规范中关于 eth_getLogs 的部分将 topics 定义为一个 32 字节值的数组,其中顺序很重要。第一个主题按惯例是事件签名哈希,后续位置对应被索引的事件参数。某个位置上的 null 条目表示“该位置可以是任意值”,这是通配符而非过滤器。

在单个位置内,值数组表示 OR。因此 topics: [[A, B], null, [C]] 的含义是:匹配第一个主题为 A 或 B、第二个主题为任意值、第三个主题为 C 的日志。这是有文档记载的协议行为,不是服务商扩展,在所有符合规范的客户端上表现一致。

实际后果是:嵌套 OR 数组会扩大该位置的匹配集合,而不会缩小区块扫描范围。如果你在索引一条高流量数据流,位置 0 上的宽泛 OR 可能返回远多于单一签名的日志,即使区块范围不变,也会增加响应体积和下游处理量。

curl -s https://your-endpoint.example \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getLogs",
    "params": [{
      "fromBlock": "0x11A0000",
      "toBlock": "0x11A07FF",
      "address": [
        "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "0xdAC17F958D2ee523a2206206994597C13D831ec7"
      ],
      "topics": [
        [
          "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
          "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
        ],
        null,
        ["0x0000000000000000000000000000000000000000000000000000000000000000"]
      ]
    }]
  }'

地址数组作为 OR 以及选择性的局限

address 参数既接受单个地址字符串,也接受地址数组。根据规范,数组按所列地址之间的 OR 进行匹配。这对多合约索引很方便,但不会减少区块扫描:节点仍会访问范围内的每个区块,并将每条日志与地址集合进行比对。

选择性影响返回多少条日志以及每条日志做多少匹配工作,但对于宽范围而言,主导成本是扫描本身。一个在 100,000 个区块中匹配零条日志的过滤器仍然可能很昂贵,因为节点必须去查找。这就是“让过滤器更具选择性”这一建议并不完整的核心原因。

当你需要索引许多合约时,要考虑单个宽泛地址数组是否优于多个较窄的请求。宽泛数组每个区块返回更多日志,但往返次数更少;多个窄请求可能更容易并行化,也更容易在重试时推理。正确的选择取决于你端点的行为,这正是测量重要的原因。

  • 单个地址:最简单,最容易缓存和推理。
  • 地址数组:OR 语义,每个区块更多日志,区块扫描不变。
  • 多个窄请求:更多往返,更容易并行化和重试隔离。

为什么区块范围主导扫描成本

日志在执行期间产生并存储在收据中。要响应 eth_getLogs,节点必须能够访问所请求范围内每个区块的日志,通常通过读取收据或索引实现。Nethermind 关于大规模 eth_getLogs 查询的工程文章描述了宽范围和重复查询如何给存储和过滤路径带来压力,这与“范围是主要成本驱动因素”的直觉一致。

这就是为什么区块范围限制这一配套主题如此重要。如果你的服务商对每请求范围设上限,你就必须分块,而分块会成倍增加请求数量。关于让每个请求保持在文档记载限制内的分块策略,请参阅 eth_getLogs 区块范围限制与安全分块。

一个有用的心智模型:成本大致与扫描的区块数成正比,再加上一个较小的项,用于返回的日志和匹配。在不缩小范围的情况下优化过滤器,通常不会触及主导项。

类 ERC-20 Transfer 数据流与稀疏管理事件的决策矩阵

不同的事件形态需要不同的过滤策略。类 Transfer 数据流流量高,通常按代币地址以及 from/to 主题进行索引。稀疏管理事件流量低,通常由单个合约以独特签名发出。下表总结了权衡;请将其视为起点,而非保证。

对于类 Transfer 数据流,优先使用有界区块范围,并在 topic 0 使用单一签名,address 使用单个合约或小数组。如果你需要多个代币,适度的地址数组通常优于宽泛的主题 OR,因为签名本身已经足够具体。对于稀疏管理事件,单个地址加单一签名配合窄范围通常就足够,而且由于返回集合很小,你可以承受更宽的范围。

这个矩阵关注的是形态,而不是绝对数字。你端点的上限和超时决定什么是可行的,而这些因服务商而异。

  • 类 Transfer,单一代币:单个地址,topic0 = Transfer 签名,有界范围。
  • 类 Transfer,多个代币:地址数组,topic0 = Transfer 签名,分块范围。
  • 稀疏管理事件,单一合约:单个地址,topic0 = 管理事件签名,可接受更宽范围。
  • 多签名索引:topic0 OR 数组,但预期更多日志和更大响应。
  • 地址与主题组合:地址数组加 topic0 OR,最宽的形态,需谨慎测量。

可运行的 Node.js 示例:嵌套主题与地址数组

以下 Node.js 示例发出一个带有地址数组和嵌套主题数组的 eth_getLogs 请求,然后打印日志数量和墙上时钟时间。它使用现代 Node.js 中可用的全局 fetch,因此不需要任何依赖。请将端点 URL 替换为你自己的。

这个示例有意设计为同时覆盖全部三个维度:有界区块范围、地址数组,以及带 null 通配符和第三位置过滤器的 topic0 OR。可将其作为你自己测量的模板。

const ENDPOINT = 'https://your-endpoint.example';

async function getLogs() {
  const payload = {
    jsonrpc: '2.0',
    id: 1,
    method: 'eth_getLogs',
    params: [{
      fromBlock: '0x11A0000',
      toBlock: '0x11A07FF',
      address: [
        '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
        '0xdAC17F958D2ee523a2206206994597C13D831ec7'
      ],
      topics: [
        [
          '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
          '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
        ],
        null,
        ['0x0000000000000000000000000000000000000000000000000000000000000000']
      ]
    }]
  };

  const started = Date.now();
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(payload)
  });
  const json = await res.json();
  const elapsed = Date.now() - started;

  if (json.error) {
    console.error('RPC error:', json.error);
    return;
  }
  console.log('logs returned:', json.result.length);
  console.log('wall ms:', elapsed);
}

getLogs().catch((err) => console.error('request failed:', err));

针对你自己端点的自测结果表

由于每请求范围上限、超时和结果限制因服务商而异,唯一可靠的模型是你自己测量出来的模型。用不同的区块范围对同一过滤器形态运行你的端点,并记录结果。下表是模板;请填入你自己的数字。

一个有用的实验是:保持过滤器不变,改变区块范围;然后保持范围不变,改变过滤器形态。这样可以把扫描项与匹配项分开。如果你的端点对某个范围返回错误,请记录该错误而不是时间,因为上限本身就是发现。

在没有记录端点、时段和过滤器形态的情况下,不要跨服务商比较数字。服务商行为被记录为存在差异,单次测量不构成基准。

  • 扫描区块数:请求的 toBlock 减 fromBlock 加一。
  • 墙上毫秒:请求前后客户端侧经过的时间。
  • 返回日志数:result 数组的长度。
  • 错误:如果请求被拒绝,记录 JSON-RPC 错误码和消息。
| Filter shape                         | Blocks scanned | Wall ms | Logs returned | Error |
|--------------------------------------|----------------|---------|---------------|-------|
| single address, single topic0        |                |         |               |       |
| address array (2), single topic0     |                |         |               |       |
| single address, topic0 OR (2)        |                |         |               |       |
| address array (2), topic0 OR (2)     |                |         |               |       |
| single address, nested [.., null, ..]|                |         |               |       |

服务商特定的上限、超时和结果限制

以太坊 JSON-RPC 规范定义了该方法及其参数,但没有定义最大区块范围、超时或最大返回日志数。这些是每个节点运营者和 RPC 服务商做出的运营决策。在实践中,每请求范围上限和超时因服务商而异,一些服务商还会限制返回的日志数量。

这意味着同一个过滤器在一个端点上成功的请求,在另一个端点上可能被拒绝。当你看到错误时,在更改过滤器之前,先检查它是范围上限、超时还是结果大小限制。下面的故障排查部分涵盖了常见情况。

如果你在比较端点,以太坊 RPC 服务商对比(RPC Assistant)页面是一个有用的起点,RPC 定价解释了请求量和范围如何与成本相互作用。更广泛的节点概览请参阅以太坊 RPC 节点指南。

过滤器形态优化的局限与权衡

过滤器形态优化无法战胜区块扫描。如果你的工作负载需要宽范围,你就要为此付出代价,要么是一个大请求,要么是许多分块请求。分块会增加往返次数,并可能与速率限制产生不良交互,因此这种权衡并非没有代价。

宽泛的主题 OR 数组和宽泛的地址数组会增加响应体积,即使区块范围很小,也会增加序列化和下游处理成本。对于高流量数据流,这可能成为主导因素。考虑你是否需要一次性获取所有签名,还是分开的数据流更容易运营。

最后,服务商行为不是协议保证。今天有效的策略明天可能撞上新的上限。构建你的索引管道时,让分块大小和过滤器形态成为配置,而不是硬编码假设。关于批量发送多个请求,请参阅 JSON-RPC 批处理最佳实践。

  • 仅靠过滤器选择性无法降低扫描成本。
  • 分块用往返次数换取更小的范围。
  • 宽泛的 OR 过滤器会增加响应体积和下游成本。
  • 服务商上限是运营层面的,不是协议定义的。

排查常见的 eth_getLogs 失败

最常见的失败是与范围相关的错误,通常以 invalid params 或服务商特定消息返回。如果错误提到区块范围或限制,请缩小范围并重试。如果提到超时,范围可能可以接受,但过滤器太宽,或者端点当时太慢。

第二种常见失败是看起来像 bug 的空结果。检查主题值是否为 32 字节十六进制字符串,事件签名哈希是否与 ABI 匹配,以及地址是否一致地使用校验和或小写形式。null 放在错误位置会静默地扩大过滤器,而不是缩小它。

第三种情况是负载下的间歇性超时。这时以太坊 RPC 超时指南会有所帮助:区分客户端超时与服务端拒绝,并为瞬时故障添加带退避的重试。如果你要获取已知区块的收据,eth_getBlockReceipts 与逐条收据对比比较了批量方法。

  • 范围错误:缩小 fromBlock/toBlock 跨度并重试。
  • 超时:缩小过滤器或范围,然后带退避重试。
  • 空结果:验证主题十六进制长度、签名哈希和地址大小写。
  • 间歇性失败:区分客户端超时与服务端拒绝。

构建成本感知日志索引器的后续步骤

先用上面的表格测量你的端点,然后选择一个舒适地处于观测限制内的分块大小。保持过滤器形态可配置,这样你无需更改代码即可在单个地址和地址数组之间切换。关于过滤参数本身的更深入讨论,请参阅以太坊 eth_getLogs:按地址和主题过滤事件日志。

如果你正在为生产索引器评估端点,OnFinality Learn 中心汇集了相关指南,API 服务页面描述了托管 RPC 端点如何运营。网络特定细节请参阅OnFinality 上的以太坊。

最后,把你在博客文章中看到的每个数字(包括本文)都当作需要针对你自己端点验证的假设。协议语义是稳定的;运营限制则不是。

永远不用担心基础设施

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

开始