From e76da717a35e95e0c8c72824be8c014780bd1180 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Mon, 11 Dec 2023 08:53:57 -0800 Subject: [PATCH] Txparse subcommand implementation (#6268) * txparse subcommand Signed-off-by: garyschulte Co-authored-by: Sally MacFarlane Signed-off-by: jflo --- .../org/hyperledger/besu/cli/BesuCommand.java | 3 + .../cli/subcommands/TxParseSubCommand.java | 129 ++++++++++++++++++ .../subcommands/TxParseSubCommandTest.java | 69 ++++++++++ 3 files changed, 201 insertions(+) create mode 100644 besu/src/main/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommand.java create mode 100644 besu/src/test/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommandTest.java diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index b42c4aad977..b8b7efca3ab 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -78,6 +78,7 @@ import org.hyperledger.besu.cli.subcommands.PasswordSubCommand; import org.hyperledger.besu.cli.subcommands.PublicKeySubCommand; import org.hyperledger.besu.cli.subcommands.RetestethSubCommand; +import org.hyperledger.besu.cli.subcommands.TxParseSubCommand; import org.hyperledger.besu.cli.subcommands.ValidateConfigSubCommand; import org.hyperledger.besu.cli.subcommands.blocks.BlocksSubCommand; import org.hyperledger.besu.cli.subcommands.operator.OperatorSubCommand; @@ -1493,6 +1494,8 @@ private void addSubCommands(final InputStream in) { jsonBlockImporterFactory, rlpBlockExporterFactory, commandLine.getOut())); + commandLine.addSubcommand( + TxParseSubCommand.COMMAND_NAME, new TxParseSubCommand(commandLine.getOut())); commandLine.addSubcommand( PublicKeySubCommand.COMMAND_NAME, new PublicKeySubCommand(commandLine.getOut())); commandLine.addSubcommand( diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommand.java new file mode 100644 index 00000000000..221675c9dc5 --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommand.java @@ -0,0 +1,129 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.cli.subcommands; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hyperledger.besu.cli.subcommands.TxParseSubCommand.COMMAND_NAME; + +import org.hyperledger.besu.cli.util.VersionProvider; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; +import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.stream.Stream; + +import org.apache.tuweni.bytes.Bytes; +import picocli.CommandLine; + +/** + * txparse sub command implementing txparse spec from + * https://github.com/holiman/txparse/tree/main/cmd/txparse + */ +@CommandLine.Command( + name = COMMAND_NAME, + description = "Parse input transactions and return the sender, or an error.", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) +public class TxParseSubCommand implements Runnable { + + /** The constant COMMAND_NAME. */ + public static final String COMMAND_NAME = "txparse"; + + @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. + @CommandLine.Option( + names = "--corpus-file", + arity = "1..1", + description = "file to read transaction data lines from, otherwise defaults to stdin") + private String corpusFile = null; + + static final BigInteger halfCurveOrder = + SignatureAlgorithmFactory.getInstance().getHalfCurveOrder(); + static final BigInteger chainId = new BigInteger("1", 10); + + private final PrintWriter out; + + /** + * Instantiates a new TxParse sub command. + * + * @param out the PrintWriter where validation results will be reported. + */ + public TxParseSubCommand(final PrintWriter out) { + this.out = out; + } + + @SuppressWarnings("StreamResourceLeak") + Stream fileStreamReader(final String filePath) { + try { + return Files.lines(Paths.get(filePath)) + // skip comments + .filter(line -> !line.startsWith("#")) + // strip out non-alphanumeric characters + .map(line -> line.replaceAll("[^a-zA-Z0-9]", "")); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public void run() { + + Stream txStream; + if (corpusFile != null) { + txStream = fileStreamReader(corpusFile); + } else { + txStream = new BufferedReader(new InputStreamReader(System.in, UTF_8)).lines(); + } + + txStream.forEach( + line -> { + try { + Bytes bytes = Bytes.fromHexStringLenient(line); + dump(bytes); + } catch (Exception ex) { + err(ex.getMessage()); + } + }); + } + + void dump(final Bytes tx) { + try { + var transaction = TransactionDecoder.decodeOpaqueBytes(tx, EncodingContext.BLOCK_BODY); + + // https://github.com/hyperledger/besu/blob/5fe49c60b30fe2954c7967e8475c3b3e9afecf35/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java#L252 + if (transaction.getChainId().isPresent() && !transaction.getChainId().get().equals(chainId)) { + throw new Exception("wrong chain id"); + } + + // https://github.com/hyperledger/besu/blob/5fe49c60b30fe2954c7967e8475c3b3e9afecf35/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java#L270 + if (transaction.getS().compareTo(halfCurveOrder) > 0) { + throw new Exception("signature s out of range"); + } + out.println(transaction.getSender()); + + } catch (Exception ex) { + err(ex.getMessage()); + } + } + + void err(final String message) { + out.println("err: " + message); + } +} diff --git a/besu/src/test/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommandTest.java new file mode 100644 index 00000000000..85839cc5b78 --- /dev/null +++ b/besu/src/test/java/org/hyperledger/besu/cli/subcommands/TxParseSubCommandTest.java @@ -0,0 +1,69 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.cli.subcommands; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +public class TxParseSubCommandTest { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, UTF_8), true); + TxParseSubCommand txParseSubCommand = new TxParseSubCommand(writer); + + @Test + public void smokeTest() { + txParseSubCommand.dump( + Bytes.fromHexString( + "0x02f875018363bc5a8477359400850c570bd200825208943893d427c770de9b92065da03a8c825f13498da28702fbf8ccf8530480c080a0dd49141ecf93eeeaed5f3499a6103d6a9778e24a37b1f5c6a336c60c8a078c15a0511b51a3050771f66c1b779210a46a51be521a2446a3f87fc656dcfd1782aa5e")); + String output = baos.toString(UTF_8); + assertThat(output).isEqualTo("0xeb2629a2734e272bcc07bda959863f316f4bd4cf\n"); + } + + @Test + public void assertInvalidRlp() { + txParseSubCommand.dump( + Bytes.fromHexString( + "0x03f90124f9011e010516058261a89403040500000000000000000000000000000000006383000000f8b6f859940000000000000000000000000000000074657374f842a00100000000000000000000000000000000000000000000000000000000000000a00200000000000000000000000000000000000000000000000000000000000000f859940000000000000000000000000000000074657374f842a00100000000000000000000000000000000000000000000000000000000000000a002000000000000000000000000000000000000000000000000000000000000000fc080a082f96cae43a3f2554cad899a6e9168a3cd613454f82e93488f9b4c1a998cc814a06b7519cd0e3159d6ce9bf811ff3ca4e9c5d7ac63fc52142370a0725e4c5273b2c0c0c0")); + String output = baos.toString(UTF_8); + assertThat(output).startsWith("err: "); + assertThat(output).contains("arbitrary precision scalar"); + } + + @Test + public void assertValidRlp() { + txParseSubCommand.dump( + Bytes.fromHexString( + "0x03f9013f010516058261a89403040500000000000000000000000000000000006383000000f8b6f859940000000000000000000000000000000074657374f842a00100000000000000000000000000000000000000000000000000000000000000a00200000000000000000000000000000000000000000000000000000000000000f859940000000000000000000000000000000074657374f842a00100000000000000000000000000000000000000000000000000000000000000a002000000000000000000000000000000000000000000000000000000000000000fe1a0010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401401a0d98465c5489a09933e6428657f1fc4461d49febd8556c1c7e7053ed0be49cc4fa02c0d67e40aed75f87ea640cc47064d79f4383e24dec283ac6eb81e19da46cc26")); + String output = baos.toString(UTF_8); + assertThat(output).isEqualTo("0x730aa72228b55236de378f5267dfc77723b1380c\n"); + } + + @Test + public void assertInvalidChainId() { + txParseSubCommand.dump( + Bytes.fromHexString( + "0xf901fc303083303030803030b901ae30303030303030303030433030303030303030303030303030303030303030303030303030303030203030413030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038390030000000002800380000413159000021000000002b0000000000003800000000003800000000000000000000002b633279315a00633200303041374222610063416200325832325a002543323861795930314130383058435931317a633043304239623900310031254363384361000023433158253041380037000027285839005a007937000000623700002761002b5a003937622e000000006300007863410000002a2e320000000000005a0000000000000037242778002039006120412e6362002138300000002a313030303030303030303030373030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030a03030303030303030303030303030303030303030303030303030303030303030a03030303030303030303030303030303030303030303030303030303030303030")); + String output = baos.toString(UTF_8); + assertThat(output).isEqualTo("err: wrong chain id\n"); + } +}