diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java index 9da28affd0..4edd723a49 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java @@ -18,6 +18,7 @@ import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.RunnerBuilder; import tech.pegasys.pantheon.cli.EthNetworkConfig; import tech.pegasys.pantheon.cli.PantheonControllerBuilder; +import tech.pegasys.pantheon.controller.KeyPairUtil; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration.Builder; @@ -60,7 +61,8 @@ public class ThreadPantheonNodeRunner implements PantheonNodeRunner { ethNetworkConfig, false, node.getMiningParameters(), - true); + true, + KeyPairUtil.getDefaultKeyFile(node.homeDirectory())); } catch (final IOException e) { throw new RuntimeException("Error building PantheonController", e); } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java index fde1acb538..83a4707652 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java @@ -32,8 +32,6 @@ import org.apache.logging.log4j.Logger; public class Runner implements AutoCloseable { - static final String KEY_PATH = "key"; - private static final Logger LOG = LogManager.getLogger(); private final Vertx vertx; 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 4cf475d4e3..4a7671f80c 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -19,6 +19,7 @@ import tech.pegasys.pantheon.RunnerBuilder; import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty; import tech.pegasys.pantheon.consensus.clique.jsonrpc.CliqueRpcApis; import tech.pegasys.pantheon.consensus.ibft.jsonrpc.IbftRpcApis; +import tech.pegasys.pantheon.controller.KeyPairUtil; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.MiningParameters; @@ -145,6 +146,14 @@ public class PantheonCommand implements Runnable { ) private final Path dataDir = getDefaultPantheonDataDir(); + @Option( + names = {"--node-private-key"}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = + "the path to the node's private key file. (default: a file named \"key\" in the Pantheon data folder.)" + ) + private final File nodePrivateKeyFile = KeyPairUtil.getDefaultKeyFile(dataDir); + // Genesis file path with null default option if the option // is not defined on command line as this default is handled by Runner // to use mainnet json file from resources @@ -442,7 +451,8 @@ public class PantheonCommand implements Runnable { ethNetworkConfig(), syncWithOttoman, new MiningParameters(coinbase, minTransactionGasPrice, extraData, isMiningEnabled), - isDevMode); + isDevMode, + nodePrivateKeyFile); } catch (final InvalidConfigurationException e) { throw new ExecutionException(new CommandLine(this), e.getMessage()); } catch (final IOException e) { diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonControllerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonControllerBuilder.java index 14688a6210..63a9136db9 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonControllerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonControllerBuilder.java @@ -23,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.core.MiningParameters; import tech.pegasys.pantheon.ethereum.development.DevelopmentProtocolSchedule; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import java.io.File; import java.io.IOException; import java.nio.file.Path; @@ -36,11 +37,12 @@ public class PantheonControllerBuilder { final EthNetworkConfig ethNetworkConfig, final boolean syncWithOttoman, final MiningParameters miningParameters, - final boolean isDevMode) + final boolean isDevMode, + final File nodePrivateKeyFile) throws IOException { // instantiate a controller with mainnet config if no genesis file is defined // otherwise use the indicated genesis file - final KeyPair nodeKeys = loadKeyPair(homePath); + final KeyPair nodeKeys = loadKeyPair(nodePrivateKeyFile); if (isDevMode) { final GenesisConfigFile genesisConfig = GenesisConfigFile.development(); return MainnetPantheonController.init( diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/controller/KeyPairUtil.java b/pantheon/src/main/java/tech/pegasys/pantheon/controller/KeyPairUtil.java index 24359f2904..1b6561212e 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/controller/KeyPairUtil.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/controller/KeyPairUtil.java @@ -24,8 +24,7 @@ import org.apache.logging.log4j.Logger; public class KeyPairUtil { private static final Logger LOG = LogManager.getLogger(); - public static SECP256K1.KeyPair loadKeyPair(final Path home) throws IOException { - final File keyFile = home.resolve("key").toFile(); + public static SECP256K1.KeyPair loadKeyPair(final File keyFile) throws IOException { final SECP256K1.KeyPair key; if (keyFile.exists()) { key = SECP256K1.KeyPair.load(keyFile); @@ -40,4 +39,12 @@ public class KeyPairUtil { } return key; } + + public static SECP256K1.KeyPair loadKeyPair(final Path homeDirectory) throws IOException { + return loadKeyPair(getDefaultKeyFile(homeDirectory)); + } + + public static File getDefaultKeyFile(final Path homeDirectory) { + return homeDirectory.resolve("key").toFile(); + } } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java index b5268761cf..26fab18278 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java @@ -137,7 +137,7 @@ public final class RunnerTest { // Setup runner with no block data final Path dbBehind = temp.newFolder().toPath(); - final KeyPair behindDbNodeKeys = loadKeyPair(dbBehind); + final KeyPair behindDbNodeKeys = loadKeyPair(dbBehind.resolve("key").toFile()); final PantheonController controllerBehind = MainnetPantheonController.init( temp.newFolder().toPath(), diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index 6b401b0aa9..b3b762fb6a 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -25,6 +25,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.util.BlockImporter; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.PrintStream; import java.nio.file.Path; import java.util.Collection; @@ -65,6 +66,7 @@ public abstract class CommandTestAbstract { @Captor ArgumentCaptor> stringListArgumentCaptor; @Captor ArgumentCaptor pathArgumentCaptor; + @Captor ArgumentCaptor fileArgumentCaptor; @Captor ArgumentCaptor stringArgumentCaptor; @Captor ArgumentCaptor intArgumentCaptor; @Captor ArgumentCaptor jsonRpcConfigArgumentCaptor; @@ -75,7 +77,7 @@ public abstract class CommandTestAbstract { // doReturn used because of generic PantheonController Mockito.doReturn(mockController) .when(mockControllerBuilder) - .build(any(), any(), any(), anyBoolean(), any(), anyBoolean()); + .build(any(), any(), any(), anyBoolean(), any(), anyBoolean(), any()); when(mockSyncConfBuilder.build()).thenReturn(mockSyncConf); } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index d15ad9f454..50bc45e02f 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -144,7 +144,14 @@ public class PantheonCommandTest extends CommandTestAbstract { final ArgumentCaptor networkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); verify(mockControllerBuilder) - .build(any(), isNotNull(), networkArg.capture(), eq(false), miningArg.capture(), eq(false)); + .build( + any(), + isNotNull(), + networkArg.capture(), + eq(false), + miningArg.capture(), + eq(false), + isNotNull()); verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FULL)); @@ -283,7 +290,8 @@ public class PantheonCommandTest extends CommandTestAbstract { eq(networkConfig), eq(false), any(), - anyBoolean()); + anyBoolean(), + any()); // TODO: Re-enable as per NC-1057/NC-1681 // verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FAST)); @@ -321,7 +329,7 @@ public class PantheonCommandTest extends CommandTestAbstract { any(), any()); - verify(mockControllerBuilder).build(any(), any(), any(), eq(false), any(), eq(false)); + verify(mockControllerBuilder).build(any(), any(), any(), eq(false), any(), eq(false), any()); // TODO: Re-enable as per NC-1057/NC-1681 // verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FULL)); @@ -332,6 +340,28 @@ public class PantheonCommandTest extends CommandTestAbstract { assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void nodekeyOptionMustBeUsed() throws Exception { + final File file = new File("./specific/key"); + + parseCommand("--node-private-key", file.getPath()); + + verify(mockControllerBuilder) + .build( + any(), + isNotNull(), + any(), + eq(false), + any(), + anyBoolean(), + fileArgumentCaptor.capture()); + + assertThat(fileArgumentCaptor.getValue()).isEqualTo(file); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void dataDirOptionMustBeUsed() throws Exception { final Path path = Paths.get("."); @@ -339,7 +369,7 @@ public class PantheonCommandTest extends CommandTestAbstract { parseCommand("--datadir", path.toString()); verify(mockControllerBuilder) - .build(any(), pathArgumentCaptor.capture(), any(), eq(false), any(), anyBoolean()); + .build(any(), pathArgumentCaptor.capture(), any(), eq(false), any(), anyBoolean(), any()); assertThat(pathArgumentCaptor.getValue()).isEqualByComparingTo(path); @@ -356,7 +386,7 @@ public class PantheonCommandTest extends CommandTestAbstract { parseCommand("--genesis", path.toString()); verify(mockControllerBuilder) - .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean()); + .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean(), any()); assertThat(networkArg.getValue().getGenesisConfig()).isEqualTo(path.toUri()); @@ -904,7 +934,7 @@ public class PantheonCommandTest extends CommandTestAbstract { ArgumentCaptor.forClass(MiningParameters.class); verify(mockControllerBuilder) - .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean()); + .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean(), any()); assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(miningArg.getValue().isMiningEnabled()).isTrue(); @@ -926,7 +956,7 @@ public class PantheonCommandTest extends CommandTestAbstract { ArgumentCaptor.forClass(MiningParameters.class); verify(mockControllerBuilder) - .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean()); + .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean(), any()); assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(miningArg.getValue().getCoinbase()).isEqualTo(Optional.of(requestedCoinbase)); @@ -938,7 +968,7 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void devModeOptionMustBeUsed() throws Exception { parseCommand("--dev-mode"); - verify(mockControllerBuilder).build(any(), any(), any(), anyBoolean(), any(), eq(true)); + verify(mockControllerBuilder).build(any(), any(), any(), anyBoolean(), any(), eq(true), any()); assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); } @@ -950,7 +980,7 @@ public class PantheonCommandTest extends CommandTestAbstract { final ArgumentCaptor networkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); verify(mockControllerBuilder) - .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean()); + .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean(), any()); assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(networkArg.getValue()).isEqualTo(EthNetworkConfig.rinkeby()); @@ -972,7 +1002,7 @@ public class PantheonCommandTest extends CommandTestAbstract { final ArgumentCaptor networkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); verify(mockControllerBuilder) - .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean()); + .build(any(), any(), networkArg.capture(), anyBoolean(), any(), anyBoolean(), any()); assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(networkArg.getValue().getGenesisConfig()).isEqualTo(path.toUri());