Solana 槽位号不是区块号:槽位按固定时间表推进,但当其领导者未产生区块时,槽位可能被跳过,因此槽位范围是区块数量的上界,而非等值。getBlocks 方法仅返回包含区块的已确认槽位,且按降序排列,这意味着返回的槽位号之间的数字间隙是跳过槽位的证据,而非 API 缺陷。无间隙索引器以分块方式遍历范围,记录返回槽位的集合,并将每个槽位分类为区块存在、已跳过或未解析,其中未解析意味着遍历无法证明任一状态。承诺级别和保留窗口会影响哪些槽位可解析,因此可复现的历史遍历应固定承诺级别并在最终确定头之下结束。分块大小、保留窗口和每次调用限制是提供商和集群属性,因提供商而异并有文档记录,并非协议常量。
槽位号与区块号是不同的标识符
在 EVM 链上,区块高度是密集序列:每次递增都对应一个已产生的区块,因此 latestBlock - fromBlock + 1 等于范围内的区块数量。Solana 打破了这一假设。槽位是分配给领导者的计划时间单位,当其领导者未产生区块时,槽位可能被跳过。官方 Solana getBlocks 方法参考 说明该方法返回包含区块的已确认槽位,而非请求跨度内的每个槽位。
实际后果是 end_slot - start_slot + 1 是范围内区块数量的上界,而非等值。假设密集性的索引器要么少计区块,要么将合法间隙误判为数据丢失。如果你来自 EVM 对账工作流,逐块 EVM 索引器对账 中的思维模型不能直接套用;Solana 需要感知槽位的遍历。
- 槽位是产生区块的计划机会;区块是产生的产物。
- 跳过槽位没有区块,属于预期行为,而非错误。
end_slot - start_slot + 1是区块数量的上界,而非计数。- getBlocks 仅返回包含区块的槽位,按降序排列。
文档化的 getBlocks 语义与降序排列
官方参考文档将 start_slot、end_slot 和 commitment 列为参数,范围包含两端,并返回包含区块的已确认槽位号的降序数组。由于数组是降序的,当向后遍历时,返回的最低槽位是下一页的自然游标,而返回的最高槽位告诉你当前分块实际从哪里开始。
返回的槽位号之间出现数字间隙的连续序列是跳过槽位的证据,而非 API 缺陷。例如,包含 100、99、97、96 的响应表明槽位 98 被跳过。间隙是你想要记录而非抑制的信号。RPC 端点指南 介绍了端点选择和提供商行为如何影响哪些槽位可解析。
- 边界包含两端:start_slot 和 end_slot 都会被考虑。
- 结果是降序的,因此返回的最低槽位是下一个排他性的 end_slot。
- 返回槽位之间的数字间隙是跳过槽位的证据。
- 不保证返回完整一页;永远不要假设返回了分块大小。
为什么朴素的逐槽位 getBlock 循环会失败
诸如 for (let s = start; s <= end; s++) getBlock(s) 的循环在三个方面是错误的。首先,它将 null 视为致命错误,而跳过槽位合法地没有区块,且 getBlock 参考 说明跳过槽位返回 null。其次,即使对于间隙,它也每个槽位消耗一次请求,在大范围上非常浪费。第三,它无法区分由跳过槽位引起的 null 与由保留限制或错误承诺级别引起的 null。
正确的方法是使用 getBlocks 发现哪些槽位包含区块,然后仅对这些槽位调用 getBlock。这减少了请求量并使分类明确。将发现与检索分离的同样原则也出现在 Solana getSignaturesForAddress 分页 中,其中签名发现和交易检索是不同的步骤。
- 没有上下文时,getBlock 返回的 null 是模糊的:跳过、超出保留窗口或承诺级别错误。
- 逐槽位循环会在永远不会产生区块的槽位上浪费请求。
- 通过 getBlocks 发现应先于通过 getBlock 检索。
- 模糊性必须记录为未解析,而非静默丢弃。
槽位范围的间隙检测算法
该算法以分块方式遍历请求范围,记录返回槽位的集合,并将请求范围内的每个槽位分类为三种状态之一:区块存在、已跳过或未解析。区块存在意味着 getBlocks 返回了该槽位。已跳过意味着该槽位落在成功返回的分块内,但响应中缺失。未解析意味着遍历无法证明任一状态,例如因为分块失败、端点返回错误,或槽位接近保留边界。
未解析槽位必须被记录,而非静默丢弃。无间隙索引器不是报告零间隙的索引器;而是对请求范围内每个槽位都给出可辩护分类的索引器。这与 通过 RPC 查询 Solana 历史数据 背后的原则相同,其中保留边界改变了可证明的内容。
- 区块存在:getBlocks 在成功的分块中返回了该槽位。
- 已跳过:该槽位在成功的分块响应中缺失。
- 未解析:分块失败或槽位接近保留限制。
- 请求范围内的每个槽位必须恰好获得一个分类。
可运行的 Node.js 遍历器与每分块摘要
以下 Node.js 脚本以分块方式遍历槽位范围,调用 getBlocks,并以表格形式打印每分块摘要。它使用可配置的分块大小和承诺级别,并在分块失败时记录未解析槽位。将端点 URL 替换为你自己的提供商端点;它打印的数字供你对照自己的端点进行测量。
该脚本不假设返回完整一页。它记录返回的最低槽位,并将其用作下一个排他性的 end_slot,这对于 getBlocks 参考文档所述的降序排列是正确的。
const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example.com';
const COMMITMENT = 'finalized';
const CHUNK_SIZE = 1000;
async function rpc(method, params) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
async function walkRange(startSlot, endSlot) {
const rows = [];
let cursor = endSlot;
while (cursor >= startSlot) {
const chunkStart = Math.max(startSlot, cursor - CHUNK_SIZE + 1);
const span = cursor - chunkStart + 1;
let returned = [];
let unresolved = 0;
try {
returned = await rpc('getBlocks', [chunkStart, cursor, { commitment: COMMITMENT }]);
} catch (err) {
unresolved = span;
}
const returnedSet = new Set(returned);
let skipped = 0;
for (let s = chunkStart; s <= cursor; s++) {
if (!returnedSet.has(s)) skipped++;
}
rows.push({
requestedSpan: `${chunkStart}-${cursor}`,
spanSize: span,
returnedCount: returned.length,
lowestReturned: returned.length ? Math.min(...returned) : null,
detectedGaps: skipped,
unresolved
});
if (returned.length === 0) break;
cursor = Math.min(...returned) - 1;
}
return rows;
}
(async () => {
const rows = await walkRange(250000000, 250010000);
console.table(rows);
})();用于你自己的端点测量结果表
针对你自己的端点运行遍历器,并用你的测量值填写下表。这些值不是协议常量;它们取决于你的提供商、你的集群以及你固定的承诺级别。明确说明分块大小、保留窗口和每次调用限制是提供商和集群属性,因提供商而异并有文档记录。
使用该表比较端点或承诺级别。如果未解析计数很高,范围可能接近保留窗口,或者端点可能正在限流。如果检测到的间隙异常高,请验证你没有在分块之间混用承诺级别。
- 请求跨度:分块的包含两端的槽位范围。
- 返回计数:getBlocks 返回的槽位数量。
- 最低返回:遍历的下一个排他性 end_slot。
- 检测到的间隙:成功分块中缺失的槽位。
- 未解析:遍历无法分类的槽位。
承诺级别与最终确定头
对于最新的分块,已确认和最终确定的槽位可能不同,因为承诺级别反映了不同级别的集群共识。可复现的历史遍历应固定一个承诺级别,并在最终确定头之下完成范围,以便分类在多次运行中保持稳定。Solana 承诺级别与交易确认 指南解释了这些级别与确认的关系。
如果你以已确认承诺级别遍历最新槽位,一个看似被跳过的槽位可能会在区块产生或集群推进后解析为区块。对于索引,固定最终确定承诺级别并在最终确定头之下结束可避免这种模糊性。将你使用的承诺级别与结果表一起记录。
- 为整个遍历固定一个承诺级别。
- 为可复现性,在最终确定头之下完成范围。
- 对于最新的分块,已确认和最终确定可能不同。
- 将使用的承诺级别与你的测量值一起记录。
保留边界与超出保留窗口分类
旧槽位最终会因端点不再保留而停止解析。因超出保留窗口而返回无区块的槽位必须记录为超出保留窗口,而非已跳过。将两者混为一谈会破坏你的间隙统计,并可能掩盖真实的数据丢失。
保留窗口是提供商和集群属性,因提供商而异并有文档记录。在遍历历史范围之前,确认你的端点的保留窗口。Solana 网络页面 和 API 服务 描述了 OnFinality 如何组织访问,而 RPC 定价 页面涵盖了计划层面的考虑。关于历史访问的更广泛处理,请参阅 通过 RPC 查询 Solana 历史数据。
- 超出保留窗口是与已跳过不同的分类。
- 保留窗口因提供商和集群而异。
- 在遍历历史范围之前确认保留窗口。
- 在输出中明确记录超出保留窗口的槽位。
槽位范围遍历的局限性与权衡
与逐槽位 getBlock 循环相比,分块遍历减少了请求量,但仍需要每个分块一次 getBlocks 调用。较大的分块减少调用次数,但增加触及每次调用限制的可能性,这些限制是提供商和集群属性。较小的分块更具弹性但更慢。
分类的质量取决于端点的响应。如果端点对某个分块返回错误,这些槽位将变为未解析,遍历无法证明其状态。没有协议级保证范围内的每个槽位都能在单次遍历中分类。OnFinality Learn 中心 收集了有关索引和 RPC 行为的相关指南。
- 较大的分块减少调用,但有触及每次调用限制的风险。
- 较小的分块有弹性但更慢。
- 未解析槽位是合法结果,而非失败。
- 没有单次遍历能保证完整分类。
排查常见的间隙检测失败
如果你的遍历报告许多间隙,首先检查你是否在分块之间混用了承诺级别。已确认分块后跟最终确定分块可能产生实际上是承诺差异的表观间隙。其次,检查范围是否超出保留窗口;如果不单独分类,超出保留窗口的槽位将表现为间隙。
如果你的遍历报告许多未解析槽位,请检查限流或端点错误。减小分块大小并重试失败的分块。如果某个分块持续失败,将其记录为未解析并继续;不要静默丢弃它。RPC 端点指南 涵盖了端点选择和故障转移的考虑。
- 混用承诺级别可能产生表观间隙。
- 超出保留窗口的槽位必须单独分类。
- 限流会增加未解析计数。
- 重试失败的分块;永远不要静默丢弃它们。
生产索引器的后续步骤
对于生产索引,持久化请求范围内每个槽位的分类,包括未解析和超出保留窗口状态。为未解析范围以及接近最终确定头的槽位安排重新遍历。固定一个承诺级别并将其与结果一起记录。
如果你在 OnFinality 上构建,请查看 Solana 网络页面 和 API 服务 了解端点选项,并查阅 RPC 定价 了解计划层面的细节。有关相关的索引模式,请参阅 Solana getSignaturesForAddress 分页 和 逐块 EVM 索引器对账。
- 持久化每个槽位分类,包括未解析。
- 按计划重新遍历未解析范围。
- 固定并记录你的承诺级别。
- 在扩展之前查看提供商保留窗口和每次调用限制。