Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

[PIE-1602] Implement operator tool. (blockchain network configuration for permisionned networks) #1511

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ff089dd
init operator subcommand
AbdelStark May 23, 2019
b899c8a
Update OperatorSubCommand.java
AbdelStark May 23, 2019
d5d395d
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 24, 2019
9220ff3
Update OperatorSubCommand.java
AbdelStark May 24, 2019
d80576e
Add extra data.
AbdelStark May 24, 2019
2f01331
Update OperatorSubCommand.java
AbdelStark May 24, 2019
9c9bd2f
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 27, 2019
ce94299
Update OperatorSubCommand.java
AbdelStark May 27, 2019
6f5180d
add javadoc
AbdelStark May 27, 2019
c53eeb2
init tests
AbdelStark May 28, 2019
68482fc
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 28, 2019
043845b
Update OperatorSubCommandTest.java
AbdelStark May 28, 2019
0ff28a3
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 29, 2019
d0895f0
add tests
AbdelStark May 29, 2019
a0e3e0b
spotless apply
AbdelStark May 29, 2019
07eaf37
add tests
AbdelStark May 29, 2019
8c2004f
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 29, 2019
9f5e7be
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 30, 2019
5f4a2e4
add tests
AbdelStark May 30, 2019
9548e27
fix Jenkins CI error prone check
AbdelStark May 30, 2019
171058d
fix error prone check
AbdelStark May 30, 2019
a1e4ebb
fix PR discussion
AbdelStark May 30, 2019
bf6b2e6
spotless apply
AbdelStark May 30, 2019
d789ef9
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 31, 2019
f949084
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark May 31, 2019
1adcfef
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark Jun 4, 2019
0d0f17a
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark Jun 5, 2019
39b9db8
Update pantheon/src/test/java/tech/pegasys/pantheon/cli/operator/Oper…
AbdelStark Jun 6, 2019
4aac80c
fix PR discussion
AbdelStark Jun 6, 2019
6526198
Merge branch 'feature/pie-1602-operator-tool' of https://github.com/a…
AbdelStark Jun 6, 2019
784b049
fix PR discussion
AbdelStark Jun 6, 2019
364320b
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark Jun 6, 2019
f081c45
fix PR discussion
AbdelStark Jun 10, 2019
5dc0dd0
Merge remote-tracking branch 'upstream/master' into feature/pie-1602-…
AbdelStark Jun 10, 2019
309c968
fix PR
AbdelStark Jun 10, 2019
4f9d0a9
Update OperatorSubCommand.java
AbdelStark Jun 10, 2019
3cb65f4
Update JsonGenesisConfigOptions.java
AbdelStark Jun 10, 2019
d85936b
Update OperatorSubCommand.java
AbdelStark Jun 10, 2019
1aa6bec
fix unit tests
AbdelStark Jun 10, 2019
b9e7cfb
remove forceOverwrite
AbdelStark Jun 10, 2019
5cacfef
fix PR discussion
AbdelStark Jun 10, 2019
2165ef0
dont overwrite files
AbdelStark Jun 10, 2019
37963f3
dont create file before writing
AbdelStark Jun 10, 2019
c9007a4
spotless apply
AbdelStark Jun 10, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -78,12 +78,14 @@ 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'

dependency 'org.xerial.snappy:snappy-java:1.1.7.3'
dependency 'com.jayway.jsonpath:json-path:2.4.0'


dependency "tech.pegasys.pantheon:plugin-api:${rootProject.version}"
}
Expand Down
4 changes: 4 additions & 0 deletions pantheon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto'
implementation 'tech.pegasys.pantheon:plugin-api'

implementation 'com.google.code.gson:gson:2.8.5'
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved


runtime 'org.apache.logging.log4j:log4j-core'
runtime 'org.apache.logging.log4j:log4j-slf4j-impl'

Expand All @@ -67,6 +70,7 @@ dependencies {
testImplementation 'org.awaitility:awaitility'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.apache.commons:commons-text'
testImplementation 'com.jayway.jsonpath:json-path'
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
}

task writeInfoFile(type: ProjectPropertiesFile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -627,6 +628,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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
/*
* 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 java.nio.file.Files.deleteIfExists;
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.cli.rlp.RLPType;
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.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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";

@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;
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved

@Option(
names = "--genesis-file-name",
paramLabel = MANDATORY_PATH_FORMAT_HELP,
description = "Name of the genesis file.",
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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.",
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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.",
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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<String> addressesForGenesisExtraData = new ArrayList<>();
private Path keysDirectory;

@Override
public void run() {
checkNotNull(parentCommand);
checkNotNull(parentCommand.parentCommand);
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
generateBlockchainConfig();
}

/** Generates output directory with all required configuration files. */
private void generateBlockchainConfig() {
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
try {
createOutputDirectory();
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());

AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
} 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.toUnprefixedString());
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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() throws IOException {
LOG.info("Generating genesis extra data.");
final Gson gson = new GsonBuilder().setPrettyPrinting().create();
final String jsonAddresses = gson.toJson(addressesForGenesisExtraData);
final String extraData =
RLPType.IBFT_EXTRA_DATA.getAdapter().encode(jsonAddresses).toString();
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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.createFile(filePath);
Files.write(filePath, content.getBytes(UTF_8));
}

/**
* 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);
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
genesisConfig = operatorConfig.getJsonObject("genesis");
blockchainConfig = operatorConfig.getJsonObject("blockchain");
nodesConfig = blockchainConfig.getJsonObject("nodes");
generateNodesKeys = nodesConfig.getBoolean("generate", false);
}

/**
* Creates the output directory if not exists.
*
* @throws IOException
*/
private void createOutputDirectory() throws IOException {
final Path outputDirectoryPath = outputDirectory.toPath();

if (outputDirectory.exists() && outputDirectory.isDirectory()) {
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
try (Stream<Path> stream = Files.walk(outputDirectoryPath)) {
stream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
deleteIfExists(outputDirectoryPath);
}
}
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 {
final Path genesisPath = directory.toPath().resolve(fileName);
deleteIfExists(genesisPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I wouldn't delete files for the user. I'd throw an error and have the user clean up the files manually.

LOG.info("Writing genesis file.");
Files.write(genesisPath, genesis.encodePrettily().getBytes(UTF_8));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import java.io.IOException;

/** Behaviour of objects that can be encoded from JSON to RLP */
interface JSONToRLP {
public interface JSONToRLP {
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved

/**
* Encodes the object into an RLP value.
Expand Down
Loading