在支持窗口内查询 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 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 · 采样日期(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.reason | state_window |
error.data.docs_url | 指向错误目录中 state_window 说明的链接 |
error.data.retryable | false:稍后原样重试不能恢复更早的状态 |
改查较新的明确块高;任务只需当前值时,使用 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 接口参考。已索引记录不能替代任意历史合约执行,也不意味着所有链都支持历史余额。
- eth_call 方法参考:调用参数与结果编码。
- eth_getLogs 区块跨度与分段查询:事件日志历史查询。
- 钱包自定义 RPC 设置:钱包接入与专用 key。
- 支持的链:网络可用性;CU 定价指南:方法费用。
最后更新: