Skip to main content

自定义升级策略

使用单个密钥保护链上包的升级能力可能会因以下几个原因而带来安全风险:

  • 拥有密钥的实体可能会进行更改,这些更改可能符合他们自己的利益,但不一定符合广大社区的利益。
  • 包的升级可能在没有给用户足够时间讨论,或在不同意时停用该包的情况下发生。
  • 密钥有可能丢失。

为了解决单键升级所有权带来的安全风险,同时仍然提供升级现有包的机会,Sui 提供了 自定义升级策略。这些策略保护 UpgradeCap 访问,并发放 UpgradeTicket 对象,根据具体情况授权升级。

兼容性

Sui 自带一组内置的包兼容性策略,按照从最严格到最宽松的顺序列在此处:

PolicyDescription
ImmutableNo one can upgrade the package.
Dependency-onlyYou can modify the dependencies of the package only.
AdditiveYou can add new functionality to the package (e.g., new public functions or structs) but you can't change any of the existing functionality (e.g., the code in existing public functions cannot change).
CompatibleThe most relaxed policy. In addition to what the more restrictive policies allow, in an upgraded version of the package:
  • You can change all function implementations.
  • You can remove the ability constraints on generic type parameters in function signatures.
  • You can change, remove, or make public any private, public(friend), and entry function signatures.
  • You cannot change public function signatures (except in the case of ability constraints mentioned previously).
  • You cannot change existing types.

每个列出的策略,按照列出的顺序,都是在升级包中允许的更改类型上一种的超集。

当你发布一个包时,默认情况下它采用最宽松的兼容性策略。你可以将包发布为一个事务块的一部分,该事务块在成功完成之前可以更改策略,使包在链上首次可用时具有所需的策略级别,而不是默认级别。

你可以通过在包的 UpgradeCap 上调用 sui::package 中的一个函数 (only_additive_upgradesonly_dep_upgradesmake_immutable) 来更改当前策略,并且策略只能变得更加严格。例如,在调用 sui::package::only_dep_upgrades 将策略限制为可添加之后,对同一包的 UpgradeCap 调用 sui::package::only_additive_upgrades 将导致错误。

升级概述

包升级必须在单个事务块中端到端进行,并由三个命令组成:

  1. 授权:UpgradeCap 获取执行升级的权限,创建一个 UpgradeTicket
  2. 执行: 消耗 UpgradeTicket,验证包字节码和与之前版本的兼容性,并创建表示已升级包的链上对象。在成功时返回 UpgradeReceipt 作为结果。
  3. 提交: 更新 UpgradeCap,提供有关新创建的包的信息。

尽管步骤 2 是一个内置命令,但步骤 1 和步骤 3 是作为 Move 函数实现的。Sui 框架提供了它们的最基本实现:

module sui::package {
public fun authorize_upgrade(
cap: &mut UpgradeCap,
policy: u8,
digest: vector<u8>
): UpgradeTicket;

public fun commit_upgrade(
cap: &mut UpgradeCap,
receipt: UpgradeReceipt,
);
}

这些是 sui client upgrade 为授权和提交调用的函数。自定义升级策略通过在额外条件(如投票、治理、权限列表、时间锁等)之后保护对包 UpgradeCap(因此也是对这些函数调用)的访问来实现。

任何一对从 UpgradeCap 生成 UpgradeTicket 并从 UpgradeReceipt 消耗以更新 UpgradeCap 的函数构成一个自定义升级策略。

UpgradeCap

UpgradeCap 是负责协调包升级的中心类型。

module sui::package {
struct UpgradeCap has key, store {
id: UID,
package: ID,
version: u64,
policy: u8,
}
}

发布一个包会创建 UpgradeCap 对象,而升级该包则会更新该对象。拥有此对象的所有者具有以下权限:

  • 更改未来升级的兼容性要求。
  • 授权未来的升级。
  • 使包变为不可变(不可升级)。

其 API 保证以下属性:

  • 只能升级包的最新版本(保证线性历史)。
  • 一次只能有一个升级在进行中(不能授权多个并发升级)。
  • 升级只能在单个事务的范围内被授权;无法 store 证明授权的 UpgradeTicket
  • 包的兼容性要求只能随时间变得更加严格。

UpgradeTicket

module sui::package {
struct UpgradeTicket {
cap: ID,
package: ID,
policy: u8,
digest: vector<u8>,
}
}

