diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7fdc02931..045968989cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - Expose transaction count by type metrics for the layered txpool [#6903](https://github.com/hyperledger/besu/pull/6903) - Expose bad block events via the BesuEvents plugin API [#6848](https://github.com/hyperledger/besu/pull/6848) - Add RPC errors metric [#6919](https://github.com/hyperledger/besu/pull/6919/) +- Add `rlp decode` subcommand to decode IBFT/QBFT extraData to validator list [#6895](https://github.com/hyperledger/besu/pull/6895) ### Bug fixes - Fix txpool dump/restore race condition [#6665](https://github.com/hyperledger/besu/pull/6665) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/IbftExtraDataCLIAdapter.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/IbftExtraDataCLIAdapter.java index 68d88478df2..c136d0f8da4 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/IbftExtraDataCLIAdapter.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/IbftExtraDataCLIAdapter.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.cli.subcommands.rlp; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.ibft.IbftExtraDataCodec; import org.hyperledger.besu.datatypes.Address; @@ -43,4 +44,13 @@ private Bytes fromJsonAddresses(final String jsonAddresses) throws IOException { return IbftExtraDataCodec.encodeFromAddresses( validatorAddresses.stream().map(Address::fromHexString).collect(Collectors.toList())); } + + @Override + public BftExtraData decode(final String rlpInput) throws IOException { + return fromRLPInput(rlpInput); + } + + private BftExtraData fromRLPInput(final String rlpInput) throws IOException { + return new IbftExtraDataCodec().decodeRaw(Bytes.fromHexString(rlpInput)); + } } diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/JSONToRLP.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/JSONToRLP.java index 101b442e8cf..ebf05cb24c4 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/JSONToRLP.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/JSONToRLP.java @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.cli.subcommands.rlp; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; + import java.io.IOException; import org.apache.tuweni.bytes.Bytes; @@ -29,4 +31,13 @@ interface JSONToRLP { * @throws IOException if an error occurs while reading data */ Bytes encode(String json) throws IOException; + + /** + * Decodes the input RLP value into a validators list Object. + * + * @param inputData the RLP hex string to convert to validators list + * @return the decoded BFT ExtraData object. + * @throws IOException if an error occurs while reading data + */ + BftExtraData decode(String inputData) throws IOException; } diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/QbftExtraDataCLIAdapter.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/QbftExtraDataCLIAdapter.java index 9caea69d936..cf02819b2b9 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/QbftExtraDataCLIAdapter.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/QbftExtraDataCLIAdapter.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.cli.subcommands.rlp; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.datatypes.Address; @@ -40,4 +41,13 @@ private Bytes fromJsonAddresses(final String jsonAddresses) throws IOException { return QbftExtraDataCodec.encodeFromAddresses( validatorAddresses.stream().map(Address::fromHexString).collect(Collectors.toList())); } + + @Override + public BftExtraData decode(final String rlpInput) throws IOException { + return fromRLPInput(rlpInput); + } + + private BftExtraData fromRLPInput(final String rlpInput) throws IOException { + return new QbftExtraDataCodec().decodeRaw(Bytes.fromHexString(rlpInput)); + } } diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommand.java index 834e238987b..774b94f9c9b 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommand.java @@ -19,8 +19,10 @@ import org.hyperledger.besu.cli.BesuCommand; import org.hyperledger.besu.cli.DefaultCommandValues; +import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand.DecodeSubCommand; import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand.EncodeSubCommand; import org.hyperledger.besu.cli.util.VersionProvider; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -30,6 +32,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; +import java.util.NoSuchElementException; import java.util.Scanner; import com.fasterxml.jackson.databind.exc.MismatchedInputException; @@ -48,7 +51,7 @@ description = "This command provides RLP data related actions.", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class, - subcommands = {EncodeSubCommand.class}) + subcommands = {EncodeSubCommand.class, DecodeSubCommand.class}) public class RLPSubCommand implements Runnable { /** The constant COMMAND_NAME. */ @@ -207,4 +210,128 @@ private void writeOutput(final Bytes rlpEncodedOutput) { } } } + + /** + * RLP decode sub-command + * + *

Decode a RLP hex string into a validator list. + */ + @Command( + name = "decode", + description = "This command decodes a JSON typed RLP hex string into validator list.", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class DecodeSubCommand implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private RLPSubCommand parentCommand; // Picocli injects reference to parent command + + @SuppressWarnings("unused") + @Spec + private CommandSpec spec; + + @Option( + names = "--type", + description = + "Type of the RLP data to Decode, possible values are ${COMPLETION-CANDIDATES}. (default: ${DEFAULT-VALUE})", + arity = "1..1") + private final RLPType type = RLPType.IBFT_EXTRA_DATA; + + @Option( + names = "--from", + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "File containing JSON object to decode", + arity = "1..1") + private final File jsonSourceFile = null; + + @Option( + names = "--to", + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "File to write decoded RLP string to.", + arity = "1..1") + private final File rlpTargetFile = null; + + @Override + public void run() { + checkNotNull(parentCommand); + readInput(); + } + + /** + * Reads the stdin or from a file if one is specified by {@link #jsonSourceFile} then goes to + * {@link #decode(String)} this data + */ + private void readInput() { + // if we have an output file defined, print to it + // otherwise print to defined output, usually standard output. + final String inputData; + + if (jsonSourceFile != null) { + try { + BufferedReader reader = Files.newBufferedReader(jsonSourceFile.toPath(), UTF_8); + + // Read only the first line if there are many lines + inputData = reader.readLine(); + } catch (IOException e) { + throw new ExecutionException(spec.commandLine(), "Unable to read input file."); + } + } else { + // get data from standard input + try (Scanner scanner = new Scanner(parentCommand.in, UTF_8.name())) { + inputData = scanner.nextLine(); + } catch (NoSuchElementException e) { + throw new ParameterException(spec.commandLine(), "Unable to read input data." + e); + } + } + + decode(inputData); + } + + /** + * Decodes the string input into an validator data based on the {@link #type} then goes to + * {@link #writeOutput(BftExtraData)} this data to file or stdout + * + * @param inputData the string data to decode + */ + private void decode(final String inputData) { + if (inputData == null || inputData.isEmpty()) { + throw new ParameterException( + spec.commandLine(), "An error occurred while trying to read the input data."); + } else { + try { + // decode and write the value + writeOutput(type.getAdapter().decode(inputData)); + } catch (MismatchedInputException e) { + throw new ParameterException( + spec.commandLine(), + "Unable to map the input data with selected type. Please check input format. " + e); + } catch (IOException e) { + throw new ParameterException( + spec.commandLine(), "Unable to load the input data. Please check input format. " + e); + } + } + } + + /** + * write the decoded result to stdout or a file if the option is specified + * + * @param bftExtraDataOutput the BFT extra data output to write to file or stdout + */ + private void writeOutput(final BftExtraData bftExtraDataOutput) { + if (rlpTargetFile != null) { + final Path targetPath = rlpTargetFile.toPath(); + + try (final BufferedWriter fileWriter = Files.newBufferedWriter(targetPath, UTF_8)) { + fileWriter.write(bftExtraDataOutput.getValidators().toString()); + } catch (final IOException e) { + throw new ParameterException( + spec.commandLine(), + "An error occurred while trying to write the validator list. " + e.getMessage()); + } + } else { + parentCommand.out.println(bftExtraDataOutput.getValidators().toString()); + } + } + } } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommandTest.java index 8c9b9aad932..27dcdc16f63 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommandTest.java @@ -68,6 +68,7 @@ public class RLPSubCommandTest extends CommandTestAbstract { private static final String RLP_SUBCOMMAND_NAME = "rlp"; private static final String RLP_ENCODE_SUBCOMMAND_NAME = "encode"; + private static final String RLP_DECODE_SUBCOMMAND_NAME = "decode"; private static final String RLP_QBFT_TYPE = "QBFT_EXTRA_DATA"; // RLP sub-command @@ -259,6 +260,157 @@ public void encodeWithEmptyStdInputMustRaiseAnError() throws Exception { .startsWith("An error occurred while trying to read the JSON data."); } + @Test + public void decodeWithoutPathMustWriteToStandardOutput() { + + final String inputData = + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d" + + "46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"; + + // set stdin + final ByteArrayInputStream stdIn = new ByteArrayInputStream(inputData.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_DECODE_SUBCOMMAND_NAME); + + final String expectedValidatorString = + "[0xbe068f726a13c8d46c44be6ce9d275600e1735a4, 0x5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193]"; + assertThat(commandOutput.toString(UTF_8)).contains(expectedValidatorString); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void decodeQBFTWithoutPathMustWriteToStandardOutput() { + + final String inputData = + "0xf84fa00000000000000000000000000000000000000000000000000000000000000000ea94241f804efb46f71acaa" + + "5be94a62f7798e89c3724946cdf72da457453063ea92e7fa5ac30afbcec28cdc080c0"; + + // set stdin + final ByteArrayInputStream stdIn = new ByteArrayInputStream(inputData.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_DECODE_SUBCOMMAND_NAME, "--type", RLP_QBFT_TYPE); + + final String expectedValidatorString = + "[0x241f804efb46f71acaa5be94a62f7798e89c3724, 0x6cdf72da457453063ea92e7fa5ac30afbcec28cd]"; + assertThat(commandOutput.toString(UTF_8)).contains(expectedValidatorString); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void decodeWithOutputFileMustWriteInThisFile() throws Exception { + + final File file = File.createTempFile("ibftValidators", "rlp"); + + final String inputData = + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d" + + "46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"; + + // set stdin + final ByteArrayInputStream stdIn = new ByteArrayInputStream(inputData.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_DECODE_SUBCOMMAND_NAME, "--to", file.getPath()); + + final String expectedValidatorString = + "[0xbe068f726a13c8d46c44be6ce9d275600e1735a4, 0x5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193]"; + + assertThat(contentOf(file)).contains(expectedValidatorString); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void decodeWithInputFilePathMustReadFromThisFile(final @TempDir Path dir) + throws Exception { + final Path tempJsonFile = Files.createTempFile(dir, "input", "json"); + try (final BufferedWriter fileWriter = Files.newBufferedWriter(tempJsonFile, UTF_8)) { + + fileWriter.write( + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"); + + fileWriter.flush(); + + parseCommand( + RLP_SUBCOMMAND_NAME, + RLP_DECODE_SUBCOMMAND_NAME, + "--from", + tempJsonFile.toFile().getAbsolutePath()); + + final String expectedValidatorString = + "[0xbe068f726a13c8d46c44be6ce9d275600e1735a4, 0x5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193]"; + + assertThat(commandOutput.toString(UTF_8)).contains(expectedValidatorString); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + } + + @Test + public void decodeWithInputFilePathToOutputFile(final @TempDir Path dir) throws Exception { + final Path tempInputFile = Files.createTempFile(dir, "input", "json"); + final File tempOutputFile = File.createTempFile("ibftValidators", "rlp"); + try (final BufferedWriter fileWriter = Files.newBufferedWriter(tempInputFile, UTF_8)) { + + fileWriter.write( + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"); + + fileWriter.flush(); + + parseCommand( + RLP_SUBCOMMAND_NAME, + RLP_DECODE_SUBCOMMAND_NAME, + "--from", + tempInputFile.toFile().getAbsolutePath(), + "--to", + tempOutputFile.getPath()); + + final String expectedValidatorString = + "[0xbe068f726a13c8d46c44be6ce9d275600e1735a4, 0x5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193]"; + + assertThat(contentOf(tempOutputFile)).contains(expectedValidatorString); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + } + + @Test + public void decodeWithEmptyStdInputMustRaiseAnError() throws Exception { + + // set empty stdin + final String jsonInput = ""; + final ByteArrayInputStream stdIn = new ByteArrayInputStream(jsonInput.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_DECODE_SUBCOMMAND_NAME); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).startsWith("Unable to read input data."); + } + + @Test + public void decodeWithInputFilePathMustThrowErrorFileNotExist(final @TempDir Path dir) + throws Exception { + + final String nonExistingFileName = "/incorrectPath/wrongFile.json"; + + parseCommand(RLP_SUBCOMMAND_NAME, RLP_DECODE_SUBCOMMAND_NAME, "--from", nonExistingFileName); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).contains("Unable to read input file"); + } + + @Test + public void decodeWithEmptyInputMustRaiseAnError(final @TempDir Path dir) throws Exception { + final Path emptyFile = Files.createTempFile(dir, "empty", "json"); + parseCommand( + RLP_SUBCOMMAND_NAME, + RLP_DECODE_SUBCOMMAND_NAME, + "--from", + emptyFile.toFile().getAbsolutePath()); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .startsWith("An error occurred while trying to read the input data."); + } + @AfterEach public void restoreStdin() { System.setIn(System.in);