diff --git a/README.md b/README.md index 3d3a07db..3a6721ca 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,12 @@ The following dependencies need to be installed in order to run applications cre | Dependencies | Version | | ------------ | -------------- | -| NodeJS | 18.16 | +| NodeJS | ^18.16 | +| NPM | 8.3.1 or later | | Lisk Core | 3.1.0 or later | +**NOTE**: It is important that NodeJS is installed using NVM. Please refer our documentation [here](https://lisk.com/documentation/lisk-core/v4/setup/npm.html#node-js-npm). + ### System requirements The following system requirements are recommended to run Lisk Migrator v2.0.0: diff --git a/config/mainnet/config.json b/config/mainnet/config.json index 8f9d40b7..0ced95a3 100644 --- a/config/mainnet/config.json +++ b/config/mainnet/config.json @@ -8,7 +8,7 @@ "modes": ["ipc", "ws"], "port": 7887, "host": "127.0.0.1", - "allowedMethods": ["generator", "system", "random"] + "allowedMethods": ["generator", "system", "random", "keys", "legacy"] }, "genesis": { "block": { diff --git a/config/testnet/config.json b/config/testnet/config.json index a7c085b4..5f84965b 100644 --- a/config/testnet/config.json +++ b/config/testnet/config.json @@ -8,7 +8,7 @@ "modes": ["ipc", "ws"], "port": 7887, "host": "127.0.0.1", - "allowedMethods": ["generator", "system", "random"] + "allowedMethods": ["generator", "system", "random", "keys", "legacy"] }, "genesis": { "block": { diff --git a/src/events.ts b/src/events.ts index ee95b836..491b901c 100644 --- a/src/events.ts +++ b/src/events.ts @@ -13,11 +13,14 @@ */ import { resolve } from 'path'; import { Command } from '@oclif/command'; - -import { APIClient } from '@liskhq/lisk-api-client'; import { Block } from '@liskhq/lisk-chain'; -import { EVENT_NEW_BLOCK } from './constants'; +import { address } from '@liskhq/lisk-cryptography'; +import { APIClient } from '@liskhq/lisk-api-client'; import { write } from './utils/fs'; +import { EVENT_NEW_BLOCK } from './constants'; +import { ForgingStatus } from './types'; + +const { getLisk32AddressFromAddress } = address; export const captureForgingStatusAtSnapshotHeight = ( _this: Command, @@ -28,17 +31,23 @@ export const captureForgingStatusAtSnapshotHeight = ( client.subscribe(EVENT_NEW_BLOCK, async data => { const { block: encodedBlock } = (data as unknown) as Record; const newBlock = client.block.decode(Buffer.from(encodedBlock, 'hex')) as Block; + if (newBlock.header.height === snapshotHeight) { - const forgingStatus = await client.invoke('app:getForgingStatus'); - if (forgingStatus.length) { - const forgingStatusJsonFilepath = resolve(outputDir, 'forgingStatus.json'); + const forgingStatuses: ForgingStatus[] = await client.invoke('app:getForgingStatus'); + const finalForgingStatuses: ForgingStatus[] = forgingStatuses.map(entry => ({ + ...entry, + lskAddress: getLisk32AddressFromAddress(Buffer.from(entry.address, 'hex')), + })); + + if (finalForgingStatuses.length) { try { - await write(forgingStatusJsonFilepath, JSON.stringify(forgingStatus, null, '\t')); + const forgingStatusJsonFilepath = resolve(outputDir, 'forgingStatus.json'); + await write(forgingStatusJsonFilepath, JSON.stringify(finalForgingStatuses, null, '\t')); _this.log(`\nFinished exporting forging status to ${forgingStatusJsonFilepath}.`); } catch (error) { _this.log( `\nUnable to save the node Forging Status information to the disk, please find it below instead:\n${JSON.stringify( - forgingStatus, + finalForgingStatuses, null, 2, )}`, diff --git a/src/index.ts b/src/index.ts index 09387e82..a31e0972 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,8 +51,8 @@ import { } from './utils/genesis_block'; import { CreateAsset } from './createAsset'; import { ApplicationConfigV3, NetworkConfigLocal, NodeInfo } from './types'; -import { installLiskCore, startLiskCore } from './utils/node'; -import { resolveAbsolutePath } from './utils/fs'; +import { installLiskCore, startLiskCore, isLiskCoreV3Running } from './utils/node'; +import { resolveAbsolutePath, verifyOutputPath } from './utils/path'; import { execAsync } from './utils/process'; let configCoreV4: PartialApplicationConfig; @@ -67,8 +67,7 @@ class LiskMigrator extends Command { output: flagsParser.string({ char: 'o', required: false, - description: - "File path to write the genesis block. If not provided, it will default to cwd/output/{v3_networkIdentifier}/genesis_block.blob. Do not use any value starting with the default data path reserved for Lisk Core: '~/.lisk/lisk-core'.", + description: `File path to write the genesis block. If not provided, it will default to cwd/output/{v3_networkIdentifier}/genesis_block.blob. Do not use any value starting with the default data path reserved for Lisk Core: '${DEFAULT_LISK_CORE_PATH}'.`, }), 'lisk-core-v3-data-path': flagsParser.string({ char: 'd', @@ -79,7 +78,7 @@ class LiskMigrator extends Command { config: flagsParser.string({ char: 'c', required: false, - description: 'Custom configuration file path for Lisk Core v3.x.', + description: 'Custom configuration file path for Lisk Core v3.1.x.', }), 'snapshot-height': flagsParser.integer({ char: 's', @@ -91,13 +90,14 @@ class LiskMigrator extends Command { 'auto-migrate-config': flagsParser.boolean({ required: false, env: 'AUTO_MIGRATE_CONFIG', - description: 'Migrate user configuration automatically. Default to false.', + description: 'Migrate user configuration automatically. Defaults to false.', default: false, }), 'auto-start-lisk-core-v4': flagsParser.boolean({ required: false, env: 'AUTO_START_LISK_CORE', - description: 'Start lisk core v4 automatically. Default to false.', + description: + 'Start Lisk Core v4 automatically. Defaults to false. When using this flag, kindly open another terminal window to stop Lisk Core v3.1.x for when the migrator prompts.', default: false, }), 'page-size': flagsParser.integer({ @@ -105,7 +105,7 @@ class LiskMigrator extends Command { required: false, default: 100000, description: - 'Maximum number of blocks to be iterated at once for computation. Default to 100000.', + 'Maximum number of blocks to be iterated at once for computation. Defaults to 100000.', }), }; @@ -122,6 +122,8 @@ class LiskMigrator extends Command { 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; @@ -138,7 +140,9 @@ class LiskMigrator extends Command { `Verifying snapshot height to be multiples of round length i.e ${ROUND_LENGTH}`, ); if (snapshotHeight % ROUND_LENGTH !== 0) { - this.error(`Invalid Snapshot Height: ${snapshotHeight}.`); + 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'); @@ -250,6 +254,7 @@ class LiskMigrator extends Command { snapshotHeight, )) as unknown) as Block; await createGenesisBlock( + this, networkConstant.name, defaultConfigFilePath, outputDir, @@ -289,19 +294,44 @@ class LiskMigrator extends Command { ); if (isLiskCoreV3Stopped) { + let numTriesLeft = 3; + while (numTriesLeft) { + numTriesLeft -= 1; + + const isCoreV3Running = await isLiskCoreV3Running(liskCoreV3DataPath); + if (!isCoreV3Running) break; + + if (numTriesLeft >= 0) { + const isStopReconfirmed = await cli.confirm( + "Lisk Core v3 still running. Please stop the node, type 'yes' to proceed and 'no' to exit. [yes/no]", + ); + if (!isStopReconfirmed) { + this.error( + `Cannot proceed with Lisk Core v4 auto-start. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + ); + } else if (numTriesLeft === 0 && isStopReconfirmed) { + const isCoreV3StillRunning = await isLiskCoreV3Running(liskCoreV3DataPath); + if (isCoreV3StillRunning) { + this.error( + `Cannot auto-start Lisk Core v4 as Lisk Core v3 is still running. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, + ); + } + } + } + } + const isUserConfirmed = await cli.confirm( - `Start Lisk Core with the following configuration? [yes/no] \n${util.inspect( - configCoreV4, - false, - 3, - )}`, + `Start Lisk Core with the following configuration? [yes/no] + ${util.inspect(configCoreV4, false, 3)} `, ); if (isUserConfirmed) { cli.action.start('Starting Lisk Core v4'); const network = networkConstant.name as string; await startLiskCore(this, liskCoreV3DataPath, configCoreV4, network, outputDir); - this.log('Started Lisk Core v4 at default data directory.'); + this.log( + `Started Lisk Core v4 at default data directory ('${DEFAULT_LISK_CORE_PATH}').`, + ); cli.action.stop(); } else { this.log( @@ -309,17 +339,17 @@ class LiskMigrator extends Command { ); } } else { - this.log( - 'User did not confirm Lisk Core v3 node shutdown. Skipping the Lisk Core v4 auto-start process.', + this.error( + `User did not confirm Lisk Core v3 node shutdown. Skipping the Lisk Core v4 auto-start process. Please continue manually. In order to access legacy blockchain information posts-migration, please copy the contents of the ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/). Exiting!!!`, ); } } catch (err) { /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */ - this.error(`Failed to auto-start Lisk Core v4.\nError: ${err}`); + this.error(`Failed to auto-start Lisk Core v4.\nError: ${(err as Error).message}`); } } else { this.log( - `Please copy the contents of ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core V4 data directory (e.g: ~/.lisk/lisk-core/data/legacy.db/) in order to access legacy blockchain information.`, + `Please copy the contents of ${snapshotDirPath} directory to 'data/legacy.db' under the Lisk Core v4 data directory (e.g: ${DEFAULT_LISK_CORE_PATH}/data/legacy.db/) in order to access legacy blockchain information.`, ); this.log('Please copy genesis block to the Lisk Core V4 network directory.'); } diff --git a/src/types.ts b/src/types.ts index ca463635..6b8515c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -389,3 +389,12 @@ export interface NodeInfo { readonly registeredModules: RegisteredModule[]; readonly backup: Backup; } + +export interface ForgingStatus { + readonly address: string; + lskAddress?: string; + readonly forging: boolean; + readonly height?: number; + readonly maxHeightPrevoted?: number; + readonly maxHeightPreviouslyForged?: number; +} diff --git a/src/utils/chain.ts b/src/utils/chain.ts index 18536c75..2711da6d 100644 --- a/src/utils/chain.ts +++ b/src/utils/chain.ts @@ -97,8 +97,9 @@ export const observeChainHeight = async (options: ObserveParams): Promise { - const homeDirectory = homedir(); - return homeDirectory ? inputPath.replace(/^~(?=$|\/|\\)/, homeDirectory) : inputPath; -}; - /* eslint-disable @typescript-eslint/no-unused-expressions */ export const copyDir = async (src: string, dest: string) => { await fs.promises.mkdir(dest, { recursive: true }); diff --git a/src/utils/genesis_block.ts b/src/utils/genesis_block.ts index d09b99f1..f42756b2 100644 --- a/src/utils/genesis_block.ts +++ b/src/utils/genesis_block.ts @@ -14,6 +14,7 @@ import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import path from 'path'; +import { Command } from '@oclif/command'; import { Block as BlockVersion3 } from '@liskhq/lisk-chain'; import { SNAPSHOT_TIME_GAP } from '../constants'; import { GenesisAssetEntry } from '../types'; @@ -48,6 +49,7 @@ export const createChecksum = async (filePath: string): Promise => { }; export const createGenesisBlock = async ( + _this: Command, network: string, configFilepath: string, outputDir: string, @@ -58,6 +60,9 @@ export const createGenesisBlock = async ( const previousBlockID = blockAtSnapshotHeight.header.id.toString('hex'); const genesisBlockCreateCommand = `lisk-core genesis-block:create --network ${network} --config=${configFilepath} --output=${outputDir} --assets-file=${outputDir}/genesis_assets.json --height=${height} --previous-block-id=${previousBlockID} --timestamp=${timestamp} --export-json`; + _this.log( + `\nExecuting the following command to generate the genesis block:\n${genesisBlockCreateCommand}`, + ); await execAsync(genesisBlockCreateCommand); }; diff --git a/src/utils/node.ts b/src/utils/node.ts index ec38c89f..5ec259e8 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -19,16 +19,16 @@ import { existsSync, renameSync } from 'fs-extra'; import { PartialApplicationConfig } from 'lisk-framework'; import { execAsync } from './process'; +import { copyDir, exists } from './fs'; 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 { copyDir, exists, resolveAbsolutePath } from './fs'; const INSTALL_LISK_CORE_COMMAND = 'npm i -g lisk-core@^4.0.0-rc.1'; const INSTALL_PM2_COMMAND = 'npm i -g pm2'; const PM2_FILE_NAME = 'pm2.migrator.config.json'; -const PM2_COMMAND_START = `pm2 start ${PM2_FILE_NAME}`; const LISK_V3_BACKUP_DATA_DIR = `${homedir()}/.lisk/lisk-core-v3`; @@ -79,9 +79,6 @@ export const startLiskCore = async ( network: string, outputDir: string, ): Promise => { - const isCoreV3Running = await isLiskCoreV3Running(liskCoreV3DataPath); - if (isCoreV3Running) throw new Error('Lisk Core v3 is still running.'); - 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.`); @@ -99,10 +96,11 @@ export const startLiskCore = async ( await installPM2(); _this.log('Finished installing pm2.'); - _this.log(`Creating PM2 config at ${process.cwd()}/${PM2_FILE_NAME}`); + const pm2FilePath = path.resolve(outputDir, PM2_FILE_NAME); + _this.log(`Creating PM2 config at ${pm2FilePath}`); const configPath = await getFinalConfigPath(outputDir, network); fs.writeFileSync( - PM2_FILE_NAME, + pm2FilePath, JSON.stringify( { name: 'lisk-core-v4', @@ -112,7 +110,8 @@ export const startLiskCore = async ( '\t', ), ); - _this.log(`Successfully created the PM2 config at ${process.cwd()}/${PM2_FILE_NAME}`); + _this.log(`Successfully created the PM2 config at ${pm2FilePath}`); + const PM2_COMMAND_START = `pm2 start ${pm2FilePath}`; _this.log(await execAsync(PM2_COMMAND_START)); }; diff --git a/src/utils/path.ts b/src/utils/path.ts new file mode 100644 index 00000000..55ee471b --- /dev/null +++ b/src/utils/path.ts @@ -0,0 +1,39 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { homedir } from 'os'; +import { isAbsolute, join } from 'path'; +import { DEFAULT_LISK_CORE_PATH } from '../constants'; + +export const resolveAbsolutePath = (path: string) => { + if (isAbsolute(path)) { + return path; + } + + if (path.startsWith('~')) { + return path.replace('~', homedir()); + } + + return join(process.cwd(), path); +}; + +export const verifyOutputPath = (_outputPath: string): void | Error => { + const absLiskCorePath = resolveAbsolutePath(DEFAULT_LISK_CORE_PATH); + const absOutputPath = resolveAbsolutePath(_outputPath); + + if (absOutputPath.startsWith(absLiskCorePath)) { + throw new Error( + `Output path '${_outputPath}' is not allowed. Please restart the migrator with a different output path.`, + ); + } +}; diff --git a/test/unit/fixtures/sub-directory/.gitkeep b/test/unit/fixtures/sub-directory/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/utils/fs.spec.ts b/test/unit/utils/fs.spec.ts index cf00d363..b1f86b6f 100644 --- a/test/unit/utils/fs.spec.ts +++ b/test/unit/utils/fs.spec.ts @@ -12,18 +12,16 @@ * Removal or modification of this copyright notice is prohibited. * */ -import { homedir } from 'os'; import { join } from 'path'; import { + createTarball, extractTarBall, exists, rmdir, - resolveAbsolutePath, copyDir, write, copyFile, - createTarball, } from '../../../src/utils/fs'; import { configV3 } from '../fixtures/config'; @@ -69,21 +67,6 @@ describe('Test rmdir method', () => { }); }); -describe('Test resolveAbsolutePath method', () => { - it('should resolve absolute path when called with valid path which contains ~', async () => { - const path = '~/.test/testFolder'; - const expectedResult = join(homedir(), '.test/testFolder'); - const absolutePath = resolveAbsolutePath(path); - expect(absolutePath).toBe(expectedResult); - }); - - it('should resolve absolute path when called with valid path', async () => { - const path = '/.test/testFolder'; - const absolutePath = resolveAbsolutePath(path); - expect(absolutePath).toBe(path); - }); -}); - describe('Test copyDir method', () => { it('should copy directory', async () => { const sourcePath = join(__dirname, '../../..', 'test/unit/fixtures'); diff --git a/test/unit/utils/path.spec.ts b/test/unit/utils/path.spec.ts new file mode 100644 index 00000000..4c96031a --- /dev/null +++ b/test/unit/utils/path.spec.ts @@ -0,0 +1,75 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +import { homedir } from 'os'; +import { join } from 'path'; + +import { DEFAULT_LISK_CORE_PATH } from '../../../src/constants'; +import { resolveAbsolutePath, verifyOutputPath } from '../../../src/utils/path'; + +describe('Test resolveAbsolutePath method', () => { + it('should resolve absolute path when called with valid path which contains ~', async () => { + const path = '~/.test/testFolder'; + const expectedResult = join(homedir(), '.test/testFolder'); + const absolutePath = resolveAbsolutePath(path); + expect(absolutePath).toBe(expectedResult); + }); + + it('should resolve absolute path when called with valid path', async () => { + const path = '/.test/testFolder'; + const absolutePath = resolveAbsolutePath(path); + expect(absolutePath).toBe(path); + }); + + it('should resolve relative path (current directory) when called with valid path', async () => { + const path = './test/testFolder'; + const absolutePath = resolveAbsolutePath(path); + expect(absolutePath).toBe(join(process.cwd(), path)); + }); + + it('should resolve relative path (parent directory) when called with valid path', async () => { + const path = '../test/testFolder'; + const absolutePath = resolveAbsolutePath(path); + expect(absolutePath).toBe(join(process.cwd(), path)); + }); +}); + +describe('Test verifyOutputPath method', () => { + it('should throw error when user provides the output path same as the default Lisk Core data directory', async () => { + expect(() => verifyOutputPath(DEFAULT_LISK_CORE_PATH)).toThrow( + `Output path '${DEFAULT_LISK_CORE_PATH}' is not allowed. Please restart the migrator with a different output path.`, + ); + }); + + it('should throw error when user provides the output path starting with the default Lisk Core data directory', async () => { + expect(() => verifyOutputPath(`${DEFAULT_LISK_CORE_PATH}/output`)).toThrow( + `Output path '${DEFAULT_LISK_CORE_PATH}/output' is not allowed. Please restart the migrator with a different output path.`, + ); + }); + + it("should not throw error when output path is '~/output'", async () => { + const outputPath = '~/output'; + expect(() => verifyOutputPath(outputPath)).not.toThrow(); + }); + + it("should not throw error when output path is '/root/output'", async () => { + const outputPath = '/root/output'; + expect(() => verifyOutputPath(outputPath)).not.toThrow(); + }); + + it("should not throw error when output path is './output'", async () => { + const outputPath = './output'; + expect(() => verifyOutputPath(outputPath)).not.toThrow(); + }); +});