UpgradeTicket 是升级已被授权的证明。此授权是特定于以下内容:

  • 要升级的特定 package: ID,它必须是由位于 cap: IDUpgradeCap 标识的家族中的最新包。
  • 特定的 policy: u8,表示升级期望遵循的兼容性保证类型。
  • 特定的 digest: vector<u8>,用于标识升级后包的内容。

当你尝试运行升级时,验证器会检查即将执行的升级是否符合所有这些方面的授权升级,并且如果不满足其中任何一个条件,则不执行升级。

创建了 UpgradeTicket 后,你必须在该事务块内使用它(不能将其存储以供以后使用,也不能丢弃或销毁它),否则事务将失败。

包摘要

UpgradeTicketdigest 字段来自于 authorize_upgradedigest 参数,调用方必须提供。虽然 authorize_upgrade 不处理 digest,但自定义策略可以使用它来仅授权提前见过其字节码或源代码的升级。Sui 计算摘要的步骤如下:

  • 获取每个模块的字节码,表示为字节数组。
  • 添加包的所有传递依赖项的列表,每个表示为字节数组。
  • 对字节数组的列表进行词典排序。
  • 将排序后的列表中的每个元素按顺序输入到 Blake2B 哈希器中。
  • 从该哈希状态计算摘要。

有关更多信息,请参阅 摘要计算的实现,但在大多数情况下,你可以依赖 Move 工具链在构建时输出摘要,当传递 --dump-bytecode-as-base64 标志时:

sui move build --dump-bytecode-as-base64
FETCHING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test
{"modules":[<MODULE-BYTES-BASE64>],"dependencies":[<DEPENDENCY-IDS>],"digest":[59,43,173,195,216,88,176,182,18,8,24,200,200,192,196,197,248,35,118,184,207,205,33,59,228,109,184,230,50,31,235,201]}

UpgradeReceipt

module sui::package {
struct UpgradeReceipt {
cap: ID,
package: ID,
}
}

UpgradeReceiptUpgrade 命令成功运行的证明,Sui 将新包添加到事务的已创建对象集中。它用于使用其家族中最新包的 ID (package: ID) 更新其 UpgradeCap(由 cap: ID 标识)。

在 Sui 创建了 UpgradeReceipt 后,你必须在同一事务块内使用它来更新其 UpgradeCap(不能将其存储以供以后使用,也不能丢弃或销毁它),否则事务将失败。

隔离策略

在编写自定义升级策略时,建议:

  • 将它们分离到它们自己的包中(不要与它们管理升级性的代码共同定位)。
  • 使该包不可变(不能升级)。
  • 锁定 UpgradeCap 的策略,以防以后无法降低策略的限制性。

这些最佳实践有助于维护知情用户同意有界风险,因为它清楚地表明用户在将价值锁定到包中时包的升级策略是什么,并确保策略不会随时间演变为更宽松的形式,而用户没有意识到并选择接受新条款。

示例:“星期几” 升级策略

通过编写一个玩具升级策略来将所有内容付诸实践,该策略仅在一周的特定某一天(由包创建者选择)授权升级。

创建升级策略

首先,为升级策略创建一个新的 Move 包:

sui move new policy

该命令创建一个名为 policy 的目录,其中包含一个名为 sources 的文件夹和一个 Move.toml 文件清单。

sources 文件夹中,创建一个名为 day_of_week.move 的源文件。将以下代码复制并粘贴到该文件中:

module policy::day_of_week {
use sui::object::{Self, UID};
use sui::package;
use sui::tx_context::TxContext;

struct UpgradeCap has key, store {
id: UID,
cap: package::UpgradeCap,
day: u8,
}

/// Day is not a week day (number in range 0 <= day < 7).
const ENotWeekDay: u64 = 1;

public fun new_policy(
cap: package::UpgradeCap,
day: u8,
ctx: &mut TxContext,
): UpgradeCap {
assert!(day < 7, ENotWeekDay);
UpgradeCap { id: object::new(ctx), cap, day }
}
}

该代码包含一个构造函数,并定义了自定义升级策略的对象类型。

接下来,你需要添加一个函数来授权升级,如果在一周的正确日期。首先,定义两个常量,一个用于标识在策略不允许的日期尝试升级的错误代码,另一个用于定义一天中的毫秒数(稍后将用到)。将这些定义直接添加到当前的 ENotWeekDay 下方。

// Request to authorize upgrade on the wrong day of the week.
const ENotAllowedDay: u64 = 2;

const MS_IN_DAY: u64 = 24 * 60 * 60 * 1000;

