diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Account.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Account.java index f64240127..f0e63fd52 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Account.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Account.java @@ -12,9 +12,12 @@ */ package tech.pegasys.web3signer.dsl; +import java.math.BigInteger; + public class Account { private final String address; + private BigInteger nonce = BigInteger.ZERO; public Account(final String address) { this.address = address; @@ -23,4 +26,16 @@ public Account(final String address) { public String address() { return address; } + + public BigInteger nextNonce() { + return nonce; + } + + public BigInteger nextNonceAndIncrement() { + + final BigInteger next = nonce; + nonce = nonce.add(BigInteger.ONE); + + return next; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Accounts.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Accounts.java index 780350530..a9e610320 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Accounts.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Accounts.java @@ -19,10 +19,26 @@ public class Accounts { + /** Private key: 8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63 */ + private static final String GENESIS_ACCOUNT_ONE_PUBLIC_KEY = + "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"; + + public static final String GENESIS_ACCOUNT_ONE_PASSWORD = "pass"; + + private final Account benefactor; private final Eth eth; public Accounts(final Eth eth) { this.eth = eth; + this.benefactor = new Account(GENESIS_ACCOUNT_ONE_PUBLIC_KEY); + } + + public Account richBenefactor() { + return benefactor; + } + + public BigInteger balance(final Account account) { + return balance(account.address()); } public BigInteger balance(final String address) { diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Contracts.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Contracts.java new file mode 100644 index 000000000..ad473bb2c --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Contracts.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.dsl; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.web3signer.dsl.utils.ExceptionUtils.failOnIOException; +import static tech.pegasys.web3signer.dsl.utils.WaitUtils.waitFor; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.core.ConditionTimeoutException; +import org.web3j.protocol.core.methods.response.TransactionReceipt; + +public abstract class Contracts { + + private static final Logger LOG = LogManager.getLogger(); + + public static final BigInteger GAS_PRICE = BigInteger.valueOf(1000); + public static final BigInteger GAS_LIMIT = BigInteger.valueOf(3000000); + + public abstract String sendTransaction(T smartContract) throws IOException; + + public abstract SignerResponse sendTransactionExpectsError(T smartContract) + throws IOException; + + public abstract Optional getTransactionReceipt(final String hash) + throws IOException; + + public String submit(final T smartContract) { + return failOnIOException(() -> sendTransaction(smartContract)); + } + + public void awaitBlockContaining(final String hash) { + try { + waitFor(() -> assertThat(getTransactionReceipt(hash).isPresent()).isTrue()); + } catch (final ConditionTimeoutException e) { + LOG.error("Timed out waiting for a block containing the transaction receipt hash: " + hash); + } + } + + public String address(final String hash) { + return failOnIOException( + () -> { + final TransactionReceipt receipt = + getTransactionReceipt(hash) + .orElseThrow(() -> new RuntimeException("No receipt found for hash: " + hash)); + assertThat(receipt.getContractAddress()).isNotEmpty(); + return receipt.getContractAddress(); + }); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Eth.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Eth.java index e490627d5..e1d42b5e4 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Eth.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Eth.java @@ -12,12 +12,21 @@ */ package tech.pegasys.web3signer.dsl; +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; + import java.io.IOException; import java.math.BigInteger; import java.util.List; +import java.util.Optional; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.protocol.core.methods.response.TransactionReceipt; public class Eth { @@ -27,11 +36,49 @@ public Eth(final Web3j jsonRpc) { this.jsonRpc = jsonRpc; } + public String sendTransaction(final Transaction transaction) throws IOException { + final EthSendTransaction response = jsonRpc.ethSendTransaction(transaction).send(); + + assertThat(response.getTransactionHash()).isNotEmpty(); + assertThat(response.getError()).isNull(); + + return response.getTransactionHash(); + } + + public SignerResponse sendTransactionExpectsError( + final Transaction transaction) throws IOException { + final EthSendTransaction response = jsonRpc.ethSendTransaction(transaction).send(); + assertThat(response.hasError()).isTrue(); + return SignerResponse.fromWeb3jErrorResponse(response); + } + + public BigInteger getTransactionCount(final String address) throws IOException { + return jsonRpc + .ethGetTransactionCount(address, DefaultBlockParameterName.LATEST) + .send() + .getTransactionCount(); + } + + public Optional getTransactionReceipt(final String hash) throws IOException { + return jsonRpc.ethGetTransactionReceipt(hash).send().getTransactionReceipt(); + } + public List getAccounts() throws IOException { return jsonRpc.ethAccounts().send().getAccounts(); } + public String getCode(final String address) throws IOException { + return jsonRpc.ethGetCode(address, DefaultBlockParameterName.LATEST).send().getResult(); + } + public BigInteger getBalance(final String account) throws IOException { return jsonRpc.ethGetBalance(account, DefaultBlockParameterName.LATEST).send().getBalance(); } + + public String call(final Transaction contractViewOperation) throws IOException { + return jsonRpc + .ethCall(contractViewOperation, DefaultBlockParameterName.LATEST) + .send() + .getValue(); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/PublicContracts.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/PublicContracts.java new file mode 100644 index 000000000..a1cae07c2 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/PublicContracts.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.dsl; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.web3signer.dsl.utils.ExceptionUtils.failOnIOException; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; + +import java.io.IOException; +import java.util.Optional; + +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.protocol.core.methods.response.TransactionReceipt; + +public class PublicContracts extends Contracts { + + private final Eth eth; + + public PublicContracts(final Eth eth) { + this.eth = eth; + } + + @Override + public String sendTransaction(final Transaction smartContract) throws IOException { + return eth.sendTransaction(smartContract); + } + + @Override + public SignerResponse sendTransactionExpectsError( + final Transaction smartContract) throws IOException { + return eth.sendTransactionExpectsError(smartContract); + } + + @Override + public Optional getTransactionReceipt(final String hash) throws IOException { + return eth.getTransactionReceipt(hash); + } + + public String code(final String address) { + return failOnIOException( + () -> { + final String code = eth.getCode(address); + assertThat(code).isNotEmpty(); + return code; + }); + } + + public String call(final Transaction contractViewOperation) { + return failOnIOException(() -> eth.call(contractViewOperation)); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Transactions.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Transactions.java new file mode 100644 index 000000000..16dbd4197 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/Transactions.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.dsl; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static tech.pegasys.web3signer.dsl.utils.ExceptionUtils.failOnIOException; +import static tech.pegasys.web3signer.dsl.utils.WaitUtils.waitFor; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; + +import java.io.IOException; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.core.ConditionTimeoutException; +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.protocol.exceptions.ClientConnectionException; + +public class Transactions { + + private static final Logger LOG = LogManager.getLogger(); + + private final Eth eth; + + public Transactions(final Eth eth) { + this.eth = eth; + } + + public String submit(final Transaction transaction) { + return failOnIOException(() -> eth.sendTransaction(transaction)); + } + + public SignerResponse submitExceptional(final Transaction transaction) { + try { + return failOnIOException(() -> eth.sendTransactionExpectsError(transaction)); + } catch (final ClientConnectionException e) { + LOG.info("ClientConnectionException with message: " + e.getMessage()); + return SignerResponse.fromError(e); + } + } + + public void awaitBlockContaining(final String hash) { + try { + waitFor(() -> assertThat(eth.getTransactionReceipt(hash).isPresent()).isTrue()); + } catch (final ConditionTimeoutException e) { + LOG.error("Timed out waiting for a block containing the transaction receipt hash: " + hash); + throw new RuntimeException("No receipt found for hash: " + hash); + } + } + + public Optional getTransactionReceipt(final String hash) { + try { + return eth.getTransactionReceipt(hash); + } catch (IOException e) { + LOG.error("IOException with message: " + e.getMessage()); + throw new RuntimeException("No tx receipt found for hash: " + hash); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/besu/BesuNode.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/besu/BesuNode.java index 0ef697543..830dc340c 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/besu/BesuNode.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/besu/BesuNode.java @@ -17,6 +17,8 @@ import tech.pegasys.web3signer.dsl.Accounts; import tech.pegasys.web3signer.dsl.Eth; +import tech.pegasys.web3signer.dsl.PublicContracts; +import tech.pegasys.web3signer.dsl.Transactions; import java.io.IOException; import java.io.StringReader; @@ -52,12 +54,17 @@ public class BesuNode { private static final BigInteger SPURIOUS_DRAGON_HARD_FORK_BLOCK = BigInteger.valueOf(1); private final BesuNodeConfig besuNodeConfig; + + @SuppressWarnings("unused") private final String[] args; + private final Map environment; private final Properties portsProperties = new Properties(); private Accounts accounts; private Future besuProcess; + private Transactions transactions; private Web3j jsonRpc; + private PublicContracts publicContracts; private BesuNodePorts besuNodePorts; BesuNode(final BesuNodeConfig besuNodeConfig, String[] args, Map environment) { @@ -130,6 +137,8 @@ public void awaitStartupCompletion() { final Eth eth = new Eth(jsonRpc); accounts = new Accounts(eth); + publicContracts = new PublicContracts(eth); + transactions = new Transactions(eth); } public BesuNodePorts ports() { @@ -159,4 +168,12 @@ private void loadPortsFile() { throw new RuntimeException("Error reading Besu ports file", e); } } + + public PublicContracts publicContracts() { + return publicContracts; + } + + public Transactions transactions() { + return transactions; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java index 52fa80b57..3e21383f0 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java @@ -21,6 +21,10 @@ import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.Eth2SigningRequestBody; +import tech.pegasys.web3signer.dsl.Accounts; +import tech.pegasys.web3signer.dsl.Eth; +import tech.pegasys.web3signer.dsl.PublicContracts; +import tech.pegasys.web3signer.dsl.Transactions; import tech.pegasys.web3signer.dsl.lotus.FilecoinJsonRpcEndpoint; import tech.pegasys.web3signer.dsl.signer.runner.Web3SignerRunner; import tech.pegasys.web3signer.dsl.tls.ClientTlsConfig; @@ -70,6 +74,9 @@ public class Signer extends FilecoinJsonRpcEndpoint { private final SignerConfiguration signerConfig; private final Web3SignerRunner runner; private final String hostname; + private Accounts accounts; + private PublicContracts publicContracts; + private Transactions transactions; private final Vertx vertx; private final String urlFormatting; private final Optional clientTlsConfig; @@ -91,6 +98,10 @@ public void start() { runner.start(); final String httpUrl = getUrl(); jsonRpc = new JsonRpc2_0Web3j(new HttpService(httpUrl)); + final Eth eth = new Eth(jsonRpc); + this.transactions = new Transactions(eth); + this.publicContracts = new PublicContracts(eth); + this.accounts = new Accounts(eth); LOG.info("Http requests being submitted to : {} ", httpUrl); } @@ -200,4 +211,16 @@ public Response healthcheck() { public Ethereum jsonRpc() { return jsonRpc; } + + public Accounts accounts() { + return accounts; + } + + public Transactions transactions() { + return this.transactions; + } + + public PublicContracts publicContracts() { + return publicContracts; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java index 38e6e3066..b9c6ef05f 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java @@ -13,12 +13,10 @@ package tech.pegasys.web3signer.dsl.signer; import static java.util.Collections.emptyList; -import static tech.pegasys.web3signer.tests.AcceptanceTestBase.DEFAULT_CHAIN_ID; import tech.pegasys.web3signer.core.config.TlsOptions; import tech.pegasys.web3signer.core.config.client.ClientTlsOptions; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ChainIdProvider; -import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; import tech.pegasys.web3signer.dsl.tls.TlsCertificateDefinition; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; @@ -34,7 +32,6 @@ import org.apache.logging.log4j.Level; public class SignerConfigurationBuilder { - private static final String LOCALHOST = "127.0.0.1"; private Level logLevel = Level.DEBUG; private int httpRpcPort = 0; @@ -77,7 +74,7 @@ public class SignerConfigurationBuilder { private int downstreamHttpPort; private ClientTlsOptions downstreamTlsOptions; - private ChainIdProvider chainIdProvider = new ConfigurationChainId(DEFAULT_CHAIN_ID); + private ChainIdProvider chainIdProvider; public SignerConfigurationBuilder withLogLevel(final Level logLevel) { this.logLevel = logLevel; diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerResponse.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerResponse.java new file mode 100644 index 000000000..27c4202f9 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.dsl.signer; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcResponse; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.json.Json; +import org.web3j.protocol.core.Response; +import org.web3j.protocol.exceptions.ClientConnectionException; + +public class SignerResponse { + + private final T rpcResponse; + private final HttpResponseStatus status; + + public SignerResponse(final T rpcResponse, final HttpResponseStatus status) { + this.rpcResponse = rpcResponse; + this.status = status; + } + + public T jsonRpc() { + return rpcResponse; + } + + public HttpResponseStatus status() { + return status; + } + + public static SignerResponse fromError(final ClientConnectionException e) { + final String message = e.getMessage(); + final String errorBody = message.substring(message.indexOf(":") + 1).trim(); + final String[] errorParts = errorBody.split(";", 2); + if (errorParts.length == 2) { + final String statusCode = errorParts[0]; + final HttpResponseStatus status = HttpResponseStatus.valueOf(Integer.parseInt(statusCode)); + final String jsonBody = errorParts[1]; + JsonRpcErrorResponse jsonRpcResponse = null; + if (!jsonBody.isEmpty()) { + jsonRpcResponse = Json.decodeValue(jsonBody, JsonRpcErrorResponse.class); + } + return new SignerResponse<>(jsonRpcResponse, status); + } else { + throw new RuntimeException("Unable to parse web3j exception message", e); + } + } + + public static SignerResponse fromWeb3jErrorResponse( + final Response response) { + if (response != null && response.hasError()) { + final Response.Error error = response.getError(); + final JsonRpcError jsonRpcError = JsonRpcError.fromJson(error.getCode(), error.getMessage()); + final JsonRpcErrorResponse jsonRpcErrorResponse = + new JsonRpcErrorResponse(response.getId(), jsonRpcError); + return new SignerResponse<>(jsonRpcErrorResponse, HttpResponseStatus.OK); + } + + return null; + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Hex.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Hex.java new file mode 100644 index 000000000..f8fa067de --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Hex.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.dsl.utils; + +import java.math.BigInteger; + +public class Hex { + + private static final int HEXADECIMAL = 16; + private static final int HEXADECIMAL_PREFIX_LENGTH = 2; + + public static BigInteger hex(final String value) { + return new BigInteger(value.substring(HEXADECIMAL_PREFIX_LENGTH), HEXADECIMAL); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/AccountManagementAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/AccountManagementAcceptanceTest.java index 15051355c..97fa4200c 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/AccountManagementAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/AccountManagementAcceptanceTest.java @@ -17,38 +17,24 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; import tech.pegasys.web3signer.dsl.signer.SignerConfiguration; import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; -import tech.pegasys.web3signer.dsl.utils.MetadataFileHelpers; -import tech.pegasys.web3signer.signing.KeyType; -import java.io.File; import java.io.IOException; import java.net.URISyntaxException; -import java.nio.file.Path; import java.util.List; -import com.google.common.io.Resources; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; public class AccountManagementAcceptanceTest extends Eth1RpcAcceptanceTestBase { - private final MetadataFileHelpers metadataFileHelpers = new MetadataFileHelpers(); @BeforeEach - public void setup(@TempDir Path testDirectory) throws URISyntaxException { + public void setup() throws URISyntaxException { startBesu(); // generate key in temp dir before start web3signer - final String keyPath = - new File(Resources.getResource("secp256k1/wallet.json").toURI()).getAbsolutePath(); - - final Path keyConfigFile = testDirectory.resolve("arbitrary_secp.yaml"); - - metadataFileHelpers.createKeyStoreYamlFileAt( - keyConfigFile, Path.of(keyPath), "pass", KeyType.SECP256K1); final SignerConfiguration web3SignerConfiguration = new SignerConfigurationBuilder() - .withKeyStoreDirectory(testDirectory) + .withKeyStoreDirectory(keyFileTempDir) .withMode("eth1") .withDownstreamHttpPort(besu.ports().getHttpRpc()) .withChainIdProvider(new ConfigurationChainId(DEFAULT_CHAIN_ID)) diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/Eth1RpcAcceptanceTestBase.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/Eth1RpcAcceptanceTestBase.java index 548bb733f..2ad1b914f 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/Eth1RpcAcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/Eth1RpcAcceptanceTestBase.java @@ -12,16 +12,25 @@ */ package tech.pegasys.web3signer.tests.eth1rpc; +import tech.pegasys.web3signer.dsl.Account; import tech.pegasys.web3signer.dsl.besu.BesuNode; import tech.pegasys.web3signer.dsl.besu.BesuNodeConfig; import tech.pegasys.web3signer.dsl.besu.BesuNodeConfigBuilder; import tech.pegasys.web3signer.dsl.besu.BesuNodeFactory; +import tech.pegasys.web3signer.dsl.utils.MetadataFileHelpers; +import tech.pegasys.web3signer.signing.KeyType; import tech.pegasys.web3signer.tests.AcceptanceTestBase; +import java.io.File; import java.math.BigInteger; +import java.net.URISyntaxException; +import java.nio.file.Path; import java.util.List; +import com.google.common.io.Resources; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; import org.web3j.utils.Convert; public class Eth1RpcAcceptanceTestBase extends AcceptanceTestBase { @@ -31,6 +40,8 @@ public class Eth1RpcAcceptanceTestBase extends AcceptanceTestBase { Convert.toWei("5", Convert.Unit.SZABO).toBigIntegerExact(); protected BesuNode besu; + protected final MetadataFileHelpers metadataFileHelpers = new MetadataFileHelpers(); + protected Path keyFileTempDir; protected void startBesu() { final BesuNodeConfig besuNodeConfig = @@ -43,6 +54,19 @@ protected void startBesu() { besu.awaitStartupCompletion(); } + @BeforeEach + public synchronized void generateTempFile(@TempDir Path testDirectory) throws URISyntaxException { + final String keyPath = + new File(Resources.getResource("secp256k1/wallet.json").toURI()).getAbsolutePath(); + + final Path keyConfigFile = testDirectory.resolve("arbitrary_secp.yaml"); + + metadataFileHelpers.createKeyStoreYamlFileAt( + keyConfigFile, Path.of(keyPath), "pass", KeyType.SECP256K1); + + keyFileTempDir = testDirectory; + } + @AfterEach public synchronized void shutdownBesu() { if (besu != null) { @@ -50,4 +74,8 @@ public synchronized void shutdownBesu() { besu = null; } } + + protected Account richBenefactor() { + return signer.accounts().richBenefactor(); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/EthRpcDownstreamTlsAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/EthRpcDownstreamTlsAcceptanceTest.java index 8b3e4ec00..b2c372597 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/EthRpcDownstreamTlsAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/EthRpcDownstreamTlsAcceptanceTest.java @@ -69,6 +69,7 @@ private void startSigner( final Path fingerPrintFilePath = workDir.resolve("known_servers"); final SignerConfigurationBuilder builder = new SignerConfigurationBuilder() + .withKeyStoreDirectory(keyFileTempDir) .withMode("eth1") .withChainIdProvider(new ConfigurationChainId(DEFAULT_CHAIN_ID)); final Optional downstreamWeb3ServerPort = diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ReplayProtectionAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ReplayProtectionAcceptanceTest.java new file mode 100644 index 000000000..9c911a83e --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ReplayProtectionAcceptanceTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.tests.eth1rpc.signing; + +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.WRONG_CHAIN_ID; + +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.besu.BesuNodeConfig; +import tech.pegasys.web3signer.dsl.besu.BesuNodeConfigBuilder; +import tech.pegasys.web3signer.dsl.besu.BesuNodeFactory; +import tech.pegasys.web3signer.dsl.signer.SignerConfiguration; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; +import tech.pegasys.web3signer.tests.eth1rpc.Eth1RpcAcceptanceTestBase; + +import java.math.BigInteger; + +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.utils.Convert; +import org.web3j.utils.Convert.Unit; + +public class ReplayProtectionAcceptanceTest extends Eth1RpcAcceptanceTestBase { + + private static final String RECIPIENT = "0x1b00ba00ca00bb00aa00bc00be00ac00ca00da00"; + private static final BigInteger TRANSFER_AMOUNT_WEI = + Convert.toWei("1.75", Unit.ETHER).toBigIntegerExact(); + + private void setUp(final String genesis) { + final BesuNodeConfig besuNodeConfig = + BesuNodeConfigBuilder.aBesuNodeConfig().withGenesisFile(genesis).build(); + + besu = BesuNodeFactory.create(besuNodeConfig); + besu.start(); + besu.awaitStartupCompletion(); + + final SignerConfiguration web3SignerConfiguration = + new SignerConfigurationBuilder() + .withKeyStoreDirectory(keyFileTempDir) + .withMode("eth1") + .withDownstreamHttpPort(besu.ports().getHttpRpc()) + .withChainIdProvider(new ConfigurationChainId(2018)) + .build(); + + startSigner(web3SignerConfiguration); + } + + @Test + public void wrongChainId() { + setUp("besu/eth_hash_4404.json"); + + final SignerResponse signerResponse = + signer + .transactions() + .submitExceptional( + Transaction.createEtherTransaction( + richBenefactor().address(), + richBenefactor().nextNonceAndIncrement(), + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + TRANSFER_AMOUNT_WEI)); + + assertThat(signerResponse.status()).isEqualTo(OK); + assertThat(signerResponse.jsonRpc().getError()).isEqualTo(WRONG_CHAIN_ID); + } + + @Test + public void unnecessaryChainId() { + setUp("besu/eth_hash_2018_no_replay_protection.json"); + + final SignerResponse signerResponse = + signer + .transactions() + .submitExceptional( + Transaction.createEtherTransaction( + richBenefactor().address(), + richBenefactor().nextNonceAndIncrement(), + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + TRANSFER_AMOUNT_WEI)); + + assertThat(signerResponse.status()).isEqualTo(OK); + assertThat(signerResponse.jsonRpc().getError()) + .isEqualTo(REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/SmartContractAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/SmartContractAcceptanceTest.java new file mode 100644 index 000000000..634812179 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/SmartContractAcceptanceTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.tests.eth1rpc.signing; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.web3signer.dsl.Contracts.GAS_LIMIT; +import static tech.pegasys.web3signer.dsl.utils.Hex.hex; + +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; +import tech.pegasys.web3signer.dsl.signer.SignerConfiguration; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.tests.eth1rpc.Eth1RpcAcceptanceTestBase; +import tech.pegasys.web3signer.tests.eth1rpc.signing.contract.generated.SimpleStorage; + +import java.math.BigInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.methods.request.Transaction; + +public class SmartContractAcceptanceTest extends Eth1RpcAcceptanceTestBase { + + private static final String SIMPLE_STORAGE_BINARY = SimpleStorage.BINARY; + private static final String SIMPLE_STORAGE_GET = "0x6d4ce63c"; + private static final String SIMPLE_STORAGE_SET_7 = + "0x60fe47b10000000000000000000000000000000000000000000000000000000000000007"; + + @BeforeEach + public void setup() { + startBesu(); + final SignerConfiguration web3SignerConfiguration = + new SignerConfigurationBuilder() + .withKeyStoreDirectory(keyFileTempDir) + .withMode("eth1") + .withDownstreamHttpPort(besu.ports().getHttpRpc()) + .withChainIdProvider(new ConfigurationChainId(2018)) + .build(); + + startSigner(web3SignerConfiguration); + } + + @Test + public void deployContract() { + final Transaction contract = + Transaction.createContractTransaction( + richBenefactor().address(), + richBenefactor().nextNonceAndIncrement(), + GAS_PRICE, + GAS_LIMIT, + BigInteger.ZERO, + SIMPLE_STORAGE_BINARY); + + final String hash = signer.publicContracts().submit(contract); + besu.publicContracts().awaitBlockContaining(hash); + + final String address = besu.publicContracts().address(hash); + final String code = besu.publicContracts().code(address); + assertThat(code) + .isEqualTo( + "0x60806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146075575b600080fd5b348015605857600080fd5b50607360048036036020811015606d57600080fd5b50356099565b005b348015608057600080fd5b506087609e565b60408051918252519081900360200190f35b600055565b6000549056fea165627a7a72305820cb1d0935d14b589300b12fcd0ab849a7e9019c81da24d6daa4f6b2f003d1b0180029"); + } + + @Test + public void invokeContract() { + final Transaction contract = + Transaction.createContractTransaction( + richBenefactor().address(), + richBenefactor().nextNonceAndIncrement(), + GAS_PRICE, + GAS_LIMIT, + BigInteger.ZERO, + SIMPLE_STORAGE_BINARY); + + final String hash = signer.publicContracts().submit(contract); + besu.publicContracts().awaitBlockContaining(hash); + + final String contractAddress = besu.publicContracts().address(hash); + final Transaction valueBeforeChange = + Transaction.createEthCallTransaction( + richBenefactor().address(), contractAddress, SIMPLE_STORAGE_GET); + final BigInteger startingValue = hex(signer.publicContracts().call(valueBeforeChange)); + final Transaction changeValue = + Transaction.createFunctionCallTransaction( + richBenefactor().address(), + richBenefactor().nextNonceAndIncrement(), + GAS_PRICE, + GAS_LIMIT, + contractAddress, + SIMPLE_STORAGE_SET_7); + + final String valueUpdate = signer.publicContracts().submit(changeValue); + besu.publicContracts().awaitBlockContaining(valueUpdate); + + final Transaction valueAfterChange = + Transaction.createEthCallTransaction( + richBenefactor().address(), contractAddress, SIMPLE_STORAGE_GET); + final BigInteger endValue = hex(signer.publicContracts().call(valueAfterChange)); + assertThat(endValue).isEqualTo(startingValue.add(BigInteger.valueOf(7))); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ValueTransferAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ValueTransferAcceptanceTest.java new file mode 100644 index 000000000..e7be6b278 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/ValueTransferAcceptanceTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.tests.eth1rpc.signing; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.crypto.transaction.type.TransactionType.EIP1559; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; + +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; +import tech.pegasys.web3signer.dsl.Account; +import tech.pegasys.web3signer.dsl.signer.SignerConfiguration; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.signer.SignerResponse; +import tech.pegasys.web3signer.signing.secp256k1.util.AddressUtil; +import tech.pegasys.web3signer.tests.eth1rpc.Eth1RpcAcceptanceTestBase; + +import java.math.BigInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.utils.Convert; +import org.web3j.utils.Convert.Unit; + +public class ValueTransferAcceptanceTest extends Eth1RpcAcceptanceTestBase { + + private static final String RECIPIENT = "0x1b00ba00ca00bb00aa00bc00be00ac00ca00da00"; + private static final long FIFTY_TRANSACTIONS = 50; + private static final String FRONTIER = "0x0"; + private static final String EIP1559 = "0x2"; + + @BeforeEach + public void setup() { + startBesu(); + final SignerConfiguration web3SignerConfiguration = + new SignerConfigurationBuilder() + .withKeyStoreDirectory(keyFileTempDir) + .withMode("eth1") + .withDownstreamHttpPort(besu.ports().getHttpRpc()) + .withChainIdProvider(new ConfigurationChainId(2018)) + .build(); + + startSigner(web3SignerConfiguration); + } + + @Test + public void valueTransfer() { + final BigInteger transferAmountWei = Convert.toWei("1.75", Unit.ETHER).toBigIntegerExact(); + final BigInteger startBalance = besu.accounts().balance(RECIPIENT); + final Transaction transaction = + Transaction.createEtherTransaction( + richBenefactor().address(), + null, + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + transferAmountWei); + + final String hash = signer.transactions().submit(transaction); + besu.transactions().awaitBlockContaining(hash); + + final BigInteger expectedEndBalance = startBalance.add(transferAmountWei); + final BigInteger actualEndBalance = besu.accounts().balance(RECIPIENT); + assertThat(actualEndBalance).isEqualTo(expectedEndBalance); + + // assert tx is FRONTIER type + final var receipt = besu.transactions().getTransactionReceipt(hash).orElseThrow(); + assertThat(receipt.getType()).isEqualTo(FRONTIER); + } + + @Test + public void valueTransferEip1559() { + final BigInteger transferAmountWei = Convert.toWei("1.75", Unit.ETHER).toBigIntegerExact(); + final BigInteger startBalance = besu.accounts().balance(RECIPIENT); + final Transaction eip1559Transaction = + new Transaction( + richBenefactor().address(), + null, + null, + INTRINSIC_GAS, + RECIPIENT, + transferAmountWei, + null, + 2018L, + GAS_PRICE, + GAS_PRICE); + + final String hash = signer.transactions().submit(eip1559Transaction); + besu.transactions().awaitBlockContaining(hash); + + final BigInteger expectedEndBalance = startBalance.add(transferAmountWei); + final BigInteger actualEndBalance = besu.accounts().balance(RECIPIENT); + assertThat(actualEndBalance).isEqualTo(expectedEndBalance); + + // assert tx is EIP1559 type + final var receipt = besu.transactions().getTransactionReceipt(hash).orElseThrow(); + assertThat(receipt.getType()).isEqualTo(EIP1559); + } + + @Test + public void valueTransferWithFromWithout0xPrefix() { + final BigInteger transferAmountWei = Convert.toWei("1.75", Unit.ETHER).toBigIntegerExact(); + final BigInteger startBalance = besu.accounts().balance(RECIPIENT); + final Transaction transaction = + Transaction.createEtherTransaction( + AddressUtil.remove0xPrefix(richBenefactor().address()), + null, + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + transferAmountWei); + + final String hash = signer.transactions().submit(transaction); + besu.transactions().awaitBlockContaining(hash); + + final BigInteger expectedEndBalance = startBalance.add(transferAmountWei); + final BigInteger actualEndBalance = besu.accounts().balance(RECIPIENT); + assertThat(actualEndBalance).isEqualTo(expectedEndBalance); + } + + @Test + public void valueTransferFromAccountWithInsufficientFunds() { + final String recipientAddress = "0x1b11ba11ca11bb11aa11bc11be11ac11ca11da11"; + final BigInteger senderStartBalance = besu.accounts().balance(richBenefactor()); + final BigInteger recipientStartBalance = besu.accounts().balance(recipientAddress); + final BigInteger transferAmountWei = senderStartBalance.add(BigInteger.ONE); + final Transaction transaction = + Transaction.createEtherTransaction( + richBenefactor().address(), + richBenefactor().nextNonce(), + GAS_PRICE, + INTRINSIC_GAS, + recipientAddress, + transferAmountWei); + + final SignerResponse signerResponse = + signer.transactions().submitExceptional(transaction); + assertThat(signerResponse.status()).isEqualTo(OK); + assertThat(signerResponse.jsonRpc().getError()) + .isEqualTo(TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE); + + final BigInteger senderEndBalance = besu.accounts().balance(richBenefactor()); + final BigInteger recipientEndBalance = besu.accounts().balance(recipientAddress); + assertThat(senderEndBalance).isEqualTo(senderStartBalance); + assertThat(recipientEndBalance).isEqualTo(recipientStartBalance); + } + + @Test + public void senderIsNotUnlockedAccount() { + final Account sender = new Account("0x223b55228fb22b89f2216b7222e5522b8222bd22"); + final String recipientAddress = "0x1b22ba22ca22bb22aa22bc22be22ac22ca22da22"; + final BigInteger senderStartBalance = besu.accounts().balance(sender); + final BigInteger recipientStartBalance = besu.accounts().balance(recipientAddress); + final Transaction transaction = + Transaction.createEtherTransaction( + sender.address(), + sender.nextNonce(), + GAS_PRICE, + INTRINSIC_GAS, + recipientAddress, + senderStartBalance); + + final SignerResponse signerResponse = + signer.transactions().submitExceptional(transaction); + assertThat(signerResponse.status()).isEqualTo(BAD_REQUEST); + assertThat(signerResponse.jsonRpc().getError()) + .isEqualTo(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); + + final BigInteger senderEndBalance = besu.accounts().balance(sender); + final BigInteger recipientEndBalance = besu.accounts().balance(recipientAddress); + assertThat(senderEndBalance).isEqualTo(senderStartBalance); + assertThat(recipientEndBalance).isEqualTo(recipientStartBalance); + } + + @Test + public void multipleValueTransfers() { + final BigInteger transferAmountWei = Convert.toWei("1", Unit.ETHER).toBigIntegerExact(); + final BigInteger startBalance = besu.accounts().balance(RECIPIENT); + final Transaction transaction = + Transaction.createEtherTransaction( + richBenefactor().address(), + null, + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + transferAmountWei); + + String hash = null; + for (int i = 0; i < FIFTY_TRANSACTIONS; i++) { + hash = signer.transactions().submit(transaction); + } + besu.transactions().awaitBlockContaining(hash); + + final BigInteger endBalance = besu.accounts().balance(RECIPIENT); + final BigInteger numberOfTransactions = BigInteger.valueOf(FIFTY_TRANSACTIONS); + assertThat(endBalance) + .isEqualTo(startBalance.add(transferAmountWei.multiply(numberOfTransactions))); + } + + @Test + public void valueTransferNonceTooLow() { + valueTransfer(); // call this test to increment the nonce + final BigInteger transferAmountWei = Convert.toWei("15.5", Unit.ETHER).toBigIntegerExact(); + final Transaction transaction = + Transaction.createEtherTransaction( + richBenefactor().address(), + BigInteger.ZERO, + GAS_PRICE, + INTRINSIC_GAS, + RECIPIENT, + transferAmountWei); + + final SignerResponse jsonRpcErrorResponseSignerResponse = + signer.transactions().submitExceptional(transaction); + + assertThat(jsonRpcErrorResponseSignerResponse.jsonRpc().getError()) + .isEqualTo(JsonRpcError.NONCE_TOO_LOW); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/SimpleStorage.sol b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/SimpleStorage.sol new file mode 100644 index 000000000..12a1ffd7c --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/SimpleStorage.sol @@ -0,0 +1,29 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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. + */ +pragma solidity >=0.4.0 <0.6.0; + +// compile with: +// solc SimpleStorage.sol --bin --abi --optimize --overwrite -o . +// then create web3j wrappers with: +// web3j solidity generate -b ./generated/SimpleStorage.bin -a ./generated/SimpleStorage.abi -o ../../../../../../ -p tech.pegasys.ethsigner.tests.signing.contract.generated +contract SimpleStorage { + uint data; + + function set(uint value) public { + data = value; + } + + function get() public view returns (uint) { + return data; + } +} \ No newline at end of file diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.abi b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.abi new file mode 100644 index 000000000..4880337f3 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.abi @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"value","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.bin b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.bin new file mode 100644 index 000000000..60203ddfa --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5060d08061001f6000396000f3fe60806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146075575b600080fd5b348015605857600080fd5b50607360048036036020811015606d57600080fd5b50356099565b005b348015608057600080fd5b506087609e565b60408051918252519081900360200190f35b600055565b6000549056fea165627a7a72305820cb1d0935d14b589300b12fcd0ab849a7e9019c81da24d6daa4f6b2f003d1b0180029 \ No newline at end of file diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.java new file mode 100644 index 000000000..029f91ea0 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/signing/contract/generated/SimpleStorage.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.tests.eth1rpc.signing.contract.generated; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +/** + * Auto generated code. + * + *

Do not modify! + * + *

Please use the web3j command line tools, + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the codegen module to update. + * + *

Generated with web3j version 4.5.5. + */ +@SuppressWarnings("rawtypes") +public class SimpleStorage extends Contract { + public static final String BINARY = + "608060405234801561001057600080fd5b5060d08061001f6000396000f3fe60806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146075575b600080fd5b348015605857600080fd5b50607360048036036020811015606d57600080fd5b50356099565b005b348015608057600080fd5b506087609e565b60408051918252519081900360200190f35b600055565b6000549056fea165627a7a72305820cb1d0935d14b589300b12fcd0ab849a7e9019c81da24d6daa4f6b2f003d1b0180029"; + + public static final String FUNC_SET = "set"; + + public static final String FUNC_GET = "get"; + + @Deprecated + protected SimpleStorage( + String contractAddress, + Web3j web3j, + Credentials credentials, + BigInteger gasPrice, + BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected SimpleStorage( + String contractAddress, + Web3j web3j, + Credentials credentials, + ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected SimpleStorage( + String contractAddress, + Web3j web3j, + TransactionManager transactionManager, + BigInteger gasPrice, + BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected SimpleStorage( + String contractAddress, + Web3j web3j, + TransactionManager transactionManager, + ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public RemoteFunctionCall set(BigInteger value) { + final Function function = + new Function( + FUNC_SET, + Arrays.asList(new Uint256(value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall get() { + final Function function = + new Function( + FUNC_GET, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + @Deprecated + public static SimpleStorage load( + String contractAddress, + Web3j web3j, + Credentials credentials, + BigInteger gasPrice, + BigInteger gasLimit) { + return new SimpleStorage(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static SimpleStorage load( + String contractAddress, + Web3j web3j, + TransactionManager transactionManager, + BigInteger gasPrice, + BigInteger gasLimit) { + return new SimpleStorage(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static SimpleStorage load( + String contractAddress, + Web3j web3j, + Credentials credentials, + ContractGasProvider contractGasProvider) { + return new SimpleStorage(contractAddress, web3j, credentials, contractGasProvider); + } + + public static SimpleStorage load( + String contractAddress, + Web3j web3j, + TransactionManager transactionManager, + ContractGasProvider contractGasProvider) { + return new SimpleStorage(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy( + Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return deployRemoteCall( + SimpleStorage.class, web3j, credentials, contractGasProvider, BINARY, ""); + } + + @Deprecated + public static RemoteCall deploy( + Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return deployRemoteCall( + SimpleStorage.class, web3j, credentials, gasPrice, gasLimit, BINARY, ""); + } + + public static RemoteCall deploy( + Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return deployRemoteCall( + SimpleStorage.class, web3j, transactionManager, contractGasProvider, BINARY, ""); + } + + @Deprecated + public static RemoteCall deploy( + Web3j web3j, + TransactionManager transactionManager, + BigInteger gasPrice, + BigInteger gasLimit) { + return deployRemoteCall( + SimpleStorage.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, ""); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/KeyIdentifiersAcceptanceTestBase.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/KeyIdentifiersAcceptanceTestBase.java index bf941efed..a0a1ef169 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/KeyIdentifiersAcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/KeyIdentifiersAcceptanceTestBase.java @@ -17,6 +17,7 @@ import tech.pegasys.teku.bls.BLSKeyPair; import tech.pegasys.teku.bls.BLSSecretKey; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; import tech.pegasys.web3signer.dsl.signer.Signer; import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; import tech.pegasys.web3signer.dsl.utils.MetadataFileHelpers; @@ -153,6 +154,8 @@ private void createSecpKey(final String privateKeyHexString) { protected void initAndStartSigner(final String mode) { final SignerConfigurationBuilder builder = new SignerConfigurationBuilder(); builder.withKeyStoreDirectory(testDirectory).withMode(mode); + if (mode.equals("eth1")) + builder.withChainIdProvider(new ConfigurationChainId(DEFAULT_CHAIN_ID)); startSigner(builder.build()); } diff --git a/acceptance-tests/src/test/resources/besu/eth_hash_2018_no_replay_protection.json b/acceptance-tests/src/test/resources/besu/eth_hash_2018_no_replay_protection.json new file mode 100644 index 000000000..a6b571926 --- /dev/null +++ b/acceptance-tests/src/test/resources/besu/eth_hash_2018_no_replay_protection.json @@ -0,0 +1,39 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "contractSizeLimit": 2147483647, + "ethash": { + "fixeddifficulty": 100 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/acceptance-tests/src/test/resources/besu/eth_hash_4404.json b/acceptance-tests/src/test/resources/besu/eth_hash_4404.json new file mode 100644 index 000000000..3dcf52227 --- /dev/null +++ b/acceptance-tests/src/test/resources/besu/eth_hash_4404.json @@ -0,0 +1,44 @@ +{ + "config": { + "chainId": 4404, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "constantinopleFixBlock": 0, + "contractSizeLimit": 2147483647, + "ethash": { + "fixeddifficulty": 100 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/acceptance-tests/src/test/resources/besu/genesis.json b/acceptance-tests/src/test/resources/besu/genesis.json index 998ec47ba..df5eec9a8 100644 --- a/acceptance-tests/src/test/resources/besu/genesis.json +++ b/acceptance-tests/src/test/resources/besu/genesis.json @@ -9,6 +9,7 @@ "byzantiumBlock": 0, "constantinopleBlock": 0, "constantinopleFixBlock": 0, + "londonBlock": 0, "contractSizeLimit": 2147483647, "ethash": { "fixeddifficulty": 100 diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/DefaultTestBase.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/DefaultTestBase.java new file mode 100644 index 000000000..59042a44c --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/DefaultTestBase.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy; + +import org.junit.jupiter.api.BeforeAll; + +public class DefaultTestBase extends IntegrationTestBase { + @SuppressWarnings("unused") + @BeforeAll + private static void setupWeb3Signer() throws Exception { + setupWeb3Signer(DEFAULT_CHAIN_ID); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/EthAccountsIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/EthAccountsIntegrationTest.java index 31b12fb11..77c10767c 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/EthAccountsIntegrationTest.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/EthAccountsIntegrationTest.java @@ -14,7 +14,6 @@ import static java.util.Collections.singletonList; -import tech.pegasys.web3signer.core.Eth1AddressSignerIdentifier; import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcSuccessResponse; import java.util.Map.Entry; @@ -26,7 +25,7 @@ import org.web3j.protocol.core.Request; import org.web3j.protocol.core.methods.response.EthAccounts; -class EthAccountsIntegrationTest extends IntegrationTestBase { +class EthAccountsIntegrationTest extends DefaultTestBase { @Test void ethAccountsRequestFromWeb3jRespondsWithNodesAddress() { @@ -35,15 +34,11 @@ void ethAccountsRequestFromWeb3jRespondsWithNodesAddress() { final Iterable> expectedHeaders = singletonList(ImmutablePair.of("Content", HttpHeaderValues.APPLICATION_JSON.toString())); - // needs the hex prefix - final String expectedAccount = - "0x" + Eth1AddressSignerIdentifier.fromPublicKey(PUBLIC_KEY_HEX_STRING); - final JsonRpcSuccessResponse responseBody = - new JsonRpcSuccessResponse(requestBody.getId(), singletonList(expectedAccount)); + new JsonRpcSuccessResponse(requestBody.getId(), singletonList(unlockedAccount)); sendPostRequestAndVerifyResponse( - request.web3signer(Json.encode(requestBody)), + request.web3Signer(Json.encode(requestBody)), response.web3Signer(expectedHeaders, Json.encode(responseBody))); } } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/FailedConnectionIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/FailedConnectionIntegrationTest.java index 9ad138f42..9a6664fcb 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/FailedConnectionIntegrationTest.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/FailedConnectionIntegrationTest.java @@ -20,7 +20,7 @@ import io.vertx.core.json.Json; import org.junit.jupiter.api.Test; -class FailedConnectionIntegrationTest extends IntegrationTestBase { +class FailedConnectionIntegrationTest extends DefaultTestBase { @Test void failsToConnectToDownStreamRaisesTimeout() { @@ -33,7 +33,7 @@ void failsToConnectToDownStreamRaisesTimeout() { request.getId(), JsonRpcError.FAILED_TO_CONNECT_TO_DOWNSTREAM_NODE)); sendPostRequestAndVerifyResponse( - this.request.web3signer(request.getEncodedRequestBody()), + this.request.web3Signer(request.getEncodedRequestBody()), response.web3Signer(expectedResponse, HttpResponseStatus.GATEWAY_TIMEOUT)); } } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IllegalSignatureCreationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IllegalSignatureCreationTest.java new file mode 100644 index 000000000..08560c2dd --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IllegalSignatureCreationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import tech.pegasys.web3signer.core.service.jsonrpc.EthSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.EthTransaction; +import tech.pegasys.web3signer.signing.secp256k1.Signature; +import tech.pegasys.web3signer.signing.secp256k1.filebased.CredentialSigner; + +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; + +class IllegalSignatureCreationTest { + + @Test + void ensureSignaturesCreatedHavePositiveValues() { + // This problem was identified in Github Issue #247, which identified a specific + // transaction being signed with a given key, resulted in the transaction being rejected by + // Besu due to "INVALID SIGNATURE" - ultimately, it was came down to byte[] --> BigInt resulting + // in negative value. + final EthSendTransactionJsonParameters txnParams = + new EthSendTransactionJsonParameters("0xf17f52151ebef6c7334fad080c5704d77216b732"); + txnParams.gasPrice("0x0"); + txnParams.gas("0x7600"); + txnParams.nonce("0x46"); + txnParams.value("0x1"); + txnParams.data("0x0"); + txnParams.receiver("0x627306090abaB3A6e1400e9345bC60c78a8BEf57"); + + final EthTransaction txn = new EthTransaction(1337L, txnParams, null, null); + final byte[] serialisedBytes = txn.rlpEncode(null); + + final CredentialSigner signer = + new CredentialSigner( + Credentials.create("ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f")); + + final Signature signature = signer.sign(serialisedBytes); + + assertThat(signature.getR().signum()).isOne(); + assertThat(signature.getS().signum()).isOne(); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IntegrationTestBase.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IntegrationTestBase.java index 3b37d4d56..89d3804f3 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IntegrationTestBase.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/IntegrationTestBase.java @@ -13,6 +13,7 @@ package tech.pegasys.web3signer.core.jsonrpcproxy; import static io.restassured.RestAssured.given; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockserver.integration.ClientAndServer.startClientAndServer; @@ -22,6 +23,8 @@ import static org.mockserver.model.JsonBody.json; import static org.web3j.utils.Async.defaultExecutorService; +import tech.pegasys.web3signer.core.Eth1AddressSignerIdentifier; +import tech.pegasys.web3signer.core.Eth1AddressSignerProvider; import tech.pegasys.web3signer.core.Eth1Runner; import tech.pegasys.web3signer.core.config.BaseConfig; import tech.pegasys.web3signer.core.config.Eth1Config; @@ -39,11 +42,15 @@ import tech.pegasys.web3signer.core.jsonrpcproxy.support.TestEth1Config; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.ConfigurationChainId; import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.secp256k1.Signer; +import tech.pegasys.web3signer.signing.secp256k1.SingleSignerProvider; +import tech.pegasys.web3signer.signing.secp256k1.filebased.FileBasedSignerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -65,14 +72,17 @@ import org.apache.logging.log4j.Logger; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.JsonBody; import org.mockserver.model.RegexBody; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.WalletUtils; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.JsonRpc2_0Web3j; +import org.web3j.protocol.eea.Eea; +import org.web3j.protocol.eea.JsonRpc2_0Eea; public class IntegrationTestBase { @@ -84,36 +94,47 @@ public class IntegrationTestBase { private static Vertx vertx; private static Eth1Runner runner; static ClientAndServer clientAndServer; - + static Credentials credentials; private JsonRpc2_0Web3j jsonRpc; + private JsonRpc2_0Eea eeaJsonRpc; protected final EthRequestFactory request = new EthRequestFactory(); protected final EthResponseFactory response = new EthResponseFactory(); + static String unlockedAccount; private static final Duration downstreamTimeout = Duration.ofSeconds(1); @TempDir static Path dataPath; @TempDir static Path keyConfigPath; - public static final String PUBLIC_KEY_HEX_STRING = - "09b02f8a5fddd222ade4ea4528faefc399623af3f736be3c44f03e2df22fb792f3931a4d9573d333ca74343305762a753388c3422a86d98b713fc91c1ea04842"; public static final long DEFAULT_CHAIN_ID = 9; + public static final int DEFAULT_ID = 77; + static final String MALFORMED_JSON = "{Bad Json: {{{}"; - @BeforeAll - static void setupWeb3Signer() throws Exception { - setupWeb3Signer(""); + static void setupWeb3Signer(final long chainId) throws Exception { + setupWeb3Signer(chainId, ""); } - static void setupWeb3Signer(final String downstreamHttpRequestPath) throws Exception { - setupWeb3Signer(downstreamHttpRequestPath, List.of("sample.com")); + static void setupWeb3Signer(final long chainId, final String downstreamHttpRequestPath) + throws Exception { + setupWeb3Signer(chainId, downstreamHttpRequestPath, List.of("sample.com")); } static void setupWeb3Signer( - final String downstreamHttpRequestPath, final List allowedCorsOrigin) + final long chainId, + final String downstreamHttpRequestPath, + final List allowedCorsOrigin) throws Exception { clientAndServer = startClientAndServer(); - createKeyStoreYamlFile(); + final File keyFile = createKeyFile(); + final File passwordFile = createFile("password"); + credentials = WalletUtils.loadCredentials("password", keyFile); + + final Eth1AddressSignerProvider transactionSignerProvider = + new Eth1AddressSignerProvider(new SingleSignerProvider(signer(keyFile, passwordFile))); + + createKeyStoreYamlFile(transactionSignerProvider); final BaseConfig baseConfig = new TestBaseConfig(dataPath, keyConfigPath, allowedCorsOrigin); final Eth1Config eth1Config = @@ -122,7 +143,7 @@ static void setupWeb3Signer( LOCALHOST, clientAndServer.getLocalPort(), downstreamTimeout, - new ConfigurationChainId(DEFAULT_CHAIN_ID)); + new ConfigurationChainId(chainId)); vertx = Vertx.vertx(); runner = new Eth1Runner(baseConfig, eth1Config); runner.run(); @@ -136,15 +157,27 @@ static void setupWeb3Signer( "Started web3signer on port {}, eth stub node on port {}", web3signerPort, clientAndServer.getLocalPort()); + + unlockedAccount = + transactionSignerProvider.availablePublicKeys().stream() + .map(Eth1AddressSignerIdentifier::fromPublicKey) + .map(signerIdentifier -> "0x" + signerIdentifier.toStringIdentifier()) + .findAny() + .orElseThrow(); } Web3j jsonRpc() { return jsonRpc; } + Eea eeaJsonRpc() { + return eeaJsonRpc; + } + @BeforeEach public void setup() { jsonRpc = new JsonRpc2_0Web3j(null, 2000, defaultExecutorService()); + eeaJsonRpc = new JsonRpc2_0Eea(null); if (clientAndServer.isRunning()) { clientAndServer.reset(); } @@ -179,6 +212,15 @@ void setupEthNodeResponse( .withStatusCode(response.getStatusCode())); } + void timeoutRequest(final String bodyRegex) { + final int ENSURE_TIMEOUT = 5; + clientAndServer + .when(request().withBody(new RegexBody(bodyRegex))) + .respond( + response() + .withDelay(TimeUnit.MILLISECONDS, downstreamTimeout.toMillis() + ENSURE_TIMEOUT)); + } + void timeoutRequest(final EthNodeRequest request) { final int ENSURE_TIMEOUT = 5; clientAndServer @@ -334,15 +376,45 @@ private static void waitForNonEmptyFileToExist(final Path path) { }); } - private static void createKeyStoreYamlFile() throws IOException, URISyntaxException { + private static void createKeyStoreYamlFile(Eth1AddressSignerProvider transactionSignerProvider) + throws IOException, URISyntaxException { final MetadataFileHelper METADATA_FILE_HELPERS = new MetadataFileHelper(); final String keyPath = - new File(Resources.getResource("secp256k1/wallet.json").toURI()).getAbsolutePath(); + new File(Resources.getResource("keyfile.json").toURI()).getAbsolutePath(); + + String unlockedAccountAddress = + transactionSignerProvider.availablePublicKeys().stream() + .map(Eth1AddressSignerIdentifier::fromPublicKey) + .map(signerIdentifier -> "0x" + signerIdentifier.toStringIdentifier()) + .findAny() + .get(); METADATA_FILE_HELPERS.createKeyStoreYamlFileAt( - keyConfigPath.resolve(PUBLIC_KEY_HEX_STRING + ".yaml"), + keyConfigPath.resolve(unlockedAccountAddress + ".yaml"), Path.of(keyPath), - "pass", + "password", KeyType.SECP256K1); } + + private static Signer signer(final File keyFile, final File passwordFile) { + return FileBasedSignerFactory.createSigner(keyFile.toPath(), passwordFile.toPath()); + } + + @SuppressWarnings("UnstableApiUsage") + private static File createKeyFile() throws IOException { + final URL walletResource = Resources.getResource("keyfile.json"); + final Path wallet = Files.createTempFile("ethsigner_intg_keyfile", ".json"); + Files.write(wallet, Resources.toString(walletResource, UTF_8).getBytes(UTF_8)); + final File keyFile = wallet.toFile(); + keyFile.deleteOnExit(); + return keyFile; + } + + private static File createFile(final String s) throws IOException { + final Path path = Files.createTempFile("file", ".file"); + Files.write(path, s.getBytes(UTF_8)); + final File file = path.toFile(); + file.deleteOnExit(); + return file; + } } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/ProxyIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/ProxyIntegrationTest.java index 75a0cbb35..2196ba2f7 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/ProxyIntegrationTest.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/ProxyIntegrationTest.java @@ -53,7 +53,7 @@ public class ProxyIntegrationTest extends IntegrationTestBase { @BeforeAll public static void localSetup() { try { - setupWeb3Signer(ROOT_PATH); + setupWeb3Signer(DEFAULT_CHAIN_ID, ROOT_PATH); } catch (final Exception e) { throw new RuntimeException("Failed to setup web3signer", e); } @@ -65,7 +65,7 @@ void rpcRequestWithHeadersIsProxied() { request.ethNode(RPC_REQUEST), response.ethNode(RESPONSE_HEADERS, RPC_RESPONSE)); sendPostRequestAndVerifyResponse( - request.web3signer(REQUEST_HEADERS, RPC_REQUEST), + request.web3Signer(REQUEST_HEADERS, RPC_REQUEST), response.web3Signer(RESPONSE_HEADERS, RPC_RESPONSE)); verifyEthNodeReceived(REQUEST_HEADERS, RPC_REQUEST); @@ -80,7 +80,7 @@ void requestWithHostHeaderIsRenamedToXForwardedHost() { List.of(ImmutablePair.of("Accept", "*.*"), ImmutablePair.of("Host", "localhost")); sendPostRequestAndVerifyResponse( - request.web3signer(requestHeaders, RPC_REQUEST), + request.web3Signer(requestHeaders, RPC_REQUEST), response.web3Signer(RESPONSE_HEADERS, RPC_RESPONSE)); final Iterable> expectedForwardedHeaders = @@ -102,7 +102,7 @@ void requestWithHostHeaderOverwritesExistingXForwardedHost() { ImmutablePair.of("X-Forwarded-Host", "nowhere")); sendPostRequestAndVerifyResponse( - request.web3signer(requestHeaders, RPC_REQUEST), + request.web3Signer(requestHeaders, RPC_REQUEST), response.web3Signer(RESPONSE_HEADERS, RPC_RESPONSE)); final Iterable> expectedForwardedHeaders = @@ -121,7 +121,7 @@ void requestReturningErrorIsProxied() { response.ethNode("Not Found", HttpResponseStatus.NOT_FOUND)); sendPostRequestAndVerifyResponse( - request.web3signer(ethProtocolVersionRequest), + request.web3Signer(ethProtocolVersionRequest), response.web3Signer("Not Found", HttpResponseStatus.NOT_FOUND)); verifyEthNodeReceived(ethProtocolVersionRequest); @@ -134,7 +134,7 @@ void postRequestToNonRootPathIsProxied() { response.ethNode(RESPONSE_HEADERS, RPC_RESPONSE, HttpResponseStatus.OK)); sendPostRequestAndVerifyResponse( - request.web3signer(REQUEST_HEADERS, RPC_REQUEST), + request.web3Signer(REQUEST_HEADERS, RPC_REQUEST), response.web3Signer(RESPONSE_HEADERS, RPC_RESPONSE), "/login"); @@ -150,7 +150,7 @@ void rpcNonPostRequestsAreNotProxied(final HttpMethod httpMethod) { sendRequestAndVerifyResponse( httpMethod, - request.web3signer(REQUEST_HEADERS, RPC_REQUEST), + request.web3Signer(REQUEST_HEADERS, RPC_REQUEST), response.web3Signer(NOT_FOUND_BODY, HttpResponseStatus.NOT_FOUND), "/login"); @@ -166,7 +166,7 @@ void nonRpcRequestsAreNotProxied(final HttpMethod httpMethod) { sendRequestAndVerifyResponse( httpMethod, - request.web3signer(REQUEST_HEADERS, NON_RPC_REQUEST), + request.web3Signer(REQUEST_HEADERS, NON_RPC_REQUEST), response.web3Signer(NOT_FOUND_BODY, HttpResponseStatus.NOT_FOUND), "/login"); @@ -192,7 +192,7 @@ void requestWithOriginHeaderProducesResponseWithCorsHeader() { ImmutablePair.of("Origin", originDomain)); sendPostRequestAndVerifyResponse( - request.web3signer(requestHeaders, RPC_REQUEST), + request.web3Signer(requestHeaders, RPC_REQUEST), response.web3Signer(expectedResponseHeaders, RPC_RESPONSE)); // Cors headers should not be forwarded to the downstream web3 provider (CORS is handled @@ -211,7 +211,7 @@ void requestWithMisMatchedDomainReceives403() { ImmutablePair.of("Origin", originDomain)); sendPostRequestAndVerifyResponse( - request.web3signer(requestHeaders, RPC_REQUEST), + request.web3Signer(requestHeaders, RPC_REQUEST), response.web3Signer("", HttpResponseStatus.FORBIDDEN)); } @@ -231,7 +231,7 @@ void multiValueHeadersFromDownstreamArePassedBackToCallingApplication() { response.ethNode(multiValueResponseHeader, RPC_RESPONSE, HttpResponseStatus.OK)); sendPostRequestAndVerifyResponse( - request.web3signer(requestHeaders, RPC_REQUEST), + request.web3Signer(requestHeaders, RPC_REQUEST), response.web3Signer(multiValueResponseHeader, RPC_RESPONSE), "/login"); } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEeaSendTransactionIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEeaSendTransactionIntegrationTest.java new file mode 100644 index 000000000..71bc16357 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEeaSendTransactionIntegrationTest.java @@ -0,0 +1,717 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy; + +import static io.netty.handler.codec.http.HttpResponseStatus.GATEWAY_TIMEOUT; +import static java.math.BigInteger.ONE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVACY_GROUP_ID; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVATE_FOR; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVATE_FROM; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.PrivateTransaction.privacyGroupIdTransaction; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_DATA_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS_PRICE_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_VALUE_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder.TRANSACTION_COUNT_METHOD.PRIV_EEA_GET_TRANSACTION_COUNT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder.TRANSACTION_COUNT_METHOD.PRIV_GET_TRANSACTION_COUNT; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.ETH_SEND_TX_REPLACEMENT_UNDERPRICED; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INTERNAL_ERROR; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INVALID_PARAMS; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.NONCE_TOO_LOW; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; + +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendRawTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.PrivateTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder; + +import java.util.Optional; + +import io.netty.handler.codec.http.HttpResponseStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +/** Signing is a step during proxying a sendTransaction() JSON-RPC request to an Ethereum node. */ +class SigningEeaSendTransactionIntegrationTest extends DefaultTestBase { + + private static final String VALID_BODY_RESPONSE = + "{\"jsonrpc\" : \"2.0\",\"id\" : 1,\"result\" : \"VALID\"}"; + private static final String INVALID_PARAMS_BODY = + "{\"jsonrpc\":\"2.0\",\"id\":77,\"error\":{\"code\":-32602,\"message\":\"Invalid params\"}}"; + + private EeaSendTransaction sendTransaction; + private EeaSendRawTransaction sendRawTransaction; + private final PrivateTransaction.Builder transactionBuilder = + PrivateTransaction.defaultTransaction(); + + private static String getTxCountRequestBody(final String account, final String groupId) { + return String.format( + "{\"jsonrpc\":\"2.0\",\"method\":\"priv_getTransactionCount\",\"params\":[\"%s\",\"%s\"]}", + account, groupId); + } + + private static String getEeaTxCountRequestBody( + final String account, final String privateFrom, final String privateFor) { + return String.format( + "{\"jsonrpc\":\"2.0\",\"method\":\"priv_getEeaTransactionCount\",\"params\":[\"%s\",\"%s\",[\"%s\"]]}", + account, privateFrom, privateFor); + } + + @BeforeEach + void setUp() { + sendTransaction = new EeaSendTransaction(); + sendRawTransaction = new EeaSendRawTransaction(eeaJsonRpc(), credentials); + + final TransactionCountResponder privEeaGetTransactionResponse = + new TransactionCountResponder(nonce -> nonce.add(ONE), PRIV_EEA_GET_TRANSACTION_COUNT); + clientAndServer + .when(privEeaGetTransactionResponse.request()) + .respond(privEeaGetTransactionResponse); + + final TransactionCountResponder privGetTransactionResponse = + new TransactionCountResponder(nonce -> nonce.add(ONE), PRIV_GET_TRANSACTION_COUNT); + clientAndServer.when(privGetTransactionResponse.request()).respond(privGetTransactionResponse); + } + + @Test + void proxyMalformedJsonResponseFromNode() { + final String rawTransaction = sendRawTransaction.request(); + setUpEthNodeResponse(request.ethNode(rawTransaction), response.ethNode(MALFORMED_JSON)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(rawTransaction), response.web3Signer(MALFORMED_JSON)); + } + + @Test + void invalidParamsResponseWhenNonceIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withNonce("I'm an invalid nonce format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void missingNonceResultsInEthNodeRespondingSuccessfully() { + final String ethNodeResponseBody = VALID_BODY_RESPONSE; + final String requestBody = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + + setUpEthNodeResponse(request.ethNode(requestBody), response.ethNode(ethNodeResponseBody)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(ethNodeResponseBody)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsTooShort() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0x577919ae5df4941180eac211965f275CDCE314D"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsTooLong() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0x1577919ae5df4941180eac211965f275CDCE314D"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsMalformedHex() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0xb60e8dd61c5d32be8058bb8eb970870f07233XXX"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsWhenFromAddressIsEmpty() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withFrom(""))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenFromAddressCaseMismatchesUnlockedAccount() { + final Request sendTransactionRequest = + sendTransaction.request( + transactionBuilder.withFrom("0x7577919ae5df4941180eac211965f275CDCE314D")); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request( + transactionBuilder.withFrom("0x7577919ae5df4941180eac211965f275cdce314d"))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenMissingFromAddress() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingPrivateFrom())), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenToAddressIsEmpty() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo("")); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenEmptyToAddress() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo("")); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenToHasAddressMissingHexPrefix() { + final Request sendTransactionRequest = + sendTransaction.request( + transactionBuilder.withTo("7577919ae5df4941180eac211965f275CDCE314D")); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request( + transactionBuilder.withTo("0x7577919ae5df4941180eac211965f275CDCE314D"))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenFromHasAddressMissingHexPrefix() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withFrom(UNLOCKED_ACCOUNT.substring(2))); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withFrom(UNLOCKED_ACCOUNT.substring(2)))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenMissingToAddress() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingTo()); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenToAddressIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenMissingValue() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingValue()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withValue(FIELD_VALUE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenValueIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withValue(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withValue(FIELD_VALUE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenValueIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withValue("I'm an invalid value format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenMissingGas() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingGas()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGas(FIELD_GAS_DEFAULT))); + + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d7777777"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenGasIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withGas(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGas(FIELD_GAS_DEFAULT))); + + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d7777777"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenGasIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withGas("I'm an invalid gas format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenMissingGasPrice() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingGasPrice()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGasPrice(FIELD_GAS_PRICE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenGasPriceIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withGasPrice((null))); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGasPrice(FIELD_GAS_PRICE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenGasPriceIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withGasPrice("I'm an invalid gas price format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signSendTransactionWhenMissingData() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingData()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withData(FIELD_DATA_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signSendTransaction() { + final PrivateTransaction privateTransaction = transactionBuilder.build(); + final Request sendTransactionRequest = + sendTransaction.request(privateTransaction); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(privateTransaction)); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signSendTransactionWithPrivacyGroupId() { + final PrivateTransaction privateTransaction = privacyGroupIdTransaction().build(); + final Request sendTransactionRequest = + sendTransaction.request(privateTransaction); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(privateTransaction)); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void missingNonceResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), response.ethNode(NONCE_TOO_LOW)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void nullNonceResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), response.ethNode(NONCE_TOO_LOW)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withNonce(null))), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void nullNonceWithUnderpricedResponseResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), + response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withNonce(null))), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void missingNonceInPrivateTransactionIsPopulated() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void missingNonceResultsInRequestToPrivGetEeaTransactionCount() { + final String ethNodeResponseBody = VALID_BODY_RESPONSE; + final String requestBody = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse(request.ethNode(requestBody), response.ethNode(ethNodeResponseBody)); + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(ethNodeResponseBody)); + final String expectedBody = + getEeaTxCountRequestBody(UNLOCKED_ACCOUNT, PRIVATE_FROM, PRIVATE_FOR); + verifyEthNodeReceived(expectedBody); + } + + @Test + void missingNonceForTransactionWithPrivacyGroupIdResultsInRequestToPrivGetTransactionCount() { + final String ethNodeResponseBody = VALID_BODY_RESPONSE; + final String requestBody = + sendRawTransaction.request( + sendTransaction.request(privacyGroupIdTransaction().withNonce("0x1"))); + setUpEthNodeResponse(request.ethNode(requestBody), response.ethNode(ethNodeResponseBody)); + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(privacyGroupIdTransaction().missingNonce())), + response.web3Signer(ethNodeResponseBody)); + final String expectedBody = getTxCountRequestBody(UNLOCKED_ACCOUNT, PRIVACY_GROUP_ID); + verifyEthNodeReceived(expectedBody); + } + + @Test + void transactionWithMissingNonceReturnsErrorsOtherThanLowNonceToCaller() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), + response.ethNode(INVALID_PARAMS_BODY, HttpResponseStatus.BAD_REQUEST)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INVALID_PARAMS_BODY, HttpResponseStatus.BAD_REQUEST)); + } + + @Test + void moreThanTenNonceTooLowErrorsReturnsAnErrorToUser() { + setupEthNodeResponse(".*eea_sendRawTransaction.*", response.ethNode(NONCE_TOO_LOW), 11); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INTERNAL_ERROR), + "/", + Optional.of(5000)); + } + + @Test + void moreThanTenUnderpricedErrorsReturnsAnErrorToUser() { + setupEthNodeResponse( + ".*eea_sendRawTransaction.*", response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED), 11); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INTERNAL_ERROR), + "/", + Optional.of(5000)); + } + + @Test + void thirdNonceRetryTimesOutAndGatewayTimeoutIsReturnedToClient() { + setupEthNodeResponse(".*eea_sendRawTransaction.*", response.ethNode(NONCE_TOO_LOW), 3); + timeoutRequest(".*eea_sendRawTransaction.*"); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT, GATEWAY_TIMEOUT)); + } + + @Test + void thirdNonceRetryForUnderpricedTimesOutAndGatewayTimeoutIsReturnedToClient() { + setupEthNodeResponse( + ".*eea_sendRawTransaction.*", response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED), 3); + timeoutRequest(".*eea_sendRawTransaction.*"); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT, GATEWAY_TIMEOUT)); + } + + @Test + void invalidParamsResponseWhenMissingPrivateFrom() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingPrivateFrom())), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenMissingPrivateFor() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingPrivateFor())), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenPrivateForIsNull() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withPrivateFor(null))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenMissingRestriction() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingRestriction())), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenRestrictionIsNull() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withRestriction(null))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenRestrictionHasInvalidValue() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withRestriction("invalid"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void invalidParamsResponseWhenBothPrivateForAndPrivacyGroupAreUsed() { + final PrivateTransaction transactionWithBothPrivateFromAndPrivacyGroupId = + transactionBuilder.withPrivacyGroupId(PRIVACY_GROUP_ID).build(); + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionWithBothPrivateFromAndPrivacyGroupId)), + response.web3Signer(INVALID_PARAMS)); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionIntegrationTest.java new file mode 100644 index 000000000..e22277b75 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionIntegrationTest.java @@ -0,0 +1,621 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy; + +import static io.netty.handler.codec.http.HttpResponseStatus.GATEWAY_TIMEOUT; +import static java.math.BigInteger.ONE; +import static java.util.Collections.singletonList; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_DATA_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS_PRICE_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_VALUE_DEFAULT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder.TRANSACTION_COUNT_METHOD.ETH_GET_TRANSACTION_COUNT; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.ETH_SEND_TX_REPLACEMENT_UNDERPRICED; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INTERNAL_ERROR; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INVALID_PARAMS; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.NONCE_TOO_LOW; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; + +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendRawTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.Transaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder; + +import java.util.Optional; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.json.Json; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +/** Signing is a step during proxying a sendTransaction() JSON-RPC request to an Ethereum node. */ +class SigningEthSendTransactionIntegrationTest extends DefaultTestBase { + + private static final String VALID_BODY_RESPONSE = + "{\"jsonrpc\" : \"2.0\",\"id\" : 1,\"result\" : \"VALID\"}"; + private static final String INVALID_PARAMS_BODY = + "{\"jsonrpc\":\"2.0\",\"id\":77,\"error\":{\"code\":-32602,\"message\":\"Invalid params\"}}"; + + private SendTransaction sendTransaction; + private SendRawTransaction sendRawTransaction; + private final Transaction.Builder transactionBuilder = Transaction.defaultTransaction(); + + @BeforeEach + void setUp() { + sendTransaction = new SendTransaction(); + sendRawTransaction = new SendRawTransaction(jsonRpc(), credentials); + final TransactionCountResponder getTransactionResponse = + new TransactionCountResponder(nonce -> nonce.add(ONE), ETH_GET_TRANSACTION_COUNT); + clientAndServer.when(getTransactionResponse.request()).respond(getTransactionResponse); + } + + @Test + void proxyMalformedJsonResponseFromNode() { + final String rawTransaction = sendRawTransaction.request(); + setUpEthNodeResponse(request.ethNode(rawTransaction), response.ethNode(MALFORMED_JSON)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(rawTransaction), response.web3Signer(MALFORMED_JSON)); + } + + @Test + void invalidParamsResponseWhenNonceIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withNonce("I'm an invalid nonce format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void missingNonceResultsInEthNodeRespondingSuccessfully() { + final String ethNodeResponseBody = VALID_BODY_RESPONSE; + final String requestBody = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + + setUpEthNodeResponse(request.ethNode(requestBody), response.ethNode(ethNodeResponseBody)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(ethNodeResponseBody)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsTooShort() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0x577919ae5df4941180eac211965f275CDCE314D"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsTooLong() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0x1577919ae5df4941180eac211965f275CDCE314D"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsResponseWhenFromAddressIsMalformedHex() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withFrom("0xb60e8dd61c5d32be8058bb8eb970870f07233XXX"))), + response.web3Signer(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + } + + @Test + void invalidParamsWhenFromAddressIsEmpty() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withFrom(""))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenFromAddressCaseMismatchesUnlockedAccount() { + final Request sendTransactionRequest = + sendTransaction.request( + transactionBuilder.withFrom("0x7577919ae5df4941180eac211965f275CDCE314D")); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request( + transactionBuilder.withFrom("0x7577919ae5df4941180eac211965f275cdce314d"))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenMissingFromAddress() { + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingFrom())), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenEmptyToAddress() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo("")); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenEmpty0xToAddress() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo("0x")); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenToHasAddressMissingHexPrefix() { + final Request sendTransactionRequest = + sendTransaction.request( + transactionBuilder.withTo("7577919ae5df4941180eac211965f275CDCE314D")); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request( + transactionBuilder.withTo("0x7577919ae5df4941180eac211965f275CDCE314D"))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenFromHasAddressMissingHexPrefix() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withFrom(UNLOCKED_ACCOUNT.substring(2))); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withFrom(UNLOCKED_ACCOUNT.substring(2)))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenMissingToAddress() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingTo()); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenToAddressIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withTo(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.missingTo())); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1355555"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenMissingValue() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingValue()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withValue(FIELD_VALUE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenValueIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withValue(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withValue(FIELD_VALUE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1666666"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenValueIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withValue("I'm an invalid value format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenMissingGas() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingGas()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGas(FIELD_GAS_DEFAULT))); + + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d7777777"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenGasIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withGas(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGas(FIELD_GAS_DEFAULT))); + + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d7777777"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenGasIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request(transactionBuilder.withGas("I'm an invalid gas format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signTransactionWhenMissingGasPrice() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingGasPrice()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGasPrice(FIELD_GAS_PRICE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signTransactionWhenGasPriceIsNull() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.withGasPrice(null)); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withGasPrice(FIELD_GAS_PRICE_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void invalidParamsResponseWhenGasPriceIsNaN() { + sendPostRequestAndVerifyResponse( + request.web3Signer( + sendTransaction.request( + transactionBuilder.withGasPrice("I'm an invalid gas price format!"))), + response.web3Signer(INVALID_PARAMS)); + } + + @Test + void signSendTransactionWhenMissingData() { + final Request sendTransactionRequest = + sendTransaction.request(transactionBuilder.missingData()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(transactionBuilder.withData(FIELD_DATA_DEFAULT))); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signSendTransactionWhenContract() { + final Request sendTransactionRequest = + sendTransaction.request(Transaction.smartContract()); + final String sendRawTransactionRequest = sendRawTransaction.request(sendTransactionRequest); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + this.request.ethNode(sendRawTransactionRequest), + response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + this.request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void signSendTransaction() { + final Request sendTransactionRequest = + sendTransaction.request(Transaction.smartContract()); + final String sendRawTransactionRequest = sendRawTransaction.request(sendTransactionRequest); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } + + @Test + void missingNonceResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), response.ethNode(NONCE_TOO_LOW)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void nullNonceResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), response.ethNode(NONCE_TOO_LOW)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withNonce(null))), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void nullNonceWithUnderpricedResponseResultsInNewNonceBeingCreatedAndResent() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x0"))); + final String rawTransactionWithNextNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), + response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED)); + + final String successResponseFromWeb3Provider = VALID_BODY_RESPONSE; + setUpEthNodeResponse( + request.ethNode(rawTransactionWithNextNonce), + response.ethNode(successResponseFromWeb3Provider)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.withNonce(null))), + response.web3Signer(successResponseFromWeb3Provider)); + } + + @Test + void transactionWithMissingNonceReturnsErrorsOtherThanLowNonceToCaller() { + final String rawTransactionWithInitialNonce = + sendRawTransaction.request(sendTransaction.request(transactionBuilder.withNonce("0x1"))); + setUpEthNodeResponse( + request.ethNode(rawTransactionWithInitialNonce), + response.ethNode(INVALID_PARAMS_BODY, HttpResponseStatus.BAD_REQUEST)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INVALID_PARAMS_BODY, HttpResponseStatus.BAD_REQUEST)); + } + + @Test + void moreThanTenNonceTooLowErrorsReturnsAnErrorToUser() { + setupEthNodeResponse(".*eth_sendRawTransaction.*", response.ethNode(NONCE_TOO_LOW), 11); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INTERNAL_ERROR), + "/", + Optional.of(5000)); + } + + @Test + void moreThanTenUnderpricedErrorsReturnsAnErrorToUser() { + setupEthNodeResponse( + ".*eth_sendRawTransaction.*", response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED), 11); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(INTERNAL_ERROR), + "/", + Optional.of(5000)); + } + + @Test + void thirdNonceRetryTimesOutAndGatewayTimeoutIsReturnedToClient() { + setupEthNodeResponse(".*eth_sendRawTransaction.*", response.ethNode(NONCE_TOO_LOW), 3); + timeoutRequest(".*eth_sendRawTransaction.*"); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT, GATEWAY_TIMEOUT)); + } + + @Test + void thirdNonceRetryForUnderpricedTimesOutAndGatewayTimeoutIsReturnedToClient() { + setupEthNodeResponse( + ".*eth_sendRawTransaction.*", response.ethNode(ETH_SEND_TX_REPLACEMENT_UNDERPRICED), 3); + timeoutRequest(".*eth_sendRawTransaction.*"); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransaction.request(transactionBuilder.missingNonce())), + response.web3Signer(CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT, GATEWAY_TIMEOUT)); + } + + @Test + void ensureTranactionResponseContainsCorsHeader() { + final Request sendTransactionRequest = + sendTransaction.request(Transaction.smartContract()); + final String sendRawTransactionRequest = sendRawTransaction.request(sendTransactionRequest); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102999999999"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + final String originDomain = "sample.com"; + + sendPostRequestAndVerifyResponse( + request.web3Signer( + singletonList(ImmutablePair.of("Origin", originDomain)), + Json.encode(sendTransactionRequest)), + response.web3Signer( + singletonList( + ImmutablePair.of( + HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), "sample.com")), + sendRawTransactionResponse)); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionWithChainIdIntegrationTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionWithChainIdIntegrationTest.java new file mode 100644 index 000000000..c2c4d7bb0 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/SigningEthSendTransactionWithChainIdIntegrationTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy; + +import static java.math.BigInteger.ONE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder.TRANSACTION_COUNT_METHOD.ETH_GET_TRANSACTION_COUNT; + +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendRawTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.Transaction; +import tech.pegasys.web3signer.core.jsonrpcproxy.support.TransactionCountResponder; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +class SigningEthSendTransactionWithChainIdIntegrationTest extends IntegrationTestBase { + + private SendTransaction sendTransaction; + private SendRawTransaction sendRawTransaction; + + @SuppressWarnings("unused") + @BeforeAll + private static void setupWeb3Signer() throws Exception { + setupWeb3Signer(4123123123L); + } + + @BeforeEach + void setUp() { + sendTransaction = new SendTransaction(); + sendRawTransaction = new SendRawTransaction(jsonRpc(), credentials); + final TransactionCountResponder getTransactionResponse = + new TransactionCountResponder(nonce -> nonce.add(ONE), ETH_GET_TRANSACTION_COUNT); + clientAndServer.when(getTransactionResponse.request()).respond(getTransactionResponse); + } + + @Test + void signSendTransactionWhenContractWithLongChainId() { + final Request sendTransactionRequest = + sendTransaction.request(Transaction.smartContract()); + final String sendRawTransactionRequest = + sendRawTransaction.request( + sendTransaction.request(Transaction.smartContract()), 4123123123L); + final String sendRawTransactionResponse = + sendRawTransaction.response( + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d0592102688888888"); + setUpEthNodeResponse( + request.ethNode(sendRawTransactionRequest), response.ethNode(sendRawTransactionResponse)); + + sendPostRequestAndVerifyResponse( + request.web3Signer(sendTransactionRequest), + response.web3Signer(sendRawTransactionResponse)); + + verifyEthNodeReceived(sendRawTransactionRequest); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/TimeoutTest.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/TimeoutTest.java index d6cf375e5..4cae0eab3 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/TimeoutTest.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/TimeoutTest.java @@ -20,7 +20,7 @@ import io.vertx.core.json.Json; import org.junit.jupiter.api.Test; -class TimeoutTest extends IntegrationTestBase { +class TimeoutTest extends DefaultTestBase { @Test void downstreamConnectsButDoesNotRespondReturnsGatewayTimeout() { @@ -34,7 +34,7 @@ void downstreamConnectsButDoesNotRespondReturnsGatewayTimeout() { request.getId(), JsonRpcError.CONNECTION_TO_DOWNSTREAM_NODE_TIMED_OUT)); sendPostRequestAndVerifyResponse( - this.request.web3signer(request.getEncodedRequestBody()), + this.request.web3Signer(request.getEncodedRequestBody()), response.web3Signer(expectedResponse, HttpResponseStatus.GATEWAY_TIMEOUT)); } } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendRawTransaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendRawTransaction.java new file mode 100644 index 000000000..c21229d9c --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendRawTransaction.java @@ -0,0 +1,130 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static org.web3j.utils.Numeric.decodeQuantity; +import static tech.pegasys.web3signer.core.jsonrpcproxy.IntegrationTestBase.DEFAULT_CHAIN_ID; + +import java.math.BigInteger; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.io.BaseEncoding; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.Response; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.protocol.eea.Eea; +import org.web3j.protocol.eea.crypto.PrivateTransactionEncoder; +import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.utils.Base64String; +import org.web3j.utils.Restriction; + +public class EeaSendRawTransaction { + + private final Eea eeaJsonRpc; + private final Credentials credentials; + + public EeaSendRawTransaction(final Eea eeaJsonRpc, final Credentials credentials) { + this.eeaJsonRpc = eeaJsonRpc; + this.credentials = credentials; + } + + public String request() { + final Request> sendRawTransactionRequest = + eeaJsonRpc.eeaSendRawTransaction( + "0xf90110a0e04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f28609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f0724456780a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567536a00b528cefb87342b2097318cd493d13b4c9bd55bf35bf1b3cf2ef96ee14cee563a06423107befab5530c42a2d7d2590b96c04ee361c868c138b054d7886966121a6aa307837353737393139616535646634393431313830656163323131393635663237356364636533313464ebaa3078643436653864643637633564333262653830353862623865623937303837306630373234343536378a72657374726963746564"); + sendRawTransactionRequest.setId(77); + return Json.encode(sendRawTransactionRequest); + } + + @SuppressWarnings("unchecked") + public String request(final Request request, final long chainId) { + final List params = (List) request.getParams(); + if (params.size() != 1) { + throw new IllegalStateException("eeaSendTransaction request must have only 1 parameter"); + } + final JsonObject transaction = params.get(0); + final String privacyGroupId = transaction.getString("privacyGroupId"); + final RawPrivateTransaction rawTransaction = + privacyGroupId == null + ? createEeaRawPrivateTransaction(transaction) + : createBesuRawPrivateTransaction(transaction, privacyGroupId); + + final byte[] signedTransaction = + PrivateTransactionEncoder.signMessage(rawTransaction, chainId, credentials); + final String value = "0x" + BaseEncoding.base16().encode(signedTransaction).toLowerCase(); + return request(value); + } + + private List privateFor(final JsonArray transaction) { + return transaction.stream() + .map(String.class::cast) + .map(this::valueToBase64String) + .collect(Collectors.toList()); + } + + public String request(final Request request) { + return request(request, DEFAULT_CHAIN_ID); + } + + private BigInteger valueToBigDecimal(final String value) { + return value == null ? null : decodeQuantity(value); + } + + private Base64String valueToBase64String(final String value) { + return value == null ? null : Base64String.wrap(value); + } + + private RawPrivateTransaction createBesuRawPrivateTransaction( + final JsonObject transaction, final String privacyGroupId) { + return RawPrivateTransaction.createTransaction( + valueToBigDecimal(transaction.getString("nonce")), + valueToBigDecimal(transaction.getString("gasPrice")), + valueToBigDecimal(transaction.getString("gas")), + transaction.getString("to"), + transaction.getString("data"), + valueToBase64String(transaction.getString("privateFrom")), + valueToBase64String(privacyGroupId), + Restriction.fromString(transaction.getString("restriction"))); + } + + private RawPrivateTransaction createEeaRawPrivateTransaction(final JsonObject transaction) { + return RawPrivateTransaction.createTransaction( + valueToBigDecimal(transaction.getString("nonce")), + valueToBigDecimal(transaction.getString("gasPrice")), + valueToBigDecimal(transaction.getString("gas")), + transaction.getString("to"), + transaction.getString("data"), + valueToBase64String(transaction.getString("privateFrom")), + privateFor(transaction.getJsonArray("privateFor")), + Restriction.fromString(transaction.getString("restriction"))); + } + + public String request(final String value) { + final Request> sendRawTransactionRequest = + eeaJsonRpc.eeaSendRawTransaction(value); + sendRawTransactionRequest.setId(77); + + return Json.encode(sendRawTransactionRequest); + } + + public String response(final String value) { + final Response sendRawTransactionResponse = new EthSendTransaction(); + sendRawTransactionResponse.setResult(value); + return Json.encode(sendRawTransactionResponse); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendTransaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendTransaction.java new file mode 100644 index 000000000..ae391c37d --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/EeaSendTransaction.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static java.util.Collections.singletonList; +import static tech.pegasys.web3signer.core.jsonrpcproxy.IntegrationTestBase.DEFAULT_ID; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_DATA; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_FROM; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_GAS_PRICE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_NONCE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_TO; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_VALUE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.TransactionJsonUtil.putValue; + +import io.vertx.core.json.JsonObject; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +public class EeaSendTransaction { + public static final String FIELD_PRIVATE_FROM = "privateFrom"; + public static final String FIELD_PRIVATE_FOR = "privateFor"; + public static final String FIELD_RESTRICTION = "restriction"; + public static final String FIELD_PRIVACY_GROUP_ID = "privacyGroupId"; + public static final String UNLOCKED_ACCOUNT = "0x7577919ae5df4941180eac211965f275cdce314d"; + public static final String PRIVATE_FROM = "ZlapEsl9qDLPy/e88+/6yvCUEVIvH83y0N4A6wHuKXI="; + public static final String PRIVATE_FOR = "GV8m0VZAccYGAAYMBuYQtKEj0XtpXeaw2APcoBmtA2w="; + public static final String PRIVACY_GROUP_ID = "/xzRjCLioUBkm5LYuzll61GXyrD5x7bvXzQk/ovJA/4="; + + public static final String DEFAULT_VALUE = "0x0"; + + /** + * Due to the underlying server mocking, When only a single request is used, the contents does not + * actually matter, only their equivalence does. + */ + public Request request(final PrivateTransaction privateTransaction) { + final JsonObject jsonObject = new JsonObject(); + putValue(jsonObject, FIELD_FROM, privateTransaction.getFrom()); + putValue(jsonObject, FIELD_NONCE, privateTransaction.getNonce()); + putValue(jsonObject, FIELD_GAS_PRICE, privateTransaction.getGasPrice()); + putValue(jsonObject, FIELD_GAS, privateTransaction.getGas()); + putValue(jsonObject, FIELD_TO, privateTransaction.getTo()); + putValue(jsonObject, FIELD_VALUE, privateTransaction.getValue()); + putValue(jsonObject, FIELD_DATA, privateTransaction.getData()); + putValue(jsonObject, FIELD_PRIVATE_FROM, privateTransaction.getPrivateFrom()); + putValue(jsonObject, FIELD_PRIVATE_FOR, privateTransaction.getPrivateFor()); + putValue(jsonObject, FIELD_PRIVACY_GROUP_ID, privateTransaction.getPrivacyGroupId()); + putValue(jsonObject, FIELD_RESTRICTION, privateTransaction.getRestriction()); + return createRequest(jsonObject); + } + + public Request request( + final PrivateTransaction.Builder privateTransactionBuilder) { + return request(privateTransactionBuilder.build()); + } + + private Request createRequest(final JsonObject transaction) { + final Request eea_sendTransaction = + new Request<>( + "eea_sendTransaction", singletonList(transaction), null, EthSendTransaction.class); + eea_sendTransaction.setId(DEFAULT_ID); + return eea_sendTransaction; + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/PrivateTransaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/PrivateTransaction.java new file mode 100644 index 000000000..ab36b05cf --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/PrivateTransaction.java @@ -0,0 +1,276 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static java.util.Collections.singletonList; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.DEFAULT_VALUE; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVACY_GROUP_ID; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVATE_FOR; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.PRIVATE_FROM; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.UNLOCKED_ACCOUNT; + +import java.util.List; +import java.util.Optional; + +public class PrivateTransaction { + // Values are held using a value holder as an Optional cannot contain a null value and we want to + // represent missing values using Optional.empty, null values and non-null values + private final Optional> from; + private final Optional> nonce; + private final Optional> gasPrice; + private final Optional> gas; + private final Optional> to; + private final Optional> value; + private final Optional> data; + private final Optional> privateFrom; + private final Optional>> privateFor; + private final Optional> restriction; + private final Optional> privacyGroupId; + + public PrivateTransaction( + final Optional> from, + final Optional> nonce, + final Optional> gasPrice, + final Optional> gas, + final Optional> to, + final Optional> value, + final Optional> data, + final Optional> privateFrom, + final Optional>> privateFor, + final Optional> restriction, + final Optional> privacyGroupId) { + this.from = from; + this.nonce = nonce; + this.gasPrice = gasPrice; + this.gas = gas; + this.to = to; + this.value = value; + this.data = data; + this.privateFrom = privateFrom; + this.privateFor = privateFor; + this.restriction = restriction; + this.privacyGroupId = privacyGroupId; + } + + public Optional> getFrom() { + return from; + } + + public Optional> getNonce() { + return nonce; + } + + public Optional> getGasPrice() { + return gasPrice; + } + + public Optional> getGas() { + return gas; + } + + public Optional> getTo() { + return to; + } + + public Optional> getValue() { + return value; + } + + public Optional> getData() { + return data; + } + + public Optional> getPrivateFrom() { + return privateFrom; + } + + public Optional>> getPrivateFor() { + return privateFor; + } + + public Optional> getRestriction() { + return restriction; + } + + public Optional> getPrivacyGroupId() { + return privacyGroupId; + } + + public static Builder defaultTransaction() { + return new Builder() + .withFrom(UNLOCKED_ACCOUNT) + .withNonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2") + .withGasPrice("0x9184e72a000") + .withGas("0x76c0") + .withTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567") + .withValue(DEFAULT_VALUE) + .withData( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675") + .withPrivateFrom(PRIVATE_FROM) + .withPrivateFor(singletonList(PRIVATE_FOR)) + .withRestriction("restricted"); + } + + public static Builder privacyGroupIdTransaction() { + return new Builder() + .withFrom(UNLOCKED_ACCOUNT) + .withNonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2") + .withGasPrice("0x9184e72a000") + .withGas("0x76c0") + .withTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567") + .withValue(DEFAULT_VALUE) + .withData( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675") + .withPrivateFrom(PRIVATE_FROM) + .withPrivacyGroupId(PRIVACY_GROUP_ID) + .withRestriction("restricted"); + } + + public static class Builder { + private Optional> from = Optional.empty(); + private Optional> nonce = Optional.empty(); + private Optional> gasPrice = Optional.empty(); + private Optional> gas = Optional.empty(); + private Optional> to = Optional.empty(); + private Optional> value = Optional.empty(); + private Optional> data = Optional.empty(); + private Optional> privateFrom = Optional.empty(); + private Optional>> privateFor = Optional.empty(); + private Optional> restriction = Optional.empty(); + private Optional> privacyGroupId = Optional.empty(); + + public Builder withFrom(final String from) { + this.from = createValue(from); + return this; + } + + public Builder withNonce(final String nonce) { + this.nonce = createValue(nonce); + return this; + } + + public Builder missingNonce() { + this.nonce = Optional.empty(); + return this; + } + + public Builder withGasPrice(final String gasPrice) { + this.gasPrice = createValue(gasPrice); + return this; + } + + public Builder missingGasPrice() { + this.gasPrice = Optional.empty(); + return this; + } + + public Builder withGas(final String gas) { + this.gas = createValue(gas); + return this; + } + + public Builder missingGas() { + this.gas = Optional.empty(); + return this; + } + + public Builder withTo(final String to) { + this.to = createValue(to); + return this; + } + + public Builder missingTo() { + this.to = Optional.empty(); + return this; + } + + public Builder withValue(final String value) { + this.value = createValue(value); + return this; + } + + public Builder missingValue() { + this.value = Optional.empty(); + return this; + } + + public Builder withData(final String data) { + this.data = createValue(data); + return this; + } + + public Builder missingData() { + this.data = Optional.empty(); + return this; + } + + public Builder withPrivateFrom(final String privateFrom) { + this.privateFrom = createValue(privateFrom); + return this; + } + + public Builder missingPrivateFrom() { + this.privateFrom = Optional.empty(); + return this; + } + + public Builder withPrivateFor(final List privateFor) { + this.privateFor = createValue(privateFor); + return this; + } + + public Builder missingPrivateFor() { + this.privateFor = Optional.empty(); + return this; + } + + public Builder withPrivacyGroupId(final String privacyGroupId) { + this.privacyGroupId = createValue(privacyGroupId); + return this; + } + + public Builder missingPrivacyGroupId() { + this.privacyGroupId = Optional.empty(); + return this; + } + + public Builder withRestriction(final String restriction) { + this.restriction = createValue(restriction); + return this; + } + + public Builder missingRestriction() { + this.restriction = Optional.empty(); + return this; + } + + public PrivateTransaction build() { + return new PrivateTransaction( + from, + nonce, + gasPrice, + gas, + to, + value, + data, + privateFrom, + privateFor, + restriction, + privacyGroupId); + } + + private Optional> createValue(final T from) { + return Optional.of(new ValueHolder<>(from)); + } + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendRawTransaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendRawTransaction.java new file mode 100644 index 000000000..52d983565 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendRawTransaction.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static org.web3j.utils.Numeric.decodeQuantity; +import static tech.pegasys.web3signer.core.jsonrpcproxy.IntegrationTestBase.DEFAULT_CHAIN_ID; + +import java.math.BigInteger; +import java.util.List; + +import com.google.common.io.BaseEncoding; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.Response; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +public class SendRawTransaction { + + private final Web3j jsonRpc; + private final Credentials credentials; + + public SendRawTransaction(final Web3j jsonRpc, final Credentials credentials) { + this.jsonRpc = jsonRpc; + this.credentials = credentials; + } + + public String request() { + final Request> sendRawTransactionRequest = + jsonRpc.ethSendRawTransaction( + "0xf8b2a0e04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f28609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f07244567849184e72aa9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567535a0f04e0e7b41adea417596550611138a3ec9a452abb6648d734107c53476e76a27a05b826d9e9b4e0dd0e7b8939c102a2079d71cfc27cd6b7bebe5a006d5ad17d780"); + sendRawTransactionRequest.setId(77); + + return Json.encode(sendRawTransactionRequest); + } + + @SuppressWarnings("unchecked") + public String request(final Request request, final long chainId) { + final List params = (List) request.getParams(); + if (params.size() != 1) { + throw new IllegalStateException("sendTransaction request must have only 1 parameter"); + } + final JsonObject transaction = params.get(0); + final RawTransaction rawTransaction = + RawTransaction.createTransaction( + valueToBigDecimal(transaction.getString("nonce")), + valueToBigDecimal(transaction.getString("gasPrice")), + valueToBigDecimal(transaction.getString("gas")), + transaction.getString("to"), + valueToBigDecimal(transaction.getString("value")), + transaction.getString("data")); + final byte[] signedTransaction = + TransactionEncoder.signMessage(rawTransaction, chainId, credentials); + final String value = "0x" + BaseEncoding.base16().encode(signedTransaction).toLowerCase(); + return request(value); + } + + public String request(final Request request) { + return request(request, DEFAULT_CHAIN_ID); + } + + private BigInteger valueToBigDecimal(final String value) { + return value == null ? null : decodeQuantity(value); + } + + public String request(final String value) { + final Request> sendRawTransactionRequest = + jsonRpc.ethSendRawTransaction(value); + sendRawTransactionRequest.setId(77); + + return Json.encode(sendRawTransactionRequest); + } + + public String response(final String value) { + final Response sendRawTransactionResponse = new EthSendTransaction(); + sendRawTransactionResponse.setResult(value); + return Json.encode(sendRawTransactionResponse); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendTransaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendTransaction.java new file mode 100644 index 000000000..bb18688e0 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/SendTransaction.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static java.util.Collections.singletonList; +import static tech.pegasys.web3signer.core.jsonrpcproxy.IntegrationTestBase.DEFAULT_ID; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.TransactionJsonUtil.putValue; + +import io.vertx.core.json.JsonObject; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +public class SendTransaction { + public static final String FIELD_VALUE_DEFAULT = "0x0"; + public static final String FIELD_GAS_DEFAULT = "0x15F90"; + public static final String FIELD_GAS_PRICE_DEFAULT = "0x0"; + public static final String FIELD_DATA_DEFAULT = ""; + public static final String FIELD_FROM = "from"; + public static final String FIELD_NONCE = "nonce"; + public static final String FIELD_TO = "to"; + public static final String FIELD_VALUE = "value"; + public static final String FIELD_GAS = "gas"; + public static final String FIELD_GAS_PRICE = "gasPrice"; + public static final String FIELD_DATA = "data"; + + /** + * Due to the underlying server mocking, When only a single request is used, the contents does not + * actually matter, only their equivalence does. + */ + public Request request(final Transaction transaction) { + final JsonObject jsonObject = new JsonObject(); + putValue(jsonObject, FIELD_FROM, transaction.getFrom()); + putValue(jsonObject, FIELD_NONCE, transaction.getNonce()); + putValue(jsonObject, FIELD_GAS_PRICE, transaction.getGasPrice()); + putValue(jsonObject, FIELD_GAS, transaction.getGas()); + putValue(jsonObject, FIELD_TO, transaction.getTo()); + putValue(jsonObject, FIELD_VALUE, transaction.getValue()); + putValue(jsonObject, FIELD_DATA, transaction.getData()); + return createRequest(jsonObject); + } + + public Request request(final Transaction.Builder transactionBuilder) { + return request(transactionBuilder.build()); + } + + private Request createRequest(final JsonObject transaction) { + final Request eea_sendTransaction = + new Request<>( + "eth_sendTransaction", singletonList(transaction), null, EthSendTransaction.class); + eea_sendTransaction.setId(DEFAULT_ID); + return eea_sendTransaction; + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/Transaction.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/Transaction.java new file mode 100644 index 000000000..e84e0f627 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/Transaction.java @@ -0,0 +1,186 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.EeaSendTransaction.UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc.SendTransaction.FIELD_VALUE_DEFAULT; + +import java.util.Optional; + +public class Transaction { + // Values are held using a value holder as an Optional cannot contain a null value and we want to + // represent missing values using Optional.empty, null values and non-null values + private final Optional> from; + private final Optional> nonce; + private final Optional> gasPrice; + private final Optional> gas; + private final Optional> to; + private final Optional> value; + private final Optional> data; + + public Transaction( + final Optional> from, + final Optional> nonce, + final Optional> gasPrice, + final Optional> gas, + final Optional> to, + final Optional> value, + final Optional> data) { + this.from = from; + this.nonce = nonce; + this.gasPrice = gasPrice; + this.gas = gas; + this.to = to; + this.value = value; + this.data = data; + } + + public Optional> getFrom() { + return from; + } + + public Optional> getNonce() { + return nonce; + } + + public Optional> getGasPrice() { + return gasPrice; + } + + public Optional> getGas() { + return gas; + } + + public Optional> getTo() { + return to; + } + + public Optional> getValue() { + return value; + } + + public Optional> getData() { + return data; + } + + public static Builder smartContract() { + return new Builder() + .withFrom(UNLOCKED_ACCOUNT) + .withGas("0x76c0") + .withGasPrice("0x9184e72a000") + .withValue(FIELD_VALUE_DEFAULT) + .withNonce("0x1") + .withData( + "0x608060405234801561001057600080fd5b50604051602080610114833981016040525160005560e1806100336000396000f30060806040526004361060525763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632a1afcd98114605757806360fe47b114607b5780636d4ce63c146092575b600080fd5b348015606257600080fd5b50606960a4565b60408051918252519081900360200190f35b348015608657600080fd5b50609060043560aa565b005b348015609d57600080fd5b50606960af565b60005481565b600055565b600054905600a165627a7a72305820ade758a90b7d6841e99ca64c339eda0498d86ec9a97d5dcdeb3f12e3500079130029000000000000000000000000000000000000000000000000000000000000000a"); + } + + public static Builder defaultTransaction() { + return new Builder() + .withFrom(UNLOCKED_ACCOUNT) + .withNonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2") + .withGasPrice("0x9184e72a000") + .withGas("0x76c0") + .withTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567") + .withValue("0x9184e72a") + .withData( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + } + + public static class Builder { + private Optional> from = Optional.empty(); + private Optional> nonce = Optional.empty(); + private Optional> gasPrice = Optional.empty(); + private Optional> gas = Optional.empty(); + private Optional> to = Optional.empty(); + private Optional> value = Optional.empty(); + private Optional> data = Optional.empty(); + + public Builder withFrom(final String from) { + this.from = createValue(from); + return this; + } + + public Builder missingFrom() { + this.from = Optional.empty(); + return this; + } + + public Builder withNonce(final String nonce) { + this.nonce = createValue(nonce); + return this; + } + + public Builder missingNonce() { + this.nonce = Optional.empty(); + return this; + } + + public Builder withGasPrice(final String gasPrice) { + this.gasPrice = createValue(gasPrice); + return this; + } + + public Builder missingGasPrice() { + this.gasPrice = Optional.empty(); + return this; + } + + public Builder withGas(final String gas) { + this.gas = createValue(gas); + return this; + } + + public Builder missingGas() { + this.gas = Optional.empty(); + return this; + } + + public Builder withTo(final String to) { + this.to = createValue(to); + return this; + } + + public Builder missingTo() { + this.to = Optional.empty(); + return this; + } + + public Builder withValue(final String value) { + this.value = createValue(value); + return this; + } + + public Builder missingValue() { + this.value = Optional.empty(); + return this; + } + + public Builder withData(final String data) { + this.data = createValue(data); + return this; + } + + public Builder missingData() { + this.data = Optional.empty(); + return this; + } + + public Transaction build() { + return new Transaction(from, nonce, gasPrice, gas, to, value, data); + } + + private Optional> createValue(final T from) { + return Optional.of(new ValueHolder<>(from)); + } + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/TransactionJsonUtil.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/TransactionJsonUtil.java new file mode 100644 index 000000000..597169919 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/TransactionJsonUtil.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +import java.util.Optional; + +import io.vertx.core.json.JsonObject; + +public class TransactionJsonUtil { + + public static void putValue( + final JsonObject jsonObject, final String field, final Optional> value) { + value.ifPresent(valueHolder -> jsonObject.put(field, valueHolder.getValue())); + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/ValueHolder.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/ValueHolder.java new file mode 100644 index 000000000..39dbdb8cc --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/jsonrpc/ValueHolder.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.model.jsonrpc; + +public class ValueHolder { + private final T value; + + public ValueHolder(final T value) { + this.value = value; + } + + public T getValue() { + return value; + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/request/EthRequestFactory.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/request/EthRequestFactory.java index 9b1e2ce7e..ed7a51444 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/request/EthRequestFactory.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/model/request/EthRequestFactory.java @@ -23,16 +23,16 @@ public class EthRequestFactory { private static final Iterable> NO_HEADERS = emptyList(); - public Web3SignerRequest web3signer( + public Web3SignerRequest web3Signer( final Iterable> headers, final String body) { return new Web3SignerRequest(headers, body); } - public Web3SignerRequest web3signer(final String body) { + public Web3SignerRequest web3Signer(final String body) { return new Web3SignerRequest(NO_HEADERS, body); } - public Web3SignerRequest web3signer(final Request request) { + public Web3SignerRequest web3Signer(final Request request) { return new Web3SignerRequest(NO_HEADERS, Json.encode(request)); } diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TransactionCountResponder.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TransactionCountResponder.java new file mode 100644 index 000000000..186151cc0 --- /dev/null +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TransactionCountResponder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.jsonrpcproxy.support; + +import static org.mockserver.model.HttpResponse.response; + +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcSuccessResponse; + +import java.math.BigInteger; +import java.util.function.Function; + +import io.vertx.core.json.Json; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.mockserver.mock.action.ExpectationResponseCallback; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.RegexBody; + +public class TransactionCountResponder implements ExpectationResponseCallback { + + private static final Logger LOG = LogManager.getLogger(); + + public enum TRANSACTION_COUNT_METHOD { + ETH_GET_TRANSACTION_COUNT(".*eth_getTransactionCount.*"), + PRIV_EEA_GET_TRANSACTION_COUNT(".*priv_getEeaTransactionCount.*"), + PRIV_GET_TRANSACTION_COUNT(".*priv_getTransactionCount.*"); + + private final String regexPattern; + + TRANSACTION_COUNT_METHOD(final String regexPattern) { + this.regexPattern = regexPattern; + } + } + + private BigInteger nonce = BigInteger.ZERO; + private final Function nonceMutator; + private final String regexPattern; + + public TransactionCountResponder( + final Function nonceMutator, final TRANSACTION_COUNT_METHOD method) { + this.nonceMutator = nonceMutator; + this.regexPattern = method.regexPattern; + } + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + final JsonRpcRequestId id = getRequestId(httpRequest.getBodyAsString()); + nonce = nonceMutator.apply(nonce); + return response(generateTransactionCountResponse(id)); + } + + private static JsonRpcRequestId getRequestId(final String jsonBody) { + final JsonRpcRequest jsonRpcRequest = Json.decodeValue(jsonBody, JsonRpcRequest.class); + return jsonRpcRequest.getId(); + } + + protected String generateTransactionCountResponse(final JsonRpcRequestId id) { + final JsonRpcSuccessResponse response = + new JsonRpcSuccessResponse(id.getValue(), "0x" + nonce.toString()); + LOG.debug("Responding with Nonce of {}", nonce.toString()); + + return Json.encode(response); + } + + public HttpRequest request() { + return HttpRequest.request().withBody(new RegexBody(regexPattern)); + } +} diff --git a/core/src/integrationTest/resources/keyfile.json b/core/src/integrationTest/resources/keyfile.json new file mode 100644 index 000000000..ff65637d0 --- /dev/null +++ b/core/src/integrationTest/resources/keyfile.json @@ -0,0 +1 @@ +{"address":"7577919ae5df4941180eac211965f275cdce314d","id":"9862127a-cd5f-4295-90ab-45fad5615fca","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"1f98ace266ca154c920554291173c0878956253e6a6637b731297551a7d34e53","cipherparams":{"iv":"c0210872526f6e4a41cbb6664e6588b4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"685fbefccd6075cd1cbd63f9f6a013b69df1cfd110652386353edcbdb0777fc9"},"mac":"186192bc59e6f2a5796df853435507e9662bfa8b98f7e47fd54f631c9a584692"}} \ No newline at end of file diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java index c9997c698..1016f6cc1 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java @@ -33,6 +33,8 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthSignResultProvider; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthSignTransactionResultProvider; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.InternalResponseHandler; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.SendTransactionHandler; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; import tech.pegasys.web3signer.keystorage.hashicorp.HashicorpConnectionFactory; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; import tech.pegasys.web3signer.signing.EthSecpArtifactSigner; @@ -65,12 +67,14 @@ public class Eth1Runner extends Runner { public static final String ROOT_PATH = "/"; public static final String SIGN_PATH = "/api/v1/eth1/sign/:identifier"; private final Eth1Config eth1Config; + private final long chainId; private final HttpResponseFactory responseFactory = new HttpResponseFactory(); public Eth1Runner(final BaseConfig baseConfig, final Eth1Config eth1Config) { super(baseConfig); this.eth1Config = eth1Config; + this.chainId = eth1Config.getChainId().id(); } @Override @@ -184,6 +188,11 @@ private RequestMapper createRequestMapper( final PassThroughHandler defaultHandler = new PassThroughHandler(transmitterFactory, jsonDecoder); + final TransactionFactory transactionFactory = + new TransactionFactory(chainId, jsonDecoder, transmitterFactory); + final SendTransactionHandler sendTransactionHandler = + new SendTransactionHandler(chainId, signerProvider, transactionFactory, transmitterFactory); + final RequestMapper requestMapper = new RequestMapper(defaultHandler); requestMapper.addHandler( "eth_accounts", @@ -196,8 +205,9 @@ private RequestMapper createRequestMapper( "eth_signTransaction", new InternalResponseHandler<>( responseFactory, - new EthSignTransactionResultProvider( - eth1Config.getChainId().id(), signerProvider, jsonDecoder))); + new EthSignTransactionResultProvider(chainId, signerProvider, jsonDecoder))); + requestMapper.addHandler("eth_sendTransaction", sendTransactionHandler); + requestMapper.addHandler("eea_sendTransaction", sendTransactionHandler); return requestMapper; } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParameters.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParameters.java new file mode 100644 index 000000000..5543faca8 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParameters.java @@ -0,0 +1,175 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc; + +import static org.web3j.utils.Numeric.decodeQuantity; +import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.decodeBigInteger; +import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.fromRpcRequestToJsonParam; +import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.validateNotEmpty; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import org.web3j.utils.Base64String; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EeaSendTransactionJsonParameters { + + private final String sender; + private final Base64String privateFrom; + private final String restriction; + + private BigInteger gas; + private BigInteger gasPrice; + private BigInteger nonce; + private BigInteger value; + private String receiver; + private String data; + private Base64String privacyGroupId; + private List privateFor; + private BigInteger maxFeePerGas; + private BigInteger maxPriorityFeePerGas; + + @JsonCreator + public EeaSendTransactionJsonParameters( + @JsonProperty("from") final String sender, + @JsonProperty("privateFrom") final String privateFrom, + @JsonProperty("restriction") final String restriction) { + validateNotEmpty(sender); + this.privateFrom = Base64String.wrap(privateFrom); + this.restriction = restriction; + this.sender = sender; + } + + @JsonSetter("gas") + public void gas(final String gas) { + this.gas = decodeBigInteger(gas); + } + + @JsonSetter("gasPrice") + public void gasPrice(final String gasPrice) { + this.gasPrice = decodeBigInteger(gasPrice); + } + + @JsonSetter("nonce") + public void nonce(final String nonce) { + this.nonce = decodeBigInteger(nonce); + } + + @JsonSetter("to") + public void receiver(final String receiver) { + this.receiver = receiver; + } + + @JsonSetter("value") + public void value(final String value) { + validateValue(value); + this.value = decodeBigInteger(value); + } + + @JsonSetter("data") + public void data(final String data) { + this.data = data; + } + + @JsonSetter("privateFor") + public void privateFor(final String[] privateFor) { + this.privateFor = + Arrays.stream(privateFor).map(Base64String::wrap).collect(Collectors.toList()); + } + + @JsonSetter("privacyGroupId") + public void privacyGroupId(final String privacyGroupId) { + this.privacyGroupId = Base64String.wrap(privacyGroupId); + } + + @JsonSetter("maxPriorityFeePerGas") + public void maxPriorityFeePerGas(final String maxPriorityFeePerGas) { + this.maxPriorityFeePerGas = decodeBigInteger(maxPriorityFeePerGas); + } + + @JsonSetter("maxFeePerGas") + public void maxFeePerGas(final String maxFeePerGas) { + this.maxFeePerGas = decodeBigInteger(maxFeePerGas); + } + + public Optional data() { + return Optional.ofNullable(data); + } + + public Optional gas() { + return Optional.ofNullable(gas); + } + + public Optional gasPrice() { + return Optional.ofNullable(gasPrice); + } + + public Optional receiver() { + return Optional.ofNullable(receiver); + } + + public Optional value() { + return Optional.ofNullable(value); + } + + public Optional nonce() { + return Optional.ofNullable(nonce); + } + + public Optional> privateFor() { + return Optional.ofNullable(privateFor); + } + + public Optional privacyGroupId() { + return Optional.ofNullable(privacyGroupId); + } + + public String sender() { + return sender; + } + + public Base64String privateFrom() { + return privateFrom; + } + + public String restriction() { + return restriction; + } + + public Optional maxPriorityFeePerGas() { + return Optional.ofNullable(maxPriorityFeePerGas); + } + + public Optional maxFeePerGas() { + return Optional.ofNullable(maxFeePerGas); + } + + public static EeaSendTransactionJsonParameters from(final JsonRpcRequest request) { + return fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + } + + private void validateValue(final String value) { + if (value != null && !decodeQuantity(value).equals(BigInteger.ZERO)) { + throw new IllegalArgumentException( + "Non-zero value, private transactions cannot transfer ether"); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParameters.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParameters.java index 7ee239dc6..736744873 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParameters.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParameters.java @@ -32,6 +32,8 @@ public class EthSendTransactionJsonParameters { private BigInteger value; private String receiver; private String data; + private BigInteger maxFeePerGas; + private BigInteger maxPriorityFeePerGas; @JsonCreator public EthSendTransactionJsonParameters(@JsonProperty("from") final String sender) { @@ -69,6 +71,16 @@ public void data(final String data) { this.data = data; } + @JsonSetter("maxPriorityFeePerGas") + public void maxPriorityFeePerGas(final String maxPriorityFeePerGas) { + this.maxPriorityFeePerGas = decodeBigInteger(maxPriorityFeePerGas); + } + + @JsonSetter("maxFeePerGas") + public void maxFeePerGas(final String maxFeePerGas) { + this.maxFeePerGas = decodeBigInteger(maxFeePerGas); + } + public Optional data() { return Optional.ofNullable(data); } @@ -96,4 +108,12 @@ public Optional nonce() { public String sender() { return sender; } + + public Optional maxPriorityFeePerGas() { + return Optional.ofNullable(maxPriorityFeePerGas); + } + + public Optional maxFeePerGas() { + return Optional.ofNullable(maxFeePerGas); + } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java index f6a0fd6f0..a26efa55a 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java @@ -13,7 +13,6 @@ package tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse; import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INVALID_PARAMS; -import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; import tech.pegasys.web3signer.core.service.jsonrpc.EthSendTransactionJsonParameters; import tech.pegasys.web3signer.core.service.jsonrpc.JsonDecoder; @@ -55,7 +54,8 @@ public String createResponseResult(final JsonRpcRequest request) { try { ethSendTransactionJsonParameters = fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); - transaction = createTransaction(request, ethSendTransactionJsonParameters); + transaction = + new EthTransaction(chainId, ethSendTransactionJsonParameters, null, request.getId()); } catch (final NumberFormatException e) { LOG.debug("Parsing values failed for request: {}", request.getParams(), e); @@ -71,19 +71,10 @@ public String createResponseResult(final JsonRpcRequest request) { } LOG.debug("Obtaining signer for {}", transaction.sender()); - try { - final TransactionSerializer transactionSerializer = - new TransactionSerializer(signerProvider, chainId, transaction.sender()); - return transactionSerializer.serialize(transaction); - } catch (Exception e) { - LOG.debug("From address ({}) does not match any available account", transaction.sender()); - throw new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); - } - } - private Transaction createTransaction( - final JsonRpcRequest request, final EthSendTransactionJsonParameters params) { - return new EthTransaction(params, null, request.getId()); + final TransactionSerializer transactionSerializer = + new TransactionSerializer(signerProvider, chainId); + return transactionSerializer.serialize(transaction); } public T fromRpcRequestToJsonParam(final Class type, final JsonRpcRequest request) { diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/NonceTooLowRetryMechanism.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/NonceTooLowRetryMechanism.java new file mode 100644 index 000000000..b8d582c4c --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/NonceTooLowRetryMechanism.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction; + +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.ETH_SEND_TX_ALREADY_KNOWN; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.ETH_SEND_TX_REPLACEMENT_UNDERPRICED; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.NONCE_TOO_LOW; + +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class NonceTooLowRetryMechanism extends RetryMechanism { + + private static final Logger LOG = LogManager.getLogger(); + + public NonceTooLowRetryMechanism(final int maxRetries) { + super(maxRetries); + } + + @Override + public boolean responseRequiresRetry(final int httpStatusCode, final String body) { + if ((httpStatusCode == HttpResponseStatus.OK.code())) { + final JsonRpcErrorResponse errorResponse; + try { + errorResponse = specialiseResponse(body); + } catch (final IllegalArgumentException | DecodeException e) { + return false; + } + if (NONCE_TOO_LOW.equals(errorResponse.getError())) { + LOG.info("Nonce too low, resend required for {}.", errorResponse.getId()); + return true; + } else if (ETH_SEND_TX_ALREADY_KNOWN.equals(errorResponse.getError()) + || ETH_SEND_TX_REPLACEMENT_UNDERPRICED.equals(errorResponse.getError())) { + LOG.info( + "Besu returned \"{}\", which means that a Tx with the same nonce is in the transaction pool, resend required for {}.", + errorResponse.getError(), + errorResponse.getId()); + return true; + } + } + return false; + } + + private JsonRpcErrorResponse specialiseResponse(final String errorResposneBody) { + final JsonObject jsonBody = new JsonObject(errorResposneBody); + return jsonBody.mapTo(JsonRpcErrorResponse.class); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryMechanism.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryMechanism.java new file mode 100644 index 000000000..91ae4d440 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryMechanism.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction; + +public abstract class RetryMechanism { + + private final int maxRetries; + private int retriesPerformed = 0; + + public RetryMechanism(final int maxRetries) { + this.maxRetries = maxRetries; + } + + public abstract boolean responseRequiresRetry(final int httpStatusCode, final String body); + + public boolean retriesAvailable() { + return retriesPerformed < maxRetries; + } + + public void incrementRetries() { + retriesPerformed++; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryingTransactionTransmitter.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryingTransactionTransmitter.java new file mode 100644 index 000000000..bdf5f7723 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/RetryingTransactionTransmitter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INTERNAL_ERROR; + +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.TransactionSerializer; + +import java.util.Map.Entry; + +import io.vertx.ext.web.RoutingContext; + +public class RetryingTransactionTransmitter extends TransactionTransmitter { + + private final RetryMechanism retryMechanism; + + public RetryingTransactionTransmitter( + final Transaction transaction, + final TransactionSerializer transactionSerializer, + final VertxRequestTransmitterFactory transmitterFactory, + final RetryMechanism retryMechanism, + final RoutingContext routingContext) { + super(transaction, transactionSerializer, transmitterFactory, routingContext); + this.retryMechanism = retryMechanism; + } + + @Override + public void handleResponse( + final Iterable> headers, final int statusCode, final String body) { + if (retryMechanism.responseRequiresRetry(statusCode, body)) { + if (retryMechanism.retriesAvailable()) { + retryMechanism.incrementRetries(); + send(); + } else { + context().fail(BAD_REQUEST.code(), new JsonRpcException(INTERNAL_ERROR)); + } + return; + } + + super.handleResponse(headers, statusCode, body); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java new file mode 100644 index 000000000..4541a4a65 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INVALID_PARAMS; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.util.Eth1AddressUtil.signerPublicKeyFromAddress; +import static tech.pegasys.web3signer.core.util.ResponseCodeSelector.jsonRPCErrorCode; + +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestHandler; +import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.TransactionSerializer; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; + +import java.util.Optional; + +import io.vertx.core.json.DecodeException; +import io.vertx.ext.web.RoutingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SendTransactionHandler implements JsonRpcRequestHandler { + + private static final Logger LOG = LogManager.getLogger(); + + private final long chainId; + private final ArtifactSignerProvider signerProvider; + private final TransactionFactory transactionFactory; + private final VertxRequestTransmitterFactory vertxTransmitterFactory; + + private static final int MAX_NONCE_RETRIES = 10; + + public SendTransactionHandler( + final long chainId, + final ArtifactSignerProvider signerProvider, + final TransactionFactory transactionFactory, + final VertxRequestTransmitterFactory vertxTransmitterFactory) { + this.chainId = chainId; + this.signerProvider = signerProvider; + this.transactionFactory = transactionFactory; + this.vertxTransmitterFactory = vertxTransmitterFactory; + } + + @Override + public void handle(final RoutingContext context, final JsonRpcRequest request) { + LOG.debug("Transforming request {}, {}", request.getId(), request.getMethod()); + final Transaction transaction; + try { + transaction = transactionFactory.createTransaction(context, request); + } catch (final NumberFormatException e) { + LOG.debug("Parsing values failed for request: {}", request.getParams(), e); + final JsonRpcException jsonRpcException = new JsonRpcException(INVALID_PARAMS); + context.fail(jsonRPCErrorCode(jsonRpcException), jsonRpcException); + return; + } catch (final JsonRpcException e) { + context.fail(jsonRPCErrorCode(e), e); + return; + } catch (final IllegalArgumentException | DecodeException e) { + LOG.debug("JSON Deserialization failed for request: {}", request.getParams(), e); + final JsonRpcException jsonRpcException = new JsonRpcException(INVALID_PARAMS); + context.fail(jsonRPCErrorCode(jsonRpcException), jsonRpcException); + return; + } + + Optional publicKey = signerPublicKeyFromAddress(signerProvider, transaction.sender()); + + if (publicKey.isEmpty()) { + LOG.debug("From address ({}) does not match any available account", transaction.sender()); + context.fail( + BAD_REQUEST.code(), new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + return; + } + + sendTransaction(transaction, context, signerProvider, request); + } + + private void sendTransaction( + final Transaction transaction, + final RoutingContext routingContext, + final ArtifactSignerProvider signerProvider, + final JsonRpcRequest request) { + + final TransactionSerializer transactionSerializer = + new TransactionSerializer(signerProvider, chainId); + + final TransactionTransmitter transmitter = + createTransactionTransmitter(transaction, transactionSerializer, routingContext, request); + transmitter.send(); + } + + private TransactionTransmitter createTransactionTransmitter( + final Transaction transaction, + final TransactionSerializer transactionSerializer, + final RoutingContext routingContext, + final JsonRpcRequest request) { + + if (!transaction.isNonceUserSpecified()) { + LOG.debug("Nonce not present in request {}", request.getId()); + return new RetryingTransactionTransmitter( + transaction, + transactionSerializer, + vertxTransmitterFactory, + new NonceTooLowRetryMechanism(MAX_NONCE_RETRIES), + routingContext); + } else { + LOG.debug("Nonce supplied by client, forwarding request"); + return new TransactionTransmitter( + transaction, transactionSerializer, vertxTransmitterFactory, routingContext); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/TransactionTransmitter.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/TransactionTransmitter.java new file mode 100644 index 000000000..65f97cc93 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/TransactionTransmitter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INTERNAL_ERROR; +import static tech.pegasys.web3signer.core.util.ResponseCodeSelector.jsonRPCErrorCode; + +import tech.pegasys.web3signer.core.service.ForwardedMessageResponder; +import tech.pegasys.web3signer.core.service.VertxRequestTransmitter; +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.HeaderHelpers; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.TransactionSerializer; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError; + +import java.util.Optional; + +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.EncodeException; +import io.vertx.core.json.Json; +import io.vertx.ext.web.RoutingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TransactionTransmitter extends ForwardedMessageResponder { + + private static final Logger LOG = LogManager.getLogger(); + + private final TransactionSerializer transactionSerializer; + private final Transaction transaction; + private final VertxRequestTransmitterFactory transmitterFactory; + + public TransactionTransmitter( + final Transaction transaction, + final TransactionSerializer transactionSerializer, + final VertxRequestTransmitterFactory transmitterFactory, + final RoutingContext context) { + super(context); + this.transmitterFactory = transmitterFactory; + this.transaction = transaction; + this.transactionSerializer = transactionSerializer; + } + + public void send() { + final Optional request = createSignedTransactionPayload(); + + if (request.isEmpty()) { + return; + } + + try { + sendTransaction(Json.encode(request.get())); + } catch (final IllegalArgumentException | EncodeException e) { + LOG.debug("JSON Serialization failed for: {}", request, e); + context().fail(BAD_REQUEST.code(), new JsonRpcException(INTERNAL_ERROR)); + } + } + + private Optional createSignedTransactionPayload() { + + if (!populateNonce()) { + return Optional.empty(); + } + + final String signedTransactionHexString; + try { + signedTransactionHexString = transactionSerializer.serialize(transaction); + } catch (final IllegalArgumentException e) { + LOG.debug("Failed to encode transaction: {}", transaction, e); + final JsonRpcException jsonRpcException = new JsonRpcException(JsonRpcError.INVALID_PARAMS); + context().fail(jsonRPCErrorCode(jsonRpcException), jsonRpcException); + return Optional.empty(); + } catch (final Throwable thrown) { + LOG.debug("Failed to encode transaction: {}", transaction, thrown); + context().fail(BAD_REQUEST.code(), new JsonRpcException(INTERNAL_ERROR)); + return Optional.empty(); + } + + return Optional.of(transaction.jsonRpcRequest(signedTransactionHexString, transaction.getId())); + } + + private boolean populateNonce() { + try { + transaction.updateFieldsIfRequired(); + return true; + } catch (final RuntimeException e) { + // It is currently recognised that the underlying nonce provider will wrap a transmission + // exception in a Runtime exception. + LOG.warn("Unable to get nonce (or enclave lookup id) from web3j provider.", e); + this.handleFailure(e.getCause()); + } catch (final Throwable thrown) { + LOG.debug("Failed to encode/serialize transaction: {}", transaction, thrown); + context().fail(BAD_REQUEST.code(), new JsonRpcException(INTERNAL_ERROR)); + } + return false; + } + + protected void sendTransaction(final String bodyContent) { + final HttpServerRequest request = context().request(); + final MultiMap headersToSend = HeaderHelpers.createHeaders(request.headers()); + final VertxRequestTransmitter transmitter = transmitterFactory.create(this); + transmitter.sendRequest(request.method(), headersToSend, request.path(), bodyContent); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateNonceProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateNonceProvider.java new file mode 100644 index 000000000..ced3e1b5d --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateNonceProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.math.BigInteger; + +import org.web3j.utils.Base64String; + +public class BesuPrivateNonceProvider implements NonceProvider { + + private final VertxNonceRequestTransmitter vertxNonceRequestTransmitter; + private final String accountAddress; + private final Base64String privacyGroupId; + + public BesuPrivateNonceProvider( + final String accountAddress, + final Base64String privacyGroupId, + final VertxNonceRequestTransmitter vertxNonceRequestTransmitter) { + this.vertxNonceRequestTransmitter = vertxNonceRequestTransmitter; + this.accountAddress = accountAddress; + this.privacyGroupId = privacyGroupId; + } + + @Override + public BigInteger getNonce() { + final JsonRpcRequest request = generateRequest(); + return vertxNonceRequestTransmitter.requestNonce(request); + } + + protected JsonRpcRequest generateRequest() { + final JsonRpcRequest request = new JsonRpcRequest("2.0", "priv_getTransactionCount"); + request.setParams(new Object[] {accountAddress, privacyGroupId}); + return request; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateTransaction.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateTransaction.java new file mode 100644 index 000000000..cffded33a --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/BesuPrivateTransaction.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.EeaSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.utils.Base64String; +import org.web3j.utils.Restriction; + +public class BesuPrivateTransaction extends PrivateTransaction { + + public static BesuPrivateTransaction from( + final long chainId, + final EeaSendTransactionJsonParameters transactionJsonParameters, + final NonceProvider nonceProvider, + final JsonRpcRequestId id) { + + if (transactionJsonParameters.privacyGroupId().isEmpty()) { + throw new IllegalArgumentException("Transaction does not contain a valid privacyGroup."); + } + + final Base64String privacyId = transactionJsonParameters.privacyGroupId().get(); + return new BesuPrivateTransaction( + chainId, transactionJsonParameters, nonceProvider, id, privacyId); + } + + private final Base64String privacyGroupId; + + private BesuPrivateTransaction( + final long chainId, + final EeaSendTransactionJsonParameters transactionJsonParameters, + final NonceProvider nonceProvider, + final JsonRpcRequestId id, + final Base64String privacyGroupId) { + super(chainId, transactionJsonParameters, nonceProvider, id); + this.privacyGroupId = privacyGroupId; + } + + @Override + protected RawPrivateTransaction createTransaction() { + if (transactionJsonParameters.maxPriorityFeePerGas().isPresent() + && transactionJsonParameters.maxFeePerGas().isPresent()) { + return RawPrivateTransaction.createTransaction( + chainId, + nonce, + transactionJsonParameters.maxPriorityFeePerGas().orElseThrow(), + transactionJsonParameters.maxFeePerGas().orElseThrow(), + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.data().orElse(DEFAULT_DATA), + transactionJsonParameters.privateFrom(), + privacyGroupId, + Restriction.fromString(transactionJsonParameters.restriction())); + } else { + return RawPrivateTransaction.createTransaction( + nonce, + transactionJsonParameters.gasPrice().orElse(DEFAULT_GAS_PRICE), + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.data().orElse(DEFAULT_DATA), + transactionJsonParameters.privateFrom(), + privacyGroupId, + Restriction.fromString(transactionJsonParameters.restriction())); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateNonceProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateNonceProvider.java new file mode 100644 index 000000000..2559bef80 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateNonceProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.math.BigInteger; +import java.util.List; + +import org.web3j.utils.Base64String; + +public class EeaPrivateNonceProvider implements NonceProvider { + + private final String accountAddress; + private final Base64String privateFrom; + private final List privateFor; + private final VertxNonceRequestTransmitter vertxNonceRequestTransmitter; + + public EeaPrivateNonceProvider( + final String accountAddress, + final Base64String privateFrom, + final List privateFor, + final VertxNonceRequestTransmitter vertxNonceRequestTransmitter) { + this.accountAddress = accountAddress; + this.privateFrom = privateFrom; + this.privateFor = privateFor; + this.vertxNonceRequestTransmitter = vertxNonceRequestTransmitter; + } + + @Override + public BigInteger getNonce() { + final JsonRpcRequest request = generateRequest(); + return vertxNonceRequestTransmitter.requestNonce(request); + } + + protected JsonRpcRequest generateRequest() { + final JsonRpcRequest request = new JsonRpcRequest("2.0", "priv_getEeaTransactionCount"); + request.setParams(new Object[] {accountAddress, privateFrom, privateFor}); + + return request; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateTransaction.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateTransaction.java new file mode 100644 index 000000000..1abec01af --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EeaPrivateTransaction.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.EeaSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.util.List; + +import com.google.common.base.MoreObjects; +import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.utils.Base64String; +import org.web3j.utils.Restriction; + +public class EeaPrivateTransaction extends PrivateTransaction { + + private final List privateFor; + + public static EeaPrivateTransaction from( + final long chainId, + final EeaSendTransactionJsonParameters transactionJsonParameters, + final NonceProvider nonceProvider, + final JsonRpcRequestId id) { + if (transactionJsonParameters.privateFor().isEmpty()) { + throw new IllegalArgumentException("Transaction does not contain a valid privateFor list."); + } + + return new EeaPrivateTransaction( + chainId, + transactionJsonParameters, + nonceProvider, + id, + transactionJsonParameters.privateFor().get()); + } + + private EeaPrivateTransaction( + final long chainId, + final EeaSendTransactionJsonParameters transactionJsonParameters, + final NonceProvider nonceProvider, + final JsonRpcRequestId id, + final List privateFor) { + super(chainId, transactionJsonParameters, nonceProvider, id); + this.privateFor = privateFor; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("chainId", chainId) + .add("transactionJsonParameters", transactionJsonParameters) + .add("id", id) + .add("nonceProvider", nonceProvider) + .add("nonce", nonce) + .toString(); + } + + @Override + protected RawPrivateTransaction createTransaction() { + if (isEip1559()) { + return RawPrivateTransaction.createTransaction( + chainId, + nonce, + transactionJsonParameters.maxPriorityFeePerGas().orElseThrow(), + transactionJsonParameters.maxFeePerGas().orElseThrow(), + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.data().orElse(DEFAULT_DATA), + transactionJsonParameters.privateFrom(), + privateFor, + Restriction.fromString(transactionJsonParameters.restriction())); + } else { + return RawPrivateTransaction.createTransaction( + nonce, + transactionJsonParameters.gasPrice().orElse(DEFAULT_GAS_PRICE), + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.data().orElse(DEFAULT_DATA), + transactionJsonParameters.privateFrom(), + privateFor, + Restriction.fromString(transactionJsonParameters.restriction())); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthNonceProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthNonceProvider.java new file mode 100644 index 000000000..33b9714d4 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthNonceProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.math.BigInteger; + +public class EthNonceProvider implements NonceProvider { + + private final String accountAddress; + private final VertxNonceRequestTransmitter vertxNonceRequestTransmitter; + + public EthNonceProvider( + final String accountAddress, + final VertxNonceRequestTransmitter vertxNonceRequestTransmitter) { + this.accountAddress = accountAddress; + this.vertxNonceRequestTransmitter = vertxNonceRequestTransmitter; + } + + @Override + public BigInteger getNonce() { + final JsonRpcRequest request = generateRequest(); + return vertxNonceRequestTransmitter.requestNonce(request); + } + + protected JsonRpcRequest generateRequest() { + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_getTransactionCount"); + request.setParams(new Object[] {accountAddress, "pending"}); + + return request; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthTransaction.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthTransaction.java index 5ee439f8e..a64d9ba44 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthTransaction.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/EthTransaction.java @@ -32,15 +32,18 @@ public class EthTransaction implements Transaction { private static final String JSON_RPC_METHOD = "eth_sendRawTransaction"; + private final long chainId; protected final EthSendTransactionJsonParameters transactionJsonParameters; protected final NonceProvider nonceProvider; protected final JsonRpcRequestId id; protected BigInteger nonce; public EthTransaction( + final long chainId, final EthSendTransactionJsonParameters transactionJsonParameters, final NonceProvider nonceProvider, final JsonRpcRequestId id) { + this.chainId = chainId; this.transactionJsonParameters = transactionJsonParameters; this.id = id; this.nonceProvider = nonceProvider; @@ -89,9 +92,16 @@ public JsonRpcRequestId getId() { return id; } + @Override + public boolean isEip1559() { + return transactionJsonParameters.maxPriorityFeePerGas().isPresent() + && transactionJsonParameters.maxFeePerGas().isPresent(); + } + @Override public String toString() { return MoreObjects.toStringHelper(this) + .add("chainId", chainId) .add("transactionJsonParameters", transactionJsonParameters) .add("nonceProvider", nonceProvider) .add("id", id) @@ -100,12 +110,24 @@ public String toString() { } protected RawTransaction createTransaction() { - return RawTransaction.createTransaction( - nonce, - transactionJsonParameters.gasPrice().orElse(DEFAULT_GAS_PRICE), - transactionJsonParameters.gas().orElse(DEFAULT_GAS), - transactionJsonParameters.receiver().orElse(DEFAULT_TO), - transactionJsonParameters.value().orElse(DEFAULT_VALUE), - transactionJsonParameters.data().orElse(DEFAULT_DATA)); + if (isEip1559()) { + return RawTransaction.createTransaction( + chainId, + nonce, + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.value().orElse(DEFAULT_VALUE), + transactionJsonParameters.data().orElse(DEFAULT_DATA), + transactionJsonParameters.maxPriorityFeePerGas().orElseThrow(), + transactionJsonParameters.maxFeePerGas().orElseThrow()); + } else { + return RawTransaction.createTransaction( + nonce, + transactionJsonParameters.gasPrice().orElse(DEFAULT_GAS_PRICE), + transactionJsonParameters.gas().orElse(DEFAULT_GAS), + transactionJsonParameters.receiver().orElse(DEFAULT_TO), + transactionJsonParameters.value().orElse(DEFAULT_VALUE), + transactionJsonParameters.data().orElse(DEFAULT_DATA)); + } } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/PrivateTransaction.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/PrivateTransaction.java new file mode 100644 index 000000000..aefc8abc0 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/PrivateTransaction.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.jsonrpc.EeaSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.math.BigInteger; +import java.util.List; + +import com.google.common.base.MoreObjects; +import org.jetbrains.annotations.NotNull; +import org.web3j.crypto.Sign.SignatureData; +import org.web3j.protocol.eea.crypto.PrivateTransactionEncoder; +import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.rlp.RlpEncoder; +import org.web3j.rlp.RlpList; +import org.web3j.rlp.RlpType; + +public abstract class PrivateTransaction implements Transaction { + + protected static final String JSON_RPC_METHOD = "eea_sendRawTransaction"; + protected final long chainId; + protected final JsonRpcRequestId id; + protected final NonceProvider nonceProvider; + protected BigInteger nonce; + protected final EeaSendTransactionJsonParameters transactionJsonParameters; + + PrivateTransaction( + final long chainId, + final EeaSendTransactionJsonParameters transactionJsonParameters, + final NonceProvider nonceProvider, + final JsonRpcRequestId id) { + this.chainId = chainId; + this.transactionJsonParameters = transactionJsonParameters; + this.nonceProvider = nonceProvider; + this.id = id; + this.nonce = transactionJsonParameters.nonce().orElse(null); + } + + @Override + public void updateFieldsIfRequired() { + if (!this.isNonceUserSpecified()) { + this.nonce = nonceProvider.getNonce(); + } + } + + @Override + public byte[] rlpEncode(final SignatureData signatureData) { + final RawPrivateTransaction rawTransaction = createTransaction(); + final List values = + PrivateTransactionEncoder.asRlpValues(rawTransaction, signatureData); + final RlpList rlpList = new RlpList(values); + return RlpEncoder.encode(rlpList); + } + + @Override + public boolean isNonceUserSpecified() { + return transactionJsonParameters.nonce().isPresent(); + } + + @Override + public String sender() { + return transactionJsonParameters.sender(); + } + + @Override + public JsonRpcRequest jsonRpcRequest( + final String signedTransactionHexString, final JsonRpcRequestId id) { + return Transaction.jsonRpcRequest(signedTransactionHexString, id, getJsonRpcMethodName()); + } + + @Override + @NotNull + public String getJsonRpcMethodName() { + return JSON_RPC_METHOD; + } + + @Override + public JsonRpcRequestId getId() { + return id; + } + + @Override + public boolean isEip1559() { + return transactionJsonParameters.maxPriorityFeePerGas().isPresent() + && transactionJsonParameters.maxFeePerGas().isPresent(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("chainId", chainId) + .add("transactionJsonParameters", transactionJsonParameters) + .add("id", id) + .add("nonceProvider", nonceProvider) + .add("nonce", nonce) + .toString(); + } + + protected abstract RawPrivateTransaction createTransaction(); +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/Transaction.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/Transaction.java index 26e03e9b2..d3f419660 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/Transaction.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/Transaction.java @@ -34,12 +34,6 @@ public interface Transaction { byte[] rlpEncode(SignatureData signatureData); - default byte[] rlpEncode(final long chainId) { - final SignatureData signatureData = - new SignatureData(longToBytes(chainId), new byte[] {}, new byte[] {}); - return rlpEncode(signatureData); - } - boolean isNonceUserSpecified(); String sender(); @@ -64,4 +58,6 @@ static JsonRpcRequest jsonRpcRequest( } JsonRpcRequestId getId(); + + boolean isEip1559(); } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/TransactionFactory.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/TransactionFactory.java new file mode 100644 index 000000000..3ad89ad33 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/TransactionFactory.java @@ -0,0 +1,125 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.EeaSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.EthSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonDecoder; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider; + +import java.util.List; + +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TransactionFactory { + + private static final Logger LOG = LogManager.getLogger(); + + private final VertxRequestTransmitterFactory transmitterFactory; + private final JsonDecoder decoder; + private final long chainId; + + public TransactionFactory( + final long chainId, + final JsonDecoder decoder, + final VertxRequestTransmitterFactory transmitterFactory) { + this.chainId = chainId; + this.transmitterFactory = transmitterFactory; + this.decoder = decoder; + } + + public Transaction createTransaction(final RoutingContext context, final JsonRpcRequest request) { + final String method = request.getMethod().toLowerCase(); + final VertxNonceRequestTransmitter nonceRequestTransmitter = + new VertxNonceRequestTransmitter(context.request().headers(), decoder, transmitterFactory); + + switch (method) { + case "eth_sendtransaction": + return createEthTransaction(chainId, request, nonceRequestTransmitter); + case "eea_sendtransaction": + return createEeaTransaction(chainId, request, nonceRequestTransmitter); + default: + throw new IllegalStateException("Unknown send transaction method " + method); + } + } + + private Transaction createEthTransaction( + final long chainId, + final JsonRpcRequest request, + final VertxNonceRequestTransmitter nonceRequestTransmitter) { + final EthSendTransactionJsonParameters params = + fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + final NonceProvider ethNonceProvider = + new EthNonceProvider(params.sender(), nonceRequestTransmitter); + + return new EthTransaction(chainId, params, ethNonceProvider, request.getId()); + } + + private Transaction createEeaTransaction( + final long chainId, + final JsonRpcRequest request, + final VertxNonceRequestTransmitter requestTransmitter) { + + final EeaSendTransactionJsonParameters params = + fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + if (params.privacyGroupId().isPresent() == params.privateFor().isPresent()) { + LOG.warn( + "Illegal private transaction received; privacyGroup (present = {}) and privateFor (present = {}) are mutually exclusive.", + params.privacyGroupId().isPresent(), + params.privateFor().isPresent()); + throw new IllegalArgumentException("PrivacyGroup and PrivateFor are mutually exclusive."); + } + + if (params.privacyGroupId().isPresent()) { + final NonceProvider nonceProvider = + new BesuPrivateNonceProvider( + params.sender(), params.privacyGroupId().get(), requestTransmitter); + return BesuPrivateTransaction.from(chainId, params, nonceProvider, request.getId()); + } + + final NonceProvider nonceProvider = + new EeaPrivateNonceProvider( + params.sender(), params.privateFrom(), params.privateFor().get(), requestTransmitter); + return EeaPrivateTransaction.from(chainId, params, nonceProvider, request.getId()); + } + + public T fromRpcRequestToJsonParam(final Class type, final JsonRpcRequest request) { + + final Object object; + final Object params = request.getParams(); + if (params instanceof List) { + @SuppressWarnings("unchecked") + final List paramList = (List) params; + if (paramList.size() != 1) { + throw new IllegalArgumentException( + type.getSimpleName() + + " json Rpc requires a single parameter, request contained " + + paramList.size()); + } + object = paramList.get(0); + } else { + object = params; + } + + final JsonObject receivedParams = JsonObject.mapFrom(object); + + return decoder.decodeValue(receivedParams.toBuffer(), type); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxNonceRequestTransmitter.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxNonceRequestTransmitter.java new file mode 100644 index 000000000..6cd7b447b --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxNonceRequestTransmitter.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.determineErrorCode; + +import tech.pegasys.web3signer.core.service.DownstreamResponseHandler; +import tech.pegasys.web3signer.core.service.RequestTransmitter; +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonDecoder; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.HeaderHelpers; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcSuccessResponse; + +import java.math.BigInteger; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.web3j.exceptions.MessageDecodingException; +import org.web3j.utils.Numeric; + +public class VertxNonceRequestTransmitter { + + private static final Logger LOG = LogManager.getLogger(); + + private final MultiMap headers; + private final JsonDecoder decoder; + private final VertxRequestTransmitterFactory transmitterFactory; + + private static final AtomicInteger nextId = new AtomicInteger(0); + + public VertxNonceRequestTransmitter( + final MultiMap headers, + final JsonDecoder decoder, + final VertxRequestTransmitterFactory transmitterFactory) { + this.headers = headers; + this.transmitterFactory = transmitterFactory; + this.decoder = decoder; + } + + public BigInteger requestNonce(final JsonRpcRequest request) { + final CompletableFuture result = getNonceFromWeb3Provider(request, headers); + + try { + final BigInteger nonce = result.get(); + LOG.debug("Supplying nonce of {}", nonce.toString()); + return nonce; + } catch (final InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to retrieve nonce:" + e.getMessage(), e.getCause()); + } + } + + private CompletableFuture getNonceFromWeb3Provider( + final JsonRpcRequest requestBody, final MultiMap headers) { + + final CompletableFuture result = new CompletableFuture<>(); + + final RequestTransmitter transmitter = transmitterFactory.create(new ResponseCallback(result)); + + final MultiMap headersToSend = HeaderHelpers.createHeaders(headers); + requestBody.setId(new JsonRpcRequestId(nextId.getAndIncrement())); + transmitter.sendRequest(HttpMethod.POST, headersToSend, "/", Json.encode(requestBody)); + + LOG.info("Transmitted {}", Json.encode(requestBody)); + + return result; + } + + private void handleResponse(final String body, final CompletableFuture result) { + try { + + final JsonRpcSuccessResponse response = + decoder.decodeValue(Buffer.buffer(body), JsonRpcSuccessResponse.class); + final Object suppliedNonce = response.getResult(); + if (suppliedNonce instanceof String) { + try { + result.complete(Numeric.decodeQuantity((String) suppliedNonce)); + return; + } catch (final MessageDecodingException ex) { + result.completeExceptionally(ex); + return; + } + } + result.completeExceptionally(new RuntimeException("Web3 did not provide a string response.")); + } catch (final DecodeException e) { + result.completeExceptionally(new JsonRpcException(determineErrorCode(body, decoder))); + } + } + + private class ResponseCallback implements DownstreamResponseHandler { + private final CompletableFuture result; + + private ResponseCallback(final CompletableFuture result) { + this.result = result; + } + + @Override + public void handleResponse( + final Iterable> headers, final int statusCode, String body) { + VertxNonceRequestTransmitter.this.handleResponse(body, result); + } + + @Override + public void handleFailure(Throwable t) { + result.completeExceptionally(t); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxStoreRawRequestTransmitter.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxStoreRawRequestTransmitter.java new file mode 100644 index 000000000..0c128edaa --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/transaction/VertxStoreRawRequestTransmitter.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction; + +import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.determineErrorCode; + +import tech.pegasys.web3signer.core.service.DownstreamResponseHandler; +import tech.pegasys.web3signer.core.service.RequestTransmitter; +import tech.pegasys.web3signer.core.service.VertxRequestTransmitterFactory; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonDecoder; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.HeaderHelpers; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcSuccessResponse; + +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.web3j.exceptions.MessageDecodingException; + +public class VertxStoreRawRequestTransmitter { + + private static final Logger LOG = LogManager.getLogger(); + + private final MultiMap headers; + private final JsonDecoder decoder; + private final VertxRequestTransmitterFactory transmitterFactory; + + private static final AtomicInteger nextId = new AtomicInteger(0); + + public VertxStoreRawRequestTransmitter( + final MultiMap headers, + final JsonDecoder decoder, + final VertxRequestTransmitterFactory transmitterFactory) { + this.headers = headers; + this.transmitterFactory = transmitterFactory; + this.decoder = decoder; + } + + public String storeRaw(final JsonRpcRequest request) { + final CompletableFuture result = storePayloadAndGetLookupId(request, headers); + + try { + final String lookupId = result.get(); + LOG.debug("storeRaw response of {}", lookupId); + return lookupId; + } catch (final InterruptedException | ExecutionException e) { + throw new RuntimeException( + "Failed to retrieve storeRaw result (enclave lookup id):" + e.getMessage(), e.getCause()); + } + } + + private CompletableFuture storePayloadAndGetLookupId( + final JsonRpcRequest requestBody, final MultiMap headers) { + + final CompletableFuture result = new CompletableFuture<>(); + + final RequestTransmitter transmitter = transmitterFactory.create(new ResponseCallback(result)); + + final MultiMap headersToSend = HeaderHelpers.createHeaders(headers); + requestBody.setId(new JsonRpcRequestId(nextId.getAndIncrement())); + transmitter.sendRequest(HttpMethod.POST, headersToSend, "/", Json.encode(requestBody)); + + LOG.info("Transmitted {}", Json.encode(requestBody)); + + return result; + } + + private void handleResponse(final String body, final CompletableFuture result) { + try { + + final JsonRpcSuccessResponse response = + decoder.decodeValue(Buffer.buffer(body), JsonRpcSuccessResponse.class); + final Object suppliedLookupId = response.getResult(); + if (suppliedLookupId instanceof String) { + try { + result.complete((String) suppliedLookupId); + return; + } catch (final MessageDecodingException ex) { + result.completeExceptionally(ex); + return; + } + } + result.completeExceptionally(new RuntimeException("Web3 did not provide a string response.")); + } catch (final DecodeException e) { + result.completeExceptionally(new JsonRpcException(determineErrorCode(body, decoder))); + } + } + + private class ResponseCallback implements DownstreamResponseHandler { + private final CompletableFuture result; + + private ResponseCallback(final CompletableFuture result) { + this.result = result; + } + + @Override + public void handleResponse( + final Iterable> headers, final int statusCode, String body) { + VertxStoreRawRequestTransmitter.this.handleResponse(body, result); + } + + @Override + public void handleFailure(Throwable t) { + result.completeExceptionally(t); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java index 59fd5b0b0..05f38452f 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java @@ -12,7 +12,9 @@ */ package tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing; +import static tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction.longToBytes; import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; +import static tech.pegasys.web3signer.core.util.Eth1AddressUtil.signerPublicKeyFromAddress; import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; @@ -20,11 +22,15 @@ import tech.pegasys.web3signer.signing.SecpArtifactSignature; import tech.pegasys.web3signer.signing.secp256k1.Signature; +import java.nio.ByteBuffer; +import java.util.Optional; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; import org.web3j.crypto.Sign.SignatureData; import org.web3j.crypto.TransactionEncoder; +import org.web3j.crypto.transaction.type.TransactionType; import org.web3j.utils.Numeric; public class TransactionSerializer { @@ -33,42 +39,69 @@ public class TransactionSerializer { protected final ArtifactSignerProvider signer; protected final long chainId; - protected final String identifier; - - public TransactionSerializer( - final ArtifactSignerProvider signer, final long chainId, final String identifier) { + public TransactionSerializer(final ArtifactSignerProvider signer, final long chainId) { this.signer = signer; this.chainId = chainId; - this.identifier = identifier; } public String serialize(final Transaction transaction) { - final byte[] bytesToSign = transaction.rlpEncode(chainId); - final SecpArtifactSignature artifactSignature = - signer - .getSigner(identifier) - .map( - artifactSigner -> - (SecpArtifactSignature) artifactSigner.sign(Bytes.of(bytesToSign))) - .orElseThrow( - () -> { - LOG.debug("From address ({}) does not match any available account", identifier); - throw new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); - }); + SignatureData signatureData = null; - final Signature signature = artifactSignature.getSignatureData(); + if (!transaction.isEip1559()) { + signatureData = new SignatureData(longToBytes(chainId), new byte[] {}, new byte[] {}); + } + + byte[] bytesToSign = transaction.rlpEncode(signatureData); + + if (transaction.isEip1559()) { + bytesToSign = prependEip1559TransactionType(bytesToSign); + } - final SignatureData web3jSignature = + final Signature signature = sign(transaction.sender(), bytesToSign); + + SignatureData web3jSignature = new SignatureData( signature.getV().toByteArray(), signature.getR().toByteArray(), signature.getS().toByteArray()); - final SignatureData eip155Signature = - TransactionEncoder.createEip155SignatureData(web3jSignature, chainId); + if (!transaction.isEip1559()) { + web3jSignature = TransactionEncoder.createEip155SignatureData(web3jSignature, chainId); + } + + byte[] serializedBytes = transaction.rlpEncode(web3jSignature); + if (transaction.isEip1559()) { + serializedBytes = prependEip1559TransactionType(serializedBytes); + } - final byte[] serializedBytes = transaction.rlpEncode(eip155Signature); return Numeric.toHexString(serializedBytes); } + + private Signature sign(final String sender, final byte[] bytesToSign) { + Optional publicKey = signerPublicKeyFromAddress(signer, sender); + + if (publicKey.isEmpty()) { + LOG.debug("From address ({}) does not match any available account", sender); + throw new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); + } + + final SecpArtifactSignature artifactSignature = + signer + .getSigner(publicKey.get()) + .map( + artifactSigner -> + (SecpArtifactSignature) artifactSigner.sign(Bytes.of(bytesToSign))) + .get(); + + final Signature signature = artifactSignature.getSignatureData(); + return signature; + } + + private static byte[] prependEip1559TransactionType(byte[] bytesToSign) { + return ByteBuffer.allocate(bytesToSign.length + 1) + .put(TransactionType.EIP1559.getRlpType()) + .put(bytesToSign) + .array(); + } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/util/Eth1AddressUtil.java b/core/src/main/java/tech/pegasys/web3signer/core/util/Eth1AddressUtil.java new file mode 100644 index 000000000..864b1af73 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/util/Eth1AddressUtil.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.util; + +import static org.web3j.crypto.Keys.getAddress; +import static tech.pegasys.web3signer.signing.util.IdentifierUtils.normaliseIdentifier; + +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; + +import java.util.Optional; + +public class Eth1AddressUtil { + + public static Optional signerPublicKeyFromAddress( + final ArtifactSignerProvider signerProvider, final String address) { + return signerProvider.availableIdentifiers().stream() + .filter( + publicKey -> + normaliseIdentifier(getAddress(publicKey)).equals(normaliseIdentifier(address))) + .findFirst(); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParametersTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParametersTest.java new file mode 100644 index 000000000..e54b6e218 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EeaSendTransactionJsonParametersTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import tech.pegasys.web3signer.core.Eth1Runner; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; + +import java.math.BigInteger; +import java.util.Optional; + +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.utils.Base64String; + +public class EeaSendTransactionJsonParametersTest { + + private TransactionFactory factory; + + @BeforeEach + public void setup() { + // NOTE: the factory has been configured as per its use in the application. + factory = new TransactionFactory(1337L, Eth1Runner.createJsonDecoder(), null); + } + + @Test + public void transactionStoredInJsonArrayCanBeDecoded() { + final JsonObject parameters = validEeaTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "gasPrice")); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + assertThat(txnParams.privateFrom()) + .isEqualTo(Base64String.wrap(parameters.getString("privateFrom"))); + assertThat(txnParams.privateFor().get()) + .containsExactly(Base64String.wrap(parameters.getJsonArray("privateFor").getString(0))); + assertThat(txnParams.restriction()).isEqualTo(parameters.getString("restriction")); + } + + @Test + public void eip1559TransactionStoredInJsonArrayCanBeDecoded() { + final JsonObject parameters = validEip1559EeaTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()).isEmpty(); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + assertThat(txnParams.privateFrom()) + .isEqualTo(Base64String.wrap(parameters.getString("privateFrom"))); + assertThat(txnParams.privateFor().get()) + .containsExactly(Base64String.wrap(parameters.getJsonArray("privateFor").getString(0))); + assertThat(txnParams.restriction()).isEqualTo(parameters.getString("restriction")); + assertThat(txnParams.maxPriorityFeePerGas()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "maxPriorityFeePerGas")); + assertThat(txnParams.maxFeePerGas()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "maxFeePerGas")); + } + + @Test + public void transactionNotStoredInJsonArrayCanBeDecoded() { + final JsonObject parameters = validEeaTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "gasPrice")); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + assertThat(txnParams.privateFrom()) + .isEqualTo(Base64String.wrap(parameters.getString("privateFrom"))); + assertThat(txnParams.privateFor().get()) + .containsExactly(Base64String.wrap(parameters.getJsonArray("privateFor").getString(0))); + assertThat(txnParams.restriction()).isEqualTo(parameters.getString("restriction")); + } + + @Test + public void transactionWithNonZeroValueFails() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("value", "0x9184e72a"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + + assertThatExceptionOfType(DecodeException.class) + .isThrownBy( + () -> + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request)); + } + + @Test + public void transactionWithInvalidPrivateFromThrowsIllegalArgumentException() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("privateFrom", "invalidThirtyTwoByteData="); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + + assertThatExceptionOfType(DecodeException.class) + .isThrownBy( + () -> + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request)); + } + + @Test + public void transactionWithInvalidPrivateForThrowsIllegalArgumentException() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("privateFor", singletonList("invalidThirtyTwoByteData=")); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + + assertThatExceptionOfType(DecodeException.class) + .isThrownBy( + () -> + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request)); + } + + @Test + public void transactionWithInvalidRestrictionCanBeDecoded() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("restriction", "invalidRestriction"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.restriction()).isEqualTo("invalidRestriction"); + } + + @Test + public void transactionWithInvalidFromCanBeDecoded() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("from", "invalidFromAddress"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.sender()).isEqualTo("invalidFromAddress"); + } + + @Test + public void transactionWithInvalidToCanBeDecoded() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("to", "invalidToAddress"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EeaSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EeaSendTransactionJsonParameters.class, request); + + assertThat(txnParams.receiver()).contains("invalidToAddress"); + } + + private Optional getStringAsOptionalBigInteger( + final JsonObject object, final String key) { + final String value = object.getString(key); + return Optional.of(new BigInteger(value.substring(2), 16)); + } + + private JsonObject validEeaTransactionParameters() { + final JsonObject parameters = new JsonObject(); + parameters.put("from", "0xb60e8dd61c5d32be8058bb8eb970870f07233155"); + parameters.put("to", "0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + parameters.put("nonce", "0x1"); + parameters.put("gas", "0x76c0"); + parameters.put("gasPrice", "0x9184e72a000"); + parameters.put("value", "0x0"); + parameters.put( + "data", + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + parameters.put("privateFrom", "ZlapEsl9qDLPy/e88+/6yvCUEVIvH83y0N4A6wHuKXI="); + parameters.put("privateFor", singletonList("GV8m0VZAccYGAAYMBuYQtKEj0XtpXeaw2APcoBmtA2w=")); + parameters.put("restriction", "restricted"); + + return parameters; + } + + private JsonObject validEip1559EeaTransactionParameters() { + final JsonObject parameters = validEeaTransactionParameters(); + parameters.put("maxFeePerGas", "0x9184e72a000"); + parameters.put("maxPriorityFeePerGas", "0x9184e72a000"); + parameters.put("gasPrice", null); + + return parameters; + } + + private JsonRpcRequest wrapParametersInRequest(final T parameters) { + final JsonObject input = new JsonObject(); + input.put("jsonrpc", 2.0); + input.put("method", "mine"); + input.put("params", parameters); + + return input.mapTo(JsonRpcRequest.class); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParametersTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParametersTest.java new file mode 100644 index 000000000..6e922fb6c --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSendTransactionJsonParametersTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.core.Eth1Runner; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; + +import java.math.BigInteger; +import java.util.Optional; + +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class EthSendTransactionJsonParametersTest { + + private TransactionFactory factory; + + @BeforeEach + public void setup() { + // NOTE: the factory has been configured as per its use in the application. + factory = new TransactionFactory(1337L, Eth1Runner.createJsonDecoder(), null); + } + + private Optional getStringAsOptionalBigInteger( + final JsonObject object, final String key) { + final String value = object.getString(key); + return Optional.of(new BigInteger(value.substring(2), 16)); + } + + @Test + public void transactionStoredInJsonArrayCanBeDecoded() throws Throwable { + final JsonObject parameters = validEthTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EthSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "gasPrice")); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + } + + @Test + public void eip1559TransactionStoredInJsonArrayCanBeDecoded() { + final JsonObject parameters = validEip1559EthTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EthSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()).isEmpty(); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + assertThat(txnParams.maxPriorityFeePerGas()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "maxPriorityFeePerGas")); + assertThat(txnParams.maxFeePerGas()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "maxFeePerGas")); + } + + @Test + public void transactionNotStoredInJsonArrayCanBeDecoded() throws Throwable { + final JsonObject parameters = validEthTransactionParameters(); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EthSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas")); + assertThat(txnParams.gasPrice()) + .isEqualTo(getStringAsOptionalBigInteger(parameters, "gasPrice")); + assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce")); + assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to"))); + assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value")); + } + + @Test + public void transactionWithInvalidFromCanBeDecoded() { + final JsonObject parameters = validEthTransactionParameters(); + parameters.put("from", "invalidFromAddress"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EthSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + assertThat(txnParams.sender()).isEqualTo("invalidFromAddress"); + } + + @Test + public void transactionWithInvalidToCanBeDecoded() { + final JsonObject parameters = validEthTransactionParameters(); + parameters.put("to", "invalidToAddress"); + + final JsonRpcRequest request = wrapParametersInRequest(parameters); + final EthSendTransactionJsonParameters txnParams = + factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + + assertThat(txnParams.receiver()).contains("invalidToAddress"); + } + + private JsonObject validEthTransactionParameters() { + final JsonObject parameters = new JsonObject(); + parameters.put("from", "0xb60e8dd61c5d32be8058bb8eb970870f07233155"); + parameters.put("to", "0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + parameters.put("nonce", "0x1"); + parameters.put("gas", "0x76c0"); + parameters.put("gasPrice", "0x9184e72a000"); + parameters.put("value", "0x9184e72a"); + parameters.put( + "data", + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + + return parameters; + } + + private JsonObject validEip1559EthTransactionParameters() { + final JsonObject parameters = validEthTransactionParameters(); + parameters.put("maxFeePerGas", "0x9184e72a000"); + parameters.put("maxPriorityFeePerGas", "0x9184e72a000"); + parameters.put("gasPrice", null); + + return parameters; + } + + private JsonRpcRequest wrapParametersInRequest(final T parameters) { + final JsonObject input = new JsonObject(); + input.put("jsonrpc", 2.0); + input.put("method", "mine"); + input.put("params", parameters); + + return input.mapTo(JsonRpcRequest.class); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java index 0d8af8034..e2dcf23fc 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -109,6 +110,10 @@ public void ifAddressIsNotUnlockedExceptionIsThrownWithSigningNotUnlocked() { @Test public void signatureHasTheExpectedFormat() { + final Credentials cs = + Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); + final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); final BigInteger v = BigInteger.ONE; final BigInteger r = BigInteger.TWO; @@ -117,6 +122,9 @@ public void signatureHasTheExpectedFormat() { .when(mockSigner) .sign(any(Bytes.class)); + doReturn(Set.of(EthPublicKeyUtils.toHexString(key))) + .when(mockSignerProvider) + .availableIdentifiers(); doReturn(Optional.of(mockSigner)).when(mockSignerProvider).getSigner(anyString()); final EthSignTransactionResultProvider resultProvider = new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); @@ -124,7 +132,9 @@ public void signatureHasTheExpectedFormat() { final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); final int id = 1; request.setId(new JsonRpcRequestId(id)); - request.setParams(List.of(getTxParameters())); + final JsonObject params = getTxParameters(); + params.put("from", addr); + request.setParams(params); final Object result = resultProvider.createResponseResult(request); assertThat(result).isInstanceOf(String.class); @@ -170,7 +180,9 @@ public void returnsExpectedSignature() { }) .when(mockSigner) .sign(any(Bytes.class)); - + doReturn(Set.of(EthPublicKeyUtils.toHexString(key))) + .when(mockSignerProvider) + .availableIdentifiers(); doReturn(Optional.of(mockSigner)).when(mockSignerProvider).getSigner(anyString()); final EthSignTransactionResultProvider resultProvider = new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); @@ -192,7 +204,7 @@ public void returnsExpectedSignature() { private static JsonObject getTxParameters() { final JsonObject jsonObject = new JsonObject(); - jsonObject.put("from", "0xf17f52151ebef6c7334fad080c5704d77216b732"); + jsonObject.put("from", "0x0c8f735bc186ea3842e640ffdcb474def3e767a0"); jsonObject.put("to", "0x627306090abaB3A6e1400e9345bC60c78a8BEf57"); jsonObject.put("gasPrice", "0x0"); jsonObject.put("gas", "0x7600"); diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/transaction/EthTransactionTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/transaction/EthTransactionTest.java index 5cf44af7d..af83fc4c9 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/transaction/EthTransactionTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/transaction/EthTransactionTest.java @@ -47,7 +47,8 @@ public void setup() { params.data( "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); - ethTransaction = new EthTransaction(params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); + ethTransaction = + new EthTransaction(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); } @Test diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/DownstreamPathCalculatorTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/DownstreamPathCalculatorTest.java new file mode 100644 index 000000000..8a7b3c4f2 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/DownstreamPathCalculatorTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.sendtransaction; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.core.service.DownstreamPathCalculator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DownstreamPathCalculatorTest { + + @ParameterizedTest + @ValueSource(strings = {"rootPath/child", "/rootPath/child", "/rootPath/child/"}) + void variousPathPrefixesResolveToTheSameDownstreamEndpoint(final String httpDownstreamPath) { + final DownstreamPathCalculator calc = new DownstreamPathCalculator(httpDownstreamPath); + final String result = calc.calculateDownstreamPath("/login"); + assertThat(result).isEqualTo("/rootPath/child/login"); + } + + @ParameterizedTest + @ValueSource(strings = {"rootPath/child", "/rootPath/child", "/rootPath/child/"}) + void variousPathPrefixesResolveToTheSameDownstreamEndpointWithNoSlashOnReceivedUri( + final String httpDownstreamPath) { + final DownstreamPathCalculator calc = new DownstreamPathCalculator(httpDownstreamPath); + final String result = calc.calculateDownstreamPath("login"); + assertThat(result).isEqualTo("/rootPath/child/login"); + } + + @Test + void blankRequestUriProducesAbsolutePathOfPrefix() { + final DownstreamPathCalculator calc = new DownstreamPathCalculator("prefix"); + final String result = calc.calculateDownstreamPath(""); + assertThat(result).isEqualTo("/prefix"); + } + + @Test + void slashRequestUriProducesAbsolutePathOfPrefix() { + final DownstreamPathCalculator calc = new DownstreamPathCalculator("prefix"); + final String result = calc.calculateDownstreamPath("/"); + assertThat(result).isEqualTo("/prefix"); + } + + @Test + void slashPrefixWithNoUriResultsInDownstreamOfJustSlash() { + final DownstreamPathCalculator calc = new DownstreamPathCalculator("/"); + final String result = calc.calculateDownstreamPath("/"); + assertThat(result).isEqualTo("/"); + } + + @Test + void emptyRootPathIsReplacedWithSlash() { + final DownstreamPathCalculator calc = new DownstreamPathCalculator(""); + final String result = calc.calculateDownstreamPath("arbitraryPath"); + assertThat(result).isEqualTo("/arbitraryPath"); + } + + @Test + void multipleSlashInRootAreDiscarded() { + final DownstreamPathCalculator calc = new DownstreamPathCalculator("////"); + final String result = calc.calculateDownstreamPath("arbitraryPath"); + assertThat(result).isEqualTo("/arbitraryPath"); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/NonceTooLowRetryMechanismTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/NonceTooLowRetryMechanismTest.java new file mode 100644 index 000000000..ff5bbec9a --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/NonceTooLowRetryMechanismTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.sendtransaction; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceTooLowRetryMechanism; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.RetryMechanism; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError; +import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class NonceTooLowRetryMechanismTest { + + private final RetryMechanism retryMechanism = new NonceTooLowRetryMechanism(2); + + @Test + public void retryIsRequiredIfErrorIsNonceTooLow() { + + final JsonRpcErrorResponse errorResponse = new JsonRpcErrorResponse(JsonRpcError.NONCE_TOO_LOW); + + assertThat( + retryMechanism.responseRequiresRetry( + HttpResponseStatus.OK.code(), Json.encode(errorResponse))) + .isTrue(); + } + + @Test + public void retryIsRequiredIfErrorIsKnownTransaction() { + + final JsonRpcErrorResponse errorResponse = + new JsonRpcErrorResponse(JsonRpcError.ETH_SEND_TX_ALREADY_KNOWN); + + assertThat( + retryMechanism.responseRequiresRetry( + HttpResponseStatus.OK.code(), Json.encode(errorResponse))) + .isTrue(); + } + + @Test + public void retryIsRequiredIfErrorIsReplacementTransactionUnderpriced() { + final JsonRpcErrorResponse errorResponse = + new JsonRpcErrorResponse(JsonRpcError.ETH_SEND_TX_REPLACEMENT_UNDERPRICED); + + assertThat( + retryMechanism.responseRequiresRetry( + HttpResponseStatus.OK.code(), Json.encode(errorResponse))) + .isTrue(); + } + + @Test + public void retryIsNotRequiredIfErrorIsNotNonceTooLow() { + final JsonRpcErrorResponse errorResponse = + new JsonRpcErrorResponse(JsonRpcError.INVALID_PARAMS); + + assertThat( + retryMechanism.responseRequiresRetry( + HttpResponseStatus.BAD_REQUEST.code(), Json.encode(errorResponse))) + .isFalse(); + } + + @Test + public void retryIsNotRequiredForUnknownErrorType() { + final JsonObject errorResponse = new JsonObject(); + final JsonObject error = new JsonObject(); + error.put("code", -9000); + error.put("message", "Unknown error"); + errorResponse.put("jsonrpc", "2.0"); + errorResponse.put("id", 1); + errorResponse.put("error", new JsonObject()); + + assertThat( + retryMechanism.responseRequiresRetry( + HttpResponseStatus.BAD_REQUEST.code(), Json.encode(errorResponse))) + .isFalse(); + } + + @Test + public void testRetryReportsFalseOnceMatchingMaxValue() { + assertThat(retryMechanism.retriesAvailable()).isTrue(); + retryMechanism.incrementRetries(); // retried once + assertThat(retryMechanism.retriesAvailable()).isTrue(); + retryMechanism.incrementRetries(); // retried twice + assertThat(retryMechanism.retriesAvailable()).isFalse(); + retryMechanism.incrementRetries(); + assertThat(retryMechanism.retriesAvailable()).isFalse(); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EeaPrivateTransactionTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EeaPrivateTransactionTest.java new file mode 100644 index 000000000..5edf3018c --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EeaPrivateTransactionTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.sendtransaction.transaction; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.utils.Bytes.trimLeadingZeroes; +import static org.web3j.utils.Numeric.decodeQuantity; + +import tech.pegasys.web3signer.core.service.jsonrpc.EeaSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.EeaPrivateTransaction; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.PrivateTransaction; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Sign.SignatureData; +import org.web3j.crypto.transaction.type.Transaction1559; +import org.web3j.crypto.transaction.type.TransactionType; +import org.web3j.protocol.eea.crypto.PrivateTransactionDecoder; +import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.protocol.eea.crypto.SignedRawPrivateTransaction; +import org.web3j.utils.Base64String; +import org.web3j.utils.Numeric; +import org.web3j.utils.Restriction; + +public class EeaPrivateTransactionTest { + + private PrivateTransaction privateTransaction; + private EeaSendTransactionJsonParameters params; + + @BeforeEach + public void setup() { + params = + new EeaSendTransactionJsonParameters( + "0x7577919ae5df4941180eac211965f275cdce314d", + "ZlapEsl9qDLPy/e88+/6yvCUEVIvH83y0N4A6wHuKXI=", + "restricted"); + params.receiver("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + params.gas("0x76c0"); + params.gasPrice("0x9184e72a000"); + params.value("0x0"); + params.nonce("0x1"); + params.data( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + params.privateFor(new String[] {"GV8m0VZAccYGAAYMBuYQtKEj0XtpXeaw2APcoBmtA2w="}); + + privateTransaction = + EeaPrivateTransaction.from(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); + } + + @Test + public void rlpEncodesTransaction() { + final SignatureData signatureData = + new SignatureData(new byte[] {1}, new byte[] {2}, new byte[] {3}); + final byte[] rlpEncodedBytes = privateTransaction.rlpEncode(signatureData); + final String rlpString = Numeric.toHexString(rlpEncodedBytes); + + final SignedRawPrivateTransaction decodedTransaction = + (SignedRawPrivateTransaction) PrivateTransactionDecoder.decode(rlpString); + assertThat(decodedTransaction.getTo()).isEqualTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + assertThat(decodedTransaction.getGasLimit()).isEqualTo(decodeQuantity("0x76c0")); + assertThat(decodedTransaction.getGasPrice()).isEqualTo(decodeQuantity("0x9184e72a000")); + assertThat(decodedTransaction.getNonce()).isEqualTo(decodeQuantity("0x1")); + assertThat(decodedTransaction.getValue()).isEqualTo(decodeQuantity("0x0")); + assertThat(decodedTransaction.getData()) + .isEqualTo( + "d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + assertThat(decodedTransaction.getRestriction()).isEqualTo(Restriction.RESTRICTED); + + final Base64String expectedDecodedPrivateFrom = params.privateFrom(); + final Base64String expectedDecodedPrivateFor = params.privateFor().get().get(0); + + assertThat(decodedTransaction.getPrivateFrom()).isEqualTo(expectedDecodedPrivateFrom); + assertThat(decodedTransaction.getPrivateFor().get().get(0)) + .isEqualTo(expectedDecodedPrivateFor); + + final SignatureData decodedSignatureData = decodedTransaction.getSignatureData(); + assertThat(trimLeadingZeroes(decodedSignatureData.getV())).isEqualTo(new byte[] {1}); + assertThat(trimLeadingZeroes(decodedSignatureData.getR())).isEqualTo(new byte[] {2}); + assertThat(trimLeadingZeroes(decodedSignatureData.getS())).isEqualTo(new byte[] {3}); + } + + @Disabled("TODO SLD") + @Test + public void rlpEncodesEip1559Transaction() { + final EeaSendTransactionJsonParameters params = + new EeaSendTransactionJsonParameters( + "0x7577919ae5df4941180eac211965f275cdce314d", + "ZlapEsl9qDLPy/e88+/6yvCUEVIvH83y0N4A6wHuKXI=", + "restricted"); + params.receiver("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + params.gas("0x76c0"); + params.maxPriorityFeePerGas("0x9184e72a000"); + params.maxFeePerGas("0x9184e72a001"); + params.value("0x0"); + params.nonce("0x1"); + params.data( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + params.privateFor(new String[] {"GV8m0VZAccYGAAYMBuYQtKEj0XtpXeaw2APcoBmtA2w="}); + + privateTransaction = + EeaPrivateTransaction.from(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); + + final SignatureData signatureData = null; + final byte[] rlpEncodedBytes = privateTransaction.rlpEncode(signatureData); + final String rlpString = Numeric.toHexString(prependEip1559TransactionType(rlpEncodedBytes)); + + final RawPrivateTransaction decodedTransaction = PrivateTransactionDecoder.decode(rlpString); + final Transaction1559 decoded1559Transaction = + (Transaction1559) decodedTransaction.getTransaction(); + assertThat(decoded1559Transaction.getTo()) + .isEqualTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + assertThat(decoded1559Transaction.getGasLimit()).isEqualTo(decodeQuantity("0x76c0")); + assertThat(decoded1559Transaction.getMaxPriorityFeePerGas()) + .isEqualTo(decodeQuantity("0x9184e72a000")); + assertThat(decoded1559Transaction.getMaxFeePerGas()).isEqualTo(decodeQuantity("0x9184e72a001")); + assertThat(decoded1559Transaction.getNonce()).isEqualTo(decodeQuantity("0x1")); + assertThat(decoded1559Transaction.getValue()).isEqualTo(decodeQuantity("0x0")); + assertThat(decoded1559Transaction.getData()) + .isEqualTo( + "d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + assertThat(decodedTransaction.getRestriction()).isEqualTo(Restriction.RESTRICTED); + + final Base64String expectedDecodedPrivateFrom = params.privateFrom(); + final Base64String expectedDecodedPrivateFor = params.privateFor().get().get(0); + + assertThat(decodedTransaction.getPrivateFrom()).isEqualTo(expectedDecodedPrivateFrom); + assertThat(decodedTransaction.getPrivateFor().get().get(0)) + .isEqualTo(expectedDecodedPrivateFor); + } + + private static byte[] prependEip1559TransactionType(byte[] bytesToSign) { + return ByteBuffer.allocate(bytesToSign.length + 1) + .put(TransactionType.EIP1559.getRlpType()) + .put(bytesToSign) + .array(); + } + + @Test + @SuppressWarnings("unchecked") + public void createsJsonEeaRequest() { + final JsonRpcRequestId id = new JsonRpcRequestId(2); + final String transactionString = + "0xf90114a0e04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f28609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f0724456704a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567536a0fe72a92aede764ce41d06b163d28700b58e5ee8bb1af91d9d54979ea3bdb3e7ea046ae10c94c322fa44ddceb86677c2cd6cc17dfbd766924f41d10a244c512996dac5a6c617045736c3971444c50792f6538382b2f36797643554556497648383379304e3441367748754b58493dedac4756386d30565a41636359474141594d42755951744b456a3058747058656177324150636f426d744132773d8a72657374726963746564"; + final JsonRpcRequest jsonRpcRequest = privateTransaction.jsonRpcRequest(transactionString, id); + + assertThat(jsonRpcRequest.getMethod()).isEqualTo("eea_sendRawTransaction"); + assertThat(jsonRpcRequest.getVersion()).isEqualTo("2.0"); + assertThat(jsonRpcRequest.getId()).isEqualTo(id); + final List params = (List) jsonRpcRequest.getParams(); + assertThat(params).isEqualTo(singletonList(transactionString)); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EthTransactionTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EthTransactionTest.java new file mode 100644 index 000000000..b37d61814 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/sendtransaction/transaction/EthTransactionTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.service.jsonrpc.sendtransaction.transaction; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.utils.Bytes.trimLeadingZeroes; + +import tech.pegasys.web3signer.core.service.jsonrpc.EthSendTransactionJsonParameters; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; +import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId; +import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.EthTransaction; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Sign.SignatureData; +import org.web3j.crypto.SignedRawTransaction; +import org.web3j.crypto.TransactionDecoder; +import org.web3j.crypto.transaction.type.Transaction1559; +import org.web3j.crypto.transaction.type.TransactionType; +import org.web3j.utils.Numeric; + +public class EthTransactionTest { + + private EthTransaction ethTransaction; + + @BeforeEach + public void setup() { + final EthSendTransactionJsonParameters params = + new EthSendTransactionJsonParameters("0x7577919ae5df4941180eac211965f275cdce314d"); + params.receiver("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + params.gas("0x76c0"); + params.gasPrice("0x9184e72a000"); + params.nonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2"); + params.value("0x0"); + params.data( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + + ethTransaction = + new EthTransaction(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); + } + + @Test + public void rlpEncodesTransaction() { + final SignatureData signatureData = + new SignatureData(new byte[] {1}, new byte[] {2}, new byte[] {3}); + final byte[] rlpEncodedBytes = ethTransaction.rlpEncode(signatureData); + final String rlpString = Numeric.toHexString(rlpEncodedBytes); + + final SignedRawTransaction decodedTransaction = + (SignedRawTransaction) TransactionDecoder.decode(rlpString); + assertThat(decodedTransaction.getTo()).isEqualTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + assertThat(decodedTransaction.getGasLimit()).isEqualTo(Numeric.decodeQuantity("0x76c0")); + assertThat(decodedTransaction.getGasPrice()).isEqualTo(Numeric.decodeQuantity("0x9184e72a000")); + assertThat(decodedTransaction.getNonce()) + .isEqualTo( + Numeric.decodeQuantity( + "0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2")); + assertThat(decodedTransaction.getValue()).isEqualTo(Numeric.decodeQuantity("0x0")); + assertThat(decodedTransaction.getData()) + .isEqualTo( + "d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + + final SignatureData decodedSignatureData = decodedTransaction.getSignatureData(); + assertThat(trimLeadingZeroes(decodedSignatureData.getV())).isEqualTo(new byte[] {1}); + assertThat(trimLeadingZeroes(decodedSignatureData.getR())).isEqualTo(new byte[] {2}); + assertThat(trimLeadingZeroes(decodedSignatureData.getS())).isEqualTo(new byte[] {3}); + } + + @Test + public void rlpEncodesEip1559Transaction() { + final EthSendTransactionJsonParameters params = + new EthSendTransactionJsonParameters("0x7577919ae5df4941180eac211965f275cdce314d"); + params.receiver("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + params.gas("0x76c0"); + params.maxPriorityFeePerGas("0x9184e72a000"); + params.maxFeePerGas("0x9184e72a001"); + params.nonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2"); + params.value("0x0"); + params.data( + "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + + ethTransaction = + new EthTransaction(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1)); + + final SignatureData signatureData = null; + final byte[] rlpEncodedBytes = ethTransaction.rlpEncode(signatureData); + final String rlpString = Numeric.toHexString(prependEip1559TransactionType(rlpEncodedBytes)); + + final Transaction1559 decodedTransaction = + (Transaction1559) TransactionDecoder.decode(rlpString).getTransaction(); + assertThat(decodedTransaction.getTo()).isEqualTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567"); + assertThat(decodedTransaction.getGasLimit()).isEqualTo(Numeric.decodeQuantity("0x76c0")); + assertThat(decodedTransaction.getMaxPriorityFeePerGas()) + .isEqualTo(Numeric.decodeQuantity("0x9184e72a000")); + assertThat(decodedTransaction.getMaxFeePerGas()) + .isEqualTo(Numeric.decodeQuantity("0x9184e72a001")); + assertThat(decodedTransaction.getNonce()) + .isEqualTo( + Numeric.decodeQuantity( + "0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2")); + assertThat(decodedTransaction.getValue()).isEqualTo(Numeric.decodeQuantity("0x0")); + assertThat(decodedTransaction.getData()) + .isEqualTo( + "d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"); + } + + private static byte[] prependEip1559TransactionType(byte[] bytesToSign) { + return ByteBuffer.allocate(bytesToSign.length + 1) + .put(TransactionType.EIP1559.getRlpType()) + .put(bytesToSign) + .array(); + } + + @Test + @SuppressWarnings("unchecked") + public void createsJsonRequest() { + final JsonRpcRequestId id = new JsonRpcRequestId(2); + final String transactionString = + "0xf90114a0e04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f28609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f0724456704a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567536a0fe72a92aede764ce41d06b163d28700b58e5ee8bb1af91d9d54979ea3bdb3e7ea046ae10c94c322fa44ddceb86677c2cd6cc17dfbd766924f41d10a244c512996dac5a6c617045736c3971444c50792f6538382b2f36797643554556497648383379304e3441367748754b58493dedac4756386d30565a41636359474141594d42755951744b456a3058747058656177324150636f426d744132773d8a72657374726963746564"; + final JsonRpcRequest jsonRpcRequest = ethTransaction.jsonRpcRequest(transactionString, id); + + assertThat(jsonRpcRequest.getMethod()).isEqualTo("eth_sendRawTransaction"); + assertThat(jsonRpcRequest.getVersion()).isEqualTo("2.0"); + assertThat(jsonRpcRequest.getId()).isEqualTo(id); + final List params = (List) jsonRpcRequest.getParams(); + assertThat(params).isEqualTo(singletonList(transactionString)); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/util/ByteUtilsTest.java b/core/src/test/java/tech/pegasys/web3signer/core/util/ByteUtilsTest.java new file mode 100644 index 000000000..1bc388917 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/util/ByteUtilsTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.core.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.signing.util.ByteUtils; + +import java.math.BigInteger; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +class ByteUtilsTest { + + @Test + public void omitsSignIndicationByteProperly() { + final BigInteger a = new BigInteger(1, Hex.decode("ff12345678")); + final byte[] a5 = ByteUtils.bigIntegerToBytes(a); + assertThat(a5.length).isEqualTo(5); + assertThat(a5).containsExactly(Hex.decode("ff12345678")); + + final BigInteger b = new BigInteger(1, Hex.decode("0f12345678")); + final byte[] b5 = ByteUtils.bigIntegerToBytes(b); + assertThat(b5.length).isEqualTo(5); + assertThat(b5).containsExactly(Hex.decode("0f12345678")); + } + + @Test + public void ifParameterIsNullReturnsNull() { + final byte[] a = ByteUtils.bigIntegerToBytes(null); + assertThat(a).isNull(); + } + + @Test + public void ifBigIntegerZeroReturnsZeroValueArray() { + final byte[] a = ByteUtils.bigIntegerToBytes(BigInteger.ZERO); + assertThat(a).containsExactly(0); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/SingleSignerProvider.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/SingleSignerProvider.java new file mode 100644 index 000000000..db531a0b4 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/SingleSignerProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.web3signer.signing.secp256k1; + +import java.security.interfaces.ECPublicKey; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +public class SingleSignerProvider implements SignerProvider { + private final Signer signer; + + public SingleSignerProvider(Signer signer) { + if (signer == null) { + throw new IllegalArgumentException("SingleSignerFactory requires a non-null Signer"); + } else { + this.signer = signer; + } + } + + @Override + public Optional getSigner(SignerIdentifier signerIdentifier) { + if (signerIdentifier == null) { + return Optional.empty(); + } else { + return signerIdentifier.validate(this.signer.getPublicKey()) + ? Optional.of(this.signer) + : Optional.empty(); + } + } + + @Override + public Set availablePublicKeys( + Function identifierFunction) { + return this.signer.getPublicKey() != null + ? Set.of(this.signer.getPublicKey()) + : Collections.emptySet(); + } +}