16.轻松入门Move: 升级(上)

在编写合约部署上链后,如果发现有Bug怎么办?在web2中我们可以修改代码,重新部署即可。但是在Move中包是一个不可变对象,也就是说一旦发布就无法修改和删除,以此保证不会因为修改线上包对使用者造成不可预见的问题。不过虽然无法修改链上合约,但Move提供了一个升级包的方法来重新生成一个包。下面我先演示一下升级的方法。

升级的方法

1.发布包

在我们写好合约之后,在项目根目录执行发布包的命令:

sui client publish

注意:在Sui v1.24.1版本之后,--gas-budget选项不再是必填项。

包发布成功后,我们查看本次交易的对象变更:

╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Object Changes                                                                                   │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Created Objects:                                                                                 │
│  ┌──                                                                                             │
│  │ ObjectID: 0xba952a24d7855908cf825f789e8318219c410068aab1448b349edc0ad97019df                  │
│  │ Sender: 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb                    │
│  │ Owner: Account Address ( 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb ) │
│  │ ObjectType: 0x2::package::UpgradeCap                                                          │
│  │ Version: 6                                                                                    │
│  │ Digest: 4mHhhDaL4sfSN6QQ86FhxbZhNUPtR6vaSoNJeQFVbSYo                                          │
│  └──                                                                                             │
│ Mutated Objects:                                                                                 │
│  ┌──                                                                                             │
│  │ ObjectID: 0x28cdaee082d3a58b5b0f31dd396655920f0f7c2109f46a61c8eb79d7c46ce5dd                  │
│  │ Sender: 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb                    │
│  │ Owner: Account Address ( 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb ) │
│  │ ObjectType: 0x2::coin::Coin<0x2::sui::SUI>                                                    │
│  │ Version: 6                                                                                    │
│  │ Digest: B3xgxJ9YStCzkqW2wyXNYeSBu2sjyMaHZa8sH743MzqB                                          │
│  └──                                                                                             │
│ Published Objects:                                                                               │
│  ┌──                                                                                             │
│  │ PackageID: 0x272713c478b3f04670f65056b36f03c0602925227e743344e80bd161e037da69                 │
│  │ Version: 1                                                                                    │
│  │ Digest: E63ByxG6cj8BQ8DRUgkfJuXWWs1c7wtBD6hWsP74BLZ8                                          │
│  │ Modules: test6                                                                                │
│  └──                                                                                             │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

可以看到创建了一个UpgradeCap对象,这个对象的ID需要保存好,在升级的时候需要使用它生成升级需要的“票”。这个对象也保存了一些基本信息,在源代码中的定义如下:

#![allow(unused)]
fn main() {
///可用于控制是否可以升级
public struct UpgradeCap has key, store {
    id: UID,
    /// 可以升级的包ID,值是最新版本的包ID
    package: ID,
   	///成功升级的次数,原始值是0
    version: u64,
    ///使用的升级的策略,有哪些可选策略将在下一章节详解
    policy: u8,
}
}
2.编辑Move.toml

在发布之后我们需要编辑Move.toml以保证其他包能正确的引用这个包。这里只展示片段:

[package]
name = "test6"
version = "0.0.0"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
published-at = "0x272713c478b3f04670f65056b36f03c0602925227e743344e80bd161e037da69"
[addresses]
test6 = "0x272713c478b3f04670f65056b36f03c0602925227e743344e80bd161e037da69"

设置published-at和test6包的地址为发布后包的地址。

3.升级操作

在升级之前,需要再次修改Move.toml,将包的地址设置为0x0,以便验证器给升级后的包分配一个新的包地址。也不要忘记修改version字段:

[package]
name = "test6"
version = "0.0.1"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
published-at = "0x272713c478b3f04670f65056b36f03c0602925227e743344e80bd161e037da69"
[addresses]
test6 = "0x0"

现在我们来执行升级的命令

sui client upgrade --upgrade-capability <UPGRADE-CAP-ID>

这个命令会生成升级的摘要,使用UpgradeCap授权升级以获取UpgradeTicket,生成新的包,并在升级成功后使用UpgradeReceipt确保成功升级后会更新UpgradeCap。

命令执行完后,返回如下:

╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Object Changes                                                                                   │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Mutated Objects:                                                                                 │
│  ┌──                                                                                             │
│  │ ObjectID: 0x28cdaee082d3a58b5b0f31dd396655920f0f7c2109f46a61c8eb79d7c46ce5dd                  │
│  │ Sender: 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb                    │
│  │ Owner: Account Address ( 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb ) │
│  │ ObjectType: 0x2::coin::Coin<0x2::sui::SUI>                                                    │
│  │ Version: 7                                                                                    │
│  │ Digest: A7cV91fjF4MLEYLXwbmQnfrza5WbmxKh5B2rVFvUpGh3                                          │
│  └──                                                                                             │
│  ┌──                                                                                             │
│  │ ObjectID: 0xba952a24d7855908cf825f789e8318219c410068aab1448b349edc0ad97019df                  │
│  │ Sender: 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb                    │
│  │ Owner: Account Address ( 0xc571b07c805118eb0177af2e4e69913af6e9de1bf3fb3fc4df52a8b9d31343cb ) │
│  │ ObjectType: 0x2::package::UpgradeCap                                                          │
│  │ Version: 7                                                                                    │
│  │ Digest: 8eKeChSfT5nv1SLfiE893c83fU1H5C2L4J3wYXSUTaVD                                          │
│  └──                                                                                             │
│ Published Objects:                                                                               │
│  ┌──                                                                                             │
│  │ PackageID: 0x68f4a731627e2fb1b212e685ce041e8d2cb834d3d4429c7a74eeb622e3aa9536                 │
│  │ Version: 2                                                                                    │
│  │ Digest: 9utSnMUMTHdDd4aCeKKpmv7Go4TRgynTtEngUqhvfUfG                                          │
│  │ Modules: test6                                                                                │
│  └──                                                                                             │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

我们可以看到,除了创建了新的包对象以外,UpgradeCap对象也被修改了,version字段加一,package变为了最新发布的包地址。

4.设置Move.toml

注意:每次升级完,都要把published-at设置为最新版本包地址。而包的地址则一直是原始地址。

[package]
name = "test6"
version = "0.0.1"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
#设置为最新版本包的地址
published-at = "0x68f4a731627e2fb1b212e685ce041e8d2cb834d3d4429c7a74eeb622e3aa9536"
[addresses]
#设置为最初版本包的地址
test6 = "0x272713c478b3f04670f65056b36f03c0602925227e743344e80bd161e037da69"

升级可能会不成功!

要想顺利的升级包,默认情况下必须要满足以下要求:

  • 必须要有发布包需要的票,也就是前面提到的UpgradeTicket。在升级的示例中我们使用的是UpgradeCap来自动生成的UpgradeTicket

  • public函数的签名,必须与上个版本保持一致

    • 非public函数,包括friend和entry函数的签名可以在升级中修改

    • 不可以改public函数签名,但是可以修改public函数(及其他函数)的实现

    • 可以删除函数中泛型的约束

    • 可以添加新的函数

  • 结构体的定义,包括ability都必须与上个版本保持一致

    • 不可以在结构体中新增字段
    • 可以添加新的结构体

那在满足以上条件之后,是不是就可以保证升级后没有兼容性问题了呢?然而事情并没有那么简单。

public函数允许修改代码的实现,那我在一个public函数中新增一个事件的发布。可以顺利升级。升级后前一个版本的包和当前版本的包都在链上且都可以调用。如果调用方依然调用旧版本,监听事件的程序就会错过这个事件,导致程序运行不正确。那这种情况怎么处理呢?我们可以使用一些方法强制调用方调用最新版本的包。

强制调用方使用最新版本的包

方法一:在新包中使用新类型

在新包中定义新的类型,并新增一个方法用于将旧类型的数据转换为新类型。新包只支持新类型的访问,而旧类型数据已经被转换,以此强制接入方使用最新版本的包。

值得注意的是,数据迁移的方法需要做好权限管理,确保是包的拥有者才能调用此方法。可以通过在init的时候生成一个AdminCap对象,并在迁移数据的方法中使用这个对象验证实现。

方法二:使用版本标记共享对象

在共享对象中记录包的最新版本,并在访问中验证版本,如果不是最新版本的包访问共享对象就直接报错。

参考资料:

https://docs.sui.io/concepts/sui-move-concepts/packages/upgrade

了解更多Move内容:

  • telegram: t.me/move_cn
  • cQQ群: 79489587