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 680eeec2f4..adc662a4a1 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -27,6 +27,7 @@ import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.RunnerBuilder; +import tech.pegasys.pantheon.cli.PublicKeySubCommand.KeyLoader; import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty; import tech.pegasys.pantheon.cli.custom.EnodeToURIPropertyConverter; import tech.pegasys.pantheon.cli.custom.JsonRPCWhitelistHostsProperty; @@ -139,6 +140,10 @@ public static class RpcApisConversionException extends Exception { private final SynchronizerConfiguration.Builder synchronizerConfigurationBuilder; private final RunnerBuilder runnerBuilder; + protected KeyLoader getKeyLoader() { + return KeyPairUtil::loadKeyPair; + } + // Public IP stored to prevent having to research it each time we need it. private InetAddress autoDiscoveredDefaultIP = null; @@ -522,7 +527,8 @@ public void parse( commandLine.addSubcommand( BlocksSubCommand.COMMAND_NAME, new BlocksSubCommand(blockImporter, resultHandler.out())); commandLine.addSubcommand( - PublicKeySubCommand.COMMAND_NAME, new PublicKeySubCommand(resultHandler.out())); + PublicKeySubCommand.COMMAND_NAME, + new PublicKeySubCommand(resultHandler.out(), getKeyLoader())); commandLine.addSubcommand( PasswordSubCommand.COMMAND_NAME, new PasswordSubCommand(resultHandler.out())); commandLine.addSubcommand( @@ -977,7 +983,7 @@ private Path dataDir() { } } - private File nodePrivateKeyFile() { + File nodePrivateKeyFile() { File nodePrivateKeyFile = null; if (isFullInstantiation()) { nodePrivateKeyFile = standaloneCommands.nodePrivateKeyFile; diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PublicKeySubCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PublicKeySubCommand.java index 5ddcadfd7c..e079717770 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PublicKeySubCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PublicKeySubCommand.java @@ -19,7 +19,7 @@ import tech.pegasys.pantheon.cli.PublicKeySubCommand.AddressSubCommand; import tech.pegasys.pantheon.cli.PublicKeySubCommand.ExportSubCommand; -import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.crypto.SECP256K1; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Util; @@ -30,6 +30,7 @@ import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -59,9 +60,11 @@ class PublicKeySubCommand implements Runnable { private CommandSpec spec; // Picocli injects reference to command spec private final PrintStream out; + private final KeyLoader keyLoader; - PublicKeySubCommand(final PrintStream out) { + PublicKeySubCommand(final PrintStream out, final KeyLoader keyLoader) { this.out = out; + this.keyLoader = keyLoader; } @Override @@ -69,6 +72,15 @@ public void run() { spec.commandLine().usage(out); } + private Optional getKeyPair() { + try { + return Optional.of(keyLoader.load(parentCommand.nodePrivateKeyFile())); + } catch (IOException e) { + LOG.error("An error occurred while trying to read the private key", e); + return Optional.empty(); + } + } + /** * Public key export sub-command * @@ -99,9 +111,10 @@ public void run() { checkNotNull(parentCommand); checkNotNull(parentCommand.parentCommand); - final PantheonController controller = parentCommand.parentCommand.buildController(); - final KeyPair keyPair = controller.getLocalNodeKeyPair(); + parentCommand.getKeyPair().ifPresent(this::outputPublicKey); + } + private void outputPublicKey(final KeyPair keyPair) { // if we have an output file defined, print to it // otherwise print to standard output. if (publicKeyExportFile != null) { @@ -150,8 +163,10 @@ public void run() { checkNotNull(parentCommand); checkNotNull(parentCommand.parentCommand); - final PantheonController controller = parentCommand.parentCommand.buildController(); - final KeyPair keyPair = controller.getLocalNodeKeyPair(); + parentCommand.getKeyPair().ifPresent(this::outputAddress); + } + + private void outputAddress(final KeyPair keyPair) { final Address address = Util.publicKeyToAddress(keyPair.getPublicKey()); // if we have an output file defined, print to it @@ -169,4 +184,9 @@ public void run() { } } } + + @FunctionalInterface + public interface KeyLoader { + SECP256K1.KeyPair load(final File keyFile) throws IOException; + } } 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 4e1a8a258f..062d8baa80 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -20,7 +20,9 @@ import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.RunnerBuilder; +import tech.pegasys.pantheon.cli.PublicKeySubCommand.KeyLoader; import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; @@ -141,7 +143,17 @@ protected CommandLine.Model.CommandSpec parseCommand(final String... args) { return parseCommand(System.in, args); } + protected CommandLine.Model.CommandSpec parseCommand( + final KeyLoader keyLoader, final String... args) { + return parseCommand(keyLoader, System.in, args); + } + protected CommandLine.Model.CommandSpec parseCommand(final InputStream in, final String... args) { + return parseCommand(f -> KeyPair.generate(), in, args); + } + + private CommandLine.Model.CommandSpec parseCommand( + final KeyLoader keyLoader, final InputStream in, final String... args) { // turn off ansi usage globally in picocli System.setProperty("picocli.ansi", "false"); @@ -151,7 +163,8 @@ protected CommandLine.Model.CommandSpec parseCommand(final InputStream in, final mockBlockImporter, mockRunnerBuilder, mockControllerBuilder, - mockSyncConfBuilder); + mockSyncConfBuilder, + keyLoader); // parse using Ansi.OFF to be able to assert on non formatted output results pantheonCommand.parse( @@ -165,19 +178,27 @@ protected CommandLine.Model.CommandSpec parseCommand(final InputStream in, final @CommandLine.Command static class TestPantheonCommand extends PantheonCommand { @CommandLine.Spec CommandLine.Model.CommandSpec spec; + private final KeyLoader keyLoader; + + @Override + protected KeyLoader getKeyLoader() { + return keyLoader; + } TestPantheonCommand( final Logger mockLogger, final BlockImporter mockBlockImporter, final RunnerBuilder mockRunnerBuilder, final PantheonControllerBuilder mockControllerBuilder, - final SynchronizerConfiguration.Builder mockSyncConfBuilder) { + final SynchronizerConfiguration.Builder mockSyncConfBuilder, + final KeyLoader keyLoader) { super( mockLogger, mockBlockImporter, mockRunnerBuilder, mockControllerBuilder, mockSyncConfBuilder); + this.keyLoader = keyLoader; } } } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PublicKeySubCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PublicKeySubCommandTest.java index 4b5db6326a..5f7e7a3476 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PublicKeySubCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PublicKeySubCommandTest.java @@ -14,7 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; -import static org.mockito.Mockito.when; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.core.Util; @@ -103,37 +102,38 @@ public void callingPublicKeySubCommandHelpMustDisplayUsage() { } // Export public key sub-sub-command + @Test + public void callingPublicKeyExportSubCommandHelpMustDisplayUsage() { + parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME, "--help"); + assertThat(commandOutput.toString()).startsWith(EXPECTED_PUBLIC_KEY_EXPORT_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void callingPublicKeyExportSubCommandWithoutPathMustWriteKeyToStandardOutput() { final KeyPair keyPair = KeyPair.generate(); - when(mockController.getLocalNodeKeyPair()).thenReturn(keyPair); - parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME); + parseCommand(f -> keyPair, PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME); final String expectedOutputStart = keyPair.getPublicKey().toString(); assertThat(commandOutput.toString()).startsWith(expectedOutputStart); assertThat(commandErrorOutput.toString()).isEmpty(); } - @Test - public void callingPublicKeyExportSubCommandHelpMustDisplayUsage() { - parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME, "--help"); - assertThat(commandOutput.toString()).startsWith(EXPECTED_PUBLIC_KEY_EXPORT_USAGE); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - @Test public void callingPublicKeyExportSubCommandWithFilePathMustWritePublicKeyInThisFile() throws Exception { final KeyPair keyPair = KeyPair.generate(); - when(mockController.getLocalNodeKeyPair()).thenReturn(keyPair); - final File file = File.createTempFile("public", "key"); parseCommand( - PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME, "--to", file.getPath()); + f -> keyPair, + PUBLIC_KEY_SUBCOMMAND_NAME, + PUBLIC_KEY_EXPORT_SUBCOMMAND_NAME, + "--to", + file.getPath()); assertThat(contentOf(file)) .startsWith(keyPair.getPublicKey().toString()) @@ -144,36 +144,35 @@ public void callingPublicKeyExportSubCommandWithFilePathMustWritePublicKeyInThis } // Export address sub-sub-command + @Test + public void callingPublicKeyExportAddressSubCommandHelpMustDisplayUsage() { + parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_ADDRESS_SUBCOMMAND_NAME, "--help"); + assertThat(commandOutput.toString()).startsWith(EXPECTED_PUBLIC_KEY_EXPORT_ADDRESS_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void callingPublicKeyExportAddressSubCommandWithoutPathMustWriteAddressToStandardOutput() { final KeyPair keyPair = KeyPair.generate(); - when(mockController.getLocalNodeKeyPair()).thenReturn(keyPair); - parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_ADDRESS_SUBCOMMAND_NAME); + parseCommand( + f -> keyPair, PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_ADDRESS_SUBCOMMAND_NAME); final String expectedOutputStart = Util.publicKeyToAddress(keyPair.getPublicKey()).toString(); assertThat(commandOutput.toString()).startsWith(expectedOutputStart); assertThat(commandErrorOutput.toString()).isEmpty(); } - @Test - public void callingPublicKeyExportAddressSubCommandHelpMustDisplayUsage() { - parseCommand(PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_ADDRESS_SUBCOMMAND_NAME, "--help"); - assertThat(commandOutput.toString()).startsWith(EXPECTED_PUBLIC_KEY_EXPORT_ADDRESS_USAGE); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - @Test public void callingPublicKeyExportAddressSubCommandWithFilePathMustWriteAddressInThisFile() throws Exception { final KeyPair keyPair = KeyPair.generate(); - when(mockController.getLocalNodeKeyPair()).thenReturn(keyPair); - final File file = File.createTempFile("public", "address"); parseCommand( + f -> keyPair, PUBLIC_KEY_SUBCOMMAND_NAME, PUBLIC_KEY_EXPORT_ADDRESS_SUBCOMMAND_NAME, "--to",