Sui 检查点是对一组交易效果的仅追加、单调递增序列的承诺,由序列号和摘要标识,并携带其所属纪元。JSON-RPC 接口提供按检查点的摘要调用(sui_getCheckpoint),需要精确的序列号或摘要;以及分页调用(sui_getCheckpoints / suix_getCheckpoints),接受游标和降序标志并返回 nextCursor。gRPC 账本服务将同一日志暴露为服务器流式检查点订阅,推送每个已认证的检查点及其内容,消除轮询延迟,但将消费者耦合到单个连接。生产索引器通常流式传输以保证实时性,并通过游标读取进行对账以保证正确性,持久化最后处理的序列号,使重启时不会跳过或重复。
Sui 检查点实际承诺了什么
Sui 检查点是数据连续性的单位:它承诺一组交易效果,并由单调递增的序列号和摘要标识。每个检查点还携带其纪元,每个纪元以标记为该纪元结束的检查点终结。这使得检查点链成为纪元、对象(id + 版本)、交易效果和事件所依附的主干。Sui 开发者文档更详细地描述了检查点、纪元和 API 参考。
摘要字段和更重的内容字段在响应结构中分离。摘要包括序列号、摘要、纪元、纪元结束标志、网络总交易数以及交易摘要列表。内容包括这些交易的执行和效果数据,这就是为什么每个检查点的内容与摘要分开检索。
由于序列号是单调的且摘要是承诺,你可以通过检查每个检查点的序列号恰好比前一个大一来证明连续性,并确保摘要链一致。这是无间隙索引的基础。有关 Sui 数据如何提供的更广泛视图,请参阅 Sui 网络页面 和 OnFinality Learn 中心。
- 序列号:检查点的单调递增标识符。
- 摘要:对检查点内容的加密承诺。
- 纪元:此检查点所属的纪元。
- 纪元结束标志:标记一个纪元的最终检查点。
- 网络总交易数:此检查点处的累计计数。
- 交易摘要列表:此检查点中包含的交易。
JSON-RPC 接口:精确读取与分页读取
JSON-RPC 接口有一个按检查点的摘要调用 sui_getCheckpoint,它接受精确的序列号或摘要,并返回摘要字段以及单独的内容部分。当你确切知道需要哪个检查点时,例如对账特定序列号或验证摘要时,这是正确的调用。
它还有一个范围/分页调用 sui_getCheckpoints(或 suix_getCheckpoints,取决于客户端代际),它接受游标和降序标志并返回 nextCursor。按游标升序分页是安全的,因为页面边界就是序列边界。使用 descending=true 向后遍历是从顶端回填的方式。
区别很重要:sui_getCheckpoint 需要精确的序列号,而 sui_getCheckpoints 使用游标在序列号上分页。混淆它们会导致未分页的“给我从创世以来的所有数据”请求,这是一种常见的失败模式。有关相关的分页模式,请参阅 读取 Sui 对象、动态字段和分页 和 使用 suix_queryEvents 查询 Sui 事件。
- sui_getCheckpoint:精确序列号或摘要,返回摘要 + 内容。
- sui_getCheckpoints / suix_getCheckpoints:游标 + 降序标志,返回 nextCursor。
- 升序游标分页是安全的;页面边界就是序列边界。
- descending=true 是从顶端回填的路径。
gRPC 账本服务和检查点订阅
gRPC 接口将同一日志暴露为账本服务,具有服务器流式检查点订阅,在每次新检查点被认证时推送它(及其内容)。这消除了轮询延迟,因为你不需要反复询问;服务器在检查点可用时推送每个检查点。
权衡是流将消费者耦合到单个连接。如果连接断开,你必须重新连接并对账。生产索引器通常流式传输以保证实时性,并通过游标读取进行对账以保证正确性,使用摘要作为幂等键,这样重新连接不会重放重复项。
权威的主要来源是 Sui 开发者文档、Sui API 参考以及 GitHub 上的 sui-apis ledger_service.proto,它定义了检查点订阅。任何客户端或提供商特定的内容,例如连接限制或保留策略,均记录 / 因提供商而异。有关端点选择,请参阅 Sui RPC 提供商和端点(RPC Assistant)。
- 服务器流式订阅推送每个已认证的检查点及其内容。
- 消除轮询延迟,但将消费者耦合到一个连接。
- 流式传输保证实时性,游标读取对账保证正确性。
- 重新连接时使用摘要作为幂等键。
可运行的分页读取器与持久化游标
以下 Node.js 示例使用游标向前分页检查点,将最后处理的序列号持久化到磁盘,并断言下一页的第一个序列号等于上一页的最后一个序列号加一。它还检查所有已见序列号的并集在顶端以下没有空洞。
在你自己的端点上运行它。代码使用通用 JSON-RPC 调用;根据你的客户端代际,将方法名替换为 sui_getCheckpoints 或 suix_getCheckpoints。持久化文件是一个简单的 JSON 文件,因此你可以在页面中途中断进程,并从最后持久化的序列号重新启动,而不会跳过或重复。
const fs = require('fs');
const fetch = require('node-fetch');
const RPC_URL = process.env.SUI_RPC_URL || 'https://your-endpoint.example';
const STATE_FILE = './checkpoint-cursor.json';
function loadState() {
if (fs.existsSync(STATE_FILE)) {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
}
return { lastSeq: null, seen: [] };
}
function saveState(state) {
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
}
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 state = loadState();
let cursor = state.lastSeq ? String(state.lastSeq) : null;
let descending = false;
while (true) {
const page = await rpc('sui_getCheckpoints', [cursor, descending, 50]);
const data = page.data || [];
if (data.length === 0) break;
for (const cp of data) {
const seq = Number(cp.sequenceNumber);
if (state.lastSeq !== null && seq !== state.lastSeq + 1) {
throw new Error(`Gap detected: expected ${state.lastSeq + 1}, got ${seq}`);
}
state.lastSeq = seq;
state.seen.push(seq);
}
saveState(state);
cursor = page.nextCursor;
if (!cursor) break;
}
const sorted = [...state.seen].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i] !== sorted[i - 1] + 1) {
throw new Error(`Hole in seen sequence numbers: ${sorted[i - 1]} -> ${sorted[i]}`);
}
}
console.log('No holes below tip. Last sequence:', state.lastSeq);
}
main().catch((err) => { console.error(err); process.exit(1); });恢复测试:在页面中途中断并重启
为了验证连续性,在页面中途中断进程并重启。读取器必须从最后持久化的序列号重新启动,而不会跳过或重复。断言下一页的第一个序列号等于上一页的最后一个序列号加一,并且所有已见序列号的并集在顶端以下没有空洞。
此测试捕获跨纪元边界重用游标的常见失败,其中一个纪元的游标在下一个纪元可能无效。它还捕获将检查点的纪元与其序列号混淆,以及将纪元结束检查点视为普通检查点。纪元结束检查点仍然是一个带有序列号的检查点,但它携带关闭纪元的标志。
如果你从顶端回填,设置 descending=true 并向后遍历,然后在持久化之前反转顺序。相同的连续性断言反向适用:每个前一个序列号必须恰好比当前小一。有关超时周围的重试模式,请参阅 Sui RPC 超时和可靠重试模式。
- 每页之后持久化 lastSeq,而不是每个检查点之后,以限制重放。
- 重启时,从 lastSeq + 1 恢复并断言第一个序列号匹配。
- 检查已见序列号的并集在顶端以下是否有空洞。
- 不要跨纪元边界重用游标。
结果表:测量你自己的端点
使用下表记录你自己端点的观察行为。不要依赖供应商发布的数字;针对你的端点进行测量并填写结果。这使比较保持诚实和可重复。
运行上面的分页读取器,然后记录页面大小、观察到的 nextCursor 行为,以及端点在分页调用中返回内容还是需要单独的 sui_getCheckpoint 调用。注意任何速率限制或保留差异,这些均记录 / 因提供商而异。
- 使用的页面大小:____
- 页面的第一个序列号:____
- 页面的最后一个序列号:____
- 返回的 nextCursor:____
- 分页调用中包含内容:是 / 否
- 检测到间隙:是 / 否
- 重新连接重放重复项:是 / 否
常见失败及如何避免
未分页的“给我从创世以来的所有数据”请求是最常见的失败。它们使端点过载并经常超时。始终使用游标和有界页面大小进行分页。
跨纪元边界重用游标是另一个失败。游标与序列空间绑定;当纪元变化时,从已知序列号重新锚定。将检查点的纪元与其序列号混淆会导致分析中的纪元偏移错误。
将纪元结束检查点视为普通检查点可能会破坏纪元边界逻辑。在没有幂等键(摘要)的情况下消费检查点流意味着重新连接会重放检查点,导致重复。针对修剪端点查询精确的旧序列号会失败,因为数据不再保留;使用归档端点进行历史读取,如 Sui 归档节点和历史 RPC 所述。
- 避免未分页的从创世到顶端的请求。
- 在纪元边界重新锚定游标。
- 不要混淆纪元与序列号。
- 显式处理纪元结束检查点。
- 在流重新连接时使用摘要作为幂等键。
- 对旧的精确序列号使用归档端点。
故障排除清单
当你的索引器报告间隙或重复时,请按此清单操作。从持久化状态文件开始,确认最后的序列号。然后确认下一页的第一个序列号等于 lastSeq + 1。
如果看到重复项,检查流是否在没有幂等键的情况下重新连接。如果看到间隙,检查是否由于超时或游标错误跳过了页面。如果精确的旧序列号失败,检查端点是否修剪并切换到归档端点。
- 确认持久化的 lastSeq 存在且可读。
- 断言下一页第一个序列号 == lastSeq + 1。
- 重新连接后检查重复摘要。
- 超时后检查跳过的页面。
- 验证端点对旧序列号的保留。
- 确认逻辑中的纪元边界处理。
限制、假设和权衡
本指南假设你有一个支持检查点读取以及可选账本服务订阅的 Sui JSON-RPC 或 gRPC 端点。提供商特定的限制、保留窗口和订阅可用性均记录 / 因提供商而异。OnFinality 在此不声明具体的延迟、吞吐量或速率数字。
流式传输提供更低的延迟,但将你耦合到一个连接;轮询更简单,但增加延迟和负载。推荐模式是流式传输保证实时性,并通过游标读取进行对账以保证正确性。这假设你的消费者可以持久化状态,并能容忍至少一次交付并通过基于摘要的去重。
连续性证明依赖于单调序列号和摘要承诺。如果你的端点返回跳过序列号的游标,请将其视为提供商特定的行为,并对缺失范围使用精确的 sui_getCheckpoint 读取进行对账。
- 提供商限制和保留:记录 / 因提供商而异。
- 流式传输:低延迟,单连接耦合。
- 轮询:简单,更高的延迟和负载。
- 推荐:流式传输保证实时性,游标读取对账。
- 连续性证明依赖于单调序列和摘要。
下一步:构建无间隙的 Sui 索引器
首先选择一个支持你所需检查点方法的端点。Sui RPC 提供商和端点(RPC Assistant) 页面帮助你比较选项,Sui 网络页面 列出了网络详情。
然后实现带持久化游标的分页读取器,添加恢复测试,并接入 gRPC 账本订阅以保证实时性。使用摘要作为幂等键,并在重新连接时通过游标读取进行对账。对于历史回填,使用归档端点,如 Sui 归档节点和历史 RPC 所述。
相关阅读,请参阅 使用 suix_queryEvents 查询 Sui 事件、读取 Sui 对象、动态字段和分页 和 Sui RPC 超时和可靠重试模式。有关服务选项,请参阅 RPC 定价 和 API 服务。
- 选择支持检查点和账本服务的端点。
- 实现持久化游标分页和恢复测试。
- 添加 gRPC 流式传输以保证实时性,并使用摘要去重。
- 重新连接时通过游标读取进行对账。
- 使用归档端点进行历史回填。