接下来,再 new_policy 函数后,添加一个 week_day 函数来获取当前星期几。正如前面所承诺的,该函数使用你之前定义的 MS_IN_DAY 常量。

fun week_day(ctx: &TxContext): u8 {
let days_since_unix_epoch =
tx_context::epoch_timestamp_ms(ctx) / MS_IN_DAY;
// The unix epoch (1st Jan 1970) was a Thursday so shift days
// since the epoch by 3 so that 0 = Monday.
((days_since_unix_epoch + 3) % 7 as u8)
}

这个函数使用 TxContext 中的纪元时间戳而不是 Clock,因为它只需要每日的粒度,这意味着升级交易不需要共识。

接下来,添加一个 authorize_upgrade 函数,该函数调用之前的函数获取当前星期几,然后检查该值是否违反策略,如果是,则返回 ENotAllowedDay 错误值。

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)
}

只要 authorize_upgrade 返回一个 UpgradeTicket,自定义 authorize_upgrade 的签名可以与 sui::package::authorize_upgrade 的签名不同。

最后,提供 commit_upgrademake_immutable 的实现,它们分别委托给 sui::package 中的相应函数:

public fun commit_upgrade(
cap: &mut UpgradeCap,
receipt: package::UpgradeReceipt,
) {
package::commit_upgrade(&mut cap.cap, receipt)
}

public fun make_immutable(cap: UpgradeCap) {
let UpgradeCap { id, cap, day: _ } = cap;
object::delete(id);
package::make_immutable(cap);
}

你的 day_of_week.move 文件中的最终代码应如下所示:

module policy::day_of_week {
use sui::object::{Self, UID};
use sui::package;
use sui::tx_context::TxContext;

struct UpgradeCap has key, store {
id: UID,
cap: package::UpgradeCap,
day: u8,
}

// Day is not a week day (number in range 0 <= day < 7).
const ENotWeekDay: u64 = 1;
const ENotAllowedDay: u64 = 2;
const MS_IN_DAY: u64 = 24 * 60 * 60 * 1000;

public fun new_policy(
cap: package::UpgradeCap,
day: u8,
ctx: &mut TxContext,
): UpgradeCap {
assert!(day < 7, ENotWeekDay);
UpgradeCap { id: object::new(ctx), cap, day }
}

fun week_day(ctx: &TxContext): u8 {
let days_since_unix_epoch =
sui::tx_context::epoch_timestamp_ms(ctx) / MS_IN_DAY;
// The unix epoch (1st Jan 1970) was a Thursday so shift days
// since the epoch by 3 so that 0 = Monday.
((days_since_unix_epoch + 3) % 7 as u8)
}

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)
}

public fun commit_upgrade(
cap: &mut UpgradeCap,
receipt: package::UpgradeReceipt,
) {
package::commit_upgrade(&mut cap.cap, receipt)
}

public fun make_immutable(cap: UpgradeCap) {
let UpgradeCap { id, cap, day: _ } = cap;
object::delete(id);
package::make_immutable(cap);
}
}

Publishing an upgrade policy

使用 sui client publish 命令发布该策略。

sui client publish --gas-budget 100000000
Show output

成功发布后返回如下:

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING policy
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
CAFFD2HHnULQMCycL9xgad5JJpjFu2nuftf2xyugQu4t
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([0, 251, 96, 164, 70, 48, 195, 251, 181, 82, 206, 254, 167, 84, 165, 40, 29, 254, 102, 165, 152, 81, 244, 203, 199, 97, 33, 107, 29, 95, 120, 212, 34, 19, 233, 109, 179, 72, 246, 219, 23, 254, 108, 222, 210, 250, 166, 172, 208, 133, 108, 252, 36, 165, 71, 97, 210, 206, 144, 138, 237, 169, 15, 218, 13, 92, 225, 85, 204, 230, 61, 45, 147, 106, 193, 13, 195, 116, 230, 99, 61, 161, 251, 251, 68, 154, 46, 172, 143, 122, 101, 212, 120, 80, 164, 214, 54])))]
Transaction Kind : Programmable
Inputs: [Pure(SuiPureValue { value_type: Some(Address), value: "<SENDER>" })]
Commands: [
Publish(_,0x0000000000000000000000000000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000000000000000000000000002),
TransferObjects([Result(0)],Input(0)),
]

Sender: <SENDER-ADDRESS>
Gas Payment: Object ID: <GAS>, version: 0x5, digest: E3tu6NE34ZDzVRtQUmXdnSTyQL2ZTm5NnhQSn1sgeUZ6
Gas Owner: <SENDER-ADDRESS>
Gas Price: 1000
Gas Budget: 100000000

