add rlp decode subcommand (#6895)

* add rlp decode subcommand

Signed-off-by: Brindrajsinh-Chauhan <brindrajsinh@gmail.com>

---------

Signed-off-by: Brindrajsinh-Chauhan <brindrajsinh@gmail.com>
Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
pull/6931/head
Brindrajsinh Chauhan 8 months ago committed by GitHub
parent fbe9898653
commit 627e0a994e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 10
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/IbftExtraDataCLIAdapter.java
  3. 11
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/JSONToRLP.java
  4. 10
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/QbftExtraDataCLIAdapter.java
  5. 129
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommand.java
  6. 152
      besu/src/test/java/org/hyperledger/besu/cli/subcommands/rlp/RLPSubCommandTest.java

@ -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)

@ -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 @@ public class IbftExtraDataCLIAdapter implements JSONToRLP {
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));
}
}

@ -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;
}

@ -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 @@ public class QbftExtraDataCLIAdapter implements JSONToRLP {
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));
}
}

@ -19,8 +19,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
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.InputStream;
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 @@ import picocli.CommandLine.Spec;
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 @@ public class RLPSubCommand implements Runnable {
}
}
}
/**
* RLP decode sub-command
*
* <p>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());
}
}
}
}

@ -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 class RLPSubCommandTest extends CommandTestAbstract {
.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);

Loading…
Cancel
Save