Skip to content

Commit

Permalink
feat(dashmate): import existing Core data (#1915)
Browse files Browse the repository at this point in the history
Co-authored-by: thephez <[email protected]>
  • Loading branch information
shumkov and thephez authored Jul 9, 2024
1 parent bff4b65 commit 6632b3b
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/dashmate/src/createDIContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import createIpAndPortsFormFactory from './listr/prompts/createIpAndPortsForm.js
import registerMasternodeWithCoreWalletFactory from './listr/tasks/setup/regular/registerMasternode/registerMasternodeWithCoreWallet.js';
import registerMasternodeWithDMTFactory from './listr/tasks/setup/regular/registerMasternode/registerMasternodeWithDMT.js';
import writeConfigTemplatesFactory from './templates/writeConfigTemplatesFactory.js';
import importCoreDataTaskFactory from './listr/tasks/setup/regular/importCoreDataTaskFactory.js';

/**
* @param {Object} [options]
Expand Down Expand Up @@ -298,6 +299,7 @@ export default async function createDIContainer(options = {}) {
.singleton(),
registerMasternodeWithDMT: asFunction(registerMasternodeWithDMTFactory)
.singleton(),
importCoreDataTask: asFunction(importCoreDataTaskFactory).singleton(),
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function configureNodeTaskFactory(createIpAndPortsForm) {
task.title = `Configure ${ctx.nodeType}`;

// Masternode Operator key
if (ctx.nodeType === NODE_TYPE_MASTERNODE) {
if (ctx.nodeType === NODE_TYPE_MASTERNODE && !ctx.config.get('core.masternode.operator.privateKey')) {
const masternodeOperatorPrivateKey = await task.prompt({
type: 'input',
header: ` Please enter your Masternode Operator BLS Private key.
Expand Down Expand Up @@ -71,12 +71,23 @@ export default function configureNodeTaskFactory(createIpAndPortsForm) {

let form;
if (ctx.initialIpForm) {
// Using for testing
form = ctx.initialIpForm;
} else {
let initialIp = ctx.nodeType === NODE_TYPE_MASTERNODE ? '' : undefined;
if (ctx.importedExternalIp) {
initialIp = ctx.importedExternalIp;
}

let initialCoreP2PPort = showEmptyPort ? '' : undefined;
if (ctx.importedP2pPort) {
initialCoreP2PPort = ctx.importedP2pPort;
}

form = await task.prompt(await createIpAndPortsForm(ctx.preset, {
isHPMN: ctx.isHP,
initialIp: ctx.nodeType === NODE_TYPE_MASTERNODE ? '' : undefined,
initialCoreP2PPort: showEmptyPort ? '' : undefined,
initialIp,
initialCoreP2PPort,
initialPlatformHTTPPort: showEmptyPort ? '' : undefined,
initialPlatformP2PPort: showEmptyPort ? '' : undefined,
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { Listr } from 'listr2';
import fs from 'fs';
import path from 'path';
import {
NETWORK_TESTNET,
} from '../../../../constants.js';

/**
* @param {Config} config
* @return {validateCoreDataDirectoryPath}
*/
function validateCoreDataDirectoryPathFactory(config) {
/**
* @typedef {function} validateCoreDataDirectoryPath
* @param {string} value
* @return {string|boolean}
*/
function validateCoreDataDirectoryPath(value) {
if (value.length === 0) {
return 'should not be empty';
}

// Path must be absolute
if (!path.isAbsolute(value)) {
return 'path must be absolute';
}

// Should contain data and dashd.conf
const configFilePath = path.join(value, 'dash.conf');

let dataDirName = 'blocks';
if (config.get('network') === NETWORK_TESTNET) {
dataDirName = 'testnet3';
}

try {
fs.accessSync(configFilePath, fs.constants.R_OK);
fs.accessSync(path.join(value, dataDirName), fs.constants.R_OK);
} catch (e) {
return 'directory must be readable and contain blockchain data with dash.conf';
}

const configFileContent = fs.readFileSync(configFilePath, 'utf8');

// Config file should contain testnet=1 in case of testnet
// and shouldn't contain testnet=1, regtest=1 or devnet= in case of mainnet
if (config.get('network') === NETWORK_TESTNET) {
if (!configFileContent.includes('testnet=1')) {
return 'dash.conf should be configured for testnet';
}
} else if (configFileContent.includes('testnet=1')
|| configFileContent.includes('regtest=1')
|| configFileContent.includes('devnet=')) {
return 'dash.conf should be configured for mainnet';
}

// Config file should contain masternodeblsprivkey in case of masternode
if (config.get('core.masternode.enable')) {
if (!configFileContent.includes('masternodeblsprivkey=')) {
return 'dash.conf should contain masternodeblsprivkey';
}
}

return true;
}

return validateCoreDataDirectoryPath;
}

/**
*
* @param {Docker} docker
* @param {dockerPull} dockerPull
* @param {generateEnvs} generateEnvs
* @return {importCoreDataTask}
*/
export default function importCoreDataTaskFactory(docker, dockerPull, generateEnvs) {
/**
* @typedef {function} importCoreDataTask
* @returns {Listr}
*/
async function importCoreDataTask() {
return new Listr([
{
title: 'Import existing Core data',
task: async (ctx, task) => {
const doImport = await task.prompt({
type: 'toggle',
header: ` If you already run a masternode on this server, you can
import your existing Dash Core data instead of syncing a new dashmate node from scratch.
Your current user account must have read access to this directory.\n`,
message: 'Import existing data?',
enabled: 'Yes',
disabled: 'No',
initial: true,
});
// TODO: Wording needs to be updated

if (!doImport) {
task.skip();
return;
}

// Masternode Operator key
const coreDataPath = await task.prompt({
type: 'input',
header: ` Please enter path to your existing Dash Core data directory.
- Your current user must have read access to this directory.
- The data directory usually ends with .dashcore and contains dash.conf and the data files to import
- If dash.conf is stored separately, you should copy or link to this file in the data directory\n`,
message: 'Core data directory path',
validate: validateCoreDataDirectoryPathFactory(ctx.config),
});

// Read configuration from dashd.conf
const configPath = path.join(coreDataPath, 'dash.conf');
const configFileContent = fs.readFileSync(configPath, 'utf8');
const masternodeOperatorPrivateKey = configFileContent.match(/^masternodeblsprivkey=([^ \n]+)/m)[1];

if (masternodeOperatorPrivateKey) {
ctx.config.set('core.masternode.operator.privateKey', masternodeOperatorPrivateKey);
}

const host = configFileContent.match(/^bind=([^ \n]+)/m)[1];

if (host) {
ctx.config.set('core.p2p.host', host);
}

// Store values to fill in the configure node form

// eslint-disable-next-line prefer-destructuring
ctx.importedP2pPort = configFileContent.match(/^port=([^ \n]+)/m)[1];

// eslint-disable-next-line prefer-destructuring
ctx.importedExternalIp = configFileContent.match(/^externalip=([^ \n]+)/m)[1];

// Copy data directory to docker a volume

// Create a volume
const { COMPOSE_PROJECT_NAME: composeProjectName } = generateEnvs(ctx.config);

const volumeName = `${composeProjectName}_core_data`;

// Check if volume already exists
const volumes = await docker.listVolumes();
const exists = volumes.Volumes.some((volume) => volume.Name === volumeName);

if (exists) {
throw new Error(`Volume ${volumeName} already exists. Please remove it first.`);
}

await docker.createVolume(volumeName);

// Pull image
await dockerPull('alpine');

const commands = [
`mkdir /${volumeName}/.dashcore/`,
'cd /source',
`cp -avL * /${volumeName}/.dashcore/`,
`chown -R 1000:1000 /${volumeName}/`,
`rm /${volumeName}/.dashcore/dash.conf`,
];

// Copy data and set user dash
const [result] = await docker.run(
'alpine',
[
'/bin/sh',
'-c',
commands.join(' && '),
],
task.stdout(),
{
HostConfig: {
AutoRemove: true,
Binds: [
`${coreDataPath}:/source:ro`,
],
Mounts: [
{
Type: 'volume',
Source: volumeName,
Target: `/${volumeName}`,
},
],
},
},
);

if (result.StatusCode !== 0) {
throw new Error('Cannot copy data dir to volume');
}

// TODO: Wording needs to be updated
await task.prompt({
type: 'confirm',
header: ` Please stop your existing Dash Core node before starting the new dashmate-based
node ("dashmate start"). Also, disable any automatic startup services (e.g., cron, systemd) for the existing Dash Core installation.\n`,
message: 'Press any key to continue...',
default: ' ',
separator: () => '',
format: () => '',
initial: true,
isTrue: () => true,
});

// eslint-disable-next-line no-param-reassign
task.output = `${coreDataPath} imported`;
},
options: {
persistentOutput: true,
},
},
]);
}

return importCoreDataTask;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import generateRandomString from '../../../util/generateRandomString.js';
* @param {configureNodeTask} configureNodeTask
* @param {configureSSLCertificateTask} configureSSLCertificateTask
* @param {DefaultConfigs} defaultConfigs
* @param {importCoreDataTask} importCoreDataTask
*/
export default function setupRegularPresetTaskFactory(
configFile,
Expand All @@ -35,6 +36,7 @@ export default function setupRegularPresetTaskFactory(
configureNodeTask,
configureSSLCertificateTask,
defaultConfigs,
importCoreDataTask,
) {
/**
* @typedef {setupRegularPresetTask}
Expand Down Expand Up @@ -131,6 +133,10 @@ export default function setupRegularPresetTaskFactory(
enabled: (ctx) => !ctx.isMasternodeRegistered && ctx.nodeType === NODE_TYPE_MASTERNODE,
task: () => registerMasternodeGuideTask(),
},
{
enabled: (ctx) => ctx.isMasternodeRegistered,
task: () => importCoreDataTask(),
},
{
enabled: (ctx) => ctx.isMasternodeRegistered || ctx.nodeType === NODE_TYPE_FULLNODE,
task: () => configureNodeTask(),
Expand Down

0 comments on commit 6632b3b

Please sign in to comment.