Solana slot 时间线通过组合 getEpochInfo、getLeaderSchedule、getSlot 和 getBlockTime 来解读。getEpochInfo 返回 absoluteSlot、blockHeight、epoch、slotIndex、slotsInEpoch 和 transactionCount;getLeaderSchedule 将 slot 索引映射到某个 epoch 的验证者身份。absoluteSlot 统计自创世以来的每一个 slot,而 blockHeight 只统计产生了区块的 slot,因此两者相减可以估算某个窗口内被跳过的 slot 数量。只有当 slotsInEpoch 是从集群读取而非硬编码时,epoch 运算才是安全的。slot 到时间的映射需要 getBlockTime,对于没有区块的 slot 它会返回 null,因此时间线必须携带最近已知的时间戳。epoch 边界是最危险的读取窗口,因为 leader schedule、epoch 信息和 slot 范围会同时发生切换。
Slot 时间线的基本要素及其 RPC 来源
一条 Solana slot 时间线回答四个问题:某个 slot 属于哪个 epoch、哪个验证者被安排来领导它、该 epoch 何时结束,以及该 slot 对应哪个真实世界时刻。官方 Solana RPC 方法参考文档 getEpochInfo 和 getLeaderSchedule 定义了本指南通篇使用的字段和语义。JSON-RPC 2.0 规范规定了请求和响应的框架,而 Ethereum JSON-RPC 规范不适用于 Solana 的方法语义;Solana 使用自己的方法命名空间。
getEpochInfo 返回 absoluteSlot、blockHeight、epoch、slotIndex、slotsInEpoch 和 transactionCount。getLeaderSchedule 返回一个映射,将验证者身份映射到给定 epoch 的 slot 索引数组。getSlot 返回当前 slot,getBlockTime 返回产生了区块的 slot 的 Unix 时间戳。这些方法结合起来,让你无需依赖未文档化的假设就能重建时间线。
absoluteSlot 与 blockHeight 之间的区别是静默错误最常见的来源。absoluteSlot 统计自创世以来的每一个 slot,无论它是否产生了区块。blockHeight 只统计产生了区块的 slot。在某个窗口内用 absoluteSlot 减去 blockHeight 可以估算该窗口内被跳过的 slot 数量,但混淆两者会让每一次 epoch 计算都发生偏移。进行 epoch 运算时,使用 absoluteSlot。衡量区块生产密度时,使用 blockHeight。
- getEpochInfo:absoluteSlot、blockHeight、epoch、slotIndex、slotsInEpoch、transactionCount。
- getLeaderSchedule:某个 epoch 的验证者身份到 slot 索引数组的映射。
- getSlot:当前 slot 编号。
- getBlockTime:产生了区块的 slot 的 Unix 时间戳,否则为 null。
可以安全依赖的 Epoch 运算
当 slotsInEpoch 是从 getEpochInfo 读取时,epoch = absoluteSlot / slotsInEpoch 和 slotIndex = absoluteSlot % slotsInEpoch 这两个关系是可以安全依赖的。在实践中并非恒定的是 slotsInEpoch 以及 epoch 时间表本身。这些是运行时参数,网络此前也更改过 slot 时间。硬编码每个 epoch 432000 个 slot 或每个 slot 400 毫秒的代码,会在集群参数变化时发生偏移。
在每次重建时间线时都从 getEpochInfo 读取 slotsInEpoch,而不是在长时间窗口内缓存它。如果确实要缓存,请记录 epoch 与 slotsInEpoch 的配对,并在 epoch 变化时使其失效。epoch 时间表是集群属性,有文档记录且因集群而异,因此在一个集群上观察到的值不应假定适用于另一个集群。
对于生产环境的读取者,安全的模式是先获取 getEpochInfo,根据 absoluteSlot 和 slotsInEpoch 计算 epoch 和 slotIndex,然后获取该 epoch 的 getLeaderSchedule。如果两次调用之间 epoch 发生变化,请重新获取 getEpochInfo 并重复。这样可以避免将旧的 epoch 编号与新的 slot 索引配对。
- 使用 absoluteSlot 进行 epoch 运算,而不是 blockHeight。
- 从 getEpochInfo 读取 slotsInEpoch;不要硬编码它。
- 将 epoch 时间表视为集群属性,有文档记录且因集群而异。
- 任何跨越 epoch 边界的 slot 请求之后,都要重新获取 getEpochInfo。
Leader Schedule 的形式与历史可用性
getLeaderSchedule 有两种实用形式:不带 epoch 参数的调用返回当前 epoch 的 schedule,带 epoch 参数的调用返回该特定 epoch 的 schedule。响应将验证者身份映射到 slot 索引数组。这些 slot 索引是相对于 epoch 的,因此 slot 索引 0 是该 epoch 的第一个 slot,而不是创世 slot。
旧 epoch 的历史 schedule 可能无法从已修剪的节点获取。这必须被视为预期的边界,而不是错误。不同提供商会记录不同的保留窗口,保留期是提供商属性。如果你需要历史 schedule,请在设计回填任务之前与你的提供商确认保留期。
slot 索引与 leader 身份之间的关系是直接的:对于给定的 epoch,找到其数组包含该 slot 索引的验证者。如果没有验证者的数组包含该索引,那么你获取的 schedule 并不覆盖你认为的那个 epoch,这就是后文描述的一致性校验。
- 不带 epoch 参数:当前 epoch 的 schedule。
- 带 epoch 参数:该 epoch 的 schedule(如果被保留)。
- slot 索引是相对于 epoch 的,而不是相对于创世的。
- 历史 schedule 的可用性是提供商保留期属性,有文档记录且因提供商而异。
将 Slot 映射到真实世界时间
getBlockTime(slot) 对于产生了区块的 slot 返回 Unix 时间戳,对于没有产生区块的 slot 返回 null。时间线重建必须通过携带最近已知的时间戳并记录插值来处理 null,而不是丢弃该 slot。丢弃 null slot 会让时间线看起来比实际更密集,并掩盖被跳过的 slot。
正确的模式是遍历 slot 范围,对每个 slot 调用 getBlockTime,当结果为 null 时,将最后已知的时间戳向前携带,并将该条目标记为插值。当后续 slot 返回时间戳时,你可以选择用线性估计回填插值条目,但要保留插值标志,以便下游消费者知道该值是推导出来的。
对于粗略的时间线,你可以按间隔采样 getBlockTime 并在样本之间插值。对于精确的时间线,则对每个 slot 调用 getBlockTime。权衡在于请求量与精度。两种方法都必须显式处理 null。
- getBlockTime 对于没有区块的 slot 返回 null。
- 携带最近已知的时间戳并标记插值条目。
- 不要丢弃 null slot;丢弃会掩盖被跳过的 slot。
- 采样可减少请求量,但会增加插值误差。
Epoch 边界:最危险的读取窗口
epoch 边界是生产环境读取者最危险的一分钟。leader schedule、epoch 信息和 slot 范围会同时发生切换。跨越边界发出的请求可能会将旧的 epoch 编号与新的 slot 索引配对,产生一条看起来有效但内部不一致的时间线。
一致的读取应在任何跨越边界的 slot 请求之后重新获取 getEpochInfo。通过比较 slot 请求前后的 epoch 字段来检测边界。如果 epoch 发生变化,丢弃中间结果并重复读取。与调试一个被错误归属的 leader 相比,这样做的成本很低。
对于批处理任务,在批次开始时固定 epoch,并在结束时验证。如果 epoch 在批次中途发生变化,请在边界处拆分批次,并用新的 epoch 重新运行后半部分。这样可以保持每个批次内部一致。
- leader schedule、epoch 信息和 slot 范围会同时发生切换。
- 任何跨越边界的 slot 请求之后,都要重新获取 getEpochInfo。
- 在批次开始时固定 epoch,并在批次结束时验证。
- 在边界处拆分批次,而不是混合不同的 epoch。
一致性校验:Schedule 覆盖范围与 slotsInEpoch
一致性校验确认你获取的 schedule 覆盖了你认为的那个 epoch。将某个 epoch 中各位 leader 被安排的 slot 数量求和,并与 getEpochInfo 中的 slotsInEpoch 比较。如果总和等于 slotsInEpoch,则 schedule 覆盖了整个 epoch。如果总和小于 slotsInEpoch,则 schedule 是部分的,或者该 epoch 仍在进行中。
对于已完成的 epoch,总和小于 slotsInEpoch 表示 schedule 数据缺失,这可能是保留期边界。对于进行中的 epoch,总和小于 slotsInEpoch 是预期内的,因为剩余的 slot 尚未被安排,或者 schedule 正在被增量提供。
将一致性校验结果与时间线一起记录。一致性校验失败的时间线应被标记,而不是被静默消费。这在财务或会计工作流中尤为重要,因为被错误归属的 leader 会改变奖励归属。
- 将每个 epoch 被安排的 slot 数量求和,并与 slotsInEpoch 比较。
- 总和相等:完整覆盖。小于:部分覆盖或进行中。
- 标记一致性校验失败,而不是静默消费。
- 保留期边界可能导致旧 epoch 的 schedule 不完整。
可运行的 Node.js 示例:Epoch、Leader 与结束时间表
以下 Node.js 代码片段读取 getEpochInfo 和 getLeaderSchedule,以表格形式打印 slot 索引、epoch、所选 slot 的归属验证者,以及该 epoch 的结束时间。它使用 Node.js 18 及更高版本中可用的全局 fetch API。请将 RPC 端点替换为你的提供商的端点。
该代码片段将 epoch 结束 slot 计算为 (epoch + 1) * slotsInEpoch - 1,并通过在当前 slot 采样 getBlockTime 并使用观察到的 slot 时长进行外推来估算结束时间。外推是估计值,不是测量值,在任何输出中都应如此标注。
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
async function rpc(method, params = []) {
const res = await fetch(RPC_URL, {
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 main() {
const epochInfo = await rpc('getEpochInfo');
const { absoluteSlot, blockHeight, epoch, slotIndex, slotsInEpoch } = epochInfo;
const schedule = await rpc('getLeaderSchedule', [epoch]);
const leaderForSlot = (targetSlotIndex) => {
for (const [validator, slots] of Object.entries(schedule)) {
if (slots.includes(targetSlotIndex)) return validator;
}
return null;
};
const chosenSlotIndex = slotIndex;
const owner = leaderForSlot(chosenSlotIndex);
const epochEndSlot = (epoch + 1) * slotsInEpoch - 1;
const currentBlockTime = await rpc('getBlockTime', [absoluteSlot]);
const sampleSlot = Math.max(0, absoluteSlot - 100);
const sampleBlockTime = await rpc('getBlockTime', [sampleSlot]);
let estimatedEndTime = null;
if (currentBlockTime && sampleBlockTime && absoluteSlot > sampleSlot) {
const msPerSlot = ((currentBlockTime - sampleBlockTime) * 1000) / (absoluteSlot - sampleSlot);
estimatedEndTime = new Date((currentBlockTime * 1000) + (epochEndSlot - absoluteSlot) * msPerSlot);
}
const rows = [
{ field: 'absoluteSlot', value: absoluteSlot },
{ field: 'blockHeight', value: blockHeight },
{ field: 'epoch', value: epoch },
{ field: 'slotIndex', value: slotIndex },
{ field: 'slotsInEpoch', value: slotsInEpoch },
{ field: 'chosenSlotIndex', value: chosenSlotIndex },
{ field: 'leader', value: owner || 'not found in schedule' },
{ field: 'epochEndSlot', value: epochEndSlot },
{ field: 'estimatedEndTime', value: estimatedEndTime ? estimatedEndTime.toISOString() : 'unavailable' }
];
console.table(rows);
}
main().catch((err) => { console.error(err); process.exit(1); });结果表:针对你自己的端点进行测量
由于 slot 时间、epoch 长度和 schedule 保留期是集群和提供商属性,有文档记录且因集群而异,你应该针对自己的端点进行测量,而不是依赖已发布的数字。下表是一个模板,供你填入自己的观察结果。不要将任何一行视为通用常量。
在一天中的不同时间运行上面的 Node.js 代码片段,并至少跨越一个 epoch 边界。记录观察到的 slotsInEpoch、从 getBlockTime 样本推导出的观察到的 slot 时长、schedule 覆盖总和,以及 getLeaderSchedule 是否为旧 epoch 返回了 schedule。这为你提供了特定于提供商的基线。
如果你的提供商为旧 epoch 返回了部分 schedule,请记录可获得完整 schedule 的最旧 epoch。那就是你的保留期边界。设计回填任务时,要么使其保持在该边界内,要么回退到不同的数据源。
- 观察到的 slotsInEpoch:从 getEpochInfo 填入。
- 观察到的 slot 时长:从 getBlockTime 样本推导。
- Schedule 覆盖总和:将 leader 的 slot 数量求和并与 slotsInEpoch 比较。
- 最旧的完整覆盖 epoch:你的保留期边界。
- 边界行为:epoch 是否在读取中途发生变化并需要重新获取。
局限性与权衡
slot 时间、epoch 长度和 schedule 保留期是集群和提供商属性,有文档记录且因集群而异。任何基于硬编码常量构建的时间线都会发生偏移。安全的做法是从集群读取参数,并将它们与时间线一起记录。
getBlockTime 对于没有区块的 slot 返回 null,因此精确的时间线需要逐 slot 调用并显式处理 null。采样可减少请求量,但会增加插值误差。没有免费的午餐;根据你的工作流需要精度还是趋势来选择。
历史 leader schedule 可能无法从已修剪的节点获取。这是预期的边界,而不是错误。如果你的工作流需要历史 schedule,请与你的提供商确认保留期并设计回退方案。关于 commitment 和确认的相关阅读,请参阅 Solana commitment 级别与交易确认。
- 硬编码常量会偏移;从集群读取。
- null 区块时间需要显式处理。
- 采样以精度换取请求量。
- 历史 schedule 保留期是提供商属性。
排查常见的时间线错误
最常见的错误是混淆 absoluteSlot 与 blockHeight。如果你的 epoch 计算偏差很大,请检查你使用的是哪个字段。Epoch 运算需要 absoluteSlot。区块生产密度需要 blockHeight。
第二常见的错误是硬编码 slotsInEpoch。如果你的 slotIndex 在 epoch 边界附近出错,请验证 slotsInEpoch 是否来自与你正在检查的 slot 相同 epoch 的 getEpochInfo。此处的不匹配会产生一个看似合理但错误的 slotIndex。
第三是丢弃 null 区块时间。如果你的时间线没有显示任何间隙,但集群存在被跳过的 slot,那么你就是在丢弃 null。携带最近已知的时间戳并标记插值条目。关于相关的分页和解析模式,请参阅 Solana getSignaturesForAddress 分页 和 Solana 版本化交易与 getBlock 解析。
- Epoch 运算错误:检查 absoluteSlot 与 blockHeight。
- slotIndex 错误:验证 slotsInEpoch 与该 epoch 匹配。
- 缺少间隙:你在丢弃 null 区块时间。
- leader 归属错误:跨越边界后重新获取 getEpochInfo。
后续步骤与相关阅读
首先针对你的提供商的端点运行 Node.js 代码片段并填写结果表。然后扩展时间线以覆盖完整的 epoch,并验证一致性校验。一旦你的时间线保持一致,就将其集成到你的奖励核算或监控工作流中。
关于端点选择和提供商特定行为,请参阅 RPC 端点指南(RPC Assistant)。关于 Solana 网络详情,请参阅 /en/networks/solana。关于定价和服务选项,请参阅 RPC 定价 和 API 服务。
关于相关的 Solana 主题,请参阅 Solana blockhash 过期与持久 nonce 和 OnFinality Learn 中心。这些内容涵盖了经常与 slot 时间线出现在同一工作流中的相邻机制。
- 运行代码片段并填写结果表。
- 在完整 epoch 上验证一致性校验。
- 与奖励核算或监控集成。
- 在回填历史 schedule 之前查看提供商保留期。