Custom Policies
Relying on a single key to manage on-chain package upgrades can introduce significant security vulnerabilities:
- Conflicting Interests: The entity controlling the key may make changes that benefit themselves but are not in the best interests of the broader community.
- Lack of Consultation: Upgrades might be executed without sufficient time for package users to review the changes or decide whether to continue using the package if they disagree with the updates.
- Key Loss: If the key is lost, it could permanently prevent any future upgrades, leaving the package in a potentially vulnerable or outdated state.
To mitigate these risks while still enabling live package upgrades,
IOTA offers custom upgrade policies.
These policies secure access to the UpgradeCap
and issue UpgradeTicket
objects,
authorizing upgrades on a per-case basis.
Compatibility
IOTA provides several built-in package compatibility policies, ordered from most to least strict:
Policy | Description |
---|---|
Immutable | Prevents any upgrades to the package. |
Dependency-only | Limits modifications to the package’s dependencies only. |
Additive | Allows adding new functionalities (e.g., new public functions or structs) but restricts changes to existing functionalities. |
Compatible | The least restrictive policy. Permits changes to all function implementations, the removal of ability constraints on generic type parameters in function signatures, and modifications to private , public(friend) , and entry function signatures. However, public function signatures and existing types cannot be changed. |
Each policy is a superset of the previous one, allowing progressively more flexibility in package upgrades.
When you publish a package, it defaults to the most relaxed, compatible policy. However, you can adjust the policy during the transaction that publishes the package, setting the desired policy level before the package goes live on-chain.
You can restrict the current policy by invoking functions from the iota::package
module
(only_additive_upgrades
, only_dep_upgrades
, make_immutable
) on the package's UpgradeCap
.
Note that a policy can only become more restrictive over time.
For example, after applying the only_dep_upgrades
policy,
attempting to set a less restrictive policy like only_additive_upgrades
will result in an error.
Upgrade Process Overview
Package upgrades must be executed in a single transaction, involving three key steps:
- Authorization: Obtain permission via the
UpgradeCap
, creating anUpgradeTicket
. - Execution: Use the
UpgradeTicket
to verify package bytecode and compatibility, and create the upgraded package on-chain. A successful upgrade returns anUpgradeReceipt
. - Commit: Update the
UpgradeCap
with the new package information.
While step 2 is handled internally, steps 1 and 3 are implemented as Move functions. The IOTA framework provides basic implementations:
module iota::package {
public fun authorize_upgrade(
cap: &mut UpgradeCap,
policy: u8,
digest: vector<u8>
): UpgradeTicket;
public fun commit_upgrade(
cap: &mut UpgradeCap,
receipt: UpgradeReceipt,
);
}
These functions are invoked by the iota client upgrade
command for authorization and commit.
Custom upgrade policies can be created by adding additional conditions to these functions,
such as voting mechanisms, governance models, permission lists, or timelocks.
Any function pair that produces an UpgradeTicket
from an UpgradeCap
and consumes an UpgradeReceipt
to update the UpgradeCap
constitutes a custom upgrade policy.
UpgradeCap
The UpgradeCap
type is crucial for managing package upgrades:
module iota::package {
public struct UpgradeCap has key, store {
id: UID,
package: ID,
version: u64,
policy: u8,
}
}
Creating a package automatically generates the UpgradeCap
object, and any upgrades modify this object. The owner of the UpgradeCap
has the authority to:
- Modify compatibility requirements for future upgrades.
- Approve future upgrades.
- Make the package immutable.
The UpgradeCap
ensures the following:
- Only the latest package version can be upgraded, maintaining a linear upgrade history.
- Only one upgrade can be in progress at any given time.
- Upgrades must be authorized and completed within a single transaction.
UpgradeTicket
module iota::package {
public struct UpgradeTicket {
cap: ID,
package: ID,
policy: u8,
digest: vector<u8>,
}
}
An UpgradeTicket
is a proof of authorization for an upgrade,
specific to:
- The latest package version identified by
package: ID
. - The compatibility policy defined by
policy: u8
. - The package contents specified by
digest: vector<u8>
.
The upgrade process checks that these conditions match before proceeding.
The UpgradeTicket
must be used within the same transaction it was created; otherwise, the transaction fails.
Package Digest
The digest
in the UpgradeTicket
comes from the digest
parameter passed to authorize_upgrade
.
While the function itself does not process the digest,
custom policies can use it to verify upgrades against pre-approved bytecode or source code.
The digest is calculated by:
-
Taking the bytecode of each module as an array of bytes.
-
Appending the list of the package's transitive dependencies, each represented as an array of bytes.
-
Sorting this list lexicographically.
-
Hashing each element in the sorted list using the
Blake2B
algorithm. -
Computing the final digest from the hash state.
-
Typically, the Move toolchain outputs the digest during the build process when using the
--dump-bytecode-as-base64
flag:
iota move build --dump-bytecode-as-base64
FETCHING GIT DEPENDENCY https://github.com/iotaledger/iota.git
INCLUDING DEPENDENCY Iota
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 iota::package {
public struct UpgradeReceipt {
cap: ID,
package: ID,
}
public struct UpgradeReceipt {
cap: ID,
package: ID,
}
}
An UpgradeReceipt
confirms
that the upgrade was successfully executed, and the new package was added to the transaction's created objects.
The UpgradeReceipt
must be used within the same transaction to update the UpgradeCap
, or the transaction will fail.
Best Practices for Custom Upgrade Policies
When creating custom upgrade policies, it's advisable to:
- Implement them in a separate package from the code they govern.
- Make the policy package immutable to prevent future changes.
- Lock the
UpgradeCap
policy to prevent less restrictive changes later on.
These practices help ensure informed user consent and bounded risk, making it clear what the upgrade policy is when a user interacts with the package, and ensuring that the policy does not become more permissive over time without user knowledge and consent.
Example: "Day of the Week" Upgrade Policy
In this example, you'll create a simple upgrade policy that allows package upgrades only on a specific day of the week, as chosen by the package creator.
Creating the Upgrade Policy
Start by creating a new Move package to contain your upgrade policy:
iota move new policy
This command creates a policy
directory with a sources
folder and a Move.toml
manifest file.
Next, navigate to the sources
folder and create a new file named day_of_week.move
.
Insert the following code into this file:
module policy::day_of_week {
use iota::package;
public struct UpgradeCap has key, store {
id: UID,
cap: package::UpgradeCap,
day: u8,
}
/// Error code indicating that the provided day is not a valid weekday (must be between 0 and 6).
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 }
}
}
This code defines a new struct, UpgradeCap
,
and a constructor function new_policy
that initializes the custom upgrade policy based on the specified day of the week.
Next, add a function to authorize upgrades only on the correct day of the week. Start by defining two constants: one for the error code indicating an upgrade attempt on a disallowed day, and another for the number of milliseconds in a day:
/// Error code for an attempted upgrade on the wrong day.
const ENotAllowedDay: u64 = 2;
/// Number of milliseconds in a day.
const MS_IN_DAY: u64 = 24 * 60 * 60 * 1000;
After the new_policy
function, add the week_day
function, which calculates the current day of the week:
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)
}
This function uses the transaction context's epoch timestamp to determine the current weekday.
Next, add an authorize_upgrade
function that checks whether today matches the allowed upgrade day and authorizes the upgrade if it does:
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)
}
The signature of a custom authorize_upgrade
can be different from the signature of iota::package::authorize_upgrade
as long as it returns an UpgradeTicket
.
Finally, add implementations for the commit_upgrade
and make_immutable
functions,
which delegate to the corresponding functions in the iota::package
module:
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);
}
The final version of your day_of_week.move
file should look like this:
module policy::day_of_week {
use iota::package;
public 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 =
iota::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
You can use the iota client publish
command to publish the policy.
iota client publish
Toggle output
A successful publish
returns the following:
INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING policy
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
CAFFD2HHnULQMCycL9xgad5JJpjFu2nuftf2xyugQu4t
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([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(IotaPureValue { 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::iota::IOTA>"),
"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::iota::IOTA"),
"amount": String("-10773600"),
},
]
You should use the IOTA Client CLI to call iota::package::make_immutable
on the
UpgradeCap
to make the policy immutable.
iota client call \
--package 0x2 \
--module 'package' \
--function 'make_immutable' \
--args '<POLICY-UPGRADE-CAP>'
Toggle output
A successful call returns the following:
----- Transaction Digest ----
FqTdsEgFnyVqc3sFeu5EnBUziEDYbxhLUAaLv4FDjN6d
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([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::iota::IOTA>"),
"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::iota::IOTA"),
"amount": String("607780"),
},
]
Creating a Package for Testing
With your custom upgrade policy deployed on-chain, you’ll need a package to upgrade. This section will guide you through creating a basic package that you can reference in the following scenarios. If you already have a package, feel free to use it instead of creating a new one.
If you don’t have a package available, use the following command to create a new Move package template named example
:
iota move new example
In the example/sources
directory, create a new file named example.move
with the following code:
module example::example {
public struct Event has copy, drop { x: u64 }
entry fun nudge() {
iota::event::emit(Event { x: 41 })
}
}
This basic package contains an Event
struct and a function nudge
that emits the event with a value of 41
.
In the instructions that follow,
you'll publish this package and then upgrade it to change the value emitted in the Event
.
Using the TypeScript SDK
To publish and upgrade the package using your custom policy, you'll need to use the IOTA TypeScript SDK.
1. Set Up a Node.js Project
Create a new directory for your Node.js project. Inside this directory, initialize the project with npm init
to create a package.json
file.
Alternatively, you can create the file manually.
Ensure the package.json
includes the following JSON:
{ "type": "module" }
2. Install the IOTA TypeScript SDK
Open a terminal in your project directory and run the following command to install the IOTA TypeScript SDK as a dependency:
npm install @iota/iota-sdk
Publishing a Package with a Custom Policy
Next, you’ll publish your package, wrapping its UpgradeCap
with the "Day of the Week" policy you created earlier.
Follow these steps:
1. Create a Script to Publish the Package
In the root of your Node.js project, create a script file named publish.js
.
Open the file and start by defining constants for the IOTA CLI binary location and the POLICY_PACKAGE_ID
of your published day_of_week
package:
const IOTA = 'iota';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';
2. Get the Signer Key Pair
Add boilerplate code to retrieve the signer key pair for the currently active address in the IOTA Client CLI:
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { fromBase64 } from '@iota/iota-sdk/utils';
const sender = execSync(`${IOTA} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.iota', 'iota_config', 'iota.keystore'),
'utf8',
)
);
for (const priv of keystore) {
const raw = fromBase64(priv);
if (raw[0] !== 0) {
continue;
}
const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toIotaAddress() === sender) {
return pair;
}
}
throw new Error(`keypair not found for sender: ${sender}`);
})();
3. Define the Package Path
Define the path to the package you want to publish.
The following snippet assumes that the package is in a sibling directory to publish.js
, named 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');
4. Build the Package
Build the package using the following code:
const { modules, dependencies } = JSON.parse(
execSync(
`${IOTA} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);
5. Construct and Execute the Transaction
Construct the transaction to publish the package,
wrap its UpgradeCap
in the "Day of the Week" policy (allowing upgrades only on Tuesdays),
and send the new policy back:
import { Transaction } from '@iota/iota-sdk/transactions';
const tx = new Transaction();
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));
Finally, execute the transaction and display its effects in the console.
The following snippet assumes 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, IotaClient } from '@iota/iota-sdk/client';
const client = new IotaClient({ url: getFullnodeUrl('localnet')})
const result = await client.signAndExecuteTransaction({
signer,
transaction: tx,
options: {
showEffects: true,
showObjectChanges: true,
}
});
console.log(result)
Toggle 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, IotaClient } from '@iota/iota-sdk/client';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Transaction } from '@iota/iota-sdk/transactions';
import { fromBase64 } from '@iota/iota-sdk/utils';
const IOTA = 'iota';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';
const sender = execSync(`${IOTA} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.iota', 'iota_config', 'iota.keystore'),
'utf8',
)
);
for (const priv of keystore) {
const raw = fromBase64(priv);
if (raw[0] !== 0) {
continue;
}
const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toIotaAddress() === 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(
`${IOTA} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);
const tx = new Transaction();
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 IotaClient({ url: getFullnodeUrl('localnet')})
const result = await client.signAndExecuteTransaction({
signer,
transaction: 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
Toggle output
If the script is successful, the console prints the following response:
INCLUDING DEPENDENCY Iota
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::iota::IOTA>',
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
}
If you receive a ReferenceError: fetch is not defined
error, use Node.js version 18 or greater.
You can use the CLI to test that your newly published package works:
iota client call \
--package '<EXAMPLE-PACKAGE-ID>' \
--module 'example' \
--function 'nudge' \
Toggle output
A successful call responds with the following:
----- Transaction Digest ----
Bx1GA8EsBjoLKvXV2GG92DC5Jt58dbytf6jFcLg18dDR
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([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::iota::IOTA>"),
"objectId": String("<GAS>"),
"version": String("12"),
"previousVersion": String("11"),
"digest": String("9aNuZF63uBVaWF9L6cVmk7geimmpP9h9StigdNDPSiy3"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"coinType": String("0x2::iota::IOTA"),
"amount": String("-1009880"),
},
]
If you followed the example package provided,
you should see an Events
section containing a field x
with the 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 custom "Day of the Week" policy.
The script behaves similarly to publish.js
,
but also captures the package's digest
during the build process.
The transaction then performs the three upgrade commands:
authorize, execute, and commit.
Below is the full script for upgrade.js
:
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import { getFullnodeUrl, IotaClient } from '@iota/iota-sdk/client';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Transaction, UpgradePolicy } from '@iota/iota-sdk/transactions';
import { fromBase64 } from '@iota/iota-sdk/utils';
const IOTA = 'iota';
const POLICY_PACKAGE_ID = '<POLICY-PACKAGE>';
const EXAMPLE_PACKAGE_ID = '<EXAMPLE-PACKAGE>';
const CAP_ID = '<EXAMPLE-UPGRADE-CAP>';
const sender = execSync(`${IOTA} client active-address`, { encoding: 'utf8' }).trim();
const signer = (() => {
const keystore = JSON.parse(
readFileSync(
path.join(homedir(), '.iota', 'iota_config', 'iota.keystore'),
'utf8',
)
);
for (const priv of keystore) {
const raw = fromBase64(priv);
if (raw[0] !== 0) {
continue;
}
const pair = Ed25519Keypair.fromSecretKey(raw.slice(1));
if (pair.getPublicKey().toIotaAddress() === 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(
`${IOTA} move build --dump-bytecode-as-base64 --path ${packagePath}`,
{ encoding: 'utf-8'},
),
);
const tx = new Transaction();
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 IotaClient({ url: getFullnodeUrl('localnet') });
const result = await client.signAndExecuteTransaction({
signer,
transaction: tx,
options: {
showEffects: true,
showObjectChanges: true,
}
});
console.log(result);
If today isn't Tuesday, you'll need to wait until the next Tuesday to run the script,
as your policy only allows upgrades on that day.
Once it's Tuesday, update your example.move
file so that the event emits a different constant,
then use Node.js to run the upgrade script:
node upgrade.js
Toggle output
If the script is successful (and today is Tuesday), your console displays the following response:
INCLUDING DEPENDENCY Iota
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::iota::IOTA>',
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 IOTA Client CLI to test the upgraded package. Note that the package ID will be different from the original version of your example package:
iota client call \
--package '<UPGRADED-EXAMPLE-PACKAGE>' \
--module 'example' \
--function 'nudge'
Toggle output
If successful, the console prints the following response:
----- Transaction Digest ----
EF2rQzWHmtjPvkqzFGyFvANA8e4ETULSBqDMkzqVoshi
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([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::iota::IOTA>"),
"objectId": String("<GAS>"),
"version": String("14"),
"previousVersion": String("13"),
"digest": String("AmGocCxy6cHvCuGG3izQ8a7afp6qWWt14yhowAzBYa44"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<SENDER>"),
},
"coinType": String("0x2::iota::IOTA"),
"amount": String("-1009880"),
},
]
Now, the Events
section shows that the x
field has a value of 42
, updated from the original 41
.
If you try to perform the first upgrade before Tuesday or modify the constant and attempt the upgrade on a different day,
the script will return an error message indicating that the upgrade failed 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'
},
...