----- Transaction Effects ----
Status : Success
Created Objects:
- ID: <POLICY-UPGRADE-CAP> , Owner: Account Address ( <SENDER-ADDRESS> )
- ID: <POLICY-PACKAGE> , Owner: Immutable
Mutated Objects:
- ID: <GAS> , Owner: Account Address ( <SENDER-ADDRESS> )

----- Events ----
Array []
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<SENDER-ADDRESS>"),
"owner": Object {
"AddressOwner": String("<SENDER-ADDRESS>"),
},
"objectType": String("0x2::coin::Coin<0x2::sui::SUI>"),
"objectId": String("<GAS>"),
"version": String("6"),
"previousVersion": String("5"),
"digest": String("2x4rn2NNa9K5TKcSku17MMEc2JZTr4RZhkJqWAmmiU1u"),
},
Object {
"type": String("created"),
"sender": String("<SENDER-ADDRESS>"),
"owner": Object {
"AddressOwner": String("<SENDER-ADDRESS>"),
},
"objectType": String("0x2::package::UpgradeCap"),
"objectId": String("<POLICY-UPGRADE-CAP>"),
"version": String("6"),
"digest": String("DG1CABxqdHNhjBDzt7K4VKiJdLfnrW9qnCx8yr4jVP4"),
},
Object {
"type": String("published"),
"packageId": String("<POLICY-PACKAGE>"),
"version": String("1"),
"digest": String("XehdKX2WCyMFFds53bd5xDT1okBwczE3ajW9E1h5zgh"),
"modules": Array [
String("day_of_week"),
],
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER-ADDRESS>"),
},
"coinType": String("0x2::sui::SUI"),
"amount": String("-10773600"),
},
]

按照最佳实践,使用 Sui 客户端 CLI 在 UpgradeCap 上调用 sui::package::make_immutable 以使策略成为不可变的。

sui client call --gas-budget 10000000 \
--package 0x2 \
--module 'package' \
--function 'make_immutable' \
--args '<POLICY-UPGRADE-CAP>'
Show output

成功的调用将返回以下结果:

