From 86cc6cb19e687aff4f4e53936df3a19f9f28067c Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 20 Mar 2024 21:31:29 +0100 Subject: [PATCH] Extend error handling of plugin RPC methods (#6759) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../TransactionSimulationServiceImpl.java | 23 ++- .../EthEstimateGasIntegrationTest.java | 13 +- .../api/jsonrpc/JsonRpcErrorConverter.java | 2 + .../internal/methods/AbstractEstimateGas.java | 7 +- .../internal/methods/PluginJsonRpcMethod.java | 9 +- .../internal/response/JsonRpcError.java | 31 ++-- .../internal/response/RpcErrorType.java | 28 ++- .../jsonrpc/JsonRpcHttpServiceTestBase.java | 5 +- .../api/jsonrpc/PluginJsonRpcMethodTest.java | 173 ++++++++++++++++++ .../internal/methods/EthEstimateGasTest.java | 23 ++- .../TransactionProcessingResult.java | 5 +- .../transaction/TransactionInvalidReason.java | 1 + plugin-api/build.gradle | 2 +- .../data/TransactionSimulationResult.java | 22 +++ .../exception/PluginRpcEndpointException.java | 38 +++- .../plugin/services/rpc/RpcMethodError.java | 51 ++++++ 17 files changed, 369 insertions(+), 65 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/PluginJsonRpcMethodTest.java create mode 100644 plugin-api/src/main/java/org/hyperledger/besu/plugin/services/rpc/RpcMethodError.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6dbbde8a..eefdbec72e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Introduce `TransactionSimulationService` [#6686](https://github.com/hyperledger/besu/pull/6686) - Transaction call object to accept both `input` and `data` field simultaneously if they are set to equal values [#6702](https://github.com/hyperledger/besu/pull/6702) - `eth_call` for blob tx allows for empty `maxFeePerBlobGas` [#6731](https://github.com/hyperledger/besu/pull/6731) +- Extend error handling of plugin RPC methods [#6759](https://github.com/hyperledger/besu/pull/6759) ### Bug fixes - Fix txpool dump/restore race condition [#6665](https://github.com/hyperledger/besu/pull/6665) diff --git a/besu/src/main/java/org/hyperledger/besu/services/TransactionSimulationServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/TransactionSimulationServiceImpl.java index 5ebf48f0ce..0981e1ae34 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/TransactionSimulationServiceImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/TransactionSimulationServiceImpl.java @@ -19,7 +19,10 @@ import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.mainnet.ImmutableTransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.ethereum.transaction.CallParameter; +import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.plugin.Unstable; @@ -62,14 +65,16 @@ public class TransactionSimulationServiceImpl implements TransactionSimulationSe final CallParameter callParameter = CallParameter.fromTransaction(transaction); - final var blockHeader = - blockchain - .getBlockHeader(blockHash) - .or(() -> blockchain.getBlockHeaderSafe(blockHash)) - .orElseThrow( - () -> - new IllegalStateException( - "Block header not yet present for chain head hash: " + blockHash)); + final var maybeBlockHeader = + blockchain.getBlockHeader(blockHash).or(() -> blockchain.getBlockHeaderSafe(blockHash)); + + if (maybeBlockHeader.isEmpty()) { + return Optional.of( + new TransactionSimulationResult( + transaction, + TransactionProcessingResult.invalid( + ValidationResult.invalid(TransactionInvalidReason.BLOCK_NOT_FOUND)))); + } return transactionSimulator .process( @@ -78,7 +83,7 @@ public class TransactionSimulationServiceImpl implements TransactionSimulationSe ? SIMULATOR_ALLOWING_EXCEEDING_BALANCE : TransactionValidationParams.transactionSimulator(), operationTracer, - blockHeader) + maybeBlockHeader.get()) .map(res -> new TransactionSimulationResult(transaction, res.result())); } } diff --git a/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/EthEstimateGasIntegrationTest.java b/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/EthEstimateGasIntegrationTest.java index ca64e046fd..a461539030 100644 --- a/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/EthEstimateGasIntegrationTest.java +++ b/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/EthEstimateGasIntegrationTest.java @@ -28,7 +28,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; import org.hyperledger.besu.testutil.BlockTestUtil; import java.util.Map; @@ -171,11 +172,11 @@ public class EthEstimateGasIntegrationTest { null, null); final JsonRpcRequestContext request = requestWithParams(callParameter); - - final RpcErrorType rpcErrorType = RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; - final JsonRpcError rpcError = new JsonRpcError(rpcErrorType); - rpcError.setReason( - "transaction up-front cost 0x1cc31b3333167018 exceeds transaction sender account balance 0x140"); + final ValidationResult validationResult = + ValidationResult.invalid( + TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, + "transaction up-front cost 0x1cc31b3333167018 exceeds transaction sender account balance 0x140"); + final JsonRpcError rpcError = JsonRpcError.from(validationResult); final JsonRpcResponse expectedResponse = new JsonRpcErrorResponse(null, rpcError); final JsonRpcResponse response = method.response(request); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcErrorConverter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcErrorConverter.java index c583401b3f..30ad7917a6 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcErrorConverter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcErrorConverter.java @@ -83,6 +83,8 @@ public class JsonRpcErrorConverter { return RpcErrorType.BLOB_GAS_PRICE_BELOW_CURRENT_BLOB_BASE_FEE; case EXECUTION_HALTED: return RpcErrorType.EXECUTION_HALTED; + case BLOCK_NOT_FOUND: + return RpcErrorType.BLOCK_NOT_FOUND; default: return RpcErrorType.INTERNAL_ERROR; } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java index 0f9b1b7be1..237eef2e02 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java @@ -111,12 +111,7 @@ public abstract class AbstractEstimateGas implements JsonRpcMethod { result.getValidationResult(); if (validationResult != null && !validationResult.isValid()) { if (validationResult.getErrorMessage().length() > 0) { - final RpcErrorType rpcErrorType = - JsonRpcErrorConverter.convertTransactionInvalidReason( - validationResult.getInvalidReason()); - final JsonRpcError rpcError = new JsonRpcError(rpcErrorType); - rpcError.setReason(validationResult.getErrorMessage()); - return errorResponse(request, rpcError); + return errorResponse(request, JsonRpcError.from(validationResult)); } return errorResponse( request, diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/PluginJsonRpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/PluginJsonRpcMethod.java index 46f9986b1c..702404ae4b 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/PluginJsonRpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/PluginJsonRpcMethod.java @@ -14,9 +14,6 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; -import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType.INTERNAL_ERROR; -import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType.PLUGIN_INTERNAL_ERROR; - import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; @@ -53,13 +50,9 @@ public class PluginJsonRpcMethod implements JsonRpcMethod { final Object result = function.apply(() -> request.getRequest().getParams()); return new JsonRpcSuccessResponse(request.getRequest().getId(), result); } catch (final PluginRpcEndpointException ex) { - final JsonRpcError error = new JsonRpcError(PLUGIN_INTERNAL_ERROR, ex.getMessage()); + final JsonRpcError error = new JsonRpcError(ex.getRpcMethodError(), ex.getMessage()); LOG.error("Error calling plugin JSON-RPC endpoint", ex); return new JsonRpcErrorResponse(request.getRequest().getId(), error); - } catch (final Exception ex) { - LOG.error("Error calling plugin JSON-RPC endpoint", ex); - return new JsonRpcErrorResponse( - request.getRequest().getId(), new JsonRpcError(INTERNAL_ERROR)); } } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java index a3a3427de2..59bb92c443 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java @@ -14,6 +14,11 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.response; +import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcErrorConverter; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; + import java.util.Objects; import com.fasterxml.jackson.annotation.JsonCreator; @@ -21,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.tuweni.bytes.Bytes; @JsonInclude(value = JsonInclude.Include.NON_NULL) @JsonFormat(shape = JsonFormat.Shape.OBJECT) @@ -41,16 +45,11 @@ public class JsonRpcError { this.data = data; } - public JsonRpcError(final RpcErrorType errorType, final String data) { + public JsonRpcError(final RpcMethodError errorType, final String data) { this(errorType.getCode(), errorType.getMessage(), data); - // For execution reverted errors decode the data (if present) - if (errorType == RpcErrorType.REVERT_ERROR && data != null) { - JsonRpcErrorResponse.decodeRevertReason(Bytes.fromHexString(data)) - .ifPresent( - (decodedReason) -> { - this.reason = decodedReason; - }); + if (data != null) { + errorType.decodeData(data).ifPresent(decodedData -> this.reason = decodedData); } } @@ -58,6 +57,16 @@ public class JsonRpcError { this(errorType, null); } + public static JsonRpcError from( + final ValidationResult validationResult) { + final var jsonRpcError = + new JsonRpcError( + JsonRpcErrorConverter.convertTransactionInvalidReason( + validationResult.getInvalidReason())); + jsonRpcError.reason = validationResult.getErrorMessage(); + return jsonRpcError; + } + @JsonGetter("code") public int getCode() { return code; @@ -73,10 +82,6 @@ public class JsonRpcError { return data; } - public void setReason(final String reason) { - this.reason = reason; - } - @Override public boolean equals(final Object o) { if (this == o) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java index 17d6d229fb..3a13c5a890 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java @@ -14,7 +14,14 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.response; -public enum RpcErrorType { +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; + +import java.util.Optional; +import java.util.function.Function; + +import org.apache.tuweni.bytes.Bytes; + +public enum RpcErrorType implements RpcMethodError { // Standard errors PARSE_ERROR(-32700, "Parse error"), INVALID_REQUEST(-32600, "Invalid Request"), @@ -67,7 +74,10 @@ public enum RpcErrorType { REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED(-32000, "ChainId not supported"), REPLAY_PROTECTED_SIGNATURE_REQUIRED(-32000, "ChainId is required"), TX_FEECAP_EXCEEDED(-32000, "Transaction fee cap exceeded"), - REVERT_ERROR(-32000, "Execution reverted"), + REVERT_ERROR( + -32000, + "Execution reverted", + data -> JsonRpcErrorResponse.decodeRevertReason(Bytes.fromHexString(data))), TRANSACTION_NOT_FOUND(-32000, "Transaction not found"), MAX_PRIORITY_FEE_PER_GAS_EXCEEDS_MAX_FEE_PER_GAS( -32000, "Max priority fee per gas exceeds max fee per gas"), @@ -222,17 +232,31 @@ public enum RpcErrorType { private final int code; private final String message; + private final Function> dataDecoder; RpcErrorType(final int code, final String message) { + this(code, message, null); + } + + RpcErrorType( + final int code, final String message, Function> dataDecoder) { this.code = code; this.message = message; + this.dataDecoder = dataDecoder; } + @Override public int getCode() { return code; } + @Override public String getMessage() { return message; } + + @Override + public Optional decodeData(final String data) { + return dataDecoder == null ? Optional.empty() : dataDecoder.apply(data); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTestBase.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTestBase.java index a5f90791ce..159e99b0bf 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTestBase.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTestBase.java @@ -152,8 +152,7 @@ public class JsonRpcHttpServiceTestBase { baseUrl = service.url(); } - protected static JsonRpcHttpService createJsonRpcHttpService(final JsonRpcConfiguration config) - throws Exception { + protected static JsonRpcHttpService createJsonRpcHttpService(final JsonRpcConfiguration config) { return new JsonRpcHttpService( vertx, folder, @@ -165,7 +164,7 @@ public class JsonRpcHttpServiceTestBase { HealthService.ALWAYS_HEALTHY); } - protected static JsonRpcHttpService createJsonRpcHttpService() throws Exception { + protected static JsonRpcHttpService createJsonRpcHttpService() { return new JsonRpcHttpService( vertx, folder, diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/PluginJsonRpcMethodTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/PluginJsonRpcMethodTest.java new file mode 100644 index 0000000000..560a79a8bf --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/PluginJsonRpcMethodTest.java @@ -0,0 +1,173 @@ +/* + * Copyright Hyperledger Besu contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.PluginJsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; + +import io.vertx.core.json.JsonObject; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class PluginJsonRpcMethodTest extends JsonRpcHttpServiceTestBase { + + @BeforeAll + public static void setup() throws Exception { + initServerAndClient(); + } + + /** Tears down the HTTP server. */ + @AfterAll + public static void shutdownServer() { + service.stop().join(); + } + + @Test + public void happyPath() throws Exception { + final var request = + """ + {"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":["hello"]}"""; + + try (var unused = + addRpcMethod( + "plugin_echo", + new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { + final RequestBody body = RequestBody.create(request, JSON); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, 1); + assertThat(json.getString("result")).isEqualTo("hello"); + } + } + } + + @Test + public void invalidJsonShouldReturnParseError() throws Exception { + final var malformedRequest = + """ + {"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":}"""; + + try (var unused = + addRpcMethod( + "plugin_echo", + new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { + final RequestBody body = RequestBody.create(malformedRequest, JSON); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.PARSE_ERROR); + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + } + + @Test + public void invalidParamsShouldReturnInvalidParams() throws Exception { + final var missingRequiredParam = + """ + {"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[]}"""; + try (var unused = + addRpcMethod( + "plugin_echo", + new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { + final RequestBody body = RequestBody.create(missingRequiredParam, JSON); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.INVALID_PARAMS); + testHelper.assertValidJsonRpcError( + json, 1, expectedError.getCode(), expectedError.getMessage()); + } + } + } + + @Test + public void methodErrorShouldReturnErrorResponse() throws Exception { + final var wrongParamContent = + """ + {"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[" "]}"""; + try (var unused = + addRpcMethod( + "plugin_echo", + new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { + final RequestBody body = RequestBody.create(wrongParamContent, JSON); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcError(json, 1, -1, "Blank input not allowed"); + } + } + } + + @Test + public void unhandledExceptionShouldReturnInternalErrorResponse() throws Exception { + final var nullParam = + """ + {"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[null]}"""; + try (var unused = + addRpcMethod( + "plugin_echo", + new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { + final RequestBody body = RequestBody.create(nullParam, JSON); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.INTERNAL_ERROR); + testHelper.assertValidJsonRpcError( + json, 1, expectedError.getCode(), expectedError.getMessage()); + } + } + } + + private static Object echoPluginRpcMethod(final PluginRpcRequest request) { + final var params = request.getParams(); + if (params.length == 0) { + throw new InvalidJsonRpcParameters("parameter is mandatory"); + } + final var input = params[0]; + if (input.toString().isBlank()) { + throw new PluginRpcEndpointException( + new RpcMethodError() { + @Override + public int getCode() { + return -1; + } + + @Override + public String getMessage() { + return "Blank input not allowed"; + } + }); + } + return input; + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 64d015d89a..98b9226404 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -219,9 +219,11 @@ public class EthEstimateGasTest { TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, "transaction up-front cost 10 exceeds transaction sender account balance 5"); - final RpcErrorType rpcErrorType = RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; - final JsonRpcError rpcError = new JsonRpcError(rpcErrorType); - rpcError.setReason("transaction up-front cost 10 exceeds transaction sender account balance 5"); + final ValidationResult validationResult = + ValidationResult.invalid( + TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, + "transaction up-front cost 10 exceeds transaction sender account balance 5"); + final JsonRpcError rpcError = JsonRpcError.from(validationResult); final JsonRpcResponse expectedResponse = new JsonRpcErrorResponse(null, rpcError); Assertions.assertThat(method.response(request)) @@ -235,10 +237,11 @@ public class EthEstimateGasTest { mockTransientProcessorResultTxInvalidReason( TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, "transaction up-front cost 10 exceeds transaction sender account balance 5"); - - final RpcErrorType rpcErrorType = RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; - final JsonRpcError rpcError = new JsonRpcError(rpcErrorType); - rpcError.setReason("transaction up-front cost 10 exceeds transaction sender account balance 5"); + final ValidationResult validationResult = + ValidationResult.invalid( + TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, + "transaction up-front cost 10 exceeds transaction sender account balance 5"); + final JsonRpcError rpcError = JsonRpcError.from(validationResult); final JsonRpcResponse expectedResponse = new JsonRpcErrorResponse(null, rpcError); Assertions.assertThat(method.response(request)) @@ -384,9 +387,9 @@ public class EthEstimateGasTest { mockTransientProcessorResultTxInvalidReason( TransactionInvalidReason.EXECUTION_HALTED, "INVALID_OPERATION"); - final RpcErrorType rpcErrorType = RpcErrorType.EXECUTION_HALTED; - final JsonRpcError rpcError = new JsonRpcError(rpcErrorType); - rpcError.setReason("INVALID_OPERATION"); + final ValidationResult validationResult = + ValidationResult.invalid(TransactionInvalidReason.EXECUTION_HALTED, "INVALID_OPERATION"); + final JsonRpcError rpcError = JsonRpcError.from(validationResult); final JsonRpcResponse expectedResponse = new JsonRpcErrorResponse(null, rpcError); Assertions.assertThat(method.response(request)) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java index e77062a7dd..eca28927bd 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java @@ -18,7 +18,6 @@ import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; import org.hyperledger.besu.evm.log.Log; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -56,7 +55,7 @@ public class TransactionProcessingResult public static TransactionProcessingResult invalid( final ValidationResult validationResult) { return new TransactionProcessingResult( - Status.INVALID, new ArrayList<>(), -1, -1, Bytes.EMPTY, validationResult, Optional.empty()); + Status.INVALID, List.of(), -1, -1, Bytes.EMPTY, validationResult, Optional.empty()); } public static TransactionProcessingResult failed( @@ -66,7 +65,7 @@ public class TransactionProcessingResult final Optional revertReason) { return new TransactionProcessingResult( Status.FAILED, - new ArrayList<>(), + List.of(), gasUsedByTransaction, gasRemaining, Bytes.EMPTY, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionInvalidReason.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionInvalidReason.java index 0760740467..c2d5bc3d8a 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionInvalidReason.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionInvalidReason.java @@ -32,6 +32,7 @@ public enum TransactionInvalidReason { TX_SENDER_NOT_AUTHORIZED, CHAIN_HEAD_NOT_AVAILABLE, CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE, + BLOCK_NOT_FOUND, EXCEEDS_PER_TRANSACTION_GAS_LIMIT, INVALID_TRANSACTION_FORMAT, TRANSACTION_PRICE_TOO_LOW, diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index a0129425e2..7377359a16 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -69,7 +69,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'ytjNiSzw9IR8YHyO4ikmqRTg1GTWkCX9QiQtwq2dRSg=' + knownHash = '0xiYCyr3M4oSrvqYXVkLgVDzlBg2T3fmrADub5tY5a0=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSimulationResult.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSimulationResult.java index 1651534a9f..1f2e6a8ab8 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSimulationResult.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSimulationResult.java @@ -16,6 +16,10 @@ package org.hyperledger.besu.plugin.data; import org.hyperledger.besu.datatypes.Transaction; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; + /** * TransactionSimulationResult * @@ -43,6 +47,24 @@ public record TransactionSimulationResult( return result.isInvalid(); } + /** + * Return the optional revert reason + * + * @return the optional revert reason + */ + public Optional getRevertReason() { + return result.getRevertReason(); + } + + /** + * Return the optional invalid reason + * + * @return the optional invalid reason + */ + public Optional getInvalidReason() { + return result.getInvalidReason(); + } + /** * Estimated gas used by the transaction * diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/exception/PluginRpcEndpointException.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/exception/PluginRpcEndpointException.java index 9a38b891eb..e736a1d2cf 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/exception/PluginRpcEndpointException.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/exception/PluginRpcEndpointException.java @@ -14,28 +14,58 @@ */ package org.hyperledger.besu.plugin.services.exception; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; + /** Base exception class for problems encountered in the RpcEndpointService. */ public class PluginRpcEndpointException extends RuntimeException { + /** The error */ + private final RpcMethodError rpcMethodError; + + /** + * Constructs a new PluginRpcEndpointException exception with the specified error. + * + * @param rpcMethodError the error. + */ + public PluginRpcEndpointException(final RpcMethodError rpcMethodError) { + super(); + this.rpcMethodError = rpcMethodError; + } + /** - * Constructs a new PluginRpcEndpointException exception with the specified message. + * Constructs a new PluginRpcEndpointException exception with the specified error and message. * + * @param rpcMethodError the error. * @param message the detail message (which is saved for later retrieval by the {@link * #getMessage()} method). */ - public PluginRpcEndpointException(final String message) { + public PluginRpcEndpointException(final RpcMethodError rpcMethodError, final String message) { super(message); + this.rpcMethodError = rpcMethodError; } /** - * Constructs a new PluginRpcEndpointException exception with the specified message. + * Constructs a new PluginRpcEndpointException exception with the specified error, message and + * cause. * + * @param rpcMethodError the error. * @param message the detail message (which is saved for later retrieval by the {@link * #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or * unknown.) */ - public PluginRpcEndpointException(final String message, final Throwable cause) { + public PluginRpcEndpointException( + final RpcMethodError rpcMethodError, final String message, final Throwable cause) { super(message, cause); + this.rpcMethodError = rpcMethodError; + } + + /** + * Get the error + * + * @return the error + */ + public RpcMethodError getRpcMethodError() { + return rpcMethodError; } } diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/rpc/RpcMethodError.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/rpc/RpcMethodError.java new file mode 100644 index 0000000000..c257febbc2 --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/rpc/RpcMethodError.java @@ -0,0 +1,51 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.rpc; + +import java.util.Optional; + +/** + * The {@code RpcMethodError} interface defines the structure for RPC error handling within the + * context of plugins. It provides methods to retrieve error code, message, and an optional data + * decoder function. + */ +public interface RpcMethodError { + + /** + * Retrieves the error code associated with the RPC error. + * + * @return An integer representing the error code. + */ + int getCode(); + + /** + * Retrieves the message associated with the RPC error. + * + * @return A {@code String} containing the error message. + */ + String getMessage(); + + /** + * Some errors have additional data associated with them, that is possible to decode to provide a + * more detailed error response. + * + * @param data the additional data to decode + * @return an optional containing the decoded data if the error has it and the decoding is + * successful, otherwise empty. + */ + default Optional decodeData(final String data) { + return Optional.empty(); + } +}