每个 Sui 对象都带有一个 id、一个单调递增的 u64 版本和一个摘要。拥有对象版本在每次成功变更时递增,消费拥有对象的交易必须引用其当前版本;否则会被确定性拒绝。Sui 从与交易排序绑定的逻辑 Lamport 时钟推导对象版本,使版本成为乐观并发令牌而非墙上时钟时间戳。共享对象由共识排序,其版本随每次共识提交递增。本指南展示如何使用 sui_getObject 和 sui_multiGetObjects 读取权威版本,在构建交易前比较版本和摘要,通过 sui_getTransactionBlock 和 showObjectChanges 验证变更,并设计刷新缓存版本以避免中止的客户端。
Sui 对象模型:身份、版本和摘要
Sui 的对象模型为每个链上对象分配一个稳定的 id、一个版本和一个摘要。id 是对象的永久身份;版本是一个 u64,每次成功变更时单调递增;摘要是对对象内容和元数据的密码学承诺。Sui 对象模型文档将这些字段定义为对象在交易排序中某一时刻状态的规范表示。
由于摘要承诺内容,具有相同 id 和版本但摘要不同的两个对象不可能存在于一致的账本中。只读取版本的客户端无法证明内容与其预期匹配;在构建消费该对象的交易之前,必须同时比较版本和摘要。当跨 RPC 调用或用户会话之间缓存对象状态时,这一区别很重要。
拥有对象和共享对象遵循不同的版本规则。拥有对象由消费它们的交易设置版本,而共享对象由共识排序,其版本随每次共识提交递增。Sui JSON-RPC API 参考记录了 sui_getObject 返回的字段,包括 version、digest、owner、type 和 previousTransaction,这些字段共同让客户端重建对象的近期历史。
- id:永久对象身份,永不改变。
- version:单调递增的 u64,成功变更时递增。
- digest:对内容的密码学承诺;内容验证所必需。
- owner:地址、对象或共享;决定排序路径。
- previousTransaction:最后变更该对象的交易。
Lamport 排序与乐观并发令牌
Sui 使用从交易排序推导的逻辑 Lamport 时钟为拥有对象设置版本,而非墙上时钟时间。当交易成功变更一个拥有对象时,该对象的版本递增,以反映其在触及它的逻辑交易序列中的位置。这使版本成为乐观并发令牌:客户端读取当前版本,构建引用该版本的交易并提交。如果另一笔交易已经推进了版本,提交的交易会被确定性拒绝,因为对象的版本已经前进。
这种拒绝是一种特性,而非失败。它通过确保只有一笔交易能消费给定版本来防止同一拥有对象上的双重花费。Lamport 模型还意味着版本号在对象之间不可全局比较;每个对象有自己的版本序列。一个对象上的高版本并不意味着另一个对象上的高版本,版本也不是全局账本高度。
共享对象的行为不同。它们由共识排序,其版本随每次共识提交递增,而非按拥有对象交易递增。与共享对象交互的客户端必须考虑共识延迟,不能依赖用于拥有对象的相同乐观并发模式。Sui JSON-RPC 指南涵盖了两种对象类型的 RPC 接口。
- 拥有对象:版本随每次成功的消费交易递增。
- 共享对象:版本随每次共识提交递增。
- 版本是按对象的,不是全局高度。
- 过期版本交易会确定性中止。
使用 sui_getObject 和 sui_multiGetObjects 读取权威版本
读取对象当前版本和摘要的权威方式是 sui_getObject。该方法接受对象 id,并返回包括 version、digest、owner、type 和 previousTransaction 在内的字段。客户端应将此响应视为构建消费该对象交易的真相来源。对于批量读取,sui_multiGetObjects 接受对象 id 数组,并按相同顺序返回响应数组,减少交易涉及多个对象时的往返次数。
批量读取时,保持请求 id 与返回对象之间的映射。如果对象缺失或已被删除,响应可能省略它或返回 null,具体取决于提供商;客户端应显式处理这两种情况。读取 Sui 对象、动态字段和分页指南涵盖了超出单次响应的集合的分页和动态字段遍历。
在构建交易之前,始终同时比较版本和摘要。仅版本告诉你对象已移动;摘要告诉你它移动到了什么。如果摘要与缓存值不同但版本匹配,则缓存不一致,必须刷新。如果版本不同,对象已被变更,你的交易将中止。
- sui_getObject:单对象权威读取。
- sui_multiGetObjects:批量读取,保持顺序。
- 签名前同时比较版本和摘要。
- 显式处理缺失或已删除的对象。
可运行的 Node.js 示例:获取、打印并检测版本变化
以下 Node.js 脚本获取一个对象,打印其版本和摘要,构建一个小型读取计划,并在第二次读取时检测版本变化。它使用 Node.js 18+ 中可用的标准 fetch API 和可配置的 RPC 端点。将端点和对象 id 替换为你自己环境中的值。此示例不主张任何提供商特定的延迟或速率行为;它仅演示读取并比较的模式。
该脚本执行两次读取,中间有短暂延迟。如果两次读取之间版本发生变化,它会记录警告并以非零代码退出,模拟客户端在构建交易前应执行的检测步骤。在生产环境中,你会刷新对象状态并重建交易,而不是退出。
const RPC_URL = process.env.SUI_RPC_URL || 'https://fullnode.mainnet.sui.io:443';
const OBJECT_ID = process.env.SUI_OBJECT_ID || '0x2';
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 readObject(id) {
const result = await rpc('sui_getObject', [id, { showType: true, showOwner: true }]);
const data = result.data;
return {
id,
version: data.version,
digest: data.digest,
owner: data.owner,
type: data.type,
previousTransaction: data.previousTransaction
};
}
async function main() {
const first = await readObject(OBJECT_ID);
console.log('First read:', JSON.stringify(first, null, 2));
const readPlan = [OBJECT_ID];
const batch = await rpc('sui_multiGetObjects', [readPlan, { showType: true }]);
console.log('Batch read count:', batch.length);
await new Promise(r => setTimeout(r, 2000));
const second = await readObject(OBJECT_ID);
console.log('Second read:', JSON.stringify(second, null, 2));
if (first.version !== second.version || first.digest !== second.digest) {
console.warn('Version or digest changed between reads; refresh before building a transaction.');
process.exitCode = 1;
} else {
console.log('Object state stable across reads.');
}
}
main().catch(err => { console.error(err); process.exit(1); });使用 sui_getTransactionBlock 根据效果验证变更
提交交易后,通过读取交易效果来验证变更产生了预期的新版本。带 showObjectChanges 的 sui_getTransactionBlock 返回对象变更数组,每个变更包括对象 id、变更类型(created、mutated、deleted、wrapped、unwrapped)以及被变更对象的新版本和摘要。这让客户端确认版本按预期递增,且摘要与新内容匹配。
解析 Sui objectChanges 和 balanceChanges指南涵盖了效果响应的完整形态,包括余额变化和 gas。验证变更时,将新版本与你在交易中引用的版本进行比较。对于拥有对象,成功的交易应产生严格大于所消费版本的版本。如果版本没有递增,交易可能是空操作,或者对象可能被包装而非变更。
对于缓存对象状态的客户端,效果响应是使缓存失效的权威信号。不要仅依赖交易摘要;读取对象变更并更新每个被变更对象的本地版本和摘要。这可以防止下一笔交易引用过期版本。
- showObjectChanges 返回每个对象的变更类型和新版本。
- 将新版本与交易消费的版本进行比较。
- 为每个被变更对象使缓存失效。
- 使用 queryTransactionBlocks 游标分页进行历史验证。
设计避免过期状态中止的客户端
健壮的 Sui RPC 客户端将对象版本和摘要视为单个并发令牌。在构建交易之前,使用 sui_getObject 读取对象,存储版本和摘要,并在交易中引用该版本。提交后,读取效果并更新每个被变更对象的缓存版本和摘要。如果在构建和提交之间的任何读取显示不同的版本或摘要,则丢弃交易并重建。
对于多对象交易,在单次 sui_multiGetObjects 调用中读取所有消费对象,以缩小读取之间的窗口。如果交易同时消费拥有对象和共享对象,请记住共享对象由共识排序,其版本随每次提交递增;乐观并发模式主要适用于拥有对象。Sui 检查点流和账本服务可以为需要将读取与检查点对齐的客户端提供一致的账本状态视图。
缓存是过期状态中止的主要来源。任何存储对象版本的缓存都必须在任何可能触及该对象的交易之后失效。这包括其他客户端、后台作业或同一用户在不同会话中提交的交易。如有疑问,在构建交易之前重新读取对象,而不是信任缓存版本。
- 将版本 + 摘要视为一个并发令牌。
- 使用 sui_multiGetObjects 批量读取以缩小竞争窗口。
- 在任何可能触及对象的交易之后使缓存失效。
- 构建前重新读取,而不是信任缓存版本。
结果表:针对你的端点测量版本行为
由于提供商行为各异,请针对你自己的 RPC 端点测量版本行为,而不是假设数字。下表是一个模板,供你填入自己的观察结果。针对你的端点运行上面的 Node.js 示例或等效的 curl,并在多个时间点记录已知对象的版本和摘要。然后提交一笔变更该对象的交易,并从效果响应中记录新版本。
使用该表确认拥有对象的版本单调递增,内容变化时摘要变化,以及引用过期版本的交易被拒绝。不要将这些数字发布为通用数据;它们特定于你的端点、网络和对象。RPC 定价页面描述了计划层面的考虑,但不主张延迟或吞吐量数字。
- 列:时间戳、对象 id、版本、摘要、previousTransaction、备注。
- 行:初始读取、第二次读取、变更后读取、过期版本尝试。
- 记录每行使用的确切 RPC 方法和参数。
- 注明端点是 mainnet、testnet 还是 devnet。
基于版本的并发的局限性和权衡
版本是按对象的,不是全局高度。你无法跨对象比较版本以确定哪个在全局意义上更新。一个对象上的高版本和另一个对象上的低版本并不能说明它们的相对新旧。这限制了版本作为其所属对象之外的通用排序信号的有用性。
摘要比较是必需的,因为仅版本不能证明内容。两次读取具有相同版本但不同摘要表明存在不一致,必须在构建交易之前解决。跳过摘要比较的客户端可能会基于过期或损坏的状态行事。此外,缓存对象版本的客户端必须在任何可能触及它的交易之后刷新,否则它们将构建会中止的交易。这是乐观并发的核心权衡:它避免了锁和协调,但需要仔细的缓存失效。
共享对象引入了进一步的复杂性。它们的版本随每次共识提交递增,乐观并发模式不适用相同的方式。与共享对象交互的客户端必须考虑共识排序,不能仅依赖版本比较来检测过期。
- 版本是按对象的,不是全局高度。
- 摘要比较对于内容验证是强制性的。
- 缓存版本必须在任何触及交易之后刷新。
- 共享对象遵循共识排序,而非拥有对象版本控制。
排查过期版本和摘要不匹配错误
当交易因版本相关错误中止时,第一步是使用 sui_getObject 重新读取对象,并将返回的版本和摘要与你引用的值进行比较。如果版本不同,另一笔交易先消费了该对象;使用新版本重建交易。如果版本匹配但摘要不同,你的缓存内容不一致;丢弃缓存并重新读取。
如果对象缺失或返回 null,它可能已被删除或包装。检查该对象 id 的交易效果以确定发生了什么。如果对象被包装,它可能稍后以新版本重新出现;如果被删除,则无法消费。API 服务文档描述了如何为这些情况构建 RPC 调用。
对于批量读取,验证响应数组长度与请求数组长度匹配,并且每个返回的对象 id 与请求的 id 匹配。一些提供商可能重新排序或省略条目;不要未经检查就假设位置对应。如果你看到同一对象反复中止,请考虑是否有后台进程或另一个客户端正在并发变更它。
- 使用 sui_getObject 重新读取并比较版本和摘要。
- 检查效果以了解已删除或已包装的对象。
- 验证批量响应长度和 id 对应关系。
- 如果中止重复出现,调查并发变更者。
下一步:将版本检查集成到生产客户端
要将版本检查集成到生产环境,请将 RPC 调用包装在一个辅助函数中,该函数读取版本和摘要、构建交易并验证效果。使用 Sui JSON-RPC 指南作为方法签名和响应形态的参考。关于网络和端点,请参阅 Sui 网络页面。OnFinality Learn 中心收集了有关 Sui RPC 模式的相关指南。
考虑添加一个重试循环,在版本不匹配时重新读取对象并重建交易。限制重试次数以避免在高竞争下无限循环。对于需要跨多个对象保持一致视图的客户端,使用 Sui 检查点流和账本服务将读取与检查点对齐。
最后,记录你的缓存失效策略。每个存储对象版本的地方都应有明确的刷新触发条件。这就是偶尔中止的客户端与在并发下可靠成功的客户端之间的区别。
- 将读取和效果验证包装在辅助函数中。
- 在版本不匹配时添加有界重试。
- 当一致性重要时,将多对象读取与检查点对齐。
- 记录缓存失效触发条件。