----- Transaction Digest ----
FqTdsEgFnyVqc3sFeu5EnBUziEDYbxhLUAaLv4FDjN6d
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([0, 123, 97, 9, 252, 127, 238, 10, 88, 175, 157, 155, 98, 11, 23, 234, 52, 167, 230, 45, 218, 171, 31, 174, 87, 107, 174, 117, 236, 65, 117, 18, 42, 74, 56, 149, 82, 107, 216, 199, 223, 142, 135, 165, 200, 80, 151, 32, 110, 75, 133, 128, 150, 66, 13, 40, 173, 228, 211, 94, 222, 201, 248, 221, 10, 92, 225, 85, 204, 230, 61, 45, 147, 106, 193, 13, 195, 116, 230, 99, 61, 161, 251, 251, 68, 154, 46, 172, 143, 122, 101, 212, 120, 80, 164, 214, 54])))]
Transaction Kind : Programmable
Inputs: [Object(ImmOrOwnedObject { object_id: <POLICY-UPGRADE-CAP>, version: SequenceNumber(6), digest: o#DG1CABxqdHNhjBDzt7K4VKiJdLfnrW9qnCx8yr4jVP4 })]
Commands: [
MoveCall(0x0000000000000000000000000000000000000000000000000000000000000002::package::make_immutable(Input(0))),
]

Sender: <SENDER-ADDRESS>
Gas Payment: Object ID: <GAS>, version: 0x6, digest: 2x4rn2NNa9K5TKcSku17MMEc2JZTr4RZhkJqWAmmiU1u
Gas Owner: <SENDER-ADDRESS>
Gas Price: 1000
Gas Budget: 10000000

----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: <GAS> , Owner: Account Address ( <SENDER-ADDRESS> )
Deleted Objects:
- ID: <POLICY-UPGRADE-CAP>

----- Events ----
Array []
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<SENDER-ADDRESS>"),
"owner": Object {
"AddressOwner": String("<SENDER-ADDRESS>"),
},
"objectType": String("0x2::coin::Coin<0x2::sui::SUI>"),
"objectId": String("<GAS>"),
"version": String("7"),
"previousVersion": String("6"),
"digest": String("2Awa8KHrP4wo33iLNKCeLVQ8HrKj1hrd2LigkLiacJVg"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER-ADDRESS>"),
},
"coinType": String("0x2::sui::SUI"),
"amount": String("607780"),
},
]

创建用于测试的包

现在,已经在链上准备好一个策略,你需要一个要升级的包。本主题创建一个基本的包并在后续场景中引用它,但你可以使用任何可用的包,而不是创建新的包。

如果你没有可用的包,请使用 sui move new 命令创建名为 example 的新包的模板。

sui move new example

example/sources 目录中,创建一个名为 example.move 的文件,并添加以下代码:

module example::example {
struct Event has copy, drop { x: u64 }
entry fun nudge() {
sui::event::emit(Event { x: 41 })
}
}

接下来的说明将发布此示例包,然后升级它以更改其发出的 Event 中的值。由于使用了自定义升级策略,需要使用 TypeScript SDK 构建包的发布和升级命令。

使用 TypeScript SDK

创建一个新目录来存储 Node.js 项目。你可以使用 npm init 函数创建 package.json,或手动创建该文件。根据创建 package.json 的方法,填充或添加以下 JSON:

{ "type": "module" }

打开终端或控制台到你的 Node.js 项目的根目录。运行以下命令将 Sui TypeScript SDK 添加为依赖项:

npm install @mysten/sui.js

Publishing a package with custom policy

In the root of your Node.js project, create a script file named publish.js. Open the file for editing and define some constants:

  • SUI: the location of the sui CLI binary.
  • POLICY_PACKAGE_ID: the ID of our published day_of_week package.
const SUI = 'sui';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';

Next, add boilerplate code to get the signer key pair for the currently active address in the Sui Client CLI:

import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';

import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';
import { fromB64 } from '@mysten/sui.js/utils';

const sender = execSync(`${SUI} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.sui', 'sui_config', 'sui.keystore'),
'utf8',
)
);

for (const priv of keystore) {
const raw = fromB64(priv);
if (raw[0] !== 0) {
continue;
}

const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toSuiAddress() === sender) {
return pair;
}
}

throw new Error(`keypair not found for sender: ${sender}`);
})();

Next, define the path of the package you are publishing. The following snippet assumes that the package is in a sibling directory to publish.js, called example:

import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Location of package relative to current directory
const packagePath = path.join(__dirname, 'example');

Next, build the package:

const { modules, dependencies } = JSON.parse(
execSync(
`${SUI} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);

Next, construct the transaction to publish the package. Wrap its UpgradeCap in a "day of the week" policy, which permits upgrades on Tuesdays, and send the new policy back:

import { TransactionBlock } from '@mysten/sui.js/transactions';

const tx = new TransactionBlock();
const packageUpgradeCap = tx.publish({ modules, dependencies });
const tuesdayUpgradeCap = tx.moveCall({
target: `${POLICY_PACKAGE_ID}::day_of_week::new_policy`,
arguments: [
packageUpgradeCap,
tx.pure(1), // Tuesday
],
});

tx.transferObjects([tuesdayUpgradeCap], tx.pure(sender));

And finally, execute that transaction and display its effects to the console. The following snippet assumes that you're running your examples against a local network. Pass devnet, testnet, or mainnet to the getFullnodeUrl() function to run on Devnet, Testnet, or Mainnet respectively:

import { getFullnodeUrl, SuiClient } from '@mysten/sui.js/client';

const client = new SuiClient({ url: getFullnodeUrl('localnet')})
const result = await client.signAndExecuteTransactionBlock({
signer,
transactionBlock: tx,
options: {
showEffects: true,
showObjectChanges: true,
}
});

console.log(result)
Show complete script

The complete publish.js script follows:

import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';
import { fileURLToPath } from 'url';

import { getFullnodeUrl, SuiClient } from '@mysten/sui.js/client';
import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';
import { TransactionBlock } from '@mysten/sui.js/transactions';
import { fromB64 } from '@mysten/sui.js/utils';

const SUI = 'sui';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';
const sender = execSync(`${SUI} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.sui', 'sui_config', 'sui.keystore'),
'utf8',
)
);

for (const priv of keystore) {
const raw = fromB64(priv);
if (raw[0] !== 0) {
continue;
}

const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toSuiAddress() === sender) {
return pair;
}
}

throw new Error(`keypair not found for sender: ${sender}`);
})();

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packagePath = path.join(__dirname, 'example');

const { modules, dependencies } = JSON.parse(
execSync(
`${SUI} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);

const tx = new TransactionBlock();
const packageUpgradeCap = tx.publish({ modules, dependencies });
const tuesdayUpgradeCap = tx.moveCall({
target: `${POLICY_PACKAGE_ID}::day_of_week::new_policy`,
arguments: [
packageUpgradeCap,
tx.pure(1), // Tuesday
],
});

tx.transferObjects([tuesdayUpgradeCap], tx.pure(sender));

const client = new SuiClient({ url: getFullnodeUrl('localnet')})
const result = await client.signAndExecuteTransactionBlock({
signer,
transactionBlock: tx,
options: {
showEffects: true,
showObjectChanges: true,
}
});

console.log(result)

Save your publish.js file, and then use Node.js to run the script:

node publish.js
Show output

If the script is successful, the console prints the following response:

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING example
{
digest: '9NBLe61sRqe7wS6y8mMVt6vhwA9W5Sz5YVEmuCwNMT64',
effects: {
messageVersion: 'v1',
status: { status: 'success' },
executedEpoch: '0',
gasUsed: {
computationCost: '1000000',
storageCost: '6482800',
storageRebate: '978120',
nonRefundableStorageFee: '9880'
},
modifiedAtVersions: [ [Object] ],
transactionDigest: '9NBLe61sRqe7wS6y8mMVt6vhwA9W5Sz5YVEmuCwNMT64',
created: [ [Object], [Object] ],
mutated: [ [Object] ],
gasObject: { owner: [Object], reference: [Object] },
dependencies: [
'BMVXjS7GG3d5W4Prg7gMVyvKTzEk1Hazx7Tq4WCcbcz9',
'CAFFD2HHnULQMCycL9xgad5JJpjFu2nuftf2xyugQu4t',
'GGDUeVkDoNFcyGibGNeiaGSiKsxf9QLzbjqPzdqi3dNJ'
]
},
objectChanges: [
{
type: 'mutated',
sender: '<SENDER>',
owner: [Object],
objectType: '0x2::coin::Coin<0x2::sui::SUI>',
objectId: '<GAS>',
version: '10',
previousVersion: '9',
digest: 'Dz38faAzFsRzKQyT7JTkVydCcvNNxbUdZiutGmA2Eyy6'
},
{
type: 'published',
packageId: '<EXAMPLE-PACKAGE>',
version: '1',
digest: '5JdU8hkFTjyqg4fHyC8JtdHBV11yCCKdFuyf9j4kKY3o',
modules: [Array]
},
{
type: 'created',
sender: '<SENDER>',
owner: [Object],
objectType: '<POLICY-PACKAGE>::day_of_week::UpgradeCap',
objectId: '<EXAMPLE-UPGRADE-CAP>',
version: '10',
digest: '3uAMFHFKunX9XrufMe27MHDbeLpgHBSsCPN3gSa93H3v'
}
],
confirmedLocalExecution: true
}
tip

If you receive a ReferenceError: fetch is not defined error, use Node.js version 18 or greater.

Use the CLI to test that your newly published package works:

sui client call --gas-budget 10000000 \
--package '<EXAMPLE-PACKAGE-ID>' \
--module 'example' \
--function 'nudge' \
Show output

A successful call responds with the following:

----- Transaction Digest ----
Bx1GA8EsBjoLKvXV2GG92DC5Jt58dbytf6jFcLg18dDR
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([0, 92, 22, 253, 150, 35, 134, 140, 185, 239, 72, 194, 25, 250, 153, 98, 134, 26, 219, 232, 199, 122, 56, 189, 186, 56, 126, 184, 147, 148, 184, 4, 17, 177, 156, 231, 198, 74, 118, 28, 187, 132, 94, 141, 44, 55, 70, 207, 157, 143, 182, 83, 59, 156, 116, 226, 22, 65, 211, 179, 187, 18, 76, 245, 4, 92, 225, 85, 204, 230, 61, 45, 147, 106, 193, 13, 195, 116, 230, 99, 61, 161, 251, 251, 68, 154, 46, 172, 143, 122, 101, 212, 120, 80, 164, 214, 54])))]
Transaction Kind : Programmable
Inputs: []
Commands: [
MoveCall(<EXAMPLE-PACKAGE>::example::nudge()),
]

Sender: <SENDER>
Gas Payment: Object ID: <GAS>, version: 0xb, digest: 93nZ3uLmLfJdHWoSHMuHsjFstEf45EM2pfovu3ibo4iH
Gas Owner: <SENDER>
Gas Price: 1000
Gas Budget: 10000000

----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: <GAS> , Owner: Account Address ( <SENDER> )

----- Events ----
Array [
Object {
"id": Object {
"txDigest": String("Bx1GA8EsBjoLKvXV2GG92DC5Jt58dbytf6jFcLg18dDR"),
"eventSeq": String("0"),
},
"packageId": String("<EXAMPLE-PACKAGE>"),
"transactionModule": String("example"),
"sender": String("<SENDER>"),
"type": String("<EXAMPLE-PACKAGE>::example::Event"),
"parsedJson": Object {
"x": String("41"),
},
"bcs": String("7rkaa6aDvyD"),
},
]
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<SENDER>"),
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"objectType": String("0x2::coin::Coin<0x2::sui::SUI>"),
"objectId": String("<GAS>"),
"version": String("12"),
"previousVersion": String("11"),
"digest": String("9aNuZF63uBVaWF9L6cVmk7geimmpP9h9StigdNDPSiy3"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"coinType": String("0x2::sui::SUI"),
"amount": String("-1009880"),
},
]

If you used the example package provided, notice you have an Events section that contains a field x with value 41.

Upgrading a package with custom policy

With your package published, you can prepare an upgrade.js script to perform an upgrade using the new policy. It behaves identically to publish.js up until building the package. When building the package, the script also captures its digest, and the transaction now performs the three upgrade commands (authorize, execute, commit). The full script for upgrade.js follows:

import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';
import { fileURLToPath } from 'url';

import { getFullnodeUrl, SuiClient } from '@mysten/sui.js/client';
import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';
import { TransactionBlock, UpgradePolicy } from '@mysten/sui.js/transactions';
import { fromB64 } from '@mysten/sui.js/utils';

const SUI = 'sui';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';
const EXAMPLE_PACKAGE_ID = '<EXAMPLE-PACKAGE>';
const CAP_ID = '<EXAMPLE-UPGRADE-CAP>';
const sender = execSync(`${SUI} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.sui', 'sui_config', 'sui.keystore'),
'utf8',
)
);

for (const priv of keystore) {
const raw = fromB64(priv);
if (raw[0] !== 0) {
continue;
}

const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toSuiAddress() === sender) {
return pair;
}
}

throw new Error(`keypair not found for sender: ${sender}`);
})();

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packagePath = path.join(__dirname, 'example');

const { modules, dependencies, digest } = JSON.parse(
execSync(
`${SUI} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);

const tx = new TransactionBlock();
const cap = tx.object(CAP_ID);
const ticket = tx.moveCall({
target: `${POLICY_PACKAGE_ID}::day_of_week::authorize_upgrade`,
arguments: [
cap,
tx.pure(UpgradePolicy.COMPATIBLE),
tx.pure(digest),
],
});

const receipt = tx.upgrade({
modules,
dependencies,
packageId: EXAMPLE_PACKAGE_ID,
ticket,
});

tx.moveCall({
target: `${POLICY_PACKAGE_ID}::day_of_week::commit_upgrade`,
arguments: [cap, receipt],
});

const client = new SuiClient({ url: getFullnodeUrl('localnet') });
const result = await client.signAndExecuteTransactionBlock({
signer,
transactionBlock: tx,
options: {
showEffects: true,
showObjectChanges: true,
}
});

console.log(result);

If today is not Tuesday, wait until next Tuesday to run the script, when your policy allows you to perform upgrades. At that point, update your example.move so the event is emitted with a different constant and use Node.js to run the upgrade script:

node upgrade.js
Show output

If the script is successful (and today is Tuesday), your console displays the following response:

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING example
{
digest: 'EzJyH6BX231sw4jY6UZ6r9Dr28SKsiB2hg3zw4Jh4D5P',
effects: {
messageVersion: 'v1',
status: { status: 'success' },
executedEpoch: '0',
gasUsed: {
computationCost: '1000000',
storageCost: '6482800',
storageRebate: '2874168',
nonRefundableStorageFee: '29032'
},
modifiedAtVersions: [ [Object], [Object] ],
transactionDigest: 'EzJyH6BX231sw4jY6UZ6r9Dr28SKsiB2hg3zw4Jh4D5P',
created: [ [Object] ],
mutated: [ [Object], [Object] ],
gasObject: { owner: [Object], reference: [Object] },
dependencies: [
'62BxVq24tgaRrFTXR3i944RRZ6x8sgTGbjFzpFDe2RAB',
'BMVXjS7GG3d5W4Prg7gMVyvKTzEk1Hazx7Tq4WCcbcz9',
'Bx1GA8EsBjoLKvXV2GG92DC5Jt58dbytf6jFcLg18dDR',
'CAFFD2HHnULQMCycL9xgad5JJpjFu2nuftf2xyugQu4t'
]
},
objectChanges: [
{
type: 'mutated',
sender: '<SENDER>',
owner: [Object],
objectType: '0x2::coin::Coin<0x2::sui::SUI>',
objectId: '<GAS>',
version: '13',
previousVersion: '12',
digest: 'DF4aebHRYrVdxtfAaFfET3hLHn5hqsoty4joMYxLDBuc'
},
{
type: 'mutated',
sender: '<SENDER>',
owner: [Object],
objectType: '<POLICY-PACKAGE>::day_of_week::UpgradeCap',
objectId: '<EXAMPLE-UPGRADE-CAP>',
version: '13',
previousVersion: '11',
digest: '5Wtuw9mAGBuP5qFdTzDCRxBF9LqJ7uZbpxk2UXhAkrXL'
},
{
type: 'published',
packageId: '<UPGRADED-EXAMPLE-PACKAGE>',
version: '2',
digest: '7mvnMEXezAGcWqYSt6R4QUpPjY8nqTSmb5Dv2SqkVq7a',
modules: [Array]
}
],
confirmedLocalExecution: true
}

Use the Sui Client CLI to test the upgraded package (the package ID is different from the original version of your example package):

sui client call --gas-budget 10000000 \
--package '<UPGRADED-EXAMPLE-PACKAGE>' \
--module 'example' \
--function 'nudge'
Show output

If successful, the console prints the following response:

----- Transaction Digest ----
EF2rQzWHmtjPvkqzFGyFvANA8e4ETULSBqDMkzqVoshi
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([0, 88, 98, 118, 173, 218, 55, 4, 48, 166, 42, 106, 193, 210, 159, 75, 233, 95, 77, 201, 38, 0, 234, 183, 77, 252, 178, 22, 221, 106, 202, 42, 166, 29, 130, 164, 97, 110, 201, 153, 91, 149, 50, 72, 6, 213, 183, 70, 83, 55, 5, 190, 182, 5, 98, 212, 134, 103, 181, 204, 247, 90, 28, 125, 14, 92, 225, 85, 204, 230, 61, 45, 147, 106, 193, 13, 195, 116, 230, 99, 61, 161, 251, 251, 68, 154, 46, 172, 143, 122, 101, 212, 120, 80, 164, 214, 54])))]
Transaction Kind : Programmable
Inputs: []
Commands: [
MoveCall(<UPGRADE-EXAMPLE-PACKAGE>::example::nudge()),
]

Sender: <SENDER>
Gas Payment: Object ID: <GAS>, version: 0xd, digest: DF4aebHRYrVdxtfAaFfET3hLHn5hqsoty4joMYxLDBuc
Gas Owner: <SENDER>
Gas Price: 1000
Gas Budget: 10000000

----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: <GAS> , Owner: Account Address ( <SENDER> )

----- Events ----
Array [
Object {
"id": Object {
"txDigest": String("EF2rQzWHmtjPvkqzFGyFvANA8e4ETULSBqDMkzqVoshi"),
"eventSeq": String("0"),
},
"packageId": String("<UPGRADE-EXAMPLE-PACKAGE>"),
"transactionModule": String("example"),
"sender": String("<SENDER>"),
"type": String("<EXAMPLE-PACKAGE>::example::Event"),
"parsedJson": Object {
"x": String("42"),
},
"bcs": String("82TFauPiYEj"),
},
]
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<SENDER>"),
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"objectType": String("0x2::coin::Coin<0x2::sui::SUI>"),
"objectId": String("<GAS>"),
"version": String("14"),
"previousVersion": String("13"),
"digest": String("AmGocCxy6cHvCuGG3izQ8a7afp6qWWt14yhowAzBYa44"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"coinType": String("0x2::sui::SUI"),
"amount": String("-1009880"),
},
]

Now, the Events section emitted for the x field has a value of 42 (changed from the original 41).

If you attempt the first upgrade before Tuesday or you change the constant again and try the upgrade the following day, the script receives a response that includes an error similar to the following, which indicates that the upgrade aborted with code 2 (ENotAllowedDay):

...
status: {
status: 'failure',
error: 'MoveAbort(MoveLocation { module: ModuleId { address: <POLICY-PACKAGE>, name: Identifier("day_of_week") }, function: 1, instruction: 11, function_name: Some("authorize_upgrade") }, 2) in command 0'
},
...