{"content":{"title":"Sui Move 合约升级与权限定制","body":"# 一：猜数设计\r\n\r\n让我们来设计一个简单的猜数程序，用户猜测一个数字并传入函数，判断与程序随机而成的数字是否相同，如果相同就给予一定奖励。\r\n\r\n这里的奖励可以是链上流通的货币或是其它有价值的虚拟物品，不过作为一篇适合 $\\mathit {Sui\\ Move}$ 初学者的简单用例，直接牵扯到高昂物品似乎有所不妥，所以这里的奖励就用一个整型的 $\\mathit {prize}$ 来表示，将含有奖金的 $\\mathit {object}$ 发送给猜中数字的玩家，以此来作为激励手段。\r\n\r\n同时，我们还可以用 $\\mathit {Events}$ 来记录链上发生的重大事件。在这个例子里，重大事件无疑就是 $\\mathit {Winner}$ 了，我们可以用事件来记录胜者玩家的地址，为了让数据有一定的分析价值，还可以记录到这一次猜中数之前的所有玩家的尝试总数。\r\n\r\n那么，根据分析，我们可以用三个 $\\mathit {struct}$ 来存储这些信息。\r\n\r\n```move\r\nstruct Count has key {\r\n    id: UID,\r\n    total: u64,\r\n}\r\n\r\nstruct Prize has key {\r\n    id: UID,\r\n    prize: u8,\r\n}\r\n\r\nstruct GuessEvent has copy, drop {\r\n    total_count: u64,\r\n    final_winner: address,\r\n}\r\n```\r\n\r\n- $\\mathit {Count}$ 会作为一个共享对象，让所有玩家都可以访问并修改，其中的 $\\mathit {total}$ 将存储一共的尝试猜数的次数，直到猜中时清零重置。\r\n- $\\mathit {Prize}$ 直到被猜中数时才会被创建，同时被发送给此次交易的发起者，也就是猜中数字的玩家，其中的 $\\mathit {prize}$ 可以简单设定为猜中的数等值的金额，由于猜数的范围不能太大，不妨就以 $[\\text 0,\\ \\text {10}]$ 这个闭区间为前置条件。\r\n- $\\mathit {GuessEvent}$ 是定义的一个事件，我们只关心事件当中的值，同时让它在作用域结束时自我消亡，所以设定了 $\\mathit {copy},\\ \\mathit {drop}$ 这两个能力。<br>触发事件时只需要使用`sui::event::emit(<ObjectEvent>);`<br>而交易产生的事件详情可以在 [$\\mathit {Sui\\ explorer}$](https://suiexplorer.com/) 中的 $\\mathit {Events}$ 标签页查看。\r\n\r\n根据上述分析，我们可以很轻松地编写三个函数。\r\n\r\n```move\r\nfun init(ctx: &mut TxContext) {\r\n    let count = Count {\r\n        id: object::new(ctx),\r\n        total: 0,\r\n    };\r\n    transfer::share_object(count);\r\n}\r\n\r\nfun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {\r\n    transfer::transfer(Prize {\r\n        id: object::new(ctx),\r\n        prize,\r\n    }, tx_context::sender(ctx));\r\n\r\n    event::emit(GuessEvent {\r\n        total_count: count,\r\n        final_winner: tx_context::sender(ctx),\r\n    });\r\n}\r\n\r\npublic entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {\r\n    let des_number = ((clock::timestamp_ms(clock) % 11) as u8);\r\n    if (number == des_number) {\r\n        send_prize(count.total, number, ctx);\r\n        count.total = 0;\r\n    } else {\r\n        count.total = count.total + 1;\r\n    }\r\n}\r\n```\r\n\r\n- $\\mathit {init}$ 函数只会在发布时被调用一次，用来创建 $\\mathit {Count}$ 并将其共享再好不过。\r\n- $\\mathit {send\\_prize}$ 函数是在数字被猜中时调用的，作用是将奖励 $\\mathit {Prize}$ 发送给中奖者，同时触发一个事件。\r\n- $\\mathit {entry}$ 函数是指在交易过程中能够被直接调用的，它不应该有返回值，因为即使有也没什么作用，入口函数被不被 $\\mathit {public}$ 修饰的区别在于能否被其它模块调用，以及合约升级时能否被更改等。<br>$\\mathit {guess\\_between\\_zero\\_and\\_hundred}$ 函数名就暗示了猜数的范围（一开始是想 [$\\text 0,\\ \\text {100}$] 的，但一想到测试时会测死，就缩小到了 $\\text 10$，但函数名忘改成 $\\mathit {ten}$ 了┭┮﹏┭┮），其中有个参数是 $\\mathit {Clock}$，我们可以用它来获得时间戳，用这个时间戳来转化成目标数字。<br/>这种“随机”方式其实并不安全，因为这个值是可控的，真实的交易环境也不会如此草率，不过这并不如此篇的重点，也就不再赘述。<br/>$\\mathit {timestamp\\_ms}$ 得到的值是 $\\mathit u\\text{64}$ 类型的，我们可以用 $\\mathit {as}$ 关键词简单地将其转化成 $\\mathit u\\text 8$。（这里只是为了顺带提一句 $\\mathit {as}$​ 的作用才设定成这个类型的整数的，前篇里还提到了它可以为导入的模块取别名，如果忘记了的可以去翻一翻）\r\n\r\n好，至此，简单的猜数就设计完毕，但是别忘了这一篇章的主题**合约升级与权限定制**！\r\n\r\n# 二：修改代码以支持版本更迭\r\n\r\n发布的合约 $\\mathit {package}$ 是不可变的 $\\mathit {object}$​，不可撤回也无法修改。智能合约升级的本质是在新的地址上重新发布新的合约，并且把旧版合约的数据迁移过去。\r\n\r\n当然，合约升级并不意味着可以对代码进行肆无忌惮地修改，需要满足如下几条规则：\r\n\r\n- 现有的 $\\mathit {public}$ 函数的输入输出参数格式保持不变\r\n- 可以添加新的 $\\mathit {struct}$ 和函数\r\n- 可以给现有的 $\\mathit {struct}$ 添加新的能力\r\n- 可以把现有函数中对泛型参数的约束去掉\r\n- 可以改变函数的实现\r\n- 可以修改非 $\\mathit {public}$ 函数的输入输出参数格式，包括 $\\mathit {friend}$ 和 $\\mathit {entry}$ 函数\r\n- 可以让非 $\\mathit {public}$ 函数变为 $\\mathit {public}$ 函数\r\n\r\n除此之外还需要注意的是，$\\mathit {init}$ 函数只会在第一次发布合约时执行，后续合约升级时不会被重复执行。同时，如果你的 $\\mathit {package}$ 依赖了一个外部的 $\\mathit {package}$​，你需要手动把它指向新依赖的合约地址，这一过程在升级合约时不会自动进行。\r\n\r\n根据上述内容，我们来思考，想要让一份合约支持后续迭代升级，需要添加一些什么？\r\n\r\n1. $\\mathit {VERSION}$，定义一个常量来表示版本号。因为升级合约并不会将旧的合约地址销毁，所以我们需要明确版本，非对应版本的调用都不应执行。\r\n2. 共享对象 $\\mathit {Count}$ 中添加版本号字段，用来判断数据是否已经迁移。（还没来得及迁移的数据，可以接受旧的对应版本的合约的函数调用，但它不能接受新的）\r\n3. 为了实现数据迁移，自然也就需要添加一个函数来实现这一点，在这个例子当中的主要作用就是更新 $\\mathit {Count}$ 对象当中的版本号字段。\r\n4. 相关的升级迭代的操作应该都只授权给发布者，其他人不该有权限调用。为了达到这一目的，不妨在共享对象当中再添加一个字段，用来存储发布者的 $\\mathit {ID}$。我们已经知道，$\\mathit {init}$ 只会在第一次合约发布时被调用，当时的 $\\mathit {TxContext}$ 一定是发布者，那么该阶段存储进去的 $\\mathit {ID}$ 也将是发布者的。<br>那么如何在调用数据迁移时传入这个 $\\mathit {ID}$？不如再定义一个 $\\mathit {struct}$，拥有 $\\mathit {key}$ 能力，将其交给我们的发布者，届时只需要传入该 $\\mathit {object}$ 的地址即可作为凭据。\r\n5. 至于怎么在版本不对应，或者非授权操作情况下中断并退出，可以使用`assert!(<bool>, <error code>)`，这在前面的篇章当中已有涉及。为了适应更多变的情况，同时提高代码的可读性，可以将 $\\mathit {error\\ code}$ 定义成一个常量再作为参数传入。\r\n\r\n修改并整合后的代码如下：\r\n\r\n```move\r\nmodule guess_number::guess_number {\r\n    use sui::object::{Self, ID, UID};\r\n    use sui::tx_context::{Self, TxContext};\r\n    use sui::transfer;\r\n    use sui::clock::{Self, Clock};\r\n    use sui::event;\r\n\r\n    const VERSION: u64 = 1;\r\n\r\n    const ENOTVERSION: u64 = 0;\r\n    const ENOTADMIN: u64 = 1;\r\n    const ENOTUPGRADE: u64 = 2;\r\n\r\n    struct Count has key {\r\n        id: UID,\r\n        version: u64,\r\n        admin: ID,\r\n        total: u64,\r\n    }\r\n\r\n    struct AdminCap has key {\r\n        id: UID,\r\n    }\r\n\r\n    struct Prize has key {\r\n        id: UID,\r\n        prize: u8,\r\n    }\r\n\r\n    struct GuessEvent has copy, drop {\r\n        total_count: u64,\r\n        final_winner: address,\r\n    }\r\n\r\n    fun init(ctx: &mut TxContext) {\r\n        let admin_cap = AdminCap {id: object::new(ctx)};\r\n\r\n        let count = Count {\r\n            id: object::new(ctx),\r\n            version: VERSION,\r\n            admin: object::id(&admin_cap),\r\n            total: 0,\r\n        };\r\n\r\n        transfer::transfer(admin_cap, tx_context::sender(ctx));\r\n        transfer::share_object(count);\r\n    }\r\n\r\n    fun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {\r\n        transfer::transfer(Prize {\r\n            id: object::new(ctx),\r\n            prize,\r\n        }, tx_context::sender(ctx));\r\n\r\n        event::emit(GuessEvent {\r\n            total_count: count,\r\n            final_winner: tx_context::sender(ctx),\r\n        });\r\n    }\r\n\r\n    public entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {\r\n        assert!(count.version == VERSION, ENOTVERSION);\r\n\r\n        let des_number = ((clock::timestamp_ms(clock) % 11) as u8);\r\n        if (number == des_number) {\r\n            send_prize(count.total, number, ctx);\r\n            count.total = 0;\r\n        } else {\r\n            count.total = count.total + 1;\r\n        }\r\n    }\r\n\r\n    entry fun migrate(count: &mut Count, admin_cap: &AdminCap) {\r\n        assert!(count.admin == object::id(admin_cap), ENOTADMIN);\r\n        assert!(count.version != VERSION, ENOTUPGRADE);\r\n\r\n        count.version = VERSION;\r\n    }\r\n}\r\n```\r\n\r\n别着急，配置文件 $\\mathit {Move.toml}$ 也需要做更改，主要是在 [$\\mathit {package}$] 下添加版本号。（这里的 $\\mathit {version}$ 和代码中的可以不一致）\r\n\r\n```\r\n[package]\r\nname = \"guess_number\"\r\nversion = \"0.0.0\"\r\n\r\n[addresses]\r\nguess_number = \"0x0\"\r\n```\r\n\r\n# 三：原始合约发布\r\n\r\n```\r\nsui move build\r\nsui client publish --gas-budget 100000000\r\n```\r\n\r\n发布成功后在信息里找到跟 $\\mathit {package}$​ 相关的：<br>![Object Changes.png](https://img.learnblockchain.cn/attachments/2024/03/JtShr4Ha66018b05819f7.png)\r\n\r\n所发布的包的 $\\mathit {PackageID}$，以及 $\\mathit {AdminCap},\\ \\mathit {Count},\\ \\mathit {UpgradeCap}$ 的 $\\mathit {PackageID}$ 都是后续会用到的，可以用 $\\mathit {export}$ 来赋予一个别名，方便取用。\r\n\r\n```\r\nexport PACKAGE_ID=0x628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5\r\nexport ADMIN_CAP=0x2160bdabbe0321d418cd6572ab24092554c3fbea0b5f24fd32295eaf2b8fa63a\r\nexport COUNT=0x415f0255873919cc68c217a715454b54537ad3c0677b7d3b925c777131dbf19c\r\nexport UPGRADE_CAP=0x57ae075216a7996c944af979a7fe8057f6d1b2165055cdddce9f7898579fc8cd\r\n```\r\n\r\n接下来，我们就可以用下述命令来猜数了：<br>`sui client call --package $PACKAGE_ID --module guess_number --function guess_between_zero_and_hundred --args $COUNT 6 0x6 --gas-budget 100000000`<br>其中`0x6`作为 $\\mathit {Clock}$ 参数的地址传递。\r\n\r\n通过`sui client object $COUNT`得到的信息可以发现，字段 $\\mathit {total}$ 的值为 $\\text 1$，说明一共尝试了一次没有成功，此时，重复猜数，直到猜中。\r\n\r\n尝试了好几次（永远的$6$），终于在猜数后得到的信息里，$\\mathit {Object\\ Changes}$ 看到了它新建了一个 $\\mathit {Prize}$ 对象，它的拥有者是玩家本人。分别查看 $\\mathit {Count}$ 和 $\\mathit {Prize}$ 当中存储的内容，如下图所示，发现符合预期。<br>![Count and Prize.png](https://img.learnblockchain.cn/attachments/2024/03/opklobt866018b12b801a.png)\r\n\r\n别忘了事件，除了本文最开始提到的查看方式，其实在执行猜中数的那一次给出的信息当中就有相应的信息：<br>![Event.png](https://img.learnblockchain.cn/attachments/2024/03/4fljPSCD66018b1b7b7a5.png)\r\n\r\n# 四：合约升级\r\n\r\n在上述代价的基础上，首先来思考，除了功能上的升级之外，需要修改什么？\r\n\r\n1. $\\mathit {VERSION}$ 作为版本的标志，势必要扩大。\r\n2. $\\mathit {Move.toml}$ 中 [$\\mathit {package}$] 中的 $\\mathit {version}$​​ 也需要更改，同时添加新的一行`published-at = \"<ORIGINAL-PACKAGE-ID>\"`，用来标注旧合约地址是哪儿。如果不知道这个信息，又何来数据迁移和升级。\r\n\r\n```\r\n// guess_number.move\r\nconst VERSION: u64 = 2;\r\n\r\n// Move.toml\r\n[package]\r\nname = \"guess_number\"\r\nversion = \"0.0.1\"\r\npublished-at = \"0x628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5\" //这里需要替换成你们自己的旧合约地址\r\n```\r\n\r\n接下来，我们来思考哪些功能存在优化的空间？\r\n\r\n真实情况下，奖励的东西应该更加吸引人，这样才能诱使人与之进行交易（猜数），所以我们扩大 $\\mathit {prize}$，让它等于猜中的那个数乘上一共所猜的次数的十倍，为了不超过 $\\mathit u \\text 8$ 的数据范围，还需要跟 $\\text {255}$ 取个最小值。\r\n\r\n本次示例就以这一点为代表进行，只要是符合之前所列举的修改规则的都是可以的，你可以自由发挥(๑•̀ㅂ•́)و✧\r\n\r\n经过优化后的代码如下：\r\n\r\n```move\r\nmodule guess_number::guess_number {\r\n    use sui::object::{Self, ID, UID};\r\n    use sui::tx_context::{Self, TxContext};\r\n    use sui::transfer;\r\n    use sui::clock::{Self, Clock};\r\n    use sui::event;\r\n    use sui::math;\r\n\r\n    const VERSION: u64 = 2;\r\n\r\n    const ENOTVERSION: u64 = 0;\r\n    const ENOTADMIN: u64 = 1;\r\n    const ENOTUPGRADE: u64 = 2;\r\n\r\n    struct Count has key {\r\n        id: UID,\r\n        version: u64,\r\n        admin: ID,\r\n        total: u64,\r\n    }\r\n\r\n    struct AdminCap has key {\r\n        id: UID,\r\n    }\r\n\r\n    struct Prize has key {\r\n        id: UID,\r\n        prize: u8,\r\n    }\r\n\r\n    struct GuessEvent has copy, drop {\r\n        total_count: u64,\r\n        final_winner: address,\r\n    }\r\n\r\n    fun init(ctx: &mut TxContext) {\r\n        let admin_cap = AdminCap {id: object::new(ctx)};\r\n\r\n        let count = Count {\r\n            id: object::new(ctx),\r\n            version: VERSION,\r\n            admin: object::id(&admin_cap),\r\n            total: 0,\r\n        };\r\n\r\n        transfer::transfer(admin_cap, tx_context::sender(ctx));\r\n        transfer::share_object(count);\r\n    }\r\n\r\n    fun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {\r\n        transfer::transfer(Prize {\r\n            id: object::new(ctx),\r\n            prize,\r\n        }, tx_context::sender(ctx));\r\n\r\n        event::emit(GuessEvent {\r\n            total_count: count,\r\n            final_winner: tx_context::sender(ctx),\r\n        });\r\n    }\r\n\r\n    public entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {\r\n        assert!(count.version == VERSION, ENOTVERSION);\r\n\r\n        let des_number = ((clock::timestamp_ms(clock) % 11) as u8);\r\n        if (number == des_number) {\r\n            let prize = (math::min((number as u64) * (count.total + 1) * 10, 255) as u8);\r\n            send_prize(count.total, prize, ctx);\r\n            count.total = 0;\r\n        } else {\r\n            count.total = count.total + 1;\r\n        }\r\n    }\r\n\r\n    entry fun migrate(count: &mut Count, admin_cap: &AdminCap) {\r\n        assert!(count.admin == object::id(admin_cap), ENOTADMIN);\r\n        assert!(count.version != VERSION, ENOTUPGRADE);\r\n\r\n        count.version = VERSION;\r\n    }\r\n}\r\n```\r\n\r\n用`sui client upgrade --gas-budget 100000000 --upgrade-capability $UPGRADE_CAP`尝试升级合约。\r\n\r\n如果出现了如下错误，请考虑修改是否完全遵循了那七条规则。\r\n\r\n```\r\nError executing transaction: Failure {\r\n    error: \"PackageUpgradeError { upgrade_error: IncompatibleUpgrade } in command 1\",\r\n}\r\n```\r\n\r\n如果升级成功，我们也将得到一长串信息，同样的，将新的 $\\mathit {PackageID}$ 记录一下：<br>`export NEW_PACKAGE_ID=0x5a3974941fc000890f312c538c47f98c787a89776d6da1762129a4b0379e855b`\r\n\r\n在调用函数 $\\mathit {migrate}$ 之前，旧合约可以正常使用，但是新合约不行，因为 $\\mathit {Count}$ 中的 $\\mathit {version}$ 还是 $\\text 1$，如果此时调用新合约，会出现如下报错：\r\n\r\n```\r\nError executing transaction: Failure {\r\n    error: \"MoveAbort(MoveLocation { module: ModuleId { address: 628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5, name: Identifier(\\\"guess_number\\\") }, function: 2, instruction: 14, function_name: Some(\\\"guess_between_zero_and_hundred\\\") }, 0) in command 0\",\r\n}\r\n```\r\n\r\n如果想要调用 $\\mathit {migrate}$ 的话，就需要之前存储的 $\\mathit {ADMIN\\_CAP}$ 的地址，命令如下：<br>`sui client call --package $NEW_PACKAGE_ID --module guess_number --function migrate --args $COUNT $ADMIN_CAP --gas-budget 100000000`<br>如果重复调用则会报错，因为 $\\mathit {assert}$ 不通过，从错误代码 $\\text 2$ 也可以看出，是`assert!(count.version != VERSION, ENOTUPGRADE);`出了问题，版本已经不需要更新了。\r\n\r\n现在，调用旧合约的话就会出错，新合约就没有问题。不断猜数，观察次数和猜中的数之间的关系，可以发现 $\\mathit {prize}$ 的奖励情况变得更诱人了。\r\n\r\n# 五：权限定制\r\n\r\n如果只是按照之前所叙述的七条修改规则，只要用户想（不考虑代码优美）几乎可以重写所有功能，有的时候这并不是开发者想要的，所以，$\\mathit {Sui\\ Move}$ 也提供了不同的合约升级权限。\r\n\r\n- $\\mathit {Compatible}:$ 最宽松的权限，也是最初发布合约时的默认权限，只要满足那七条规定就可以。\r\n- $\\mathit {Additive}:$ 可以添加新的函数，比如新的 $\\mathit {public}$ 函数和 $\\mathit {struct}$，但不能对现有的函数代码做任何修改。\r\n- $\\mathit {Dependency}$-$\\mathit {only}:$ 只能修改该 $\\mathit {package}$ 的依赖项。\r\n- $\\mathit {Immutable}:$ 无法再升级该 $\\mathit {package}$。\r\n\r\n我们尝试执行以下命令，调用`0x2`地址（也就是 $\\mathit {sui}$ ）下的 $\\mathit {package}$ 模块中的 $\\mathit {make\\_immutable}$ 函数，将合约升级时必须的 $\\mathit {UPGRADE\\_CAP}$ 传入：<br>`sui client call --package 0x2 --module package --function make_immutable --args $UPGRADE_CAP --gas-budget 100000000`\r\n\r\n此时，再尝试迭代升级，会报错：\r\n\r\n`Could not find upgrade capability at <address>`\r\n\r\n这是因为这个函数将 $\\mathit {UPGRADE\\_CAP}$ 销毁了，升级合约的必需品无了，自然也就无法再更新了。\r\n\r\n类似的，如果你想要切换成其它权限，也可以用类似的命令，具体的函数名以及参数请移步至此 [$\\mathit {package.move}$](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/sui-framework/sources/package.move)\r\n\r\n注意：上述四个权限定制是依次收紧且单向的，也就是说，你可以从 $\\mathit {Compatible}$ 收紧成 $\\mathit {Immutable}$，但就再也无法松开了，哪怕是 $\\mathit {Dependency}$-$\\mathit {only}$ 也不行。\r\n\r\n# 六：加入组织，共同进步！\r\n\r\n- [Sui 中文开发群(TG)](https://t.me/move_cn)\r\n- $\\mathit{Move}$ 语言学习交流群: 79489587"},"author":{"user":"https://learnblockchain.cn/people/19165","address":null},"history":null,"timestamp":1711377408,"version":1}