From 20e67c33affed175a1b673a7206b633dc8dae176 Mon Sep 17 00:00:00 2001 From: kylezs Date: Thu, 26 Oct 2023 22:42:32 +1100 Subject: [PATCH] feat: automate compatible CFE upgrades (#4149) * feat: bouncer bump release version * chore: compare SemVer versions function * feat: (bouncer) upgrade network command * refactor: pull spec version check out of compileBinaries * chore: move compileBinaries to its own file * chore: lint * chore: remove unused promptUser * prettier * chore: remove stale comment * fix: use && between commands --- bouncer/.gitignore | 3 +- ...e_upgrade.ts => simple_runtime_upgrade.ts} | 6 +- bouncer/commands/upgrade_network.ts | 36 ++++++++ bouncer/package.json | 1 + bouncer/pnpm-lock.yaml | 7 ++ bouncer/shared/bump_release_version.ts | 14 +++ bouncer/shared/prompt_user.ts | 17 ---- ...e_upgrade.ts => simple_runtime_upgrade.ts} | 21 ++--- bouncer/shared/upgrade_network.ts | 85 +++++++++++++++++++ bouncer/shared/utils.ts | 13 +++ bouncer/shared/utils/compile_binaries.ts | 14 +++ 11 files changed, 183 insertions(+), 34 deletions(-) rename bouncer/commands/{noop_runtime_upgrade.ts => simple_runtime_upgrade.ts} (85%) create mode 100755 bouncer/commands/upgrade_network.ts create mode 100755 bouncer/shared/bump_release_version.ts delete mode 100644 bouncer/shared/prompt_user.ts rename bouncer/shared/{noop_runtime_upgrade.ts => simple_runtime_upgrade.ts} (63%) create mode 100755 bouncer/shared/upgrade_network.ts create mode 100644 bouncer/shared/utils/compile_binaries.ts diff --git a/bouncer/.gitignore b/bouncer/.gitignore index 4207d4af1e..799237cf69 100644 --- a/bouncer/.gitignore +++ b/bouncer/.gitignore @@ -1,3 +1,4 @@ node_modules pnpm-lock.yaml -.idea \ No newline at end of file +.idea +tmp \ No newline at end of file diff --git a/bouncer/commands/noop_runtime_upgrade.ts b/bouncer/commands/simple_runtime_upgrade.ts similarity index 85% rename from bouncer/commands/noop_runtime_upgrade.ts rename to bouncer/commands/simple_runtime_upgrade.ts index bcfab6febc..071d907a7f 100755 --- a/bouncer/commands/noop_runtime_upgrade.ts +++ b/bouncer/commands/simple_runtime_upgrade.ts @@ -6,15 +6,17 @@ // // Optional args: // -test: Run the swap tests after the upgrade. +// // For example ./commands/noop_runtime_upgrade.ts // NB: It *must* be run from the bouncer directory. -import { noopRuntimeUpgrade } from '../shared/noop_runtime_upgrade'; +import path from 'path'; +import { simpleRuntimeUpgrade } from '../shared/simple_runtime_upgrade'; import { testAllSwaps } from '../shared/swapping'; import { runWithTimeout } from '../shared/utils'; async function main(): Promise { - await noopRuntimeUpgrade(); + await simpleRuntimeUpgrade(path.dirname(process.cwd())); if (process.argv[2] === '-test') { await testAllSwaps(); diff --git a/bouncer/commands/upgrade_network.ts b/bouncer/commands/upgrade_network.ts new file mode 100755 index 0000000000..3838be6fea --- /dev/null +++ b/bouncer/commands/upgrade_network.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env -S pnpm tsx +// INSTRUCTIONS +// Upgrades a localnet network to a new version. +// Start a network with the version you want to upgrade from. Then run this command, providing the git reference (commit, branch, tag) you wish to upgrade to. +// +// Optional args: +// patch/minor/major: If the version of the commit we're upgrading to is the same as the version of the commit we're upgrading from, we bump the version by the specified level. +// +// For example: ./commands/upgrade_network.ts v0.10.1 +// or: ./commands/upgrade_network.ts v0.10.1 patch + +import { upgradeNetwork } from '../shared/upgrade_network'; +import { runWithTimeout } from '../shared/utils'; + +async function main(): Promise { + const upgradeTo = process.argv[2]?.trim(); + + if (!upgradeTo) { + console.error('Please provide a git reference to upgrade to.'); + process.exit(-1); + } + + const optBumptTo: string = process.argv[3]?.trim().toLowerCase(); + if (optBumptTo === 'patch' || optBumptTo === 'minor' || optBumptTo === 'major') { + await upgradeNetwork(upgradeTo, optBumptTo); + } else { + await upgradeNetwork(upgradeTo); + } + + process.exit(0); +} + +runWithTimeout(main(), 15 * 60 * 1000).catch((error) => { + console.error(error); + process.exit(-1); +}); diff --git a/bouncer/package.json b/bouncer/package.json index 2f7b9db764..072c14e2ef 100644 --- a/bouncer/package.json +++ b/bouncer/package.json @@ -21,6 +21,7 @@ "md5": "^2.3.0", "minimist": "^1.2.8", "tiny-secp256k1": "^2.2.1", + "toml": "^3.0.0", "web3": "^1.9.0" }, "devDependencies": { diff --git a/bouncer/pnpm-lock.yaml b/bouncer/pnpm-lock.yaml index adba5506a4..3812a8134f 100644 --- a/bouncer/pnpm-lock.yaml +++ b/bouncer/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: tiny-secp256k1: specifier: ^2.2.1 version: 2.2.1 + toml: + specifier: ^3.0.0 + version: 3.0.0 web3: specifier: ^1.9.0 version: 1.9.0 @@ -4363,6 +4366,10 @@ packages: engines: {node: '>=0.6'} dev: false + /toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + dev: false + /tough-cookie@2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} diff --git a/bouncer/shared/bump_release_version.ts b/bouncer/shared/bump_release_version.ts new file mode 100755 index 0000000000..1e5fcbec43 --- /dev/null +++ b/bouncer/shared/bump_release_version.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process'; + +export type SemVerLevel = 'major' | 'minor' | 'patch'; + +// Bumps the version of all the packages in the workspace by the specified level. +export async function bumpReleaseVersion(level: SemVerLevel, projectRoot: string) { + console.log(`Bumping the version of all packages in the workspace by ${level}...`); + try { + execSync(`cd ${projectRoot} && cargo ws version ${level} --no-git-commit -y`); + } catch (error) { + console.log(error); + console.log('Ensure you have cargo workspaces installed: `cargo install cargo-workspaces`'); + } +} diff --git a/bouncer/shared/prompt_user.ts b/bouncer/shared/prompt_user.ts deleted file mode 100644 index 7228fbcd0a..0000000000 --- a/bouncer/shared/prompt_user.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createInterface } from 'readline/promises'; - -export async function promptUser(prompt: string) { - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - try { - const completePrompt = prompt + '\nEnter to continue. Ctrl + C to exit.\n'; - const ans = await rl.question(completePrompt); - rl.close(); - return ans; - } finally { - rl.close(); - } -} diff --git a/bouncer/shared/noop_runtime_upgrade.ts b/bouncer/shared/simple_runtime_upgrade.ts similarity index 63% rename from bouncer/shared/noop_runtime_upgrade.ts rename to bouncer/shared/simple_runtime_upgrade.ts index 742689cbea..d785d2c46d 100755 --- a/bouncer/shared/noop_runtime_upgrade.ts +++ b/bouncer/shared/simple_runtime_upgrade.ts @@ -1,33 +1,26 @@ -// Do a runtime upgrade that does nothing - the runtime should be identical except for the `spec_version` field. -// Needs to be run from the bouncer directory. -import { execSync } from 'node:child_process'; - import { submitRuntimeUpgrade } from './submit_runtime_upgrade'; import { jsonRpc } from './json_rpc'; import { getChainflipApi, observeEvent } from './utils'; import { bumpSpecVersion } from './utils/bump_spec_version'; +import { compileBinaries } from './utils/compile_binaries'; async function getCurrentSpecVersion(): Promise { return Number((await jsonRpc('state_getRuntimeVersion', [], 9944)).specVersion); } -export async function noopRuntimeUpgrade(): Promise { +// Do a runtime upgrade using the code in the projectRoot directory. +export async function simpleRuntimeUpgrade(projectRoot: string): Promise { const chainflip = await getChainflipApi(); - const currentSpecVersion = await getCurrentSpecVersion(); - console.log('Current spec_version: ' + currentSpecVersion); - const nextSpecVersion = currentSpecVersion + 1; + bumpSpecVersion(`${projectRoot}/state-chain/runtime/src/lib.rs`, nextSpecVersion); - bumpSpecVersion('../state-chain/runtime/src/lib.rs', nextSpecVersion); - - console.log('Building the new runtime'); - execSync('cd ../state-chain/runtime && cargo build --release'); + await compileBinaries('runtime', projectRoot); - console.log('Built the new runtime. Applying runtime upgrade.'); + console.log('Applying runtime upgrade.'); await submitRuntimeUpgrade( - '../target/release/wbuild/state-chain-runtime/state_chain_runtime.compact.compressed.wasm', + `${projectRoot}/target/release/wbuild/state-chain-runtime/state_chain_runtime.compact.compressed.wasm`, ); await observeEvent('system:CodeUpdated', chainflip); diff --git a/bouncer/shared/upgrade_network.ts b/bouncer/shared/upgrade_network.ts new file mode 100755 index 0000000000..8e92d780b2 --- /dev/null +++ b/bouncer/shared/upgrade_network.ts @@ -0,0 +1,85 @@ +import { execSync } from 'child_process'; +import fs from 'fs/promises'; +import * as toml from 'toml'; +import path from 'path'; +import { SemVerLevel, bumpReleaseVersion } from './bump_release_version'; +import { simpleRuntimeUpgrade } from './simple_runtime_upgrade'; +import { compareSemVer } from './utils'; + +async function readPackageTomlVersion(projectRoot: string): Promise { + const data = await fs.readFile(path.join(projectRoot, '/state-chain/runtime/Cargo.toml'), 'utf8'); + const parsedData = toml.parse(data); + const version = parsedData.package.version; + return version; +} + +// The javascript version of state-chain/primitives/src/lib.rs - SemVer::is_compatible_with() +function isCompatibleWith(semVer1: string, semVer2: string) { + const [major1, minor1] = semVer1.split('.').map(Number); + const [major2, minor2] = semVer2.split('.').map(Number); + + return major1 === major2 && minor1 === minor2; +} + +// Create a git workspace in the tmp/ directory and check out the specified commit. +// Remember to delete it when you're done! +function createGitWorkspaceAt(absoluteWorkspacePath: string, toGitRef: string) { + try { + // Create a directory for the new workspace + execSync(`mkdir -p ${absoluteWorkspacePath}`); + + // Create a new workspace using git worktree. + execSync(`git worktree add ${absoluteWorkspacePath}`); + + // Navigate to the new workspace and checkout the specific commit + execSync(`cd ${absoluteWorkspacePath} && git checkout ${toGitRef}`); + + console.log('Commit checked out successfully in new workspace.'); + } catch (error) { + console.error(`Error: ${error}`); + } +} + +// Upgrades a bouncer network from the commit currently running on localnet to the provided git reference (commit, branch, tag). +// If the version of the commit we're upgrading to is the same as the version of the commit we're upgrading from, we bump the version by the specified level. +export async function upgradeNetwork(toGitRef: string, bumpByIfEqual: SemVerLevel = 'patch') { + const fromTomlVersion = await readPackageTomlVersion(path.dirname(process.cwd())); + console.log("Version we're upgrading from: " + fromTomlVersion); + + // tmp/ is ignored in the bouncer .gitignore file. + const absoluteWorkspacePath = path.join(process.cwd(), 'tmp/upgrade-network'); + + console.log('Creating a new git workspace at: ' + absoluteWorkspacePath); + + createGitWorkspaceAt(absoluteWorkspacePath, toGitRef); + + const toTomlVersion = await readPackageTomlVersion(`${absoluteWorkspacePath}`); + console.log("Version we're upgrading to: " + toTomlVersion); + + if (compareSemVer(fromTomlVersion, toTomlVersion) === 'greater') { + throw new Error( + "The version we're upgrading to is older than the version we're upgrading from. Ensure you selected the correct commits.", + ); + } + + if (fromTomlVersion === toTomlVersion) { + await bumpReleaseVersion(bumpByIfEqual, absoluteWorkspacePath); + } + + const newToTomlVersion = await readPackageTomlVersion(path.join(absoluteWorkspacePath)); + const isCompatible = isCompatibleWith(fromTomlVersion, newToTomlVersion); + + if (isCompatible) { + // The CFE could be upgraded too. But an incompatible CFE upgrade would mean it's... incompatible, so covered in the other path. + console.log('The versions are compatible.'); + + // Runtime upgrade using the *new* version. + await simpleRuntimeUpgrade(absoluteWorkspacePath); + console.log('Upgrade complete.'); + } else if (!isCompatible) { + // Incompatible upgrades requires running two versions of the CFEs side by side. + console.log('Incompatible CFE upgrades are not yet supported :('); + } + + execSync(`cd ${absoluteWorkspacePath} && git worktree remove . --force`); +} diff --git a/bouncer/shared/utils.ts b/bouncer/shared/utils.ts index 2dda9f2258..615cf13026 100644 --- a/bouncer/shared/utils.ts +++ b/bouncer/shared/utils.ts @@ -466,3 +466,16 @@ export function isValidEthAddress(address: string): boolean { const ethRegex = /^0x[a-fA-F0-9]{40}$/; return ethRegex.test(address); } + +// "v1 is greater than v2" -> "greater" +export function compareSemVer(version1: string, version2: string) { + const v1 = version1.split('.').map(Number); + const v2 = version2.split('.').map(Number); + + for (let i = 0; i < 3; i++) { + if (v1[i] > v2[i]) return 'greater'; + if (v1[i] < v2[i]) return 'less'; + } + + return 'equal'; +} diff --git a/bouncer/shared/utils/compile_binaries.ts b/bouncer/shared/utils/compile_binaries.ts new file mode 100644 index 0000000000..564d2b68b9 --- /dev/null +++ b/bouncer/shared/utils/compile_binaries.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process'; + +// Returns the expected next version of the runtime. +export async function compileBinaries(type: 'runtime' | 'all', projectRoot: string) { + if (type === 'all') { + console.log('Building all the binaries...'); + execSync(`cd ${projectRoot} && cargo build --release`); + } else { + console.log('Building the new runtime...'); + execSync(`cd ${projectRoot}/state-chain/runtime && cargo build --release`); + } + + console.log('Build complete.'); +}