diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/JsonRpcErrorCodes.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/JsonRpcErrorCodes.java new file mode 100644 index 00000000000..d83f6429dfe --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/JsonRpcErrorCodes.java @@ -0,0 +1,61 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * 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.teku.ethereum.executionclient.web3j; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +enum JsonRpcErrorCodes { + PARSE_ERROR(-32700, "Parse error"), + INVALID_REQUEST(-32600, "Invalid Request"), + METHOD_NOT_FOUND(-32601, "Method not found"), + INVALID_PARAMS(-32602, "Invalid params"), + INTERNAL_ERROR(-32603, "Internal error"), + SERVER_ERROR(-32000, "Server error"); + + private final int errorCode; + private final String description; + private static final Int2ObjectOpenHashMap CODE_TO_ERROR_MAP; + + static { + CODE_TO_ERROR_MAP = new Int2ObjectOpenHashMap<>(); + for (final JsonRpcErrorCodes error : values()) { + CODE_TO_ERROR_MAP.put(error.getErrorCode(), error); + } + } + + JsonRpcErrorCodes(final int errorCode, final String description) { + this.errorCode = errorCode; + this.description = description; + } + + public int getErrorCode() { + return errorCode; + } + + public String getDescription() { + return description; + } + + public static String getDescription(final int errorCode) { + return fromCode(errorCode).getDescription(); + } + + public static JsonRpcErrorCodes fromCode(final int errorCode) { + final JsonRpcErrorCodes error = CODE_TO_ERROR_MAP.get(errorCode); + if (error != null) { + return error; + } + return errorCode >= -32099 && errorCode <= -32000 ? SERVER_ERROR : INTERNAL_ERROR; + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClient.java index b8cf68f6729..c1bcc331e29 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClient.java @@ -15,7 +15,6 @@ import static tech.pegasys.teku.infrastructure.exceptions.ExceptionUtil.getMessageOrSimpleName; -import java.io.IOException; import java.net.ConnectException; import java.time.Duration; import java.util.Collection; @@ -86,14 +85,9 @@ public SafeFuture> doRequest( (response, exception) -> { final boolean isCriticalRequest = isCriticalRequest(web3jRequest); if (exception != null) { - final boolean couldBeAuthError = isAuthenticationException(exception); - handleError(isCriticalRequest, exception, couldBeAuthError); - return Response.withErrorMessage(getMessageOrSimpleName(exception)); + return handleException(exception, isCriticalRequest); } else if (response.hasError()) { - final String errorMessage = - response.getError().getCode() + ": " + response.getError().getMessage(); - handleError(isCriticalRequest, new IOException(errorMessage), false); - return Response.withErrorMessage(errorMessage); + return handleJsonRpcError(response.getError(), isCriticalRequest); } else { handleSuccess(isCriticalRequest); return new Response<>(response.getResult()); @@ -101,6 +95,31 @@ public SafeFuture> doRequest( }); } + private Response handleException( + final Throwable exception, final boolean isCriticalRequest) { + final boolean couldBeAuthError = isAuthenticationException(exception); + handleError(isCriticalRequest, exception, couldBeAuthError); + return Response.withErrorMessage(getMessageOrSimpleName(exception)); + } + + private Response handleJsonRpcError( + final org.web3j.protocol.core.Response.Error error, final boolean isCriticalRequest) { + final int errorCode = error.getCode(); + final String errorType = JsonRpcErrorCodes.getDescription(errorCode); + final String formattedError = + String.format("JSON-RPC error: %s (%d): %s", errorType, errorCode, error.getMessage()); + + if (isCriticalRequest) { + logError(formattedError); + } + + return Response.withErrorMessage(formattedError); + } + + private void logError(final String errorMessage) { + eventLog.executionClientRequestFailed(new Exception(errorMessage), false); + } + private boolean isCriticalRequest(final Request request) { return !nonCriticalMethods.contains(request.getMethod()); } diff --git a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClientTest.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClientTest.java index dce5771ce05..5ef46e59e4a 100644 --- a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClientTest.java +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JClientTest.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.ethereum.executionclient.web3j; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -272,6 +273,40 @@ void shouldNotUpdateAvailabilityWhenNonCriticalMethodFailsWithErrorResponse( verifyNoInteractions(executionClientEventsPublisher); } + @ParameterizedTest + @MethodSource("getClientInstances") + void shouldDecodeJsonRpcErrorCodesCorrectly(final ClientFactory clientFactory) throws Exception { + final Web3JClient client = clientFactory.create(eventLog, executionClientEventsPublisher); + Request request = createRequest(client); + + // Create a response with a specific JSON-RPC error + VoidResponse errorResponse = new VoidResponse(); + Error rpcError = + new Error( + JsonRpcErrorCodes.INVALID_PARAMS.getErrorCode(), + "engine_newPayload method has been called with invalid parameters"); + errorResponse.setError(rpcError); + + when(client.getWeb3jService().sendAsync(request, VoidResponse.class)) + .thenReturn(SafeFuture.completedFuture(errorResponse)); + + final SafeFuture> result = client.doRequest(request, DEFAULT_TIMEOUT); + Waiter.waitFor(result); + + SafeFutureAssert.assertThatSafeFuture(result).isCompleted(); + final Response response = SafeFutureAssert.safeJoin(result); + + assertThat(response.getErrorMessage()) + .isEqualTo( + String.format( + "JSON-RPC error: %s (%d): %s", + JsonRpcErrorCodes.INVALID_PARAMS.getDescription(), + JsonRpcErrorCodes.INVALID_PARAMS.getErrorCode(), + "engine_newPayload method has been called with invalid parameters")); + + verify(eventLog).executionClientRequestFailed(any(Exception.class), eq(false)); + } + private static Request createRequest(final Web3JClient client) { return new Request<>("test", new ArrayList<>(), client.getWeb3jService(), VoidResponse.class); }