Skip to content

Commit

Permalink
add rlp decode subcommand (hyperledger#6895)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
2 people authored and matthew1001 committed Jun 7, 2024
1 parent b692230 commit 5e39769
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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. */
Expand Down Expand Up @@ -207,4 +210,128 @@ private void writeOutput(final Bytes rlpEncodedOutput) {
}
}
}

/**
* 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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 5e39769

Please sign in to comment.