@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
176 lines (152 loc) • 7.23 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {AccountId, PrivateKey, PublicKey} from '@hiero-ledger/sdk';
import {GenesisNetworkNodeDataWrapper} from './genesis-network-node-data-wrapper.js';
import * as constants from '../constants.js';
import {type KeyManager} from '../key-manager.js';
import {type ToJSON} from '../../types/index.js';
import {type JsonString, type NodeAlias} from '../../types/aliases.js';
import {GenesisNetworkRosterEntryDataWrapper} from './genesis-network-roster-entry-data-wrapper.js';
import {Templates} from '../templates.js';
import {SoloError} from '../errors/solo-error.js';
import {Flags as flags} from '../../commands/flags.js';
import {type AccountManager} from '../account-manager.js';
import {type ConsensusNode} from '../model/consensus-node.js';
import {PathEx} from '../../business/utils/path-ex.js';
import {type NodeServiceMapping} from '../../types/mappings/node-service-mapping.js';
import {type NetworkNodeServices} from '../network-node-services.js';
import {type NamespaceName} from '../../types/namespace/namespace-name.js';
/**
* Used to construct the nodes data and convert them to JSON
*/
export class GenesisNetworkDataConstructor implements ToJSON {
public readonly nodes: Record<NodeAlias, GenesisNetworkNodeDataWrapper> = {};
public readonly rosters: Record<NodeAlias, GenesisNetworkRosterEntryDataWrapper> = {};
private readonly initializationPromise: Promise<void>;
private constructor(
private readonly consensusNodes: ConsensusNode[],
private readonly keyManager: KeyManager,
private readonly accountManager: AccountManager,
private readonly keysDirectory: string,
public networkNodeServiceMap: NodeServiceMapping,
public adminPublicKeyMap: Map<NodeAlias, string>,
public domainNamesMapping?: Record<NodeAlias, string>,
) {
this.initializationPromise = (async (): Promise<void> => {
for (const consensusNode of consensusNodes) {
let adminPublicKey: PublicKey;
const networkNodeService: NetworkNodeServices = this.networkNodeServiceMap.get(consensusNode.name);
const accountId: AccountId = AccountId.fromString(networkNodeService.accountId);
const namespace: NamespaceName = networkNodeService.namespace;
if (adminPublicKeyMap.has(consensusNode.name as NodeAlias)) {
try {
if (PublicKey.fromStringED25519(adminPublicKeyMap.get(consensusNode.name))) {
adminPublicKey = PublicKey.fromStringED25519(adminPublicKeyMap.get(consensusNode.name));
}
} catch {
// Ignore error
}
}
try {
// not found existing one, generate a new key, and save to k8s secret
if (!adminPublicKey) {
const newKey: PrivateKey = PrivateKey.generateED25519();
adminPublicKey = newKey.publicKey;
try {
await this.accountManager.createOrReplaceAccountKeySecret(newKey, accountId, false, namespace);
} catch {
throw new SoloError(`failed to create secret for admin key of: ${accountId.toString()}`);
}
}
const nodeDataWrapper: GenesisNetworkNodeDataWrapper = new GenesisNetworkNodeDataWrapper(
+networkNodeService.nodeId,
adminPublicKey,
consensusNode.name,
);
this.nodes[consensusNode.name] = nodeDataWrapper;
nodeDataWrapper.accountId = accountId;
const rosterDataWrapper: GenesisNetworkRosterEntryDataWrapper = new GenesisNetworkRosterEntryDataWrapper(
+networkNodeService.nodeId,
);
this.rosters[consensusNode.name] = rosterDataWrapper;
rosterDataWrapper.weight = this.nodes[consensusNode.name].weight = constants.HEDERA_NODE_DEFAULT_STAKE_AMOUNT;
const externalPort: number = +constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT;
// Add gossip endpoints
nodeDataWrapper.addGossipEndpoint(networkNodeService.externalAddress, externalPort);
rosterDataWrapper.addGossipEndpoint(networkNodeService.externalAddress, externalPort);
const domainName: string = domainNamesMapping?.[consensusNode.name];
// Add service endpoints
nodeDataWrapper.addServiceEndpoint(domainName ?? networkNodeService.externalAddress, constants.GRPC_PORT);
} catch (error) {
throw new SoloError(error.message, error);
}
}
})();
}
public static async initialize(
consensusNodes: ConsensusNode[],
keyManager: KeyManager,
accountManager: AccountManager,
keysDirectory: string,
networkNodeServiceMap: NodeServiceMapping,
adminPublicKeys: string[],
domainNamesMapping?: Record<NodeAlias, string>,
): Promise<GenesisNetworkDataConstructor> {
const adminPublicKeyMap: Map<NodeAlias, string> = new Map();
let adminPublicKeyIsDefaultValue: boolean = true;
for (const publicKey of adminPublicKeys) {
if (publicKey !== flags.adminPublicKeys.definition.defaultValue) {
adminPublicKeyIsDefaultValue = false;
}
}
// If admin keys are passed and if it is not the default value from flags then validate and build the adminPublicKeyMap
if (adminPublicKeys.length > 0 && !adminPublicKeyIsDefaultValue) {
if (adminPublicKeys.length !== consensusNodes.length) {
throw new SoloError(
`Provide a comma separated list of DER encoded ED25519 public keys for each node, adminPublicKeys.length=${adminPublicKeys.length} does not match consensusNodes.length=${consensusNodes.length}`,
);
}
for (const [index, key] of adminPublicKeys.entries()) {
adminPublicKeyMap.set(consensusNodes[index].name, key);
}
}
const instance = new GenesisNetworkDataConstructor(
consensusNodes,
keyManager,
accountManager,
keysDirectory,
networkNodeServiceMap,
adminPublicKeyMap,
domainNamesMapping,
);
await instance.load();
return instance;
}
/**
* Loads the gossipCaCertificate and grpcCertificateHash
*/
private async load() {
await this.initializationPromise;
await Promise.all(
this.consensusNodes.map(async consensusNode => {
const signingCertFile = Templates.renderGossipPemPublicKeyFile(consensusNode.name as NodeAlias);
const signingCertFullPath = PathEx.joinWithRealPath(this.keysDirectory, signingCertFile);
const derCertificate = this.keyManager.getDerFromPemCertificate(signingCertFullPath);
//* Assign the DER formatted certificate
this.rosters[consensusNode.name].gossipCaCertificate = this.nodes[consensusNode.name].gossipCaCertificate =
Buffer.from(derCertificate).toString('base64');
//* Generate the SHA-384 hash
this.nodes[consensusNode.name].grpcCertificateHash = '';
}),
);
}
public toJSON(): JsonString {
const nodeMetadata = [];
for (const nodeAlias of Object.keys(this.nodes)) {
nodeMetadata.push({
node: this.nodes[nodeAlias].toObject(),
rosterEntry: this.rosters[nodeAlias].toObject(),
});
}
return JSON.stringify({nodeMetadata: nodeMetadata});
}
}