# 在支持窗口内查询 EVM 历史状态

> 原文地址: https://docs.blockvectra.com/zh/guides/evm-historical-state/

历史 `eth_call` 取决于该链状态窗口，而不是 `eth_getLogs` 的区块跨度上限。读取合约过去的值之前，先核对状态窗口、端点的认证方式与目标块高。

## 三种历史限制分别管什么

| [GET /v1/chains](https://api.blockvectra.com/v1/chains) 中的字段 | 控制范围                                                                           | 调用前检查                                                                           |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `state_window_blocks`                                        | 认证状态读取可回溯的区块窗口，例如 `eth_call`、`eth_getBalance`、`eth_getCode`、`eth_getStorageAt` | 链头为 `H`、声明窗口为 `W` 时，早于 `H − W` 的编号区块超出窗口。同时核对 `methods.allow` 与 `methods.deny`。 |
| `public.history_blocks`                                      | 免 key 端点 `public.url` 上的历史区块引用                                                 | 只能调用 `public.methods` 中的方法；状态读取取公开历史范围与已声明状态窗口中较小的限制。                           |
| `max_logs_block_range`                                       | 一次认证 `eth_getLogs` 请求的区块数                                                      | 按 `toBlock − fromBlock + 1` 计数。跨度合规不代表早期合约状态或日志一定可用。                            |

这些限制的单位是区块，不是天。状态窗口为 `null` 或未声明，不代表归档覆盖。免 key 与认证方法列表分别生效：仅声明日志跨度，不代表公开端点支持 `eth_getLogs`。

## 对照各链状态窗口

下表从公开快照展示状态窗口、免 key 历史、日志跨度与已声明的 Data API 数据集。实际发请求前，再读取 [GET /v1/chains](https://api.blockvectra.com/v1/chains) 与 [GET /v1/status](https://api.blockvectra.com/v1/status)。

开发者与 AI Agent 应分别核对状态窗口与日志查询跨度。状态窗口为 null 不代表归档覆盖；公开历史范围只适用于声明的公开方法。

| 链 | 链标识 | 认证状态窗口：state_window_blocks（区块数） | 免 key 历史：public.history_blocks（区块数） | 认证日志查询跨度：max_logs_block_range（区块数） | 明确声明的 Data API 数据集 |
| --- | --- | --- | --- | --- | --- |
| Arbitrum One | `arb_mainnet` | 6,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Base | `base_mainnet` | 10,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| BNB Smart Chain | `bsc_mainnet` | 100 | 100 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum | `eth_mainnet` | 250,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum Sepolia | `eth_sepolia` | 未声明 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| HyperEVM | `hyperevm_mainnet` | 未声明 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, freshness |
| Polygon | `polygon_mainnet` | 126 | 126 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Robinhood Chain | `robinhood_mainnet` | 900 | 900 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, dex_swaps, dex_prices, stocks, traces, freshness |
| Robinhood Chain Testnet | `robinhood_testnet` | 1,023 | 1,000 | 1,000 | Data API 不可用 |

[GET /v1/chains](https://api.blockvectra.com/v1/chains) · 采样日期（UTC）: 2026-10-08

[GET /v1/status](https://api.blockvectra.com/v1/status) · 采样日期（UTC）: 2026-10-08

## 选择区块标签

查询当前值时使用 `latest`。对比历史值时，先读取一次 `eth_blockNumber`，选定块高并转为十六进制数量，例如 `0x18efa2f`。对比中的每次调用保持这个块高不变；重复使用 `latest` 可能读到不同区块。

状态读取使用 `earliest`、`safe`、`finalized` 时，状态窗口策略返回 `-32011`；应改为窗口内的明确块高。区块哈希形式也不能扩大历史覆盖：免 key 状态读取会拒绝它，认证请求仍依赖状态是否可用。

链重组后，同一个块高可能对应不同区块。需要标识结果对应的区块时，用 `eth_getBlockByNumber` 记录区块哈希。即使块高位于窗口内，也需要链已同步，且合约在那个高度已存在。

## 在固定块高读取同一合约

Ethereum 上的 WETH 合约 `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` 提供 `decimals()`，选择器为 `0x313ce567`。2026-10-08（UTC）采样的一次免 key 调用在块高 `0x18efa2f` 上返回 HTTP 200，结果如下：

发往该链 `public.url` 的请求：

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "eth_call",
  "params": [
    { "to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "data": "0x313ce567" },
    "0x18efa2f"
  ]
}
```

响应：

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": "0x0000000000000000000000000000000000000000000000000000000000000012"
}
```

ABI 编码的整数是 18。这是小数位数，不是余额，也不能证明其他块高都可查询。固定块高会随时间移出有界窗口；以后运行下方示例时应选择近期区块。

将示例保存为 `historical-state.mjs`，在 Node.js 24 或更高版本中设置自己的 `BLOCKVECTRA_API_KEY` 环境变量，再运行 `node historical-state.mjs`。示例使用认证端点，保持合约与 calldata 相同，对比 `latest`、一个固定近期块高和一个超出已公布认证窗口的块高。每次输出都包含实际 HTTP 状态与 JSON-RPC 正文；HTTP 200 也可能包含错误。遇到非预期响应时，示例停止，不将它当作读取成功。

```js
const key = process.env.BLOCKVECTRA_API_KEY;
if (!key) throw new Error('Set BLOCKVECTRA_API_KEY');
const chainsUrl = 'https://api.blockvectra.com/v1/chains';
const catalogResponse = await fetch(chainsUrl, { signal: AbortSignal.timeout(15_000) });
if (!catalogResponse.ok) throw new Error(`Chains HTTP ${catalogResponse.status}`);
const catalog = await catalogResponse.json();
const chain = catalog.chains.find(item => item.chain === 'eth_mainnet');
const matches = (method, pattern) => pattern.endsWith('*')
  ? method.startsWith(pattern.slice(0, -1)) : method === pattern;
if (!chain?.jsonrpc || !['eth_call', 'eth_blockNumber'].every(method =>
  chain.methods?.allow?.some(pattern => matches(method, pattern)) &&
  !chain.methods?.deny?.some(pattern => matches(method, pattern)))) {
  throw new Error('Required methods are unavailable');
}
const window = chain.state_window_blocks;
if (!Number.isSafeInteger(window) || window < 10) {
  throw new Error('This example needs a declared state window of at least 10 blocks');
}
const rpcUrl = new URL('./eth_mainnet', chainsUrl).href;
let id = 0;
async function rpc(method, params) {
  const response = await fetch(rpcUrl, {
    method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
    headers: { 'Content-Type': 'application/json', 'x-api-key': key },
    body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
  });
  return { http: response.status, body: await response.json() };
}
const headResponse = await rpc('eth_blockNumber', []);
if (headResponse.http !== 200 || headResponse.body.error ||
    !/^0x[0-9a-f]+$/i.test(headResponse.body.result ?? '')) {
  throw new Error(`Cannot read head: ${JSON.stringify(headResponse)}`);
}
const head = BigInt(headResponse.body.result);
if (head <= BigInt(window)) throw new Error('Head is too low for an out-of-window block');
const hex = value => `0x${value.toString(16)}`;
const fixedBlock = hex(head - 10n);
const outsideBlock = hex(head - BigInt(window) - 1n);
const call = { to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', data: '0x313ce567' };
for (const block of ['latest', fixedBlock, outsideBlock]) {
  const reply = await rpc('eth_call', [call, block]);
  console.log(JSON.stringify({ head: hex(head), block, ...reply }));
  if (block === outsideBlock) {
    if (reply.http !== 200 || reply.body.error?.code !== -32011 ||
        reply.body.error?.data?.reason !== 'state_window') {
      throw new Error('Expected state_window; inspect the actual response above');
    }
  } else if (reply.http !== 200 || reply.body.error ||
      reply.body.result !== '0x0000000000000000000000000000000000000000000000000000000000000012') {
    throw new Error('Expected the WETH decimals result; inspect the actual response above');
  }
}
```

上方记录的响应来自 `public.url`，脚本则使用 API key。免 key 读取时，直接从 `public.url` 取端点，省略 key，并选择同时处于 `public.history_blocks` 与状态窗口内的块高。即使合约与 calldata 相同，认证方式不同，可查询的历史范围也可能不同。

## 识别超窗口错误并恢复

同一免 key 端点上，2026-10-08（UTC）采样的调用仅将目标块高改为 `0x18ef650`（并调整请求 ID），返回 HTTP 200、`error.code: -32011`、`error.data.reason: state_window` 与 `error.data.retryable: false`，消息为 `block reference is outside the public history window`。这是公开历史范围导致的失败；认证端点有自己的状态窗口。

通过[错误目录的 state\_window 条目](https://docs.blockvectra.com/zh/errors/#state_window)中的字段识别失败，不要依赖消息里某个具体窗口数字：

| 字段                     | 目录中的值或含义                            |
| ---------------------- | ----------------------------------- |
| HTTP 状态                | `200`；HTTP 成功时仍需检查 JSON-RPC `error` |
| `error.code`           | `-32011`                            |
| `error.message`        | 认证状态窗口错误描述最近可查询的区块数；公开历史范围错误的消息可能不同 |
| `error.data.reason`    | `state_window`                      |
| `error.data.docs_url`  | 指向错误目录中 `state_window` 说明的链接        |
| `error.data.retryable` | `false`：稍后原样重试不能恢复更早的状态             |

改查较新的明确块高；任务只需当前值时，使用 `latest`。缩小 `eth_getLogs` 跨度不能恢复历史 `eth_call` 状态。同为 `-32011` 的其他原因处理不同：[range\_not\_indexed](https://docs.blockvectra.com/zh/errors/#range_not_indexed) 需要改查已覆盖的区间；[history\_not\_ready](https://docs.blockvectra.com/zh/errors/#history_not_ready) 可在索引追上后重试。检查 `error.data.reason`，不要只看数字错误码。

底层状态不可用还可能返回 `-32000`，历史区块被裁剪可能返回 `4444`，详见[错误目录](https://docs.blockvectra.com/zh/errors/)。不要原样重试旧块高，也不要把较大的声明窗口当作每次都能成功的保证。

## 选择下一步查询

完整的工作负载清单与自测方法见[如何选择 RPC 服务商](https://docs.blockvectra.com/zh/guides/choose-rpc-provider/)。

为重复合约读取选择服务商时，可[比较 EVM 读取的每日和周期预算](https://docs.blockvectra.com/zh/guides/infura-alternative/)。先核对所需历史块高，再安排任务的每日分布与吞吐；额度预算足够不代表历史状态可用。

为历史读取比较服务商时，先确认双方能查询目标块高。[full 请求超额费用比较](https://docs.blockvectra.com/zh/guides/chainstack-alternative/)比较额外 RU 价格与按方法计算的费用，区分套餐包含量和额外用量，并说明 full 与 archive 计费分类。

需要更早的已索引区块、交易、转账或其他数据集时，核对表格中的 Data API 数据集与 [Data API 接口参考](https://docs.blockvectra.com/zh/api/data/)。已索引记录不能替代任意历史合约执行，也不意味着所有链都支持历史余额。

* [eth\_call 方法参考](https://docs.blockvectra.com/zh/api/json-rpc/methods/eth_call/)：调用参数与结果编码。
* [eth\_getLogs 区块跨度与分段查询](https://docs.blockvectra.com/zh/guides/getlogs-block-range/)：事件日志历史查询。
* [钱包自定义 RPC 设置](https://docs.blockvectra.com/zh/guides/wallet-custom-rpc/)：钱包接入与专用 key。
* [支持的链](https://docs.blockvectra.com/zh/chains/)：网络可用性；[CU 定价指南](https://docs.blockvectra.com/zh/guides/reading-cu-pricing/)：方法费用。
