animiassumenda
Version:
Blox live, desktop app for managing staking accounts
362 lines (326 loc) • 13 kB
text/typescript
import config from '~app/backend/common/config';
import { Catch, Step } from '~app/backend/decorators';
import { Log } from '~app/backend/common/logger/logger';
import { isVersionHigherOrEqual } from '~app/utils/service';
import Connection from '~app/backend/common/store-manager/connection';
import BloxApi from '~app/backend/common/communication-manager/blox-api';
import VersionService from '~app/backend/services/version/version.service';
import { METHOD } from '~app/backend/common/communication-manager/constants';
import KeyVaultSsh from '~app/backend/common/communication-manager/key-vault-ssh';
import KeyVaultApi from '~app/backend/common/communication-manager/key-vault-api';
function sleep(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
export default class KeyVaultService {
private readonly keyVaultSsh: KeyVaultSsh;
private readonly keyVaultApi: KeyVaultApi;
private readonly versionService: VersionService;
private readonly bloxApi: BloxApi;
private storePrefix: string;
private logger: Log;
constructor(prefix: string = '') {
this.storePrefix = prefix;
this.keyVaultSsh = new KeyVaultSsh(this.storePrefix);
this.versionService = new VersionService();
this.keyVaultApi = new KeyVaultApi(this.storePrefix);
this.bloxApi = new BloxApi();
this.bloxApi.init();
this.logger = new Log('key-vault');
}
async updateStorage(payload: any) {
return await this.keyVaultApi.requestThruSsh({
method: METHOD.POST,
path: 'storage',
data: payload
});
}
async listAccounts() {
this.logger.info('try to get keyVault server accounts...');
try {
const response = await this.keyVaultApi.requestThruSsh({
method: METHOD.LIST,
path: 'accounts'
});
return response?.data.accounts || [];
} catch (e) {
this.logger.error(e);
const { errors } = JSON.parse(e.message);
if (Array.isArray(errors)) {
// eslint-disable-next-line no-restricted-syntax
for (const err of errors) {
if (err.includes('wallet not found')) {
return [];
}
}
}
throw e;
}
}
async healthCheck() {
return await this.keyVaultApi.requestThruSsh({
method: METHOD.GET,
path: 'sys/health',
isNetworkRequired: false
});
}
async getVersion() {
const mainnetVersion = await this.keyVaultApi.requestThruSsh({
method: METHOD.GET,
path: `ethereum/${config.env.MAINNET_NETWORK}/version`,
isNetworkRequired: false
});
let testNetwork = 'pyrmont';
const keyVaultVersion = await this.versionService.getLatestKeyVaultVersion();
// TODO: move gap version to constant in config.ts
if (isVersionHigherOrEqual(keyVaultVersion, 'v1.2.0')) {
testNetwork = 'prater';
}
const testnetVersion = await this.keyVaultApi.requestThruSsh({
method: METHOD.GET,
path: `ethereum/${testNetwork}/version`,
isNetworkRequired: false
});
const result = {
mainnetVersion,
testnetVersion
};
this.logger.trace('KV server healthcheck', result);
return result;
}
async getSlashingStorage() {
try {
const response = await this.keyVaultApi.requestThruSsh({
method: METHOD.GET,
path: 'storage/slashing'
});
return response?.data || {};
} catch (e) {
this.logger.error(e);
const { errors } = JSON.parse(e.message);
if (Array.isArray(errors)) {
// eslint-disable-next-line no-restricted-syntax
for (const err of errors) {
if (err.includes('wallet not found')) {
return {};
}
}
}
throw e;
}
}
async getContainerId() {
console.log('getcontainerid');
const ssh = await this.keyVaultSsh.getConnection({ force: true });
console.log('getcontainerid ssh', ssh);
const { stdout: containerId, stderr: error } = await ssh.execCommand('docker ps -aq -f "status=running" -f "name=key_vault"', {});
console.log('getcontainerid containerId', containerId, error);
if (error) {
throw new Error('Could not reach Docker Container');
}
return containerId;
}
async installDockerScope(): Promise<void> {
const ssh = await this.keyVaultSsh.getConnection();
const { stdout } = await ssh.execCommand('docker -v', {});
const installedAlready = stdout.includes('version');
if (installedAlready) return;
await ssh.execCommand('sudo yum update -y', {});
await ssh.execCommand('sudo yum install docker -y', {});
await ssh.execCommand('sudo service docker start', {});
await ssh.execCommand('sudo usermod -a -G docker ec2-user', {});
}
async getKeyVaultRootToken(): Promise<void> {
const ssh = await this.keyVaultSsh.getConnection();
const { stdout: rootToken } = await ssh.execCommand('sudo cat data/keys/vault.root.token', {});
if (!rootToken) throw new Error('vault-plugin rootToken not found');
Connection.db(this.storePrefix).set('vaultRootToken', rootToken);
const { stdout: signerToken } = await ssh.execCommand('sudo cat data/keys/vault.signer.token', {});
if (!signerToken) throw new Error('vault-plugin signerToken not found');
Connection.db(this.storePrefix).set('vaultSignerToken', signerToken);
this.logger.debug('ROOTSIGNER', rootToken, signerToken);
}
async runDockerContainer(): Promise<void> {
const containerId = await this.getContainerId();
console.log('containerId', containerId);
if (containerId) {
return;
}
const keyVaultVersion = await this.versionService.getLatestKeyVaultVersion();
console.log('keyVaultVersion', keyVaultVersion);
const envKey = (Connection.db(this.storePrefix).get('env') || 'production');
const dockerHubImage = `bloxstaking/key-vault${envKey === 'production' ? '' : '-rc'}:${keyVaultVersion}`;
this.logger.info(`Going to run docker based on ${dockerHubImage} keyvault image`);
const dockerCMD = 'docker start key_vault 2>/dev/null || ' +
`docker pull ${dockerHubImage} && docker run -d --restart unless-stopped --cap-add=IPC_LOCK --name=key_vault ` +
'-v $(pwd)/data:/data ' +
'-v $(pwd)/policies:/policies ' +
'-p 8200:8200 ' +
`-e VAULT_EXTERNAL_ADDRESS='${Connection.db(this.storePrefix).get('publicIp')}' ` +
'-e UNSEAL=true ' +
`-e VAULT_CLIENT_TIMEOUT='30s' '${dockerHubImage}'`;
console.log('dockerCMD', dockerCMD);
const ssh = await this.keyVaultSsh.getConnection();
console.log('ssh', ssh);
const { stdout, stderr: error } = await ssh.execCommand(
dockerCMD,
{}
);
console.log('stdout', stdout);
console.log('stderr', error);
Connection.db(this.storePrefix).set('keyVaultVersion', keyVaultVersion);
Connection.db(this.storePrefix).set('keyVaultPluginVersion', keyVaultVersion);
await sleep(12000);
if (error) {
this.logger.error(error);
throw new Error(`Failed to run Key Vault docker container: ${error}`);
}
console.log('completed setup docker');
}
async upgradePlugin(): Promise<void> {
const keyVaultVersion = await this.versionService.getLatestKeyVaultVersion();
const envKey = (Connection.db(this.storePrefix).get('env') || 'production');
const dockerHubImage = `bloxstaking/key-vault${envKey === 'production' ? '' : '-rc'}:${keyVaultVersion}`;
this.logger.info(`Going to run docker based on ${dockerHubImage} keyvault image`);
const dockerCMD =
`docker pull ${dockerHubImage} && docker run -d --name=upgrade_key_vault ${dockerHubImage} && ` +
'docker cp upgrade_key_vault:/vault/plugins/ethsign ./ &&' +
'docker cp ethsign key_vault:/vault/plugins/';
const vaultCMD =
'docker exec key_vault bash -c ' +
'"vault plugin register -tls-skip-verify -sha256=$(sha256sum ethsign | cut -d\' \' -f1) secret ethsign &&' +
'vault plugin reload -tls-skip-verify -plugin ethsign"';
const cleanCMD =
'docker rm -f upgrade_key_vault && docker image prune -f -a && rm ethsign';
// 1. ssh connect to the instance
const ssh = await this.keyVaultSsh.getConnection();
// 2. exec docker command
const { stderr: errorDockerCMD } = await ssh.execCommand(dockerCMD);
if (errorDockerCMD) {
this.logger.error(errorDockerCMD);
throw new Error(`Failed to run docker command: ${errorDockerCMD}`);
}
// 3. exec vault command
const { stderr: errorVaultCMD } = await ssh.execCommand(vaultCMD);
if (errorVaultCMD) {
this.logger.error(errorVaultCMD);
throw new Error(`Failed to run vault command: ${errorVaultCMD}`);
}
// 4. exec clean command
const { stderr: errorCleanCMD } = await ssh.execCommand(cleanCMD);
if (errorCleanCMD) {
this.logger.error(errorCleanCMD);
throw new Error(`Failed to run clean command: ${errorCleanCMD}`);
}
Connection.db(this.storePrefix).set('keyVaultPluginVersion', keyVaultVersion);
}
async updateVaultStorage(): Promise<void> {
const network = Connection.db(this.storePrefix).get('network');
if (Connection.db(this.storePrefix).exists(`keyVaultStorage.${network}`)) {
await this.updateStorage({ data: Connection.db(this.storePrefix).get(`keyVaultStorage.${network}`) });
Connection.db(this.storePrefix).delete(`keyVaultStorage.${network}`);
}
}
async updateVaultMountsStorage(): Promise<any> {
const keyVaultStorage = Connection.db(this.storePrefix).get('keyVaultStorage');
if (keyVaultStorage) {
// eslint-disable-next-line no-restricted-syntax
for (const [network, storage] of Object.entries(keyVaultStorage)) {
if (storage) {
Connection.db(this.storePrefix).set('network', network);
// eslint-disable-next-line no-await-in-loop
await this.updateVaultStorage();
}
}
Connection.db(this.storePrefix).delete('keyVaultStorage');
}
return { isActive: true };
}
async importKeyVaultData(): Promise<any> {
const supportedNetworks = [config.TESTNET_NETWORK, config.env.MAINNET_NETWORK];
// eslint-disable-next-line no-restricted-syntax
for (const network of supportedNetworks) {
Connection.db(this.storePrefix).set('network', network);
// save latest network index
// eslint-disable-next-line no-await-in-loop
const accounts = await this.listAccounts();
Connection.db(this.storePrefix).set(`index.${network}`, (accounts.length - 1).toString());
// eslint-disable-next-line no-await-in-loop
await this.importSlashingData();
}
}
async importSlashingData(): Promise<any> {
const keyVaultVersion = Connection.db(this.storePrefix).get('keyVaultVersion');
if (keyVaultVersion && isVersionHigherOrEqual(keyVaultVersion, config.env.HIGHEST_ATTESTATION_SUPPORTED_TAG)) {
const network = Connection.db(this.storePrefix).get('network');
const slashingData = await this.getSlashingStorage();
if (Object.keys(slashingData).length) {
Connection.db(this.storePrefix).set(`slashingData.${network}`, slashingData);
}
}
}
async getKeyVaultStatus(retries = 2) {
// check if the key vault is alive
try {
await this.getVersion();
return { isActive: true };
} catch (e) {
if (retries === 0) {
throw e;
}
this.logger.error('Sync keyvault server healthcheck failed', e);
await new Promise((resolve) => setTimeout(resolve, 3000)); // hard delay for 2sec
return await this.getKeyVaultStatus(retries - 1);
}
}
async configurateSshd() {
if (Connection.db(this.storePrefix).get('port')) {
return;
}
try {
await this.keyVaultSsh.getConnection({ customPort: config.env.TARGET_SSH_PORT });
} catch (e) {
const ssh = await this.keyVaultSsh.getConnection();
const { stderr: error } = await ssh.execCommand(`sudo sed -i '1iPort ${config.env.TARGET_SSH_PORT}\\nLoginGraceTime 30s\\nUseDNS no' /etc/ssh/sshd_config && sudo service sshd restart`, {});
if (error) {
this.logger.error(error);
throw new Error('Could not setup sshd configuration');
}
}
Connection.db(this.storePrefix).set('port', config.env.TARGET_SSH_PORT);
}
}