Skip to content

Commit

Permalink
Script to upgrade a Centrifuge parachain live (#1884)
Browse files Browse the repository at this point in the history
* Add script to upgrade a live parachain (testnet) with sudo

* Separate council upgrades and rename folder

* use development wasm for demo

* use development wasm for demo

* remove council proposal and adapt to producion live parachains

* fix: upgrade script

* fix: bash script

* refactor: remove unused config

* refactor: replace npm lock with yarn lock

* refactor: remove unused noise

* feat: improve error catching

---------

Co-authored-by: William Freudenberger <[email protected]>
  • Loading branch information
gpmayorga and wischli authored Jun 25, 2024
1 parent 47384e9 commit de2886f
Show file tree
Hide file tree
Showing 10 changed files with 782 additions and 0 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
7 changes: 7 additions & 0 deletions scripts/js/runtime-upgrade-remote/configs/demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"endpoint": "wss://fullnode.demo.k-f.dev",
"wasmFile": "./development.wasm",
"privateKey": "",
"sudo": true,
"councilMembers": []
}
7 changes: 7 additions & 0 deletions scripts/js/runtime-upgrade-remote/configs/development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"endpoint": "wss://fullnode.development.cntrfg.com",
"wasmFile": "./development.wasm",
"privateKey": "//Alice",
"sudo": true,
"councilMembers": []
}
148 changes: 148 additions & 0 deletions scripts/js/runtime-upgrade-remote/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api');
const { u8aToHex } = require('@polkadot/util');
const { blake2AsHex, blake2AsU8a } = require('@polkadot/util-crypto');
const fs = require('fs');
const path = require('path');

// Load configuration
const configPath = path.resolve(__dirname, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

const run = async () => {
let exitCode = 0;
try {
// Validate configuration
if (!config.endpoint || !config.wasmFile || !config.privateKey) {
console.error("Missing configuration parameters. Please ensure 'endpoint', 'wasmFile', and 'privateKey' are specified in the corresponding configs/*.json.");
process.exit(1);
}

console.log("Configuration loaded:", config);

const wsProvider = new WsProvider(config.endpoint);
const api = await ApiPromise.create({ provider: wsProvider });

console.log("Connected to the parachain at:", config.endpoint);

const keyring = new Keyring({ type: "sr25519" });
let user;
if (config.privateKey.startsWith('//')) {
user = keyring.addFromUri(config.privateKey);
} else {
user = keyring.addFromSeed(config.privateKey);
}

console.log(`Using account: ${user.address}`);

const wasm = fs.readFileSync(config.wasmFile);
const wasmHash = blake2AsHex(wasm);
const wasmBytes = u8aToHex(wasm);

console.log("WASM file loaded and ready for deployment");

if (config.sudo) {
console.log("Using sudo to perform the runtime upgrade");
await sudoAuthorize(api, user, wasmHash);
await enactUpgrade(api, user, wasmBytes);
} else {
console.error("Unsupported");
}
// Check for specific events or transaction success as needed
} catch (error) {
console.error('Error:', error);
exitCode = 1;
} finally {
process.exit(exitCode);
}
};

async function sudoAuthorize(api, sudoAccount, wasmHex) {
const nonce = await api.rpc.system.accountNextIndex(sudoAccount.address)

return new Promise(async (resolve, reject) => {
try {
// Authorize the upgrade
const authorizeTx = api.tx.sudo.sudo(
api.tx.parachainSystem.authorizeUpgrade(wasmHex, true)
);

const unsub = await authorizeTx.signAndSend(sudoAccount, { nonce }, ({ status, dispatchError, events }) => {
console.log(`Authorizing upgrade with status ${status}`);
if (status.isInBlock) {
console.log(`Authorization included in block ${status.asInBlock}`);
resolve();
unsub();
}
checkError(api, reject, dispatchError, events)
});
}
catch (error) {
reject(error)
}
});
}

async function enactUpgrade(api, sudoAccount, wasmFile) {
const nonce = await api.rpc.system.accountNextIndex(sudoAccount.address)

return new Promise(async (resolve, reject) => {
try {
// Enact the authorized upgrade
const enactTx = api.tx.parachainSystem.enactAuthorizedUpgrade(wasmFile);

const unsub = await enactTx.signAndSend(sudoAccount, { nonce }, ({ status, dispatchError, events }) => {
console.log(`Enacting upgrade with status ${status}`);
if (status.isInBlock) {
console.log(`Enactment included in block ${status}`);
resolve();
unsub();
}
checkError(api, reject, dispatchError, events)
});
}
catch (error) {
reject(error)
}
});
}

function checkError(api, reject, dispatchError, events) {
if (dispatchError) {
if (dispatchError.isModule) {
// for module errors, we have the section indexed, lookup
const decoded = api.registry.findMetaError(dispatchError.asModule);
const { docs, name, section } = decoded;

console.error(`${section}.${name}: ${docs.join(' ')}`);
} else {
// Other, CannotLookup, BadOrigin, no extra info
console.error(dispatchError.toString());
}
reject(dispatchError)
} else if (events) {
events
// find/filter for failed events
.filter(({ event }) =>
api.events.system.ExtrinsicFailed.is(event)
)
// we know that data for system.ExtrinsicFailed is
// (DispatchError, DispatchInfo)
.forEach(({ event: { data: [error, info] } }) => {
if (error.isModule) {
// for module errors, we have the section indexed, lookup
const decoded = api.registry.findMetaError(error.asModule);
const { docs, method, section } = decoded;
const error = `${section}.${method}: ${docs.join(' ')}`

console.error(error);
reject(error)
} else {
// Other, CannotLookup, BadOrigin, no extra info
console.error(error.toString());
reject(error.toString())
}
});
}
}

run();
16 changes: 16 additions & 0 deletions scripts/js/runtime-upgrade-remote/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "parachain-upgrade",
"version": "1.0.0",
"description": "Script to handle parachain upgrades",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@polkadot/api": "^11.3.1",
"@polkadot/util": "^12.5.1",
"@polkadot/util-crypto": "^12.5.1"
},
"author": "",
"license": "ISC"
}
40 changes: 40 additions & 0 deletions scripts/js/runtime-upgrade-remote/perform-upgrade.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
echo "Please select the environment (development, demo):"
read -r ENVIRONMENT

# Check if the privateKey is empty for demo environment
if [ "$ENVIRONMENT" == "demo" ]; then
PRIVATE_KEY=$(jq -r '.privateKey' ./configs/demo.json)
if [ -z "$PRIVATE_KEY" ]; then
echo "Error: privateKey is empty in ./configs/demo.json. Please retrieve it from 1Password."
exit 1
fi
fi

# # Install NVM and node if not present in your mac:
# brew install nvm && echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc && echo '[ -s "$NVM_DIR/nvm.sh" ] \
# && \. "$NVM_DIR/nvm.sh"' >> ~/.zshrc && source ~/.zshrc && nvm install node

# Define the tag and calculate the short git hash
TAG="v0.11.1-rc1"
GIT_HASH=$(git rev-parse --short=7 $TAG)

# Download the WASM file from Google Cloud Storage
echo "Downloading WASM file..."
if [ "$ENVIRONMENT" == "demo" ]; then
gsutil cp gs://centrifuge-wasm-repo/development/development-"$GIT_HASH".wasm ./"${ENVIRONMENT}".wasm
else
gsutil cp gs://centrifuge-wasm-repo/"${ENVIRONMENT}"/"${ENVIRONMENT}"-"$GIT_HASH".wasm ./"${ENVIRONMENT}".wasm
fi

# Copy the corresponding configuration file
echo "Copying configuration file..."
cp ./configs/"${ENVIRONMENT}".json ./config.json

# Run the node script
echo "Running node index.js..."
node index.js
echo "Cleaning up..."
rm ./config.json
rm ./"${ENVIRONMENT}".wasm

Loading

0 comments on commit de2886f

Please sign in to comment.