diff --git a/src/index.ts b/src/index.ts index 984951e4..bdc2f410 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,7 @@ import { captureForgingStatusAtSnapshotHeight } from './events'; import { copyGenesisBlock, createGenesisBlock, + getGenesisBlockCreateCommand, writeGenesisAssets, writeGenesisBlock, } from './utils/genesis_block'; @@ -56,9 +57,9 @@ import { installLiskCore, startLiskCore, isLiskCoreV3Running } from './utils/nod import { resolveAbsolutePath, verifyOutputPath } from './utils/path'; import { execAsync } from './utils/process'; import { CustomError } from './utils/exception'; +import { getCommandsToExecPostMigration, writeCommandsToExecute } from './utils/commands'; let configCoreV4: PartialApplicationConfig; - class LiskMigrator extends Command { public static description = 'Migrate Lisk Core to latest version'; @@ -113,48 +114,48 @@ class LiskMigrator extends Command { }; public async run(): Promise { - try { - const { flags } = this.parse(LiskMigrator); - const liskCoreV3DataPath = resolveAbsolutePath( - flags['lisk-core-v3-data-path'] ?? DEFAULT_LISK_CORE_PATH, + const { flags } = this.parse(LiskMigrator); + const liskCoreV3DataPath = resolveAbsolutePath( + flags['lisk-core-v3-data-path'] ?? DEFAULT_LISK_CORE_PATH, + ); + const outputPath = flags.output ?? join(__dirname, '..', 'output'); + const snapshotHeight = flags['snapshot-height']; + const customConfigPath = flags.config; + const autoMigrateUserConfig = flags['auto-migrate-config'] ?? false; + const autoStartLiskCoreV4 = flags['auto-start-lisk-core-v4']; + const pageSize = Number(flags['page-size']); + + verifyOutputPath(outputPath); + + const client = await getAPIClient(liskCoreV3DataPath); + const nodeInfo = (await client.node.getNodeInfo()) as NodeInfo; + const { version: appVersion, networkIdentifier } = nodeInfo; + + cli.action.start('Verifying if backup height from node config matches snapshot height'); + if (snapshotHeight !== nodeInfo.backup.height) { + this.error( + `Lisk Core v3 backup height (${nodeInfo.backup.height}) does not match the expected snapshot height (${snapshotHeight}).`, ); - const outputPath = flags.output ?? join(__dirname, '..', 'output'); - const snapshotHeight = flags['snapshot-height']; - const customConfigPath = flags.config; - const autoMigrateUserConfig = flags['auto-migrate-config'] ?? false; - const autoStartLiskCoreV4 = flags['auto-start-lisk-core-v4']; - const pageSize = Number(flags['page-size']); - - verifyOutputPath(outputPath); - - const client = await getAPIClient(liskCoreV3DataPath); - const nodeInfo = (await client.node.getNodeInfo()) as NodeInfo; - const { version: appVersion, networkIdentifier } = nodeInfo; - - cli.action.start('Verifying if backup height from node config matches snapshot height'); - if (snapshotHeight !== nodeInfo.backup.height) { - this.error( - `Lisk Core v3 backup height (${nodeInfo.backup.height}) does not match the expected snapshot height (${snapshotHeight}).`, - ); - } - cli.action.stop('Snapshot height matches backup height'); - - cli.action.start( - `Verifying snapshot height to be multiples of round length i.e ${ROUND_LENGTH}`, + } + cli.action.stop('Snapshot height matches backup height'); + + cli.action.start( + `Verifying snapshot height to be multiples of round length i.e ${ROUND_LENGTH}`, + ); + if (snapshotHeight % ROUND_LENGTH !== 0) { + this.error( + `Invalid snapshot height provided: ${snapshotHeight}. It must be an exact multiple of round length (${ROUND_LENGTH}).`, ); - if (snapshotHeight % ROUND_LENGTH !== 0) { - this.error( - `Invalid snapshot height provided: ${snapshotHeight}. It must be an exact multiple of round length (${ROUND_LENGTH}).`, - ); - } - cli.action.stop('Snapshot height is valid'); + } + cli.action.stop('Snapshot height is valid'); - const networkConstant = NETWORK_CONSTANT[networkIdentifier] as NetworkConfigLocal; - const outputDir = flags.output ? outputPath : `${outputPath}/${networkIdentifier}`; + const networkConstant = NETWORK_CONSTANT[networkIdentifier] as NetworkConfigLocal; + const outputDir = flags.output ? outputPath : `${outputPath}/${networkIdentifier}`; - // Ensure the output directory is present - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + // Ensure the output directory is present + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + try { // Asynchronously capture the node's Forging Status information at the snapshot height // This information is necessary for the node operators to enable generator post-migration without getting PoM'd captureForgingStatusAtSnapshotHeight(this, client, snapshotHeight, outputDir); @@ -363,10 +364,33 @@ class LiskMigrator extends Command { this.log('Please copy genesis block to the Lisk Core V4 network directory.'); } } catch (error) { - this.error(error as string); + const commandsToExecute: string[] = []; + const code = Number(`${(error as { message: string; code: number }).code}`); + + if (code === ERROR_CODES.GENESIS_BLOCK_CREATE) { + const genesisBlockCreateCommand = getGenesisBlockCreateCommand(); + commandsToExecute.push(genesisBlockCreateCommand); + } + if (code === ERROR_CODES.LISK_CORE_START) { + const liskCoreStartCommand = `lisk core start--network ${networkConstant.name}`; + commandsToExecute.push(liskCoreStartCommand); + } + await writeCommandsToExecute( + [...commandsToExecute, ...(await getCommandsToExecPostMigration(outputDir))], + outputDir, + ); + this.error(`${(error as Error).message}`); } this.log('Successfully finished migration. Exiting!!!'); + this.log( + `Creating file with the commands to execute post migration: ${outputDir}/commandsToExecute.txt`, + ); + await writeCommandsToExecute(await getCommandsToExecPostMigration(outputDir), outputDir); + this.log( + `Successfully created file with the commands to execute post migration: ${outputDir}/commandsToExecute.txt`, + ); + process.exit(0); } } diff --git a/src/utils/commands.ts b/src/utils/commands.ts index 93002716..66a719ba 100644 --- a/src/utils/commands.ts +++ b/src/utils/commands.ts @@ -14,9 +14,9 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { resolve } from 'path'; -import { read, write } from './fs'; +import { read, write, exists } from './fs'; -export const getCommandsToExecute = async (outputDir: string) => { +export const getCommandsToExecPostMigration = async (outputDir: string) => { const commandsToExecute = []; commandsToExecute.push( @@ -25,22 +25,24 @@ export const getCommandsToExecute = async (outputDir: string) => { commandsToExecute.push('lisk-core keys:import --file-path config/keys.json'); const forgingStatusJsonFilepath = resolve(outputDir, 'forgingStatus.json'); - const forgingStatusString = (await read(forgingStatusJsonFilepath)) as string; - const forgingStatusJson = JSON.parse(forgingStatusString); - - if (forgingStatusJson.length) { - for (const forgingStatus of forgingStatusJson) { - commandsToExecute.push( - `lisk-core endpoint:invoke random_setHashOnion '{"address":"${forgingStatus.lskAddress}"}'`, - ); - - commandsToExecute.push( - `lisk-core endpoint:invoke generator_setStatus '{"address":"${forgingStatus.lskAddress}", "height": ${forgingStatus.height}, "maxHeightGenerated": ${forgingStatus.maxHeightPreviouslyForged}, "maxHeightPrevoted": ${forgingStatus.maxHeightPrevoted} }' --pretty`, - ); - - commandsToExecute.push( - `lisk-core generator:enable ${forgingStatus.lskAddress} --use-status-value`, - ); + if (await exists(forgingStatusJsonFilepath)) { + const forgingStatusString = (await read(forgingStatusJsonFilepath)) as string; + const forgingStatusJson = JSON.parse(forgingStatusString); + + if (forgingStatusJson.length) { + for (const forgingStatus of forgingStatusJson) { + commandsToExecute.push( + `lisk-core endpoint:invoke random_setHashOnion '{"address":"${forgingStatus.lskAddress}"}'`, + ); + + commandsToExecute.push( + `lisk-core endpoint:invoke generator_setStatus '{"address":"${forgingStatus.lskAddress}", "height": ${forgingStatus.height}, "maxHeightGenerated": ${forgingStatus.maxHeightPreviouslyForged}, "maxHeightPrevoted": ${forgingStatus.maxHeightPrevoted} }' --pretty`, + ); + + commandsToExecute.push( + `lisk-core generator:enable ${forgingStatus.lskAddress} --use-status-value`, + ); + } } } diff --git a/src/utils/genesis_block.ts b/src/utils/genesis_block.ts index c212ac86..c0c3d3a2 100644 --- a/src/utils/genesis_block.ts +++ b/src/utils/genesis_block.ts @@ -33,6 +33,8 @@ import { CustomError } from './exception'; let genesisBlockCreateCommand: string; +export const getGenesisBlockCreateCommand = () => genesisBlockCreateCommand; + export const createChecksum = async (filePath: string): Promise => { const fileStream = fs.createReadStream(filePath); const dataHash = crypto.createHash('sha256'); diff --git a/src/utils/node.ts b/src/utils/node.ts index 918f8281..e6ca6217 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -26,7 +26,14 @@ import { isPortAvailable } from './network'; import { resolveAbsolutePath } from './path'; import { Port } from '../types'; import { getAPIClient } from '../client'; -import { DEFAULT_PORT_P2P, DEFAULT_PORT_RPC, LEGACY_DB_PATH, SNAPSHOT_DIR } from '../constants'; +import { + DEFAULT_PORT_P2P, + DEFAULT_PORT_RPC, + ERROR_CODES, + LEGACY_DB_PATH, + SNAPSHOT_DIR, +} from '../constants'; +import { CustomError } from './exception'; const INSTALL_LISK_CORE_COMMAND = 'npm i -g lisk-core@^4.0.0-rc.1'; const INSTALL_PM2_COMMAND = 'npm i -g pm2'; @@ -175,50 +182,54 @@ export const startLiskCore = async ( network: string, outputDir: string, ): Promise => { - const networkPort = (_config?.network?.port as Port) ?? DEFAULT_PORT_P2P; - if (!(await isPortAvailable(networkPort))) { - throw new Error(`Port ${networkPort} is not available for P2P communication.`); - } - - const rpcPort = (_config?.network?.port as Port) ?? DEFAULT_PORT_RPC; - if (!(await isPortAvailable(rpcPort))) { - throw new Error(`Port ${rpcPort} is not available to start the RPC server.`); - } + try { + const networkPort = (_config?.network?.port as Port) ?? DEFAULT_PORT_P2P; + if (!(await isPortAvailable(networkPort))) { + throw new Error(`Port ${networkPort} is not available for P2P communication.`); + } - await backupDefaultDirectoryIfExists(_this, liskCoreV3DataPath); - await copyLegacyDB(_this); + const rpcPort = (_config?.network?.port as Port) ?? DEFAULT_PORT_RPC; + if (!(await isPortAvailable(rpcPort))) { + throw new Error(`Port ${rpcPort} is not available to start the RPC server.`); + } - const configPath = await getFinalConfigPath(outputDir, network); - const liskCoreStartCommand = await resolveLiskCoreStartCommand(_this, network, configPath); + await backupDefaultDirectoryIfExists(_this, liskCoreV3DataPath); + await copyLegacyDB(_this); - const pm2Config = { - name: 'lisk-core-v4', - script: liskCoreStartCommand, - }; + const configPath = await getFinalConfigPath(outputDir, network); + const liskCoreStartCommand = await resolveLiskCoreStartCommand(_this, network, configPath); - const isUserConfirmed = await cli.confirm( - `Start Lisk Core with the following pm2 configuration? [yes/no]\n${JSON.stringify( - pm2Config, - null, - '\t', - )}`, - ); + const pm2Config = { + name: 'lisk-core-v4', + script: liskCoreStartCommand, + }; - if (!isUserConfirmed) { - _this.error( - 'User did not confirm to start Lisk Core v4 with the customized PM2 config. Skipping the Lisk Core v4 auto-start process. Please start the node manually.', + const isUserConfirmed = await cli.confirm( + `Start Lisk Core with the following pm2 configuration? [yes/no]\n${JSON.stringify( + pm2Config, + null, + '\t', + )}`, ); - } - _this.log('Installing PM2...'); - await installPM2(); - _this.log('Finished installing PM2.'); + if (!isUserConfirmed) { + _this.error( + 'User did not confirm to start Lisk Core v4 with the customized PM2 config. Skipping the Lisk Core v4 auto-start process. Please start the node manually.', + ); + } - const pm2FilePath = path.resolve(outputDir, PM2_FILE_NAME); - _this.log(`Creating PM2 config at ${pm2FilePath}`); - fs.writeFileSync(pm2FilePath, JSON.stringify(pm2Config, null, '\t')); - _this.log(`Successfully created the PM2 config at ${pm2FilePath}`); + _this.log('Installing PM2...'); + await installPM2(); + _this.log('Finished installing PM2.'); - const PM2_COMMAND_START = `pm2 start ${pm2FilePath}`; - _this.log(await execAsync(PM2_COMMAND_START)); + const pm2FilePath = path.resolve(outputDir, PM2_FILE_NAME); + _this.log(`Creating PM2 config at ${pm2FilePath}`); + fs.writeFileSync(pm2FilePath, JSON.stringify(pm2Config, null, '\t')); + _this.log(`Successfully created the PM2 config at ${pm2FilePath}`); + + const PM2_COMMAND_START = `pm2 start ${pm2FilePath}`; + _this.log(await execAsync(PM2_COMMAND_START)); + } catch (error) { + throw new CustomError(`${(error as Error).message}`, ERROR_CODES.LISK_CORE_START); + } }; diff --git a/test/unit/utils/commands.spec.ts b/test/unit/utils/commands.spec.ts index 25c998c6..c089c9a4 100644 --- a/test/unit/utils/commands.spec.ts +++ b/test/unit/utils/commands.spec.ts @@ -13,7 +13,10 @@ */ import * as fs from 'fs-extra'; import { join } from 'path'; -import { getCommandsToExecute, writeCommandsToExecute } from '../../../src/utils/commands'; +import { + getCommandsToExecPostMigration, + writeCommandsToExecute, +} from '../../../src/utils/commands'; import { exists } from '../../../src/utils/fs'; const outputDir = join(__dirname, '../../..', 'test/unit/fixtures'); @@ -22,14 +25,10 @@ afterAll(() => { fs.removeSync(join(outputDir, 'commandsToExecute.txt')); }); -describe('Test getCommandsToExecute method', () => { +describe('Test getCommandsToExecPostMigration method', () => { it('should create commandsToExecute text file', async () => { - const commandsToExecute = await getCommandsToExecute(outputDir); + const commandsToExecute = await getCommandsToExecPostMigration(outputDir); await writeCommandsToExecute(commandsToExecute, outputDir); expect(await exists(`${outputDir}/commandsToExecute.txt`)).toBe(true); }); - - it('should throw error when outputDir is invalid', async () => { - await expect(getCommandsToExecute('invalidPath')).rejects.toThrow(); - }); });