17.轻松入门Move: 升级(下)

上一章我们讲到UpgradeCap的定义,讲到它有一个policy字段表示这个包使用的升级策略。那Move内置的升级策略有哪些呢?

内置的升级策略

Move内置了几种升级策略供我们选择,下面我们分别介绍各种策略以及设置方法。不过在这之前,设置升级策略的规则需要了解:

  • 默认的升级策略是最宽松的策略
  • 如果想设置升级策略只能设置比当前策略更严苛的策略,设置更宽松的策略将会导致报错。
升级策略

我们从严苛到宽松列举内置的升级策略:

  • 不可升级

    这是最严格的升级策略,发布之后不允许任何升级。可以通过调用sui::package::make_immutable方法设置:

    sui client call --package 0x2 --module package --function make_immutable --args <UpgradeCap ID>
    
  • 只允许修改依赖项

    只能修改包的依赖项,其他一律不允修改

    sui client call --package 0x2 --module package --function only_dep_upgrades --args <UpgradeCap ID>
    
  • 只允许添加

    只能在包内添加新的函数和结构体,不允许修改原来的代码(即便是私有函数的内部实现也不行)

    sui client call --package 0x2 --module package --function only_additive_upgrades --args <UpgradeCap ID>
    
  • 兼容

​ 最宽松的升级策略,也是默认的升级策略。允许修改函数的实现代码,但是不允许修改public函数签名(非public函数的签名允许修改);允许祛除函数的泛型约束(只允许放松约束); 只允许新增结构体不允许修改。

​ 兼容策略是默认的升级策略,并且不允许从更严苛的策略设置为此策略,所以兼容策略没有设置方法。

自定义升级策略

除了内置的升级策略外,我们也可以自定义升级策略,但是自定义之前我们需要了解包的升级过程,才能理解如何编写策略。

升级包的流程

我们上一章讲解包升级方法,了解到包的升级是在一个事务中完成的。不过在这个事务中,实际上是分别调用了三个命令,下面我们通过介绍这三个命令来了解升级的底层原理。

授权(authorize_upgrade)

这个命令的作用是使用UpgradeCap验证权限、验证通过则返回UpgradeTicket。这个命令在sui包package模块中的签名是:

#![allow(unused)]
fn main() {
public fun authorize_upgrade(
        cap: &mut UpgradeCap,
        policy: u8,
        digest: vector<u8>
): UpgradeTicket 
}

第一个传参是UpgradeCap对象,在发布包的时候返回它的ID, 包的发布者即是它的拥有者。它的拥有者可以使用它升级包或者修改升级策略。它的定义再上一章已经讲过,这章不再赘述。

第二个参数是正在使用的升级策略

第三个参数则是摘要,摘要如何计算的可以参考

我们可以通过在build包的时候通过--dump-bytecode-as-base64选项来指示程序打印base64编码的字节码,在打印的内容中可以获取digest值

sui move build --dump-bytecode-as-base64

打印内容:

{"modules":["oRzrCwYAAAALAQAKAgoUAx4pBEcEBUslB3CKAQj6AWAG2gIkCv4CEgyQA1AN4AMCAA8BDQILAhACEQAACAAAAQgAAQIHAAIEBAAEAwIAAAoAAQAACQABAAAFAgEAAA4CAQABEgQFAAIIAAMAAxAJAQEIBAwGBwAGCAYKAQcIBAACBwgABwgEAQgDAQoCAQgCAQYIBAEFAQgBAgkABQEIAAZQZXJzb24FUGhvbmUGU3RyaW5nCVR4Q29udGV4dANVSUQNY2hhbmdlX3BlcnNvbgJpZARuYW1lA25ldwpuZXdfcGVyc29uCW5ld19waG9uZQZvYmplY3QGc2VuZGVyBnN0cmluZwR0ZXN0BXRlc3Q2CHRyYW5zZmVyCnR4X2NvbnRleHQEdXRmOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgoCBwYxMjM0NTYKAgkIdmVyc2lvbjEKAgsKdmVyc2lvbjE5MAACAgYIAwcIAgECAgYIAwcIAgABBAABCgoAEQUHABEEEgELAC4RBzgAAgEBBAABCgoAEQUHABEEEgALAC4RBzgBAgIBBAABBgcBEQQLAA8AFQIDAAAAAQYHAhEECwAPABUCAAEA"],"dependencies":["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002"],"digest":[79,58,190,101,62,35,128,195,167,4,23,212,223,242,100,90,123,173,107,231,106,142,168,236,51,207,220,158,121,59,154,142]}

其中digest字段就是第三个参数的值。

这个函数的返回是UpgradeTicket,旨在作为授权成功的凭证。它的定义为:

#![allow(unused)]
fn main() {
public struct UpgradeTicket {
    /// 生成ticket的UpgradeCap对象ID
    cap: ID,
    /// 可以升级的包的ID,一定是最新版本包的ID,以此保证只有最新包能升级
    package: ID,
    /// 在本次升级中使用的升级策略
    policy: u8,
    /// 在本次升级中使用的摘要,用于标识升级后的包的内容
    digest: vector<u8>,
}
}

每个UpgradeCap一次只能生成一个UpgradeTicket,以此保证不会并发升级或者修改升级策略等操作。

UpgradeTicket是一个“烫手山芋”,所以在拿到这个UpgradeTicket之后,必须要调用后续的升级命令来将UpgradeTicket传递出去,否则事务将失败。

执行升级

执行升级这一步是使用的内建命令,作用是消费UpgradeTicket、验证包的更新是否符合升级策略、生成升级包的对象并返回一个代表升级成功的UpgradeReceipt对象。

在这个命令中,拿到UpgradeTicket后验证器便会使用这个类型的所有字段值来验证,只要有一个不符合要求都会升级失败。比如,要升级的包的字节码必须要跟digest字段匹配,policy字段至少要跟UpgradeCap的升级策略一样严格等。

值得注意的是,UpgradeTicket是一次性“票据”,会在这一步被析构,不能将其保存多次使用。

这一步骤返回的UpgradeReceipt是升级成功的证明,它也是一个“烫手山芋”用来保证一定会调用提交升级的方法。它的定义如下:

#![allow(unused)]
fn main() {
public struct UpgradeReceipt {
    ///UpgraceCap的ID
    cap: ID,
    ///升级后的包的ID
    package: ID,
}
}
提交升级(commit_upgrade)

这个命令的作用是消费UpgradeReceipt,并更新UpgradeCap的version字段和package字段。它的定义:

#![allow(unused)]
fn main() {
public fun commit_upgrade(
    cap: &mut UpgradeCap,
    receipt: UpgradeReceipt,
) {
    //析构UpgradeReceipt
    let UpgradeReceipt { cap: cap_id, package } = receipt;

    assert!(object::id(cap) == cap_id, EWrongUpgradeCap);
    assert!(cap.package.to_address() == @0x0, ENotAuthorized);
	//修改UpgradeCap对象的值
    cap.package = package;
    cap.version = cap.version + 1;
}
}
自定义升级策略

我们可以通过重写授权和提交升级这两个命令来自定义升级策略。但是要自定义升级策略之前我们最好先了解官方建议的最佳实践:

最佳实践
  • 自定义升级策略单独一个包,不要与使用这个策略的代码放同一个包
  • 自定义的升级策略的包升级策略设置为不可升级
  • 锁定UpgradeCap的policy字段,不允许违反只能收紧升级策略的原则。
自定义升级策略

下面我们使用官网的例子来演示如何升级。我们自定义一个升级策略:在兼容的升级策略基础上,只允许在一周内的指定一天进行升级操作。

我们新创建一个包,自定义一个UpgradeCap对象,在这个对象中保存验证规则所需字段:

#![allow(unused)]
fn main() {
public struct UpgradeCap has key, store {
    id: UID,
    //包含package的UpgradeCap对象,用于调用基础的授权和提交升级等函数
    cap: package::UpgradeCap,
    //指定可发布的时间,值在1-7
    day: u8,
}
}

定义新建升级策略的方法,这个方法返回UpgraceCap对象:

#![allow(unused)]
fn main() {
public fun new_policy(
    cap: package::UpgradeCap,
    day: u8,
    ctx: &mut TxContext,
): UpgradeCap {
    assert!(day < 7, ENotWeekDay);
    UpgradeCap { id: object::new(ctx), cap, day }
}
}

重写授权函数,在这个函数中验证是否满足升级策略:

#![allow(unused)]
fn main() {
public fun authorize_upgrade(
    cap: &mut UpgradeCap,
    policy: u8,
    digest: vector<u8>,
    ctx: &TxContext,
): package::UpgradeTicket {
    //如果不是可升级的日期,就报错
    assert!(week_day(ctx) == cap.day, ENotAllowedDay);
    //调用基础的授权方法,也就是说在遵循自定义升级策略的基础上还要遵循内建的升级策略!
    package::authorize_upgrade(&mut cap.cap, policy, digest)
}
}

重写提交升级函数:

#![allow(unused)]
fn main() {
public fun commit_upgrade(
    cap: &mut UpgradeCap,
    receipt: package::UpgradeReceipt,
) {
    package::commit_upgrade(&mut cap.cap, receipt)
}
}
发布升级策略

编写完成后,将包发布到链上

sui client publish

遵循最佳实践,将这个包设置为不可升级

sui client call --gas-budget 10000000 \
    --package 0x2 \
    --module 'package' \
    --function 'make_immutable' \
    --args '<POLICY-UPGRADE-CAP>'
使用升级策略

我们在发布完一个包之后,拿着这个发布包返回的UpgradeCap对象的ID,调用new_policy函数,并保存返回的自定义UpgradeCap对象的ID。

在后续的升级中,按照上面讲得升级的流程,依次调用自定义的authorize_upgrade函数、内置的升级函数和commit_upgrade函数即可。

参考资料:

https://docs.sui.io/concepts/sui-move-concepts/packages/custom-policies

了解更多Move内容:

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