规范
为方便阅读的 AI 翻译,可能不准确或已过时。在治理、投票和争议中,英文原文是唯一权威文本。准确表述请参阅 GitHub 原文。
GitHub 英文原文 ↗## 摘要
本提案引入了五种新的 JSON-RPC 方法,以便高效查询原始链历史数据:
- `eth_queryBlocks`
- `eth_queryTransactions`
- `eth_queryLogs`
- `eth_queryTraces`
- `eth_queryTransfers`
这五种方法共享一个共同的请求和响应结构。请求指定了区块范围、遍历顺序、目标页面大小、可选过滤器和可选字段选择,这也控制了哪些相关对象被连接到响应中。响应返回按对象类型键入的标准化、去重的结果数组,以及三个区块引用,客户端用于分页和重组检测。
## 动机
标准以太坊 JSON-RPC 接口提供的原语不足以查询链历史。许多有用的查询无法实现,而支持的查询则存在过度获取和低效分页的问题。
本提案旨在解决四个具体的不足之处。
- **过滤。** `eth_getLogs` 方法支持日志过滤,但没有办法过滤交易或追踪。要查询发送到特定地址的所有交易,用户必须获取整个区块并在客户端进行过滤。
- **关系。** 无法在单个请求中连接相关对象。要获取一组日志和相关的交易输入,用户必须进行 N+1 次 RPC 请求(一次获取日志,然后每个唯一交易一次)。
- **字段选择。** 每个 RPC 方法返回固定的对象模式。仅需要区块号和时间戳的用户别无选择,只能获取像 `logsBloom` 这样的大型无关字段,然后立即丢弃它们。
- **分页。** `eth_getLogs` 的分页设计导致频繁的超时和客户端的变通方法。区块、交易和追踪的 RPC 方法根本不支持范围查询。
这些不足给用户和节点操作员带来了不必要的计算、内存和带宽成本。
## 示例
此请求使用 `eth_queryLogs` 方法从 Monad 主网获取 USDC 合约发出的 `Transfer` 事件日志,包括每个日志的父区块的时间戳。
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_queryLogs",
"params": [{
"filter": {
"address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
},
"fields": {
"logs": ["blockNumber", "logIndex", "address", "data", "topics"],
"blocks": ["number", "timestamp"]
},
"order": "asc",
"fromBlock": "0x5E69EC4",
"toBlock": "0x5E69ECB"
}]
}
```
响应包括每个匹配日志和相关区块的指定字段,以及 `fromBlock`、`toBlock` 和 `cursorBlock` 的解析区块引用。
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"data": {
"logs": [
{
"blockNumber": "0x5E69EC6",
"logIndex": "0x4F",
"address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
"data": "0x000000000000000000000000000000000000000000000000000000000432d69f",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000928dc8afe312df45576b15b08c086c5427fd8207",
"0x000000000000000000000000d6aeaa631c867347afacf038cb3be58ca5c9cedf"
]
},
{
"blockNumber": "0x5E69EC6",
"logIndex": "0x57",
"address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
"data": "0x0000000000000000000000000000000000000000000000000000000003f87ab2",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000928dc8afe312df45576b15b08c086c5427fd8207",
"0x0000000000000000000000009bbc4b5339f7a24ae4548bd923c36aebc5e7c870"
]
}
],
"blocks": [
{
"number": "0x5E69EC6",
"timestamp": "0x6a8d5688"
}
]
},
"fromBlock": {
"number": "0x5E69EC4",
"hash": "0x93c7d639e007fd25626e326ad7a8a20977d6c0e3ab9da32c81ae7ec1d39577d8",
"parentHash": "0xcb3a39ea3177e88617c2ad5a48e0922277638fe0ce08f0548b7f639e90894b58"
},
"toBlock": {
"number": "0x5E69ECB",
"hash": "0x323e8aaf8d9b65afc6d7e3d0d589dac6b2d4050af146b7a1bd7226df604c4587",
"parentHash": "0xd1b683ccca914f70da48d36031f3c1332c0d3af8af1dc5a82ecbd19d8e6ec3d8"
},
"cursorBlock": {
"number": "0x5E69ECB",
"hash": "0x323e8aaf8d9b65afc6d7e3d0d589dac6b2d4050af146b7a1bd7226df604c4587",
"parentHash": "0xd1b683ccca914f70da48d36031f3c1332c0d3af8af1dc5a82ecbd19d8e6ec3d8"
}
}
}
```
## 规范
本文档中的关键字“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“NOT RECOMMENDED”、“MAY”和“OPTIONAL”应按[RFC 2119](https://www.ietf.org/rfc/rfc2119.html)和[RFC 8174](https://www.ietf.org/rfc/rfc8174.html)中的描述进行解释。
### 约定
#### 值类型
本文档使用以太坊 JSON-RPC 接口的值类型约定:
- `QUANTITY` — 一个无符号整数,编码为以 `0x` 为前缀的大端十六进制字符串,且没有前导零。零必须编码为 `"0x0"`。
- `DATA` — 一个字节序列,编码为以 `0x` 为前缀的十六进制字符串,每个字节两个十六进制数字,因此是偶数个数字。空字节序列必须编码为 `"0x"`。
- `TAG` — `fromBlock` 和 `toBlock` 接受的区块标签字符串之一;见[区块范围](#block-range)。
本文档中提到的所有其他类型(`string`、`number`、`boolean`、`object`以及数组形式如 `DATA[]`、`number[]`和 `string[]`)都是相应的 JSON 类型。
#### 字段可用性
每个方法的响应表都有一个可用性列,描述字段何时在对象中存在。
| 值 | 意义 |
| --- | --- |
| 必需 | 在该类型的每个对象中都存在。 |
| 依赖于分叉 | 仅在引入该字段的特性对返回的区块有效时存在。 |
| 依赖于类型 | 仅在携带该字段的交易类型中存在,例如 EIP-1559 交易中的 `maxFeePerGas`。 |
| 可选 | 即使被选择,也可能缺失,原因可能是它不适用于该对象——例如在成功的调用跟踪中 `error`——或因为服务器未填充它。 |
### 请求和响应
#### 请求
对于所有方法,`params` 必须是一个单元素数组,其第一个元素是下面定义的请求对象。
| 字段 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `fromBlock` | `QUANTITY` 或 `TAG` | 否 | 包含范围的开始。见[区块范围](#block-range)。 |
| `toBlock` | `QUANTITY` 或 `TAG` | 否 | 包含范围的结束。见[区块范围](#block-range)。 |
| `order` | `string` | 否 | 遍历方向,`asc`(默认)或 `desc`。见[区块范围](#block-range)。 |
| `target` | `QUANTITY` | 否 | 每页的目标主对象数量。见[区块对齐分页](#block-aligned-pagination)。 |
| `filter` | `object` | 否 | 方法特定的过滤对象。见[过滤器](#filters)和每个方法的过滤部分。 |
| `fields` | `object` | 否 | 方法特定的字段选择和关系连接。见[字段和关系](#fields-and-relations)和每个方法的字段部分。 |
#### 响应
| 字段 | 类型 | 描述 |
| --- | --- | --- |
| `data` | `object` | 方法特定的查询结果对象,以对象名称为键。每个值是包含请求字段的对象数组。 |
| `fromBlock` | `{ number: QUANTITY, hash: DATA, parentHash: DATA }` | 查询执行时解析的起始区块。如果请求中的 `fromBlock` 是一个标签,则反映该标签解析到的区块。 |
| `toBlock` | `{ number: QUANTITY, hash: DATA, parentHash: DATA }` | 查询执行时解析的结束区块。如果 `toBlock` 是 `"latest"` 或在 `asc` 模式中省略,则反映查询执行时节点认为最新的区块。 |
| `cursorBlock` | `{ number: QUANTITY, hash: DATA, parentHash: DATA }` | 页中的最后一个区块(包含)。响应包含从 `fromBlock` 到 `cursorBlock` 的每个匹配主对象。见[区块对齐分页](#block-aligned-pagination)。 |
### 查询语义
#### 区块范围
每个请求扫描从 `fromBlock` 到 `toBlock` 的连续、包含的区块范围,方向由 `order` 指定:
- `asc`(默认):从 `fromBlock` 向上扫描,返回结果按时间从旧到新。`fromBlock` 是下限,`toBlock` 是上限。
- `desc`:从 `fromBlock` 向下扫描,返回结果按时间从新到旧。`fromBlock` 是上限,`toBlock` 是下限。
`fromBlock` 和 `toBlock` 都接受一个十六进制编码的区块编号(例如 `"0xF4240"`)或一个标签:`"latest"`、`"earliest"`、`"safe"`、`"finalized"`。标签必须在查询执行时由服务器解析。如果省略,`fromBlock` 在 `asc` 模式下默认为 `"earliest"`,在 `desc` 模式下默认为 `"latest"`,而 `toBlock` 在 `asc` 模式下默认为 `"latest"`,在 `desc` 模式下默认为 `"earliest"`。因此,省略 `toBlock` 的 `asc` 扫描将运行到链顶,而省略 `toBlock` 的 `desc` 扫描将运行到创世区块。
如果在 `asc` 模式下 `fromBlock` 大于 `toBlock`,或者在 `desc` 模式下 `fromBlock` 小于 `toBlock`,则解析的范围是反转的。服务器必须以 `-32602` 失败反转范围的请求。
节点不需要持有整个链的历史。每个方法都有一个 *可用窗口*:该节点可以提供该方法对象的连续区块范围。由于节点可能保留从创世区块到当前的每个区块,同时仅保留最近历史的痕迹,因此同一节点上的方法之间的窗口可能不同。
在将 `fromBlock` 和 `toBlock` 解析为区块编号后,服务器必须将解析的范围与被调用方法的可用窗口进行比较。如果该范围内的任何区块超出窗口——无论是因为数据被修剪、从未被索引,还是区块尚不存在——服务器必须以 `-32001` 失败请求,而不是返回截断的结果。
#### 区块对齐分页
每个响应覆盖一个 *页面*:从 `fromBlock` 到 `cursorBlock` 的连续区块,按遍历顺序。页面是区块对齐的:响应包含页面中每个区块的每个匹配主对象,而不包含外部区块的对象。页面永远不会包含部分区块。
`target` 请求字段设置每页的目标主对象数量。它必须至少为 `0x1`,相关对象不得计入其中。
服务器通过逐个扫描解析的范围来构建页面,从 `fromBlock` 开始,并在以下条件中的第一个条件下结束页面:
1. **范围结束。** 扫描达到 `toBlock`。`cursorBlock` 为 `toBlock`,且没有剩余区块。
2. **目标达到。** 扫描达到一个区块,使页面的主对象数量达到 `target` 或更高。`cursorBlock` 必须设置为此最终区块,响应必须包括其中的每个匹配主对象,即使数量超过 `target`。如果省略 `target`,则此条件不适用。
3. **预算达到。** 服务器耗尽其 *预算*——任何服务器施加的对单个响应的工作或大小的限制,例如执行时间、响应大小或主对象数量——在完成一个区块之前。服务器必须丢弃该区块的部分结果,并在上一个区块结束页面。
每个成功的响应必须至少包括 `fromBlock` 区块,因此每个页面至少向前推进一个区块。如果服务器在完成 `fromBlock` 之前耗尽其预算,则不存在区块对齐页面,服务器必须以 `-32005` 失败请求。一旦服务器完成至少一个区块,它必须返回页面,而不是以 `-32005` 失败。
MIP-16: JSON-RPC 查询方法
==== MARKDOWN TO TRANSLATE ====
因此,当最终区块包含的匹配项超过所需时,页面包含超过 `target` 的主要对象,而当范围结束或预算首先达到时,则包含更少的对象 — 可能为零。因此,响应中的主要对象数量并不表示是否还有更多数据;只有 `cursorBlock` 才能指示这一点。
例如,考虑一个 `asc` 请求,`fromBlock` 为 10,`toBlock` 为 20,`target` 为 100(为了可读性使用十进制),其中区块 10 到 13 包含 40、0、55 和 30 个匹配的主要对象:
| 场景 | 结束条件 | `cursorBlock` | 返回的主要对象 |
| --- | --- | --- | --- |
| 预算未耗尽 | 在区块 13 达到目标(40 + 0 + 55 + 30 = 125) | 13 | 125 |
| 在扫描区块 13 时预算耗尽 | 预算达到 | 12 | 95 |
| 在扫描区块 10 时预算耗尽 | — | — | 请求失败,返回 `-32005` |
#### 链一致性
响应中的每个对象和区块引用必须来自同一规范链,如服务器在查询执行时所见。响应不得混合重组前后的区块。
此保证适用于单个响应。连续页面可能反映不同的链,如果在请求之间发生重组;请参见 [重组检测](#reorg-detection)。
#### 过滤器
每个方法定义自己的一组过滤字段;请参见该方法的过滤器部分。以下规则适用于所有方法。
`filter` 对象中的所有条件使用 AND 语义组合。除非方法的过滤器部分另有说明,否则每个过滤字段接受单个值或值数组;如果字段等于数组中的任何元素,则数组匹配(字段内的 OR)。省略的过滤字段对结果没有约束。如果省略 `filter`,则返回区块范围内该方法主要类型的每个对象。
#### 字段和关系
`fields` 对象选择响应中包含的内容。每个键命名一个对象模式,每个值是要从该模式中包含的字段名称数组或 `true`,以包含该模式的每个字段。命名方法主要对象类型的键选择主要对象的字段;每个其他键命名要连接的关系。如果省略 `fields`,则包含主要对象的所有字段,并且不连接任何关系。有关每个方法接受的键,请参见该方法的字段部分。
关系是从主要对象到另一种类型的单个对象的引用。只有多对一关系可以连接。每个方法必须拒绝命名其不支持的关系的 `fields` 键。
结果必须以规范化形式返回。相关对象在 `data` 下以自己的数组出现,键名与在 `fields` 中选择它们的名称相同。相关数组必须仅包含在同一响应中由主要对象引用的对象,并且由多个主要对象引用的相关对象必须仅出现一次。
相关对象使用相应方法的响应部分中定义的模式:`blocks` 对象如 `eth_queryBlocks` 下定义,`transactions` 对象如 `eth_queryTransactions` 下定义。
#### 排序
每个对象类型都有一个排序键,在返回其主要类型的方法的排序部分中定义。`data` 中的主要和相关对象数组必须按这些排序键排序 — 在 `asc` 模式下升序,在 `desc` 模式下降序。排序适用于整个数组,而不仅仅是区块粒度:在 `desc` 模式下,单个区块内的对象也按相反顺序返回。
### `eth_queryBlocks`
查询区块头。
#### 过滤器
| 字段 | 接受类型 | 描述 |
| --- | --- | --- |
| `miner` | `DATA` 或 `DATA[]` | 区块矿工 / coinbase 地址。 |
#### 字段
| 键 | 类型 | 描述 |
| --- | --- | --- |
| `blocks` | `string[]` 或 `true` | 主要。要从 `blocks` 模式中包含的字段。 |
`eth_queryBlocks` 不支持任何关系。
##
==== END ====论坛讨论
-
github.com/monad-crypto/MIPs MIP-16: JSON RPC Query Methods (#88) main ← mip-json-rpc-query-methods opened 06:03PM - 21 Sep 26 UTC typedarray +603 -0 EDIT: Discussion continued in https://github.com/monad-crypto/MIPs/pull/89. - … -- Introduces five new JSON-RPC methods that enable efficient queries for raw chain history data: - `eth_queryBlocks` - `eth_queryTransactions` - `eth_queryLogs` - `eth_queryTraces` - `eth_queryTransfers` Introduces five new JSON-RPC methods that enable efficient queries for raw chain history data: - eth_queryBlocks - eth_queryTransactions - eth_queryLogs - eth_queryTraces - eth_queryTransfers Please reference the draft PR for our motivation, full specification, rationale, and usage guidelines.
-
Good work! Two important corner cases I hope will be addressed, as I have found out this being very annoying to deal with in the past, causing bad developer experience or “headache” - Distinguishing between incomplete node data and empty replies. Especially important for Monad. The JSON-RPC should return an error instead of no data found response if the node does not have the data because of its internal purging rules. Example: ask events in Monad block 1. Because nodes purge this data, we should could get reply “data not available on this node” instead of no events. Because Monad RPCs have various purge and censorship rules, it is otherwise impossible to know whether the data exists or this particular RPC just doesn’t have it. - Pagination / limit within the block: If th pagination limit gets interrupted in the middle a block, we should be somehow know about this and be able to...
-
Thanks for the feedback, here are my responses. - Distinguishing between incomplete node data and empty replies . The latest version of the proposal better specifies this behavior ( link ). It introduces the concept of a server / implementation availability window for each method, which the server must compare against each request, and respond with an error if the requested block range falls outside of the window. - Pagination / limit within the block . We also added a section that better specifies the “Block-aligned pagination” behavior ( link ). TL;DR - the server is allowed to return greater than the user-requested number of events to ensure that the response is block-aligned. We also renamed limit to target , which better describes how it works. To address your scenario directly, the server would finish collecting data from block 2 and then return 200 events.
-
Thanks @typedarray , This is definitely amazing work. To better understand how the things work I want to ask the following: - What does “earliest” resolve to? If I am not mistaken earliest refers to the genesis block. So {order: "desc", fromBlock: "latest"} with no toBlock scans to block 0. But any range outside the availability window must return -32001 . So on a node that doesn’t keep full history, that query always fails. Did I get that right, or am I missing something? If it is an issue, maybe "earliest" could resolve to the start of the availability window instead.
-
This is also a bit out of scope for this proposal, but should be considered for the future. JSON, and thus JSON-RPC, is a super inefficient data transfer method. For high-throughput chains like Monad, this is becoming a bottleneck in the sense that your cheap ass VPS will just spend its 1 CPU core parsing JSON, making building low-end applications difficult. Same goes for a pure client-side, web browser, application that tries to query Monad. It might be better to just to have “alternative” data delivery method field in the JSON-RPC request, defaulting to json but then you could ask for protobuf or whatever in the future to get the data out faster. As a bonus this would tax the RPC node provider less. Common choices inculde Arrow, Parquet, Flatbuffer, Profobuf, various binary JSONs, etc. Cosmos eco has been using Protobuf since 2017 and it was a good choice for them. Here is tidb...