ssv-keys
Version:
Tool for splitting a validator key into a predefined threshold of shares via Shamir-Secret-Sharing (SSS), and encrypt them with a set of operator keys.
150 lines (122 loc) • 5.69 kB
text/typescript
import path from 'path';
import { BaseAction } from './BaseAction';
import { SSVKeys } from '../../lib/SSVKeys';
import { KeySharesItem } from '../../lib/KeyShares/KeySharesItem';
import { KeyShares } from '../../lib/KeyShares/KeyShares';
import { SSVKeysException } from '../../lib/exceptions/base';
import { sanitizePath, keystorePasswordValidator } from './validators';
import {
keystoreArgument,
ownerNonceArgument,
operatorIdsArgument,
ownerAddressArgument,
keystorePasswordArgument,
outputFolderArgument,
operatorPublicKeysArgument,
} from './arguments';
import { getFilePath, getKeyStoreFiles, readFile, writeFile } from '../../lib/helpers/file.helper';
import { OperatorsCountsMismatchError } from '../../lib/exceptions/operator';
type Operator = {
id: number;
operatorKey: string;
};
/**
* Command to build keyshares from user input.
*/
export class KeySharesAction extends BaseAction {
static override get options(): any {
return {
action: 'shares',
description: 'Generate shares for a list of operators from a validator keystore file',
arguments: [
keystoreArgument,
keystorePasswordArgument,
operatorIdsArgument,
operatorPublicKeysArgument,
outputFolderArgument,
ownerAddressArgument,
ownerNonceArgument,
],
}
}
override async execute(): Promise<string> {
this.validateKeystoreArguments(); // Validate keystore arguments
const keySharesList = await this.processKeystorePath();
const keySharesFilePath = await this.saveKeyShares(keySharesList, this.args.output_folder);
return keySharesFilePath;
}
private validateKeystoreArguments(): void {
const hasKeystore = !!this.args.keystore;
if (!hasKeystore) {
throw new SSVKeysException('Please provide a path to the validator keystore file or to the folder containing multiple validator keystore files.');
}
}
private async processKeystorePath(): Promise<KeySharesItem[]> {
const keystorePath = sanitizePath(String(this.args.keystore).trim());
const { files } = await getKeyStoreFiles(keystorePath);
const validatedFiles = await this.validateKeystoreFiles(files);
const singleKeySharesList = await Promise.all(validatedFiles.map((file, index) =>
this.processFile(file, this.args.password, this.getOperators(), this.args.owner_address, this.args.owner_nonce + index)
));
return singleKeySharesList;
}
private async validateKeystoreFiles(files: string[]): Promise<string[]> {
const validatedFiles = [];
let failedValidation = 0;
for (const [index, file] of files.entries()) {
const isKeyStoreValid = await keystoreArgument.interactive.options.validate(file);
const isValidPassword = await keystorePasswordValidator.validatePassword(this.args.password, file);
let status = '✅';
if (isKeyStoreValid === true && isValidPassword === true) {
validatedFiles.push(file);
} else {
failedValidation++;
status = '❌';
}
const fileName = path.basename(file); // Extract the file name
process.stdout.write(`\r\n${index+ 1}/${files.length} ${status} ${fileName}`);
}
process.stdout.write(`\n\n${files.length - failedValidation} of ${files.length} keystore files successfully validated. ${failedValidation} failed validation`);
process.stdout.write('\n');
return validatedFiles;
}
private getOperators(): Operator[] {
const operatorIds = this.args.operator_ids.split(',');
const operatorKeys = this.args.operator_keys.split(',');
if (operatorIds.length !== operatorKeys.length) {
throw new OperatorsCountsMismatchError(operatorIds, operatorKeys, 'Mismatch amount of operator ids and operator keys.');
}
if (operatorIds.includes('') || operatorKeys.includes('')) {
throw new SSVKeysException('Operator IDs or keys cannot contain empty strings.');
}
return operatorIds.map((idString: string, index: number) => {
const id = parseInt(idString, 10);
if (isNaN(id)) {
throw new SSVKeysException(`Invalid operator ID at position ${index}: ${idString}`);
}
const operatorKey = operatorKeys[index];
return { id, operatorKey };
});
}
private async processFile(keystoreFilePath: string, password: string, operators: Operator[], ownerAddress: string, ownerNonce: number): Promise<KeySharesItem> {
const keystoreData = await readFile(keystoreFilePath);
const ssvKeys = new SSVKeys();
const { privateKey, publicKey } = await ssvKeys.extractKeys(keystoreData, password);
const encryptedShares = await ssvKeys.buildShares(privateKey, operators);
const keySharesItem = new KeySharesItem();
await keySharesItem.update({ ownerAddress, ownerNonce, operators, publicKey });
await keySharesItem.buildPayload({ publicKey, operators, encryptedShares }, { ownerAddress, ownerNonce, privateKey });
return keySharesItem;
}
private async saveKeyShares(keySharesItems: KeySharesItem[], outputFolder: string): Promise<string> {
if (keySharesItems.length === 0) {
throw new SSVKeysException('Unable to locate valid keystore files. Please verify that the keystore files are valid and the password is correct.')
}
process.stdout.write(`\n\nGenerating Keyshares file, this might take a few minutes do not close terminal.`);
const keyShares = new KeyShares();
keySharesItems.forEach(keySharesItem => keyShares.add(keySharesItem));
const keySharesFilePath = await getFilePath('keyshares', outputFolder.trim());
await writeFile(keySharesFilePath, keyShares.toJson());
return keySharesFilePath;
}
}