同一秒两笔交易改一个对象:Sui 的版本引用怎么决定谁失败 图 1
同一秒两笔交易改一个对象:Sui 的版本引用怎么决定谁失败 · 图 1

在 Sui 上做过自动转账或批量操作的开发者,多半遇到过这样的报错:交易没进块,原因是输入对象的版本对不上。这不是节点抽风,而是 Sui 存储模型的基本设定——对象有版本,交易必须点名版本,同一版本只能被一笔交易消费掉。本文按官方对象文档把这套规则讲清,再说重试时到底要重新取哪些字段。

对象在链上长什么样

官方文档把对象定义成网络上的基本存储单元:链上每一项资源、资产或数据都是一个对象。别的多数链把状态组织成”账户里挂着键值对”,Sui 则组织成一堆由唯一标识寻址的对象。每个对象带有四样东西:全局唯一标识、所有者(可以是一个地址,也可以是另一个对象)、每次被修改就递增一次的版本号,以及元数据,例如最后一次使用该对象的那笔交易的摘要。

正是版本号和元数据,让”引用一个对象”有三种粗细不同的写法。文档列出的层级是:只写标识,它稳定不变,适合查当前状态或描述藏品在两个地址之间转手;写标识加版本,也就是版本化标识,它锁定对象历史上的某个时点,用来回答”它当时值多少”或”它多久没被更新过”;最后是对象引用,由标识、版本、对象摘要组成的三元组,其中摘要是对象内容与元数据的哈希。

为什么交易只接受对象引用

文档给出的理由是一句很硬的话:交易的对象输入必须以对象引用的形式给出,这样才能确保交易的发送方与处理这笔交易的验证者对对象的内容和元数据达成一致。换句话说,你签下的不只是”我要动这个对象”,而是”我要动这个版本的、内容哈希等于这个值的对象”。如果节点上的对象已经不是那个版本,或者内容与你的摘要对不上,交易就失去了成立的前提。

图:同一根轴线上依次排列的方块,只有顶端一块亮着,其余渐暗

版本推进的规则同样明确。交易修改对象时,新内容写回同一个标识下的新版本;存储里同一个标识会出现多次,但对交易可用的只有最新版本。在同一个版本上,只允许一笔交易修改这个对象,这条保证让每个对象的历史保持线性。而且版本严格递增、标识与版本的组合永不复用,节点因此可以把旧版本裁掉,只留最新的,也可以为服务历史查询或帮别的节点追赶而保留它们。

更值得注意的是新版本怎么算。文档说明 Sui 的版本算法使用 Lamport 时间戳:一笔交易把它触及的所有对象的新版本统一设为”所有输入对象版本中的最大值加一”。文档举的例子是,转移一个版本为五的对象 O、同时用版本为三的对象 G 付燃气,这两者都会变成版本六。这意味着一个纯付燃气的小对象,也可能因为你选了它而把版本推上去,间接影响你后续交易的输入引用。

两条路径下冲突的表现不一样

官方文档把对象分成经快速路径版本化和经共识版本化两类,这个选择同时影响所有权选项与性能权衡。快速路径对象只能是地址所有或不可变对象:发送方在交易里点名要用哪个版本,因此它不需要共识来赋版本,也不与其他交易争版本。但代价是每笔快速路径交易都必须以对象的当前版本作输入,并拿这笔交易产生的输出版本作为下一笔的引用。文档的提醒很实际:如果一个快速路径对象被多个发送方频繁使用,要么在链下协调访问顺序,要么改用共识对象。

引用方式上也有硬性约束:快速路径交易的输入必须按具体标识与具体版本引用,验证者会拒绝那些需要同一个可变对象版本的冲突交易。不可变对象同样按特定版本引用,但因为内容永不改变,它不需要加锁,其版本只是标识它从何时起变为不可变。共识对象则天生允许多方并发引用,排序交给共识,多出来的成本是延迟。

重试时到底该重做什么

把上面的规则倒推成操作口径,被拒之后要做的是重新取一次对象的当前版本与摘要,用新的对象引用重新构造交易再签名,而不是把原交易原样再发一遍——原交易里嵌着那个已经被消费掉的版本与旧摘要。批量脚本尤其要注意两件事:同一批里前后两笔交易引用同一对象时,前一笔的输出版本必须成为后一笔的输入版本,否则必然撞车;以及被选来付燃气的对象版本会参与最大值计算,重试序列里换燃气对象也可能改变其他对象的输出版本。

钱包和索引器读到的”对象版本”因此是有语义的字段:版本跳了一次,说明这个对象刚被别人动过;两个工具报出同一标识的不同版本,可能只是一个在查最新版本、一个在查版本化标识指向的历史时点。把它当成”数据不一致”去投诉之前,先确认查询用的是哪一种引用。

风险与阅读建议

本文只讲 Sui 官方文档记载的对象与版本机制,不涉及任何资产估值或收益预期;快速路径、共识路径与所有权类型仍在演进,具体接口行为以你使用的节点与 SDK 版本对应的官方文档为准。签名前核对输入对象与版本能减少误用,但不能替代对合约逻辑本身的判断。内容不构成投资建议。