diff --git a/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java b/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java index 5b534a5db4..339c7f3735 100644 --- a/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java +++ b/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java @@ -31,6 +31,10 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions { private static final String CLIQUE_CONFIG_KEY = "clique"; private final JsonObject configRoot; + public static JsonGenesisConfigOptions fromJsonObject(final JsonObject configRoot) { + return new JsonGenesisConfigOptions(configRoot); + } + JsonGenesisConfigOptions(final JsonObject configRoot) { this.configRoot = configRoot != null ? configRoot : new JsonObject(); } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java index 3be7f4d659..13400b5e0c 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java @@ -65,6 +65,11 @@ public IbftExtraData( this.vote = vote; } + public static IbftExtraData fromAddresses(final Collection
addresses) { + return new IbftExtraData( + BytesValue.wrap(new byte[32]), Collections.emptyList(), Optional.empty(), 0, addresses); + } + public static IbftExtraData decode(final BlockHeader blockHeader) { final Object inputExtraData = blockHeader.getParsedExtraData(); if (inputExtraData instanceof IbftExtraData) { diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 3b554f502d..853558ff0d 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -26,7 +26,7 @@ dependencyManagement { dependency 'com.google.errorprone:error_prone_test_helpers:2.3.3' dependency 'com.graphql-java:graphql-java:12.0' - + dependency 'com.google.guava:guava:27.1-jre' dependency 'com.squareup.okhttp3:okhttp:3.14.2' @@ -78,7 +78,7 @@ dependencyManagement { dependency 'org.rocksdb:rocksdbjni:5.15.10' dependency 'org.springframework.security:spring-security-crypto:5.1.5.RELEASE' - + dependency 'org.web3j:abi:4.3.0' dependency 'org.web3j:core:4.3.0' dependency 'org.web3j:crypto:4.3.0' diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index c95795ff44..84c218b0b1 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -36,6 +36,7 @@ import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty; import tech.pegasys.pantheon.cli.custom.JsonRPCWhitelistHostsProperty; import tech.pegasys.pantheon.cli.custom.RpcAuthFileValidator; +import tech.pegasys.pantheon.cli.operator.OperatorSubCommand; import tech.pegasys.pantheon.cli.rlp.RLPSubCommand; import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.controller.KeyPairUtil; @@ -645,6 +646,8 @@ public void parse( PasswordSubCommand.COMMAND_NAME, new PasswordSubCommand(resultHandler.out())); commandLine.addSubcommand( RLPSubCommand.COMMAND_NAME, new RLPSubCommand(resultHandler.out(), in)); + commandLine.addSubcommand( + OperatorSubCommand.COMMAND_NAME, new OperatorSubCommand(resultHandler.out())); commandLine.registerConverter(Address.class, Address::fromHexStringStrict); commandLine.registerConverter(BytesValue.class, BytesValue::fromHexString); diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommand.java new file mode 100644 index 0000000000..5c2293e21d --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommand.java @@ -0,0 +1,320 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.cli.operator; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; +import static tech.pegasys.pantheon.cli.DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP; +import static tech.pegasys.pantheon.cli.DefaultCommandValues.MANDATORY_PATH_FORMAT_HELP; +import static tech.pegasys.pantheon.cli.operator.OperatorSubCommand.COMMAND_NAME; + +import tech.pegasys.pantheon.cli.PantheonCommand; +import tech.pegasys.pantheon.config.JsonGenesisConfigOptions; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +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.Set; +import java.util.stream.IntStream; + +import com.google.common.io.Resources; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; + +/** Operator related sub-command */ +@Command( + name = COMMAND_NAME, + description = "This command provides operator related actions.", + mixinStandardHelpOptions = true, + subcommands = {OperatorSubCommand.GenerateNetworkConfigSubCommand.class}) +public class OperatorSubCommand implements Runnable { + private static final Logger LOG = LogManager.getLogger(); + + public static final String COMMAND_NAME = "operator"; + public static final String GENERATE_BLOCKCHAIN_CONFIG_SUBCOMMAND_NAME = + "generate-blockchain-config"; + + @SuppressWarnings("unused") + @ParentCommand + private PantheonCommand parentCommand; // Picocli injects reference to parent command + + @SuppressWarnings("unused") + @Spec + private CommandSpec spec; // Picocli injects reference to command spec + + private final PrintStream out; + + public OperatorSubCommand(final PrintStream out) { + this.out = out; + } + + @Override + public void run() { + spec.commandLine().usage(out); + } + + @Command( + name = "generate-blockchain-config", + description = "This command generates blockchain network configuration files.", + mixinStandardHelpOptions = true) + static class GenerateNetworkConfigSubCommand implements Runnable { + @Option( + required = true, + names = "--config-file", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "Configuration file.", + arity = "1..1") + private File configurationFile = null; + + @Option( + required = true, + names = "--to", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "Directory to write output files to.", + arity = "1..1") + private File outputDirectory = null; + + @Option( + names = "--genesis-file-name", + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "Name of the genesis file. (default: ${DEFAULT-VALUE})", + arity = "1..1") + private String genesisFileName = "genesis.json"; + + @Option( + names = "--private-key-file-name", + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "Name of the private key file. (default: ${DEFAULT-VALUE})", + arity = "1..1") + private String privateKeyFileName = "key.priv"; + + @Option( + names = "--public-key-file-name", + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "Name of the public key file. (default: ${DEFAULT-VALUE})", + arity = "1..1") + private String publicKeyFileName = "key.pub"; + + @SuppressWarnings("unused") + @ParentCommand + private OperatorSubCommand parentCommand; // Picocli injects reference to parent command + + private JsonObject operatorConfig; + private JsonObject genesisConfig; + private JsonObject blockchainConfig; + private JsonObject nodesConfig; + private boolean generateNodesKeys; + private List
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(); + if (generateNodesKeys) { + generateNodesKeys(); + } else { + importPublicKeysFromConfig(); + } + processExtraData(); + writeGenesisFile(outputDirectory, genesisFileName, genesisConfig); + } catch (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."); + JsonArray keys = nodesConfig.getJsonArray("keys"); + keys.stream().forEach(this::importPublicKey); + } + + /** + * Imports a single public key. + * + * @param publicKeyObject The public key. + */ + private void importPublicKey(final Object publicKeyObject) { + try { + final SECP256K1.PublicKey publicKey = + SECP256K1.PublicKey.create(BytesValue.fromHexString((String) publicKeyObject)); + writeKeypair(publicKey, null); + LOG.info("Public key imported from configuration.({})", publicKey.toString()); + } catch (IOException e) { + LOG.error("An error occurred while trying to import node public key.", e); + } + } + + /** Generates nodes keypairs. */ + private void generateNodesKeys() { + final int nodesCount = nodesConfig.getInteger("count"); + 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 SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + writeKeypair(keyPair.getPublicKey(), keyPair.getPrivateKey()); + + } catch (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 + */ + private void writeKeypair( + final SECP256K1.PublicKey publicKey, final SECP256K1.PrivateKey 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. + * + * @throws IOException + */ + private void processExtraData() { + final JsonGenesisConfigOptions genesisConfigOptions = + JsonGenesisConfigOptions.fromJsonObject(genesisConfig.getJsonObject("config")); + if (genesisConfigOptions.isIbft2()) { + LOG.info("Generating IBFT extra data."); + final String extraData = + IbftExtraData.fromAddresses(addressesForGenesisExtraData).encode().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 + */ + private void parseConfig() throws IOException { + final String configString = + Resources.toString(configurationFile.toPath().toUri().toURL(), UTF_8); + operatorConfig = new JsonObject(configString); + genesisConfig = operatorConfig.getJsonObject("genesis"); + blockchainConfig = operatorConfig.getJsonObject("blockchain"); + nodesConfig = blockchainConfig.getJsonObject("nodes"); + generateNodesKeys = nodesConfig.getBoolean("generate", false); + } + + /** + * Checks if the output directory exists. + * + * @throws IOException + */ + 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 must be empty."); + } 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 + */ + private void writeGenesisFile( + final File directory, final String fileName, final JsonObject genesis) throws IOException { + LOG.info("Writing genesis file."); + Files.write( + directory.toPath().resolve(fileName), + genesis.encodePrettily().getBytes(UTF_8), + StandardOpenOption.CREATE_NEW); + } + } + + private static boolean isAnyDuplicate(final String... values) { + final Set set = new HashSet<>(); + for (String value : values) { + if (!set.add(value)) { + return true; + } + } + return false; + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java index 9230d67600..2dad10fa9b 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java @@ -18,8 +18,6 @@ import java.io.IOException; import java.util.Collection; -import java.util.Collections; -import java.util.Optional; import java.util.stream.Collectors; import com.fasterxml.jackson.core.type.TypeReference; @@ -30,18 +28,18 @@ * encoding */ public class IbftExtraDataCLIAdapter implements JSONToRLP { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final TypeReference> TYPE_REF = + new TypeReference>() {}; @Override public BytesValue encode(final String json) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - TypeReference> typeRef = new TypeReference>() {}; - Collection validatorAddresse = mapper.readValue(json, typeRef); - - Collection
addresses = - validatorAddresse.stream().map(Address::fromHexString).collect(Collectors.toList()); + return fromJsonAddresses(json).encode(); + } - return new IbftExtraData( - BytesValue.wrap(new byte[32]), Collections.emptyList(), Optional.empty(), 0, addresses) - .encode(); + public static IbftExtraData fromJsonAddresses(final String jsonAddresses) throws IOException { + final Collection validatorAddresses = MAPPER.readValue(jsonAddresses, TYPE_REF); + return IbftExtraData.fromAddresses( + validatorAddresses.stream().map(Address::fromHexString).collect(Collectors.toList())); } } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommandTest.java new file mode 100644 index 0000000000..09d9a9f977 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommandTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2019 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. + */ +package tech.pegasys.pantheon.cli.operator; + +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.createTempDirectory; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.junit.Assert.assertTrue; +import static tech.pegasys.pantheon.cli.operator.OperatorSubCommand.COMMAND_NAME; +import static tech.pegasys.pantheon.cli.operator.OperatorSubCommand.GENERATE_BLOCKCHAIN_CONFIG_SUBCOMMAND_NAME; +import static tech.pegasys.pantheon.cli.operator.OperatorSubCommandTest.Cmd.cmd; + +import tech.pegasys.pantheon.cli.CommandTestAbstract; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.json.JsonObject; +import org.junit.Before; +import org.junit.Test; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +public class OperatorSubCommandTest extends CommandTestAbstract { + + private static final String EXPECTED_OPERATOR_USAGE = + "Usage: pantheon operator [-hV] [COMMAND]" + + System.lineSeparator() + + "This command provides operator related actions." + + System.lineSeparator() + + " -h, --help Show this help message and exit." + + System.lineSeparator() + + " -V, --version Print version information and exit." + + System.lineSeparator() + + "Commands:" + + System.lineSeparator() + + " generate-blockchain-config This command generates blockchain network" + + System.lineSeparator() + + " configuration files."; + + private Path tmpOutputDirectoryPath; + + @Before + public void init() throws IOException { + tmpOutputDirectoryPath = createTempDirectory(format("output-%d", currentTimeMillis())); + } + + @Test + public void operatorSubCommandExistAndHaveSubCommands() { + final CommandSpec spec = parseCommand(); + assertThat(spec.subcommands()).containsKeys(COMMAND_NAME); + assertThat(spec.subcommands().get(COMMAND_NAME).getSubcommands()) + .containsKeys(GENERATE_BLOCKCHAIN_CONFIG_SUBCOMMAND_NAME); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingOperatorSubCommandWithoutSubSubcommandMustDisplayUsage() { + parseCommand(COMMAND_NAME); + assertThat(commandOutput.toString()).startsWith(EXPECTED_OPERATOR_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingOperatorCommandHelpMustDisplayUsage() { + parseCommand(COMMAND_NAME, "--help"); + assertThat(commandOutput.toString()).startsWith(EXPECTED_OPERATOR_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void generateBlockchainConfigMustGenerateKeysWhenGenerateIsTrue() throws IOException { + runCmdAndCheckOutput( + cmd(), + "/operator/config_generate_keys.json", + tmpOutputDirectoryPath, + "genesis.json", + true, + asList("key.pub", "key.priv")); + } + + @Test + public void generateBlockchainConfigMustImportKeysWhenGenerateIsFalse() throws IOException { + runCmdAndCheckOutput( + cmd(), + "/operator/config_import_keys.json", + tmpOutputDirectoryPath, + "genesis.json", + false, + singletonList("key.pub")); + } + + @Test + public void genesisFileNameShouldBeEqualToOption() throws IOException { + runCmdAndCheckOutput( + cmd("--genesis-file-name", "option.json"), + "/operator/config_generate_keys.json", + tmpOutputDirectoryPath, + "option.json", + true, + asList("key.pub", "key.priv")); + } + + @Test + public void publicKeyFileNameShouldBeEqualToOption() throws IOException { + runCmdAndCheckOutput( + cmd("--public-key-file-name", "pub.test"), + "/operator/config_generate_keys.json", + tmpOutputDirectoryPath, + "genesis.json", + true, + asList("pub.test", "key.priv")); + } + + @Test + public void privateKeyFileNameShouldBeEqualToOption() throws IOException { + runCmdAndCheckOutput( + cmd("--private-key-file-name", "priv.test"), + "/operator/config_generate_keys.json", + tmpOutputDirectoryPath, + "genesis.json", + true, + asList("key.pub", "priv.test")); + } + + @Test(expected = CommandLine.ExecutionException.class) + public void shouldFailIfDuplicateFiles() throws IOException { + runCmdAndCheckOutput( + cmd("--private-key-file-name", "dup.test", "--public-key-file-name", "dup.test"), + "/operator/config_generate_keys.json", + tmpOutputDirectoryPath, + "genesis.json", + true, + asList("key.pub", "priv.test")); + } + + @Test(expected = CommandLine.ExecutionException.class) + public void shouldFailIfOutputDirectoryNonEmpty() throws IOException { + runCmdAndCheckOutput( + cmd(), + "/operator/config_generate_keys.json", + FileSystems.getDefault().getPath("."), + "genesis.json", + true, + asList("key.pub", "key.priv")); + } + + private void runCmdAndCheckOutput( + final Cmd cmd, + final String configFile, + final Path outputDirectoryPath, + final String genesisFileName, + final boolean generate, + final Collection expectedKeyFiles) + throws IOException { + final URL configFilePath = this.getClass().getResource(configFile); + parseCommand( + cmd( + COMMAND_NAME, + GENERATE_BLOCKCHAIN_CONFIG_SUBCOMMAND_NAME, + "--config-file", + configFilePath.getPath(), + "--to", + outputDirectoryPath.toString()) + .args(cmd.argsArray()) + .argsArray()); + assertThat(commandErrorOutput.toString()).isEmpty(); + + final Path outputGenesisExpectedPath = outputDirectoryPath.resolve(genesisFileName); + final File outputGenesisFile = new File(outputGenesisExpectedPath.toUri()); + assertTrue("Output genesis file must exist.", outputGenesisFile.exists()); + final String genesisString = contentOf(outputGenesisFile, UTF_8); + final JsonObject genesisContent = new JsonObject(genesisString); + assertTrue(genesisContent.containsKey("extraData")); + + final Path expectedKeysPath = outputDirectoryPath.resolve("keys"); + final File keysDirectory = new File(expectedKeysPath.toUri()); + assertTrue(keysDirectory.exists()); + final File[] nodesKeysFolders = keysDirectory.listFiles(); + assert nodesKeysFolders != null; + if (generate) { + final JsonFactory jsonFactory = new JsonFactory(); + final JsonParser jp = jsonFactory.createParser(configFilePath); + jp.setCodec(new ObjectMapper()); + final JsonNode jsonNode = jp.readValueAsTree(); + final int nodeCount = jsonNode.get("blockchain").get("nodes").get("count").asInt(); + assertThat(nodeCount).isEqualTo(nodesKeysFolders.length); + } + final Stream nodesKeysFoldersStream = stream(nodesKeysFolders); + + nodesKeysFoldersStream.forEach( + nodeFolder -> + assertTrue(asList(requireNonNull(nodeFolder.list())).containsAll(expectedKeyFiles))); + } + + static class Cmd { + private List args; + + private Cmd(final List args) { + this.args = args; + } + + static Cmd cmd(final String... args) { + return new Cmd(new ArrayList<>(asList(args))); + } + + Cmd args(final String... args) { + this.args.addAll(asList(args)); + return this; + } + + String[] argsArray() { + String[] wrapper = new String[] {}; + return args.toArray(wrapper); + } + } +} diff --git a/pantheon/src/test/resources/operator/config_generate_keys.json b/pantheon/src/test/resources/operator/config_generate_keys.json new file mode 100644 index 0000000000..6402efbbf8 --- /dev/null +++ b/pantheon/src/test/resources/operator/config_generate_keys.json @@ -0,0 +1,42 @@ +{ + "genesis": { + "config": { + "chainId": 2017, + "constantinoplefixblock": 0, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "ibft2": { + + } + }, + "nonce": "0x0", + "timestamp": "0x5b3c3d18", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "ibft2": { + "blockperiodseconds": 2, + "epochlength": 30000, + "requesttimeoutseconds": 10 + }, + "alloc": { + "24defc2d149861d3d245749b81fe0e6b28e04f31": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "2a813d7db3de19b07f92268b6d4125ed295cbe00": { + "balance": "0x446c3b15f9926687d2c40534fdb542000000000000" + } + } + }, + "blockchain": { + "nodes": { + "generate": true, + "count": 4 + } + } +} \ No newline at end of file diff --git a/pantheon/src/test/resources/operator/config_import_keys.json b/pantheon/src/test/resources/operator/config_import_keys.json new file mode 100644 index 0000000000..b8b55169cb --- /dev/null +++ b/pantheon/src/test/resources/operator/config_import_keys.json @@ -0,0 +1,44 @@ +{ + "genesis": { + "config": { + "chainId": 2017, + "constantinoplefixblock": 0, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "ibft2": { + + } + }, + "nonce": "0x0", + "timestamp": "0x5b3c3d18", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "ibft2": { + "blockperiodseconds": 2, + "epochlength": 30000, + "requesttimeoutseconds": 10 + }, + "alloc": { + "24defc2d149861d3d245749b81fe0e6b28e04f31": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "2a813d7db3de19b07f92268b6d4125ed295cbe00": { + "balance": "0x446c3b15f9926687d2c40534fdb542000000000000" + } + } + }, + "blockchain": { + "nodes": { + "keys": [ + "0xb295c4242fb40c6e8ac7b831c916846050f191adc560b8098ba6ad513079571ec1be6e5e1a715857a13a91963097962e048c36c5863014b59e8f67ed3f667680", + "0x6295c4242fb40c6e8ac7b831c916846050f191adc560b8098ba6ad513079571ec1be6e5e1a715857a13a91963097962e048c36c5863014b59e8f67ed3f667680" + ] + } + } +}