GenerateBlockchainConfig.java
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.subcommands.operator;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.hyperledger.besu.cli.DefaultCommandValues;
import org.hyperledger.besu.cli.util.VersionProvider;
import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.JsonGenesisConfigOptions;
import org.hyperledger.besu.config.JsonUtil;
import org.hyperledger.besu.consensus.ibft.IbftExtraDataCodec;
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SECPPrivateKey;
import org.hyperledger.besu.crypto.SECPPublicKey;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.crypto.SignatureAlgorithmType;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.core.Util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.IntStream;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.io.Resources;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParentCommand;
@Command(
name = "generate-blockchain-config",
description = "Generate node keypairs and genesis file with RLP encoded extra data.",
mixinStandardHelpOptions = true,
versionProvider = VersionProvider.class)
class GenerateBlockchainConfig implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(GenerateBlockchainConfig.class);
private final Supplier<SignatureAlgorithm> SIGNATURE_ALGORITHM =
Suppliers.memoize(SignatureAlgorithmFactory::getInstance);
@Option(
required = true,
names = "--config-file",
paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
description = "Configuration file.",
arity = "1..1")
private final File configurationFile = null;
@Option(
required = true,
names = "--to",
paramLabel = DefaultCommandValues.MANDATORY_DIRECTORY_FORMAT_HELP,
description = "Directory to write output files to.",
arity = "1..1")
private final File outputDirectory = null;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = "--genesis-file-name",
paramLabel = DefaultCommandValues.MANDATORY_PATH_FORMAT_HELP,
description = "Name of the genesis file. (default: ${DEFAULT-VALUE})",
arity = "1..1")
private String genesisFileName = "genesis.json";
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = "--private-key-file-name",
paramLabel = DefaultCommandValues.MANDATORY_PATH_FORMAT_HELP,
description = "Name of the private key file. (default: ${DEFAULT-VALUE})",
arity = "1..1")
private String privateKeyFileName = "key.priv";
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = "--public-key-file-name",
paramLabel = DefaultCommandValues.MANDATORY_PATH_FORMAT_HELP,
description = "Name of the public key file. (default: ${DEFAULT-VALUE})",
arity = "1..1")
private String publicKeyFileName = "key.pub";
@ParentCommand
private OperatorSubCommand parentCommand; // Picocli injects reference to parent command
private ObjectNode operatorConfig;
private ObjectNode genesisConfig;
private ObjectNode blockchainConfig;
private ObjectNode nodesConfig;
private boolean generateNodesKeys;
private final List<Address> addressesForGenesisExtraData = new ArrayList<>();
private Path keysDirectory;
@Override
public void run() {
checkPreconditions();
generateBlockchainConfig();
}
private void checkPreconditions() {
checkNotNull(parentCommand);
checkNotNull(parentCommand.parentCommand);
if (isAnyDuplicate(genesisFileName, publicKeyFileName, privateKeyFileName)) {
throw new IllegalArgumentException("Output file paths must be unique.");
}
}
/** Generates output directory with all required configuration files. */
private void generateBlockchainConfig() {
try {
handleOutputDirectory();
parseConfig();
processEcCurve();
if (generateNodesKeys) {
generateNodesKeys();
} else {
importPublicKeysFromConfig();
}
processExtraData();
writeGenesisFile(outputDirectory, genesisFileName, genesisConfig);
} catch (final IOException e) {
LOG.error("An error occurred while trying to generate network configuration.", e);
}
}
/** Imports public keys from input configuration. */
private void importPublicKeysFromConfig() {
LOG.info("Importing public keys from configuration.");
JsonUtil.getArrayNode(nodesConfig, "keys")
.ifPresent(keys -> keys.forEach(this::importPublicKey));
}
/**
* Imports a single public key.
*
* @param publicKeyJson The public key.
*/
private void importPublicKey(final JsonNode publicKeyJson) {
if (publicKeyJson.getNodeType() != JsonNodeType.STRING) {
throw new IllegalArgumentException(
"Invalid key json of type: " + publicKeyJson.getNodeType());
}
final String publicKeyText = publicKeyJson.asText();
try {
final SECPPublicKey publicKey =
SIGNATURE_ALGORITHM.get().createPublicKey(Bytes.fromHexString(publicKeyText));
if (!SIGNATURE_ALGORITHM.get().isValidPublicKey(publicKey)) {
throw new IllegalArgumentException(
publicKeyText
+ " is not a valid public key for elliptic curve "
+ SIGNATURE_ALGORITHM.get().getCurveName());
}
writeKeypair(publicKey, null);
LOG.info("Public key imported from configuration.({})", publicKey.toString());
} catch (final IOException e) {
LOG.error("An error occurred while trying to import node public key.", e);
}
}
/** Generates nodes keypairs. */
private void generateNodesKeys() {
final int nodesCount = JsonUtil.getInt(nodesConfig, "count", 0);
LOG.info("Generating {} nodes keys.", nodesCount);
IntStream.range(0, nodesCount).forEach(this::generateNodeKeypair);
}
/**
* Generate a keypair for a node.
*
* @param node The number of the node.
*/
private void generateNodeKeypair(final int node) {
try {
LOG.info("Generating keypair for node {}.", node);
final KeyPair keyPair = SIGNATURE_ALGORITHM.get().generateKeyPair();
writeKeypair(keyPair.getPublicKey(), keyPair.getPrivateKey());
} catch (final IOException e) {
LOG.error("An error occurred while trying to generate node keypair.", e);
}
}
/**
* Writes public and private keys in separate files. Both are written in the same directory named
* with the address derived from the public key.
*
* @param publicKey The public key.
* @param privateKey The private key. No file is created if privateKey is NULL.
* @throws IOException If the file cannot be written or accessed.
*/
private void writeKeypair(final SECPPublicKey publicKey, final SECPPrivateKey privateKey)
throws IOException {
final Address nodeAddress = Util.publicKeyToAddress(publicKey);
addressesForGenesisExtraData.add(nodeAddress);
final Path nodeDirectoryPath = keysDirectory.resolve(nodeAddress.toString());
Files.createDirectory(nodeDirectoryPath);
createFileAndWrite(nodeDirectoryPath, publicKeyFileName, publicKey.toString());
if (privateKey != null) {
createFileAndWrite(nodeDirectoryPath, privateKeyFileName, privateKey.toString());
}
}
/** Computes RLP encoded exta data from pre filled list of addresses. */
private void processExtraData() {
final ObjectNode configNode =
JsonUtil.getObjectNode(genesisConfig, "config")
.orElseThrow(
() -> new IllegalArgumentException("Missing config section in config file"));
final JsonGenesisConfigOptions genesisConfigOptions =
JsonGenesisConfigOptions.fromJsonObject(configNode);
if (genesisConfigOptions.isIbft2()) {
LOG.info("Generating IBFT extra data.");
final String extraData =
IbftExtraDataCodec.encodeFromAddresses(addressesForGenesisExtraData).toString();
genesisConfig.put("extraData", extraData);
} else if (genesisConfigOptions.isQbft()) {
LOG.info("Generating QBFT extra data.");
final String extraData =
QbftExtraDataCodec.encodeFromAddresses(addressesForGenesisExtraData).toString();
genesisConfig.put("extraData", extraData);
}
}
private void createFileAndWrite(final Path directory, final String fileName, final String content)
throws IOException {
final Path filePath = directory.resolve(fileName);
Files.write(filePath, content.getBytes(UTF_8), StandardOpenOption.CREATE_NEW);
}
/**
* Parses the root configuration file and related sub elements.
*
* @throws IOException If the file cannot be read or accessed.
*/
private void parseConfig() throws IOException {
final String configString =
Resources.toString(configurationFile.toPath().toUri().toURL(), UTF_8);
final ObjectNode root = JsonUtil.objectNodeFromString(configString);
operatorConfig = root;
genesisConfig =
JsonUtil.getObjectNode(operatorConfig, "genesis").orElse(JsonUtil.createEmptyObjectNode());
blockchainConfig =
JsonUtil.getObjectNode(operatorConfig, "blockchain")
.orElse(JsonUtil.createEmptyObjectNode());
nodesConfig =
JsonUtil.getObjectNode(blockchainConfig, "nodes").orElse(JsonUtil.createEmptyObjectNode());
generateNodesKeys = JsonUtil.getBoolean(nodesConfig, "generate", false);
}
/** Sets the selected signature algorithm instance in SignatureAlgorithmFactory. */
private void processEcCurve() {
GenesisConfigOptions options =
GenesisConfigFile.fromConfigWithoutAccounts(String.valueOf(genesisConfig))
.getConfigOptions();
Optional<String> ecCurve = options.getEcCurve();
if (ecCurve.isEmpty()) {
SignatureAlgorithmFactory.setInstance(SignatureAlgorithmType.createDefault());
return;
}
try {
SignatureAlgorithmFactory.setInstance(SignatureAlgorithmType.create(ecCurve.get()));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid parameter for ecCurve in genesis config: " + e.getMessage());
}
}
/**
* Checks if the output directory exists.
*
* @throws IOException If the cannot be accessed or created.
*/
private void handleOutputDirectory() throws IOException {
checkNotNull(outputDirectory);
final Path outputDirectoryPath = outputDirectory.toPath();
if (outputDirectory.exists()
&& outputDirectory.isDirectory()
&& outputDirectory.list() != null
&& outputDirectory.list().length > 0) {
throw new IllegalArgumentException("Output directory already exists.");
} else if (!outputDirectory.exists()) {
Files.createDirectory(outputDirectoryPath);
}
keysDirectory = outputDirectoryPath.resolve("keys");
Files.createDirectory(keysDirectory);
}
/**
* Write the content of the genesis to the output file.
*
* @param directory The directory to write the file to.
* @param fileName The name of the output file.
* @param genesis The genesis content.
* @throws IOException If the genesis file cannot be written or accessed.
*/
private void writeGenesisFile(
final File directory, final String fileName, final ObjectNode genesis) throws IOException {
LOG.info("Writing genesis file.");
Files.write(
directory.toPath().resolve(fileName),
JsonUtil.getJson(genesis).getBytes(UTF_8),
StandardOpenOption.CREATE_NEW);
}
private static boolean isAnyDuplicate(final String... values) {
final Set<String> set = new HashSet<>();
for (final String value : values) {
if (!set.add(value)) {
return true;
}
}
return false;
}
}