Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add rlp decode subcommand #6895

Merged
merged 5 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading