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

区分认证状态窗口、免 key 历史与日志跨度,选择固定块高调用 eth_call,并诊断 state_window 错误。

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

三种历史限制分别管什么

GET /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 与 GET /v1/status。

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

链链标识认证状态窗口:state_window_blocks(区块数)免 key 历史:public.history_blocks(区块数)认证日志查询跨度:max_logs_block_range(区块数)明确声明的 Data API 数据集
Arbitrum Onearb_mainnet6,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Basebase_mainnet10,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
BNB Smart Chainbsc_mainnet1001001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Ethereumeth_mainnet250,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Ethereum Sepoliaeth_sepolia未声明1,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
HyperEVMhyperevm_mainnet未声明1,0001,000blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, freshness
Polygonpolygon_mainnet1261261,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Robinhood Chainrobinhood_mainnet9009001,000blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, dex_swaps, dex_prices, stocks, traces, freshness
Robinhood Chain Testnetrobinhood_testnet1,0231,0001,000Data API 不可用

GET /v1/chains · 采样日期(UTC):

GET /v1/status · 采样日期(UTC):

选择区块标签

查询当前值时使用 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 的请求:

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

响应:

{
  "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 也可能包含错误。遇到非预期响应时,示例停止,不将它当作读取成功。

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 条目中的字段识别失败,不要依赖消息里某个具体窗口数字:

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

改查较新的明确块高;任务只需当前值时,使用 latest。缩小 eth_getLogs 跨度不能恢复历史 eth_call 状态。同为 -32011 的其他原因处理不同:range_not_indexed 需要改查已覆盖的区间;history_not_ready 可在索引追上后重试。检查 error.data.reason,不要只看数字错误码。

底层状态不可用还可能返回 -32000,历史区块被裁剪可能返回 4444,详见错误目录。不要原样重试旧块高,也不要把较大的声明窗口当作每次都能成功的保证。

选择下一步查询

完整的工作负载清单与自测方法见如何选择 RPC 服务商。

为重复合约读取选择服务商时,可比较 EVM 读取的每日和周期预算。先核对所需历史块高,再安排任务的每日分布与吞吐;额度预算足够不代表历史状态可用。

为历史读取比较服务商时,先确认双方能查询目标块高。full 请求超额费用比较比较额外 RU 价格与按方法计算的费用,区分套餐包含量和额外用量,并说明 full 与 archive 计费分类。

需要更早的已索引区块、交易、转账或其他数据集时,核对表格中的 Data API 数据集与 Data API 接口参考。已索引记录不能替代任意历史合约执行,也不意味着所有链都支持历史余额。

最后更新:

本页目录