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 40 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
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public IbftExtraData(
this.vote = vote;
}

public static IbftExtraData fromAddresses(final Collection<Address> 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) {
Expand Down
4 changes: 2 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,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'
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 @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
/*
* 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.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;
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. (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<Address> addressesForGenesisExtraData = new ArrayList<>();
private Path keysDirectory;

@Override
public void run() {
checkPreconditions();
generateBlockchainConfig();
}

private void checkPreconditions() {
checkNotNull(parentCommand);
checkNotNull(parentCommand.parentCommand);
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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() {
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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());

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);
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 {
final JsonGenesisConfigOptions genesisConfigOptions =
JsonGenesisConfigOptions.fromJsonObject(genesisConfig.getJsonObject("config"));
if (genesisConfigOptions.isIbft2() || genesisConfigOptions.isIbftLegacy()) {
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
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.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);
}

/**
* Checks if the output directory exists. If forceOverwrite the output directory will be
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
* flushed.
*
* @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.");
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
} 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 {
final Path genesisPath = directory.toPath().resolve(fileName);
if (Files.exists(genesisPath)) {
throw new IllegalArgumentException("Output genesis file must not exist.");
AbdelStark marked this conversation as resolved.
Show resolved Hide resolved
}
LOG.info("Writing genesis file.");
Files.write(genesisPath, genesis.encodePrettily().getBytes(UTF_8));
}
}

private static boolean isAnyDuplicate(final String... values) {
final Set<String> set = new HashSet<>();
for (String value : values) {
if (!set.add(value)) {
return true;
}
}
return false;
}
}
Loading