From dcc4c557a934dd4f356b6641048b6a87853d5e84 Mon Sep 17 00:00:00 2001 From: thinkAfCod Date: Mon, 5 Jun 2023 00:13:20 +0800 Subject: [PATCH 1/3] feat: rpc server --- .../src/main/java/io/optimism/cli/Cli.java | 2 +- .../java/io/optimism/l1/InnerWatcher.java | 13 +- .../main/java/io/optimism/rpc/RpcMethod.java | 54 +++ .../main/java/io/optimism/rpc/RpcServer.java | 183 ++++++++++ .../rpc/execution/BaseJsonRpcProcessor.java | 51 +++ .../rpc/execution/JsonRpcExecutor.java | 98 ++++++ .../rpc/execution/JsonRpcProcessor.java | 38 ++ .../rpc/execution/LoggedJsonRpcProcessor.java | 57 +++ .../rpc/handler/JsonRpcExecutorHandler.java | 124 +++++++ .../rpc/handler/JsonRpcParseHandler.java | 61 ++++ .../optimism/rpc/handler/TimeoutHandler.java | 78 +++++ .../optimism/rpc/internal/JsonRpcRequest.java | 175 ++++++++++ .../rpc/internal/JsonRpcRequestContext.java | 63 ++++ .../rpc/internal/JsonRpcRequestId.java | 89 +++++ .../rpc/internal/response/JsonRpcError.java | 99 ++++++ .../response/JsonRpcErrorResponse.java | 72 ++++ .../internal/response/JsonRpcNoResponse.java | 24 ++ .../internal/response/JsonRpcResponse.java | 28 ++ .../response/JsonRpcResponseType.java | 24 ++ .../response/JsonRpcSuccessResponse.java | 79 +++++ .../rpc/internal/result/EthGetProof.java | 329 ++++++++++++++++++ .../rpc/internal/result/OutputRootResult.java | 30 ++ .../optimism/rpc/methods/JsonRpcMethod.java | 44 +++ .../rpc/methods/JsonRpcMethodsFactory.java | 42 +++ .../optimism/rpc/methods/OutputAtBlock.java | 128 +++++++ .../provider}/RetryRateLimitInterceptor.java | 3 +- .../optimism/rpc/provider/Web3jProvider.java | 59 ++++ .../io/optimism/telemetry/InnerMetrics.java | 6 +- .../java/io/optimism/telemetry/Logging.java | 36 +- .../telemetry/LoggingExampleTest.java | 4 +- 30 files changed, 2059 insertions(+), 34 deletions(-) create mode 100644 hildr-node/src/main/java/io/optimism/rpc/RpcMethod.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/RpcServer.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/execution/BaseJsonRpcProcessor.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcExecutor.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcProcessor.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/execution/LoggedJsonRpcProcessor.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcExecutorHandler.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/handler/TimeoutHandler.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestContext.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestId.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcError.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcErrorResponse.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcNoResponse.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponse.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponseType.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcSuccessResponse.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/result/EthGetProof.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/internal/result/OutputRootResult.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethodsFactory.java create mode 100644 hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java rename hildr-node/src/main/java/io/optimism/{l1 => rpc/provider}/RetryRateLimitInterceptor.java (97%) create mode 100644 hildr-node/src/main/java/io/optimism/rpc/provider/Web3jProvider.java diff --git a/hildr-node/src/main/java/io/optimism/cli/Cli.java b/hildr-node/src/main/java/io/optimism/cli/Cli.java index 0c321a10..cc224293 100644 --- a/hildr-node/src/main/java/io/optimism/cli/Cli.java +++ b/hildr-node/src/main/java/io/optimism/cli/Cli.java @@ -95,7 +95,7 @@ public void run() { var checkpointHash = this.checkpointHash; var config = this.toConfig(); - Tracer tracer = Logging.INSTANCE.getTracer(); + Tracer tracer = Logging.INSTANCE.getTracer("hildr-cli"); InnerMetrics.start(9200); Runner runner = Runner.create(config).setSyncMode(syncMode).setCheckpointHash(checkpointHash); diff --git a/hildr-node/src/main/java/io/optimism/l1/InnerWatcher.java b/hildr-node/src/main/java/io/optimism/l1/InnerWatcher.java index f5ee432d..06004f6b 100644 --- a/hildr-node/src/main/java/io/optimism/l1/InnerWatcher.java +++ b/hildr-node/src/main/java/io/optimism/l1/InnerWatcher.java @@ -26,6 +26,7 @@ import io.optimism.derive.stages.Attributes.UserDeposited; import io.optimism.driver.L1AttributesDepositedTxNotFoundException; import io.optimism.l1.BlockUpdate.FinalityUpdate; +import io.optimism.rpc.provider.Web3jProvider; import java.math.BigInteger; import java.time.Duration; import java.util.ArrayList; @@ -36,7 +37,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; -import okhttp3.OkHttpClient; import org.jctools.queues.MessagePassingQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +55,6 @@ import org.web3j.protocol.core.methods.response.EthLog; import org.web3j.protocol.core.methods.response.EthLog.LogObject; import org.web3j.protocol.core.methods.response.EthLog.LogResult; -import org.web3j.protocol.http.HttpService; import org.web3j.tuples.generated.Tuple2; import org.web3j.utils.Numeric; @@ -143,7 +142,7 @@ public InnerWatcher( BigInteger l2StartBlock, ExecutorService executor) { this.executor = executor; - this.provider = createClient(config.l1RpcUrl()); + this.provider = Web3jProvider.createClient(config.l1RpcUrl()); this.config = config; if (l2StartBlock.equals(config.chainConfig().l2Genesis().number())) { @@ -162,7 +161,7 @@ public InnerWatcher( } private void getMetadataFromL2(BigInteger l2StartBlock) { - Web3j l2Client = createClient(config.l2RpcUrl()); + Web3j l2Client = Web3jProvider.createClient(config.l2RpcUrl()); EthBlock block; try { block = this.getBlock(l2Client, l2StartBlock.subtract(BigInteger.ONE)); @@ -460,12 +459,6 @@ private List getDeposits(BigInteger blockNum) return remv; } - private Web3j createClient(String url) { - OkHttpClient okHttpClient = - new OkHttpClient.Builder().addInterceptor(new RetryRateLimitInterceptor()).build(); - return Web3j.build(new HttpService(url, okHttpClient)); - } - @Override protected void run() { while (isRunning()) { diff --git a/hildr-node/src/main/java/io/optimism/rpc/RpcMethod.java b/hildr-node/src/main/java/io/optimism/rpc/RpcMethod.java new file mode 100644 index 00000000..11f842da --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/RpcMethod.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc; + +import java.util.HashSet; + +/** + * method handler of rpc. + * + * @author thinkAfCod + * @since 2023.06 + */ +public enum RpcMethod { + + /** optimism_outputAtBlock api. */ + OP_OUTPUT_AT_BLOCK("optimism_outputAtBlock"); + + private final String rpcMethodName; + + private static final HashSet allMethodNames; + + static { + allMethodNames = new HashSet<>(); + for (RpcMethod m : RpcMethod.values()) { + allMethodNames.add(m.getRpcMethodName()); + } + } + + RpcMethod(String rpcMethodName) { + this.rpcMethodName = rpcMethodName; + } + + public String getRpcMethodName() { + return rpcMethodName; + } + + public static boolean rpcMethodExists(final String rpcMethodName) { + return allMethodNames.contains(rpcMethodName); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java b/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java new file mode 100644 index 00000000..3f1f97de --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java @@ -0,0 +1,183 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.optimism.config.Config; +import io.optimism.rpc.execution.BaseJsonRpcProcessor; +import io.optimism.rpc.execution.LoggedJsonRpcProcessor; +import io.optimism.rpc.handler.JsonRpcExecutorHandler; +import io.optimism.rpc.handler.JsonRpcParseHandler; +import io.optimism.rpc.handler.TimeoutHandler; +import io.optimism.rpc.methods.JsonRpcMethod; +import io.optimism.rpc.methods.JsonRpcMethodsFactory; +import io.optimism.telemetry.Logging; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.http.HttpConnection; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.ServerWebSocket; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * rpc server. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class RpcServer { + + private static final Logger logger = LoggerFactory.getLogger(RpcServer.class); + private static final int DEFAULT_MAX_ACTIVE_CONNECTIONS = 80; + + private final Config config; + + private final Vertx vertx; + + private final Map methods; + + private HttpServer httpServer; + + private AtomicInteger activeConnectionsCount; + private int maxActiveConnections; + + public RpcServer(final Config config) { + this.config = config; + this.activeConnectionsCount = new AtomicInteger(); + this.maxActiveConnections = DEFAULT_MAX_ACTIVE_CONNECTIONS; + this.vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(1)); + this.methods = new JsonRpcMethodsFactory().methods(this.config); + } + + public void start() throws Exception { + this.httpServer = vertx.createHttpServer(getHttpServerOptions(config)); + httpServer.webSocketHandler(webSocketHandler()); + httpServer.connectionHandler(connectionHandler()); + + CompletableFuture future = new CompletableFuture<>(); + httpServer + .requestHandler(buildRouter()) + .listen( + res -> { + if (!res.failed()) { + logger.info("rpc server started at port {}", httpServer.actualPort()); + future.complete(null); + return; + } + future.completeExceptionally(res.cause()); + httpServer = null; + }); + future.get(); + } + + private Handler buildRouter() { + var router = Router.router(this.vertx); + router + .route() + .handler( + context -> { + Tracer tracer = Logging.INSTANCE.getTracer("jsonrpc-server"); + Span span = tracer.nextSpan().name("requestHandle").start(); + context.put("CTX_TRACE", tracer); + context.put("CTX_SPAN", span); + try (var unused = tracer.withSpan(span)) { + context.next(); + } finally { + span.end(); + } + }); + Route mainRoute = router.route("/").produces("application/json"); + mainRoute.blockingHandler(JsonRpcParseHandler.handler()); + mainRoute.blockingHandler(TimeoutHandler.handler()); + mainRoute.handler( + JsonRpcExecutorHandler.handler( + new LoggedJsonRpcProcessor(new BaseJsonRpcProcessor()), methods)); + return null; + } + + private Handler connectionHandler() { + return connection -> { + if (activeConnectionsCount.get() >= maxActiveConnections) { + // disallow new connections to prevent DoS + logger.warn( + "Rejecting new connection from {}. Max {} active connections limit reached.", + connection.remoteAddress(), + activeConnectionsCount.getAndIncrement()); + connection.close(); + } else { + logger.debug( + "Opened connection from {}. Total of active connections: {}/{}", + connection.remoteAddress(), + activeConnectionsCount.incrementAndGet(), + maxActiveConnections); + } + connection.closeHandler( + c -> + logger.debug( + "Connection closed from {}. Total of active connections: {}/{}", + connection.remoteAddress(), + activeConnectionsCount.decrementAndGet(), + maxActiveConnections)); + }; + } + + private Handler webSocketHandler() { + Handler o = null; + return o; + } + + private HttpServerOptions getHttpServerOptions(final Config config) { + final HttpServerOptions httpServerOptions = + new HttpServerOptions() + .setHost("127.0.0.1") + .setPort(config.rpcPort()) + .setHandle100ContinueAutomatically(true) + .setCompressionSupported(true); + + httpServerOptions.setMaxWebSocketFrameSize(1024 * 1024); + httpServerOptions.setMaxWebSocketMessageSize(1024 * 1024 * 4); + return httpServerOptions; + } + + public void stop() { + CompletableFuture future = new CompletableFuture<>(); + httpServer.close( + res -> { + if (res.failed()) { + future.completeExceptionally(res.cause()); + } else { + httpServer = null; + future.complete(null); + } + }); + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/execution/BaseJsonRpcProcessor.java b/hildr-node/src/main/java/io/optimism/rpc/execution/BaseJsonRpcProcessor.java new file mode 100644 index 00000000..74ec6c40 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/execution/BaseJsonRpcProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.execution; + +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcError; +import io.optimism.rpc.internal.response.JsonRpcErrorResponse; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.methods.JsonRpcMethod; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BaseJsonRpcProcessor + * + * @author thinkAfCod + * @since 2023.06 + */ +public class BaseJsonRpcProcessor implements JsonRpcProcessor { + + private static final Logger logger = LoggerFactory.getLogger(BaseJsonRpcProcessor.class); + + /** BaseJsonRpcProcessor constructor. */ + public BaseJsonRpcProcessor() {} + + @Override + public JsonRpcResponse process(JsonRpcMethod method, JsonRpcRequestContext request) { + try { + return method.response(request); + } catch (final RuntimeException e) { + final JsonArray params = JsonObject.mapFrom(request.getRequest()).getJsonArray("params"); + logger.error(String.format("Error processing method: %s %s", method.getName(), params), e); + return new JsonRpcErrorResponse(request.getRequest().getId(), JsonRpcError.INTERNAL_ERROR); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcExecutor.java b/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcExecutor.java new file mode 100644 index 00000000..065150a8 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcExecutor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.execution; + +import static io.optimism.rpc.internal.response.JsonRpcError.INVALID_REQUEST; + +import io.optimism.rpc.RpcMethod; +import io.optimism.rpc.internal.JsonRpcRequest; +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.JsonRpcRequestId; +import io.optimism.rpc.internal.response.JsonRpcError; +import io.optimism.rpc.internal.response.JsonRpcErrorResponse; +import io.optimism.rpc.internal.response.JsonRpcNoResponse; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.methods.JsonRpcMethod; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** base on besu. */ +public class JsonRpcExecutor { + + private static final Logger LOG = LoggerFactory.getLogger(JsonRpcExecutor.class); + + private final JsonRpcProcessor rpcProcessor; + private final Map rpcMethods; + + public JsonRpcExecutor( + final JsonRpcProcessor rpcProcessor, final Map rpcMethods) { + this.rpcProcessor = rpcProcessor; + this.rpcMethods = rpcMethods; + } + + public JsonRpcResponse execute(JsonRpcRequestContext context) { + JsonRpcRequest requestBody = context.getRequest(); + try { + final JsonRpcRequestId id = new JsonRpcRequestId(requestBody.getId()); + // Handle notifications + if (requestBody.isNotification()) { + // Notifications aren't handled so create empty result for now. + return new JsonRpcNoResponse(); + } + + final Optional unavailableMethod = validateMethodAvailability(requestBody); + if (unavailableMethod.isPresent()) { + return new JsonRpcErrorResponse(id, unavailableMethod.get()); + } + + final JsonRpcMethod method = rpcMethods.get(requestBody.getMethod()); + return rpcProcessor.process(method, context); + } catch (final IllegalArgumentException e) { + try { + return new JsonRpcErrorResponse(requestBody.getId(), INVALID_REQUEST); + } catch (final ClassCastException idNotIntegerException) { + return new JsonRpcErrorResponse(null, INVALID_REQUEST); + } + } + } + + private Optional validateMethodAvailability(final JsonRpcRequest request) { + final String name = request.getMethod(); + + if (LOG.isDebugEnabled()) { + final JsonArray params = JsonObject.mapFrom(request).getJsonArray("params"); + LOG.debug("JSON-RPC request -> {} {}", name, params); + } + + final JsonRpcMethod method = rpcMethods.get(name); + + if (method == null) { + if (!RpcMethod.rpcMethodExists(name)) { + return Optional.of(JsonRpcError.METHOD_NOT_FOUND); + } + if (!rpcMethods.containsKey(name)) { + return Optional.of(JsonRpcError.METHOD_NOT_ENABLED); + } + } + + return Optional.empty(); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcProcessor.java b/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcProcessor.java new file mode 100644 index 00000000..1a29a0fd --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/execution/JsonRpcProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.execution; + +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.methods.JsonRpcMethod; + +/** + * JsonRpcProcessor interface. + * + * @author thinkAfCod + * @since 2023.06 + */ +public interface JsonRpcProcessor { + + /** + * process jsonRpcMethod with JsonRpcRequestContext + * + * @param method JsonRpcMethod instant + * @param request JsonRpcRequestContext instant + * @return json rpc process response + */ + JsonRpcResponse process(final JsonRpcMethod method, final JsonRpcRequestContext request); +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/execution/LoggedJsonRpcProcessor.java b/hildr-node/src/main/java/io/optimism/rpc/execution/LoggedJsonRpcProcessor.java new file mode 100644 index 00000000..ecb6c935 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/execution/LoggedJsonRpcProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.execution; + +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcErrorResponse; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.internal.response.JsonRpcResponseType; +import io.optimism.rpc.methods.JsonRpcMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LoggedJsonRpcProcessor. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class LoggedJsonRpcProcessor implements JsonRpcProcessor { + + private static final Logger logger = LoggerFactory.getLogger(LoggedJsonRpcProcessor.class); + + private final JsonRpcProcessor rpcProcessor; + + public LoggedJsonRpcProcessor(final JsonRpcProcessor rpcProcessor) { + this.rpcProcessor = rpcProcessor; + } + + @Override + public JsonRpcResponse process(final JsonRpcMethod method, final JsonRpcRequestContext context) { + JsonRpcResponse jsonRpcResponse = rpcProcessor.process(method, context); + if (JsonRpcResponseType.ERROR == jsonRpcResponse.getType()) { + JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) jsonRpcResponse; + switch (errorResponse.getError()) { + case INVALID_PARAMS -> logger.info("jsonrpc has error: {}", "Invalid Params"); + case UNAUTHORIZED -> logger.info("jsonrpc has error: {}", "Unauthorized"); + case INTERNAL_ERROR -> logger.info( + "jsonrpc has error: {}", "Error processing JSON-RPC requestBody"); + default -> logger.info("jsonrpc has error: {}", "Unexpected error"); + } + } + return jsonRpcResponse; + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcExecutorHandler.java b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcExecutorHandler.java new file mode 100644 index 00000000..fab93c65 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcExecutorHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.handler; + +import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.optimism.rpc.execution.JsonRpcExecutor; +import io.optimism.rpc.execution.JsonRpcProcessor; +import io.optimism.rpc.internal.JsonRpcRequest; +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcError; +import io.optimism.rpc.internal.response.JsonRpcErrorResponse; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.internal.response.JsonRpcResponseType; +import io.optimism.rpc.methods.JsonRpcMethod; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import java.io.IOException; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * base on besu. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class JsonRpcExecutorHandler { + + private static final Logger LOG = LoggerFactory.getLogger(JsonRpcExecutorHandler.class); + + private static final ObjectMapper mapper = new ObjectMapper(); + + private JsonRpcExecutorHandler() {} + + public static Handler handler( + JsonRpcProcessor processor, Map methods) { + final JsonRpcExecutor jsonRpcExecutor = new JsonRpcExecutor(processor, methods); + return ctx -> { + try { + if (!isJsonObjectRequest(ctx)) { + handleJsonRpcError(ctx, null, JsonRpcError.PARSE_ERROR); + return; + } + JsonObject req = ctx.get("REQUEST_BODY_AS_JSON_OBJECT"); + try { + JsonRpcRequest jsonRpcRequest = req.mapTo(JsonRpcRequest.class); + JsonRpcResponse jsonRpcResponse = + jsonRpcExecutor.execute(new JsonRpcRequestContext(ctx, jsonRpcRequest)); + + HttpServerResponse response = ctx.response(); + response = response.putHeader("Content-Type", APPLICATION_JSON); + handleJsonObjectResponse(response, jsonRpcResponse); + } catch (IOException e) { + final String method = req.getString("method"); + LOG.error("{} - Error streaming JSON-RPC response", method, e); + throw new RuntimeException(e); + } + } catch (final RuntimeException e) { + handleJsonRpcError(ctx, null, JsonRpcError.INTERNAL_ERROR); + } + }; + } + + private static boolean isJsonObjectRequest(final RoutingContext ctx) { + return ctx.data().containsKey("REQUEST_BODY_AS_JSON_OBJECT"); + } + + private static void handleJsonRpcError( + final RoutingContext routingContext, final Object id, final JsonRpcError error) { + final HttpServerResponse response = routingContext.response(); + if (!response.closed()) { + response + .setStatusCode(statusCodeFromError(error).code()) + .end(Json.encode(new JsonRpcErrorResponse(id, error))); + } + } + + private static HttpResponseStatus statusCodeFromError(final JsonRpcError error) { + return switch (error) { + case INVALID_REQUEST, PARSE_ERROR -> HttpResponseStatus.BAD_REQUEST; + default -> HttpResponseStatus.OK; + }; + } + + private static HttpResponseStatus status(final JsonRpcResponse response) { + return switch (response.getType()) { + case UNAUTHORIZED -> HttpResponseStatus.UNAUTHORIZED; + case ERROR -> statusCodeFromError(((JsonRpcErrorResponse) response).getError()); + default -> HttpResponseStatus.OK; + }; + } + + private static void handleJsonObjectResponse( + final HttpServerResponse response, final JsonRpcResponse jsonRpcResponse) throws IOException { + + response.setStatusCode(status(jsonRpcResponse).code()); + if (jsonRpcResponse.getType() == JsonRpcResponseType.NONE) { + response.end(); + } else { + response.end(mapper.writeValueAsString(jsonRpcResponse)); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java new file mode 100644 index 00000000..6af0592a --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.handler; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.optimism.rpc.internal.response.JsonRpcError; +import io.optimism.rpc.internal.response.JsonRpcErrorResponse; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.ext.web.RoutingContext; + +/** + * copied from besu(). + * + * @author thinkAfCod + * @since 2023.06 + */ +public class JsonRpcParseHandler { + + private JsonRpcParseHandler() {} + + public static Handler handler() { + return ctx -> { + final HttpServerResponse response = ctx.response(); + if (ctx.getBody() == null) { + errorResponse(response, JsonRpcError.PARSE_ERROR); + } else { + try { + ctx.put("REQUEST_BODY_AS_JSON_OBJECT", ctx.getBodyAsJson()); + } catch (DecodeException | ClassCastException jsonObjectDecodeException) { + errorResponse(response, JsonRpcError.PARSE_ERROR); + } + ctx.next(); + } + }; + } + + private static void errorResponse( + final HttpServerResponse response, final JsonRpcError rpcError) { + if (!response.closed()) { + response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(Json.encode(new JsonRpcErrorResponse(null, rpcError))); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/handler/TimeoutHandler.java b/hildr-node/src/main/java/io/optimism/rpc/handler/TimeoutHandler.java new file mode 100644 index 00000000..a2c48a5d --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/handler/TimeoutHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.handler; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * Timeout handler. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class TimeoutHandler { + + private static final int DEFAULT_ERROR_CODE = 504; + + private static final long DEFAULT_TIMEOUT_SECONDS = Duration.ofMinutes(5).toSeconds(); + + private TimeoutHandler() {} + + /** + * create TimeoutHandler. + * + * @param timeoutSeconds time duration. + * @param errorCode timeout error code + * @return vertx handler. + */ + public static Handler handler(int timeoutSeconds, int errorCode) { + final long timeoutDuration = + timeoutSeconds <= 0 + ? DEFAULT_TIMEOUT_SECONDS + : Duration.ofSeconds(timeoutSeconds).toSeconds(); + final var finalErrorCode = errorCode == 0 ? DEFAULT_ERROR_CODE : errorCode; + return ctx -> processHandler(ctx, timeoutDuration, finalErrorCode); + } + + /** + * create TimeoutHandler use default timeout value and default error code. + * + * @return vertx handler. + */ + public static Handler handler() { + return TimeoutHandler.handler(0, 0); + } + + private static void processHandler( + final RoutingContext ctx, final long timeoutDuration, final int errorCode) { + try { + long tid = + ctx.vertx() + .setTimer( + TimeUnit.SECONDS.toMillis(timeoutDuration), + t -> { + ctx.fail(errorCode); + ctx.response().close(); + }); + ctx.addBodyEndHandler(v -> ctx.vertx().cancelTimer(tid)); + } finally { + ctx.next(); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java new file mode 100644 index 00000000..234ae02a --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java @@ -0,0 +1,175 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.Objects; + +/** + * copied from project besu(https://github.com/hyperledger/besu). + * + * @author thinkAfCod + * @since 2023.06 + */ +public class JsonRpcRequest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private JsonRpcRequestId id; + private final String method; + private final Object[] params; + private final String version; + private boolean isNotification = true; + + @JsonCreator + public JsonRpcRequest( + @JsonProperty("jsonrpc") final String version, + @JsonProperty("method") final String method, + @JsonProperty("params") final Object[] params) { + this.version = version; + this.method = method; + this.params = params; + if (method == null) { + throw new RuntimeException("Field 'method' is required"); + } + } + + @JsonGetter("id") + public Object getId() { + return id == null ? null : id.getValue(); + } + + @JsonGetter("method") + public String getMethod() { + return method; + } + + @JsonGetter("jsonrpc") + public String getVersion() { + return version; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonGetter("params") + public Object[] getParams() { + return params; + } + + @JsonIgnore + public boolean isNotification() { + return isNotification; + } + + @JsonIgnore + public int getParamLength() { + return hasParams() ? params.length : 0; + } + + @JsonIgnore + public boolean hasParams() { + + // Null Object: "params":null + if (params == null) { + return false; + } + + // Null Array: "params":[null] + if (params.length == 0 || params[0] == null) { + return false; + } + + return true; + } + + @JsonSetter("id") + protected void setId(final JsonRpcRequestId id) { + // If an id is explicitly set, it is not a notification + isNotification = false; + this.id = id; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof JsonRpcRequest)) { + return false; + } + final JsonRpcRequest that = (JsonRpcRequest) o; + return isNotification == that.isNotification + && Objects.equals(id, that.id) + && Objects.equals(method, that.method) + && Arrays.equals(params, that.params) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, method, Arrays.hashCode(params), version, isNotification); + } + + public T getParameter(final int index, final Class paramClass) { + if (params == null || params.length <= index || params[index] == null) { + return null; + } + + final T param; + final Object rawParam = params[index]; + if (paramClass.isAssignableFrom(rawParam.getClass())) { + param = (T) rawParam; + } else { + try { + final String json = mapper.writeValueAsString(rawParam); + param = mapper.readValue(json, paramClass); + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException( + String.format( + "Invalid json rpc parameter at index %d. Supplied value was: '%s' of type: '%s' - expected type: '%s'", + index, rawParam, rawParam.getClass().getName(), paramClass.getName()), + e); + } + } + + return param; + } + + @Override + public String toString() { + return "JsonRpcRequest{" + + "id=" + + id + + ", method='" + + method + + '\'' + + ", params=" + + Arrays.toString(params) + + ", version='" + + version + + '\'' + + ", isNotification=" + + isNotification + + '}'; + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestContext.java b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestContext.java new file mode 100644 index 00000000..ff86fc83 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestContext.java @@ -0,0 +1,63 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal; + +import io.vertx.ext.web.RoutingContext; +import java.util.Objects; + +/** + * json rpc request context. + * + * @author thinkAfCod + * @since 2023.06 + */ +@SuppressWarnings("UnusedVariable") +public class JsonRpcRequestContext { + + private final RoutingContext context; + + private final JsonRpcRequest jsonRpcRequest; + + public JsonRpcRequestContext(final RoutingContext context, final JsonRpcRequest jsonRpcRequest) { + this.context = context; + this.jsonRpcRequest = jsonRpcRequest; + } + + public JsonRpcRequest getRequest() { + return jsonRpcRequest; + } + + public T getParameter(final int index, final Class paramClass) { + return jsonRpcRequest.getParameter(index, paramClass); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || (o instanceof JsonRpcRequestContext)) { + return false; + } + final JsonRpcRequestContext that = (JsonRpcRequestContext) o; + return Objects.equals(jsonRpcRequest, that.jsonRpcRequest); + } + + @Override + public int hashCode() { + return Objects.hash(jsonRpcRequest); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestId.java b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestId.java new file mode 100644 index 00000000..bdc0ab50 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequestId.java @@ -0,0 +1,89 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.math.BigInteger; +import java.util.Objects; + +/** + * Json rpc request id. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class JsonRpcRequestId { + + private static final Class[] VALID_ID_TYPES = + new Class[] { + String.class, Integer.class, Long.class, Float.class, Double.class, BigInteger.class + }; + + private final Object id; + + @JsonCreator + public JsonRpcRequestId(final Object id) { + if (isRequestTypeInvalid(id)) { + throw new RuntimeException("Invalid id"); + } + this.id = id; + } + + @JsonValue + public Object getValue() { + return id; + } + + private boolean isRequestTypeInvalid(final Object id) { + return isNotNull(id) && isTypeInvalid(id); + } + + /** + * The JSON spec says "The use of Null as a value for the id member in a Request object is + * discouraged" Both geth and parity accept null values, so we decided to support them as well. + */ + private boolean isNotNull(final Object id) { + return id != null; + } + + private boolean isTypeInvalid(final Object id) { + for (final Class validType : VALID_ID_TYPES) { + if (validType.isInstance(id)) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof JsonRpcRequestId)) { + return false; + } + final JsonRpcRequestId that = (JsonRpcRequestId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcError.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcError.java new file mode 100644 index 00000000..f632175a --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcError.java @@ -0,0 +1,99 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +@SuppressWarnings("ImmutableEnumChecker") +public enum JsonRpcError { + // Standard errors + 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"), + TIMEOUT_ERROR(-32603, "Timeout expired"), + + UNAUTHORIZED(-40100, "Unauthorized"), + METHOD_NOT_ENABLED(-32604, "Method not enabled"); + + private final int code; + private final String message; + private String data; + + JsonRpcError(final int code, final String message, final String data) { + this.code = code; + this.message = message; + this.data = data; + } + + JsonRpcError(final int code, final String message) { + this(code, message, null); + } + + /** + * get error code. + * + * @return error code + */ + @JsonGetter("code") + public int getCode() { + return code; + } + + /** + * get error message + * + * @return error message + */ + @JsonGetter("message") + public String getMessage() { + return message; + } + + /** + * get error data + * + * @return error data + */ + @JsonGetter("data") + public String getData() { + return data; + } + + public void setData(final String data) { + this.data = data; + } + + @JsonCreator + public static JsonRpcError fromJson( + @JsonProperty("code") final int code, + @JsonProperty("message") final String message, + @JsonProperty("data") final String data) { + for (final JsonRpcError error : JsonRpcError.values()) { + if (error.code == code && error.message.equals(message) && error.data.equals(data)) { + return error; + } + } + return null; + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcErrorResponse.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcErrorResponse.java new file mode 100644 index 00000000..5864c427 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcErrorResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.google.common.base.MoreObjects; +import java.util.Objects; + +@JsonPropertyOrder({"jsonrpc", "id", "error"}) +public class JsonRpcErrorResponse implements JsonRpcResponse { + + private final Object id; + private final JsonRpcError error; + + public JsonRpcErrorResponse(final Object id, final JsonRpcError error) { + this.id = id; + this.error = error; + } + + @JsonGetter("id") + public Object getId() { + return id; + } + + @JsonGetter("error") + public JsonRpcError getError() { + return error; + } + + @Override + @JsonIgnore + public JsonRpcResponseType getType() { + return JsonRpcResponseType.ERROR; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof JsonRpcErrorResponse)) { + return false; + } + final JsonRpcErrorResponse that = (JsonRpcErrorResponse) o; + return Objects.equals(id, that.id) && error == that.error; + } + + @Override + public int hashCode() { + return Objects.hash(id, error); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("id", id).add("error", error).toString(); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcNoResponse.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcNoResponse.java new file mode 100644 index 00000000..57eed720 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcNoResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +public class JsonRpcNoResponse implements JsonRpcResponse { + + @Override + public JsonRpcResponseType getType() { + return JsonRpcResponseType.NONE; + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponse.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponse.java new file mode 100644 index 00000000..7876be02 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public interface JsonRpcResponse { + + @JsonGetter("jsonrpc") + default String getVersion() { + return "2.0"; + } + + JsonRpcResponseType getType(); +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponseType.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponseType.java new file mode 100644 index 00000000..1901f50b --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcResponseType.java @@ -0,0 +1,24 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +/** Various types of responses that the JSON-RPC component may produce. */ +public enum JsonRpcResponseType { + NONE, + SUCCESS, + ERROR, + UNAUTHORIZED +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcSuccessResponse.java b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcSuccessResponse.java new file mode 100644 index 00000000..cb287c4c --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/response/JsonRpcSuccessResponse.java @@ -0,0 +1,79 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.Objects; + +/** + * json rpc success response. Copied from besu. + * + * @author thinkAfCod + * @since 2023.06 + */ +@JsonPropertyOrder({"jsonrpc", "id", "result"}) +public class JsonRpcSuccessResponse implements JsonRpcResponse { + + public static final String SUCCESS_RESULT = "Success"; + + private final Object id; + private final Object result; + + public JsonRpcSuccessResponse(final Object id, final Object result) { + this.id = id; + this.result = result; + } + + public JsonRpcSuccessResponse(final Object id) { + this.id = id; + this.result = SUCCESS_RESULT; + } + + @JsonGetter("id") + public Object getId() { + return id; + } + + @JsonGetter("result") + public Object getResult() { + return result; + } + + @Override + @JsonIgnore + public JsonRpcResponseType getType() { + return JsonRpcResponseType.SUCCESS; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof JsonRpcSuccessResponse)) { + return false; + } + final JsonRpcSuccessResponse that = (JsonRpcSuccessResponse) o; + return Objects.equals(id, that.id) && Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(id, result); + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/result/EthGetProof.java b/hildr-node/src/main/java/io/optimism/rpc/internal/result/EthGetProof.java new file mode 100644 index 00000000..512ff188 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/result/EthGetProof.java @@ -0,0 +1,329 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.internal.result; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.io.IOException; +import java.math.BigInteger; +import java.util.List; +import org.web3j.protocol.ObjectMapperFactory; +import org.web3j.protocol.core.Response; +import org.web3j.utils.Numeric; + +/** + * eth_getProof api response. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class EthGetProof extends Response { + + @Override + @JsonDeserialize(using = EthGetProof.ResponseDeserializer.class) + public void setResult(EthGetProof.Proof result) { + super.setResult(result); + } + + /** + * get proof result. + * + * @return proof result + */ + public EthGetProof.Proof getProof() { + return getResult(); + } + + /** eth_getProof response. */ + public EthGetProof() {} + + /** json rpc result of object */ + public static class Proof { + + private String address; + + private String balance; + + private String codeHash; + + private String nonce; + + private String storageHash; + + private List accountProof; + + private List storageProof; + + public Proof() {} + + public Proof( + String address, + String balance, + String codeHash, + String nonce, + String storageHash, + List accountProof, + List storageProof) { + this.address = address; + this.balance = balance; + this.codeHash = codeHash; + this.nonce = nonce; + this.storageHash = storageHash; + this.accountProof = accountProof; + this.storageProof = storageProof; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getBalanceRaw() { + return this.balance; + } + + public BigInteger getBalance() { + return Numeric.decodeQuantity(balance); + } + + public void setBalance(String balance) { + this.balance = balance; + } + + public String getCodeHash() { + return codeHash; + } + + public void setCodeHash(String codeHash) { + this.codeHash = codeHash; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getStorageHash() { + return storageHash; + } + + public void setStorageHash(String storageHash) { + this.storageHash = storageHash; + } + + public List getAccountProof() { + return accountProof; + } + + public void setAccountProof(List accountProof) { + this.accountProof = accountProof; + } + + public List getStorageProof() { + return storageProof; + } + + public void setStorageProof(List storageProof) { + this.storageProof = storageProof; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EthGetProof.Proof)) { + return false; + } + EthGetProof.Proof proof = (EthGetProof.Proof) o; + + if (getAddress() != null + ? !getAddress().equals(proof.getAddress()) + : proof.getAddress() != null) { + return false; + } + + if (getBalanceRaw() != null + ? !getBalanceRaw().equals(proof.getBalanceRaw()) + : proof.getBalanceRaw() != null) { + return false; + } + + if (getCodeHash() != null + ? !getCodeHash().equals(proof.getCodeHash()) + : proof.getCodeHash() != null) { + return false; + } + if (getNonce() != null ? !getNonce().equals(proof.getNonce()) : proof.getNonce() != null) { + return false; + } + + if (getStorageHash() != null + ? !getStorageHash().equals(proof.getStorageHash()) + : proof.getStorageHash() != null) { + return false; + } + + if (getAccountProof() != null + ? !getAccountProof().equals(proof.getAccountProof()) + : proof.getAccountProof() != null) { + return false; + } + + return getStorageProof() != null + ? !getStorageProof().equals(proof.getStorageProof()) + : proof.getStorageProof() != null; + } + + @Override + public int hashCode() { + int result = getAddress() != null ? getAddress().hashCode() : 0; + result = 31 * result + (getBalanceRaw() != null ? getBalanceRaw().hashCode() : 0); + result = 31 * result + (getCodeHash() != null ? getCodeHash().hashCode() : 0); + result = 31 * result + (getNonce() != null ? getNonce().hashCode() : 0); + result = 31 * result + (getStorageHash() != null ? getStorageHash().hashCode() : 0); + result = 31 * result + (getAccountProof() != null ? getAccountProof().hashCode() : 0); + result = 31 * result + (getStorageProof() != null ? getStorageProof().hashCode() : 0); + return result; + } + } + + /** storage proof. */ + public static class StorageProof { + private String key; + + private String value; + + private List proof; + + /** Storage proof. */ + public StorageProof() {} + + public StorageProof(String key, String value, List proof) { + this.key = key; + this.value = value; + this.proof = proof; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getProof() { + return proof; + } + + public void setProof(List proof) { + this.proof = proof; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StorageProof)) { + return false; + } + + StorageProof proof = (EthGetProof.StorageProof) o; + + if (getKey() != null ? !getKey().equals(proof.getKey()) : proof.getKey() != null) { + return false; + } + if (getValue() != null ? !getValue().equals(proof.getValue()) : proof.getValue() != null) { + return false; + } + return getProof() != null ? getProof().equals(proof.getProof()) : proof.getProof() == null; + } + + @Override + public int hashCode() { + int result = getKey() != null ? getKey().hashCode() : 0; + result = 31 * result + (getValue() != null ? getValue().hashCode() : 0); + result = 31 * result + (getProof() != null ? getProof().hashCode() : 0); + return result; + } + } + + /** Json Deserializer of Proof. */ + public static class ResponseDeserializer extends JsonDeserializer { + + private ObjectReader objectReader = ObjectMapperFactory.getObjectReader(); + + @Override + public EthGetProof.Proof deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + if (jsonParser.getCurrentToken() != JsonToken.VALUE_NULL) { + return objectReader.readValue(jsonParser, EthGetProof.Proof.class); + } else { + return null; // null is wrapped by Optional in above getter + } + } + } + + // { + // "id": 1, + // "jsonrpc": "2.0", + // "result": { + // "accountProof": [ + // "0xf90211a...0701bc80", + // "0xf90211a...0d832380", + // "0xf90211a...5fb20c80", + // "0xf90211a...0675b80", + // "0xf90151a0...ca08080" + // ], + // "balance": "0x0", + // "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + // "nonce": "0x0", + // "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + // "storageProof": [ + // { + // "key": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + // "proof": [ + // "0xf90211a...0701bc80", + // "0xf90211a...0d832380" + // ], + // "value": "0x1" + // } + // ] + // } + // } + +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/result/OutputRootResult.java b/hildr-node/src/main/java/io/optimism/rpc/internal/result/OutputRootResult.java new file mode 100644 index 00000000..c63c9603 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/result/OutputRootResult.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.internal.result; + +/** + * output root result. + * + * @param outputRoot output root + * @param version version + * @param stateRoot state root + * @param withdrawalStorageRoot withdrawal storage root + * @author thinkAfCod + * @since 2023.06 + */ +public record OutputRootResult( + String outputRoot, String version, String stateRoot, String withdrawalStorageRoot) {} diff --git a/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java new file mode 100644 index 00000000..5ad58c4e --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java @@ -0,0 +1,44 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.methods; + +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcResponse; + +/** + * jsonrpc handle method interface. + * base on besu + * + * @author thinkAfCod + * @since 2023.06 + */ +public interface JsonRpcMethod { + + /** + * Standardized JSON-RPC method name. + * + * @return identification of the JSON-RPC method. + */ + String getName(); + + /** + * Applies the method to given request. + * + * @param request input data for the JSON-RPC method. + * @return output from applying the JSON-RPC method to the input. + */ + JsonRpcResponse response(JsonRpcRequestContext request); +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethodsFactory.java b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethodsFactory.java new file mode 100644 index 00000000..3005f606 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethodsFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.optimism.rpc.methods; + +import io.optimism.config.Config; +import java.util.HashMap; +import java.util.Map; + +/** + * JsonRpc method factory. copied from besu. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class JsonRpcMethodsFactory { + + /** JsonRpcMethodsFactory constructor. */ + public JsonRpcMethodsFactory() {} + + public Map methods(Config config) { + final Map methods = new HashMap<>(); + JsonRpcMethod outputAtBlock = + new OutputAtBlock(config.l2RpcUrl(), config.chainConfig().l2Tol1MessagePasser()); + + methods.put(outputAtBlock.getName(), outputAtBlock); + + return methods; + } +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java b/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java new file mode 100644 index 00000000..703846f9 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.methods; + +import io.optimism.rpc.RpcMethod; +import io.optimism.rpc.internal.JsonRpcRequestContext; +import io.optimism.rpc.internal.response.JsonRpcResponse; +import io.optimism.rpc.internal.response.JsonRpcSuccessResponse; +import io.optimism.rpc.internal.result.EthGetProof; +import io.optimism.rpc.internal.result.OutputRootResult; +import io.optimism.rpc.provider.Web3jProvider; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import jdk.incubator.concurrent.StructuredTaskScope; +import org.apache.commons.lang3.ArrayUtils; +import org.bouncycastle.jcajce.provider.digest.Keccak; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.Web3jService; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthBlock; +import org.web3j.tuples.generated.Tuple2; +import org.web3j.utils.Numeric; + +/** + * jsonRpc api that get output at block. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class OutputAtBlock implements JsonRpcMethod { + + private static final String ETH_GET_PROOF = "eth_getProof"; + + private final String l2ToL1MessagePasser; + + private Web3j client; + + private Web3jService service; + + public OutputAtBlock(final String l2RpcUrl, final String l2ToL1MessagePasser) { + Tuple2 tuple = Web3jProvider.create(l2RpcUrl); + this.client = tuple.component1(); + this.service = tuple.component2(); + this.l2ToL1MessagePasser = l2ToL1MessagePasser; + } + + @Override + public String getName() { + return RpcMethod.OP_OUTPUT_AT_BLOCK.getRpcMethodName(); + } + + @Override + public JsonRpcResponse response(JsonRpcRequestContext context) { + final BigInteger blockNumber = new BigInteger(context.getParameter(0, String.class), 10); + + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + Future ethBlockFuture = + scope.fork( + () -> + client + .ethGetBlockByNumber(DefaultBlockParameter.valueOf(blockNumber), true) + .send()); + Future ehtGetProofFuture = + scope.fork( + () -> { + return new Request<>( + ETH_GET_PROOF, + Arrays.asList( + this.l2ToL1MessagePasser, + Collections.emptyList(), + DefaultBlockParameter.valueOf(blockNumber)), + this.service, + EthGetProof.class) + .send(); + }); + scope.join(); + scope.throwIfFailed(); + EthBlock.Block block = ethBlockFuture.resultNow().getBlock(); + String stateRoot = block.getStateRoot(); + + EthGetProof.Proof stateProof = ehtGetProofFuture.resultNow().getProof(); + String withdrawalStorageRoot = stateProof.getStorageHash(); + var outputRoot = computeL2OutputRoot(block, withdrawalStorageRoot); + var version = new byte[32]; + var result = + new OutputRootResult( + outputRoot, Numeric.toHexString(version), stateRoot, withdrawalStorageRoot); + return new JsonRpcSuccessResponse(context.getRequest().getId(), result); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private String computeL2OutputRoot(EthBlock.Block block, String storageRoot) { + var version = new byte[32]; + + var digest = new Keccak.Digest256(); + byte[] digestBytes = null; + digestBytes = ArrayUtils.addAll(digestBytes, version); + digestBytes = + ArrayUtils.addAll(digestBytes, Numeric.hexStringToByteArray(block.getStateRoot())); + digestBytes = ArrayUtils.addAll(digestBytes, Numeric.hexStringToByteArray(storageRoot)); + digestBytes = ArrayUtils.addAll(digestBytes, Numeric.hexStringToByteArray(block.getHash())); + + byte[] hash = digest.digest(digestBytes); + return Numeric.toHexString(hash); + } +} diff --git a/hildr-node/src/main/java/io/optimism/l1/RetryRateLimitInterceptor.java b/hildr-node/src/main/java/io/optimism/rpc/provider/RetryRateLimitInterceptor.java similarity index 97% rename from hildr-node/src/main/java/io/optimism/l1/RetryRateLimitInterceptor.java rename to hildr-node/src/main/java/io/optimism/rpc/provider/RetryRateLimitInterceptor.java index 14712cdc..16a6420c 100644 --- a/hildr-node/src/main/java/io/optimism/l1/RetryRateLimitInterceptor.java +++ b/hildr-node/src/main/java/io/optimism/rpc/provider/RetryRateLimitInterceptor.java @@ -14,7 +14,7 @@ * specific language governing permissions and limitations under the License. */ -package io.optimism.l1; +package io.optimism.rpc.provider; import com.github.rholder.retry.RetryException; import com.github.rholder.retry.Retryer; @@ -22,6 +22,7 @@ import com.github.rholder.retry.StopStrategies; import com.github.rholder.retry.WaitStrategies; import com.google.common.util.concurrent.RateLimiter; +import io.optimism.l1.InnerWatcher; import java.io.IOException; import java.time.Duration; import java.util.concurrent.ExecutionException; diff --git a/hildr-node/src/main/java/io/optimism/rpc/provider/Web3jProvider.java b/hildr-node/src/main/java/io/optimism/rpc/provider/Web3jProvider.java new file mode 100644 index 00000000..7af89145 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/rpc/provider/Web3jProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.rpc.provider; + +import okhttp3.OkHttpClient; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.Web3jService; +import org.web3j.protocol.http.HttpService; +import org.web3j.tuples.generated.Tuple2; + +/** + * Web3j client provider. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class Web3jProvider { + + private Web3jProvider() {} + + /** + * create web3j client. + * + * @param url ethereum/optimism client node url + * @return web3j client + */ + public static Web3j createClient(String url) { + OkHttpClient okHttpClient = + new OkHttpClient.Builder().addInterceptor(new RetryRateLimitInterceptor()).build(); + return Web3j.build(new HttpService(url, okHttpClient)); + } + + /** + * create web3j client. + * + * @param url ethereum/optimism client node url + * @return web3j client and web3j service + */ + public static Tuple2 create(String url) { + OkHttpClient okHttpClient = + new OkHttpClient.Builder().addInterceptor(new RetryRateLimitInterceptor()).build(); + Web3jService web3jService = new HttpService(url, okHttpClient); + return new Tuple2<>(Web3j.build(web3jService), web3jService); + } +} diff --git a/hildr-node/src/main/java/io/optimism/telemetry/InnerMetrics.java b/hildr-node/src/main/java/io/optimism/telemetry/InnerMetrics.java index 0a9ed24f..7341abc5 100644 --- a/hildr-node/src/main/java/io/optimism/telemetry/InnerMetrics.java +++ b/hildr-node/src/main/java/io/optimism/telemetry/InnerMetrics.java @@ -108,7 +108,7 @@ public static void stop() { * @param finalizedHead finalized head block */ public static void setFinalizedHead(BigInteger finalizedHead) { - FINALIZED_HEAD.compareAndSet(FINALIZED_HEAD.get(), finalizedHead); + FINALIZED_HEAD.getAndSet(finalizedHead); } /** @@ -117,7 +117,7 @@ public static void setFinalizedHead(BigInteger finalizedHead) { * @param safeHead safe head block */ public static void setSafeHead(BigInteger safeHead) { - SAFE_HEAD.compareAndSet(SAFE_HEAD.get(), safeHead); + SAFE_HEAD.getAndSet(safeHead); } /** @@ -126,6 +126,6 @@ public static void setSafeHead(BigInteger safeHead) { * @param synced synced block count */ public static void setSynced(BigInteger synced) { - SYNCED.compareAndSet(SYNCED.get(), synced); + SYNCED.getAndSet(synced); } } diff --git a/hildr-node/src/main/java/io/optimism/telemetry/Logging.java b/hildr-node/src/main/java/io/optimism/telemetry/Logging.java index 4d327adb..ecc942ec 100644 --- a/hildr-node/src/main/java/io/optimism/telemetry/Logging.java +++ b/hildr-node/src/main/java/io/optimism/telemetry/Logging.java @@ -41,27 +41,19 @@ public enum Logging { /** Logging single instance. */ INSTANCE; - private final Tracer tracer; + private final Slf4JEventListener slf4JEventListener; + private final Slf4JBaggageEventListener slf4JBaggageEventListener; @SuppressWarnings("AbbreviationAsWordInName") Logging() { initializeOpenTelemetry(); - final var slf4JEventListener = new Slf4JEventListener(); - final var slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList()); + this.slf4JEventListener = new Slf4JEventListener(); + this.slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList()); + } - var otelTracer = GlobalOpenTelemetry.getTracer("global"); - OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); - this.tracer = - new OtelTracer( - otelTracer, - otelCurrentTraceContext, - event -> { - slf4JEventListener.onEvent(event); - slf4JBaggageEventListener.onEvent(event); - }, - new OtelBaggageManager( - otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList())); + public Tracer getTracer() { + return this.getTracer(Thread.currentThread().getName()); } /** @@ -69,8 +61,18 @@ public enum Logging { * * @return Tracer single instance */ - public Tracer getTracer() { - return tracer; + public Tracer getTracer(String tracerName) { + var otelTracer = GlobalOpenTelemetry.getTracer(tracerName); + OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); + return new OtelTracer( + otelTracer, + otelCurrentTraceContext, + event -> { + slf4JEventListener.onEvent(event); + slf4JBaggageEventListener.onEvent(event); + }, + new OtelBaggageManager( + otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList())); } private static void initializeOpenTelemetry() { diff --git a/hildr-node/src/test/java/io/optimism/telemetry/LoggingExampleTest.java b/hildr-node/src/test/java/io/optimism/telemetry/LoggingExampleTest.java index e590e1b9..7c58ee42 100644 --- a/hildr-node/src/test/java/io/optimism/telemetry/LoggingExampleTest.java +++ b/hildr-node/src/test/java/io/optimism/telemetry/LoggingExampleTest.java @@ -41,10 +41,10 @@ void testLogging() throws InterruptedException { Thread thread = new Thread( () -> { - Tracer tracer = Logging.INSTANCE.getTracer(); + Tracer tracer = Logging.INSTANCE.getTracer("unit-test-case"); Span span = tracer.nextSpan().name("my-span").start(); logger.info("step 1:parent {} log", logId); - try (var unusedScope1 = Logging.INSTANCE.getTracer().withSpan(span)) { + try (var unusedScope1 = tracer.withSpan(span)) { logger.info("step 2:parent {} log", logId); Span childSpan = tracer.nextSpan().name("childSpan").start(); try (var unusedBag2 = tracer.withSpan(childSpan)) { From 8253c3a2c5769e07dded9174c4b45db8b3025bde Mon Sep 17 00:00:00 2001 From: thinkAfCod Date: Tue, 6 Jun 2023 12:31:20 +0800 Subject: [PATCH 2/3] feat: native dockerfile and scripts --- docker/Dockerfile | 2 +- docker/binary.dock | 34 ++++++++++++++++++ docker/start-hildr-node-java.sh | 28 +++++++++++++++ docker/start-hildr-node.sh | 4 +-- hildr-node/build.gradle | 36 +++++++++++++++---- .../optimism/rpc/methods/JsonRpcMethod.java | 3 +- 6 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 docker/binary.dock create mode 100755 docker/start-hildr-node-java.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 8283e293..688758a0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 debian:bullseye-slim +FROM debian:bullseye-slim RUN apt update && \ apt install --no-install-recommends -q --assume-yes curl=7* libjemalloc-dev=5.* && \ diff --git a/docker/binary.dock b/docker/binary.dock new file mode 100644 index 00000000..187fb200 --- /dev/null +++ b/docker/binary.dock @@ -0,0 +1,34 @@ +FROM debian:bullseye-slim + +RUN apt update && \ + apt install --no-install-recommends -q --assume-yes curl=7* libjemalloc-dev=5.* && \ + apt clean + +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "aarch64" ]; then \ + curl -kL -o jdk-19.tar.gz https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jre_aarch64_linux_hotspot_19.0.2_7.tar.gz ; \ + elif [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "arm64" ]; then \ + curl -kL -o jdk-19.tar.gz https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jre_aarch64_linux_hotspot_19.0.2_7.tar.gz ; \ + elif [ "$ARCH" = "x86_64" ]; then \ + curl -kL -o jdk-19.tar.gz https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jre_x64_linux_hotspot_19.0.2_7.tar.gz ; \ + else \ + echo "Unsupported platform: $ARCH"; exit 1; \ + fi + +RUN tar -xzf jdk-19.tar.gz && \ + rm jdk-19.tar.gz && \ + mv jdk-19.0.2+7-jre /usr/bin/ && \ + update-alternatives --install "/usr/bin/java" "java" "/usr/bin/jdk-19.0.2+7-jre/bin/java" 1 + +ENV JAVA_HOME /usr/bin/jdk-19.0.2+7-jre +RUN export JAVA_HOME +RUN export PATH=$JAVA_HOME/bin:$PATH + +WORKDIR /usr/local/bin +COPY . . + +RUN chmod 0755 hildr-node && export PATH=/usr/local/bin:$PATH + + + + diff --git a/docker/start-hildr-node-java.sh b/docker/start-hildr-node-java.sh new file mode 100755 index 00000000..f25149f6 --- /dev/null +++ b/docker/start-hildr-node-java.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +if [ $SYNC_MODE = "full" ] +then + exec java -cp $HILDR_JAR $HILDR_MAIN_CLASS \ + --network $NETWORK \ + --jwt-secret $JWT_SECRET \ + --l1-rpc-url $L1_RPC_URL \ + --l2-rpc-url http://${EXECUTION_CLIENT}:8545 \ + --l2-engine-url http://${EXECUTION_CLIENT}:8551 \ + --rpc-port $RPC_PORT \ + --sync-mode $SYNC_MODE +elif [ $SYNC_MODE = "checkpoint"] +then + exec java -cp $HILDR_JAR $HILDR_MAIN_CLASS \ + --network $NETWORK \ + --jwt-secret $JWT_SECRET \ + --l1-rpc-url $L1_RPC_URL \ + --l2-rpc-url http://${EXECUTION_CLIENT}:8545 \ + --l2-engine-url http://${EXECUTION_CLIENT}:8551 \ + --rpc-port $RPC_PORT \ + --sync-mode $SYNC_MODE \ + --checkpoint-sync-url $CHECKPOINT_SYNC_URL \ + --checkpoint-hash $CHECKPOINT_HASH +else + echo "Sync mode not recognized. Available options are full and checkpoint" +fi diff --git a/docker/start-hildr-node.sh b/docker/start-hildr-node.sh index f25149f6..3890fdbe 100755 --- a/docker/start-hildr-node.sh +++ b/docker/start-hildr-node.sh @@ -3,7 +3,7 @@ set -e if [ $SYNC_MODE = "full" ] then - exec java -cp $HILDR_JAR $HILDR_MAIN_CLASS \ + exec hildr-node \ --network $NETWORK \ --jwt-secret $JWT_SECRET \ --l1-rpc-url $L1_RPC_URL \ @@ -13,7 +13,7 @@ then --sync-mode $SYNC_MODE elif [ $SYNC_MODE = "checkpoint"] then - exec java -cp $HILDR_JAR $HILDR_MAIN_CLASS \ + exec hildr-node \ --network $NETWORK \ --jwt-secret $JWT_SECRET \ --l1-rpc-url $L1_RPC_URL \ diff --git a/hildr-node/build.gradle b/hildr-node/build.gradle index 64dc2487..52f277f5 100644 --- a/hildr-node/build.gradle +++ b/hildr-node/build.gradle @@ -192,7 +192,7 @@ spotless { // make sure every file has the following copyright header. // optionally, Spotless can set copyright years by digging // through git history (see "license" section below) - licenseHeaderFile project(":").file("config/spotless/java.license") +// licenseHeaderFile project(":").file("config/spotless/java.license") importOrder() removeUnusedImports() @@ -268,7 +268,7 @@ task buildBinary { exec { workingDir buildBinaryDir executable "sh" - args "-c", "native-image -jar ${project.name}.jar --initialize-at-build-time=ch.qos.logback,org.slf4j,io.opentelemetry ${project.name}" + args "-c", "native-image -jar ${project.name}.jar --static --initialize-at-build-time=ch.qos.logback,org.slf4j,io.opentelemetry,java.io ${project.name}" standardOutput out } } @@ -281,10 +281,6 @@ task buildDocker { def out = new ByteArrayOutputStream() doFirst { new File(buildImageDir).mkdirs() - copy { - from "../docker/start-hildr-node.sh" - into buildImageDir - } copy { from "../docker/Dockerfile" into buildImageDir @@ -305,3 +301,31 @@ task buildDocker { } println(out.toString()) } + +task buildNativeDocker { + dependsOn buildBinary + def buildImageDir = "build/docker" + def out = new ByteArrayOutputStream() + doFirst { + new File(buildImageDir).mkdirs() + copy { + from "../docker/binary.dock" + into buildImageDir + } + copy { + from "build/binary/" + into buildImageDir + include "${project.name}" + } + } + doLast { + exec { + workingDir buildImageDir + executable "sh" + args "-c", "docker build --platform=linux/amd64 -f binary.dock -t optimism-java/${project.name}:latest ." + standardOutput out + } + } + println(out.toString()) + +} diff --git a/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java index 5ad58c4e..6294e14e 100644 --- a/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java +++ b/hildr-node/src/main/java/io/optimism/rpc/methods/JsonRpcMethod.java @@ -19,8 +19,7 @@ import io.optimism.rpc.internal.response.JsonRpcResponse; /** - * jsonrpc handle method interface. - * base on besu + * jsonrpc handle method interface. base on besu * * @author thinkAfCod * @since 2023.06 From 0a33a0e1008d232ad2e7817cd82d29d0b5020888 Mon Sep 17 00:00:00 2001 From: thinkAfCod Date: Tue, 6 Jun 2023 16:14:12 +0800 Subject: [PATCH 3/3] feat: output_at_block rpc api test --- .../main/java/io/optimism/rpc/RpcServer.java | 4 +- .../rpc/handler/JsonRpcParseHandler.java | 7 +- .../optimism/rpc/internal/JsonRpcRequest.java | 2 +- .../optimism/rpc/methods/OutputAtBlock.java | 66 ++++++----- .../test/java/io/optimism/TestConstants.java | 50 +++++++++ .../java/io/optimism/l1/InnerWatcherTest.java | 29 +---- .../java/io/optimism/rpc/RpcServerTest.java | 105 ++++++++++++++++++ 7 files changed, 208 insertions(+), 55 deletions(-) create mode 100644 hildr-node/src/test/java/io/optimism/TestConstants.java create mode 100644 hildr-node/src/test/java/io/optimism/rpc/RpcServerTest.java diff --git a/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java b/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java index 3f1f97de..c3a9d020 100644 --- a/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java +++ b/hildr-node/src/main/java/io/optimism/rpc/RpcServer.java @@ -37,6 +37,7 @@ import io.vertx.core.http.ServerWebSocket; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; @@ -98,6 +99,7 @@ private Handler buildRouter() { var router = Router.router(this.vertx); router .route() + .handler(BodyHandler.create()) .handler( context -> { Tracer tracer = Logging.INSTANCE.getTracer("jsonrpc-server"); @@ -116,7 +118,7 @@ private Handler buildRouter() { mainRoute.handler( JsonRpcExecutorHandler.handler( new LoggedJsonRpcProcessor(new BaseJsonRpcProcessor()), methods)); - return null; + return router; } private Handler connectionHandler() { diff --git a/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java index 6af0592a..3a588c14 100644 --- a/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java +++ b/hildr-node/src/main/java/io/optimism/rpc/handler/JsonRpcParseHandler.java @@ -25,7 +25,7 @@ import io.vertx.ext.web.RoutingContext; /** - * copied from besu(). + * copied from besu. * * @author thinkAfCod * @since 2023.06 @@ -37,11 +37,12 @@ private JsonRpcParseHandler() {} public static Handler handler() { return ctx -> { final HttpServerResponse response = ctx.response(); - if (ctx.getBody() == null) { + var body = ctx.body(); + if (body == null) { errorResponse(response, JsonRpcError.PARSE_ERROR); } else { try { - ctx.put("REQUEST_BODY_AS_JSON_OBJECT", ctx.getBodyAsJson()); + ctx.put("REQUEST_BODY_AS_JSON_OBJECT", body.asJsonObject()); } catch (DecodeException | ClassCastException jsonObjectDecodeException) { errorResponse(response, JsonRpcError.PARSE_ERROR); } diff --git a/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java index 234ae02a..c2f28bb9 100644 --- a/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java +++ b/hildr-node/src/main/java/io/optimism/rpc/internal/JsonRpcRequest.java @@ -103,7 +103,7 @@ public boolean hasParams() { } @JsonSetter("id") - protected void setId(final JsonRpcRequestId id) { + public void setId(final JsonRpcRequestId id) { // If an id is explicitly set, it is not a notification isNotification = false; this.id = id; diff --git a/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java b/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java index 703846f9..b5f5124d 100644 --- a/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java +++ b/hildr-node/src/main/java/io/optimism/rpc/methods/OutputAtBlock.java @@ -71,32 +71,13 @@ public String getName() { public JsonRpcResponse response(JsonRpcRequestContext context) { final BigInteger blockNumber = new BigInteger(context.getParameter(0, String.class), 10); - try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { - Future ethBlockFuture = - scope.fork( - () -> - client - .ethGetBlockByNumber(DefaultBlockParameter.valueOf(blockNumber), true) - .send()); - Future ehtGetProofFuture = - scope.fork( - () -> { - return new Request<>( - ETH_GET_PROOF, - Arrays.asList( - this.l2ToL1MessagePasser, - Collections.emptyList(), - DefaultBlockParameter.valueOf(blockNumber)), - this.service, - EthGetProof.class) - .send(); - }); - scope.join(); - scope.throwIfFailed(); - EthBlock.Block block = ethBlockFuture.resultNow().getBlock(); - String stateRoot = block.getStateRoot(); + try { + EthBlock.Block block = this.getBlock(blockNumber); + + final String blockHash = block.getHash(); + EthGetProof.Proof stateProof = this.getProof(blockHash); - EthGetProof.Proof stateProof = ehtGetProofFuture.resultNow().getProof(); + String stateRoot = block.getStateRoot(); String withdrawalStorageRoot = stateProof.getStorageHash(); var outputRoot = computeL2OutputRoot(block, withdrawalStorageRoot); var version = new byte[32]; @@ -125,4 +106,39 @@ private String computeL2OutputRoot(EthBlock.Block block, String storageRoot) { byte[] hash = digest.digest(digestBytes); return Numeric.toHexString(hash); } + + private EthBlock.Block getBlock(final BigInteger blockNumber) + throws InterruptedException, ExecutionException { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + Future ethBlockFuture = + scope.fork( + () -> + client + .ethGetBlockByNumber(DefaultBlockParameter.valueOf(blockNumber), true) + .send()); + scope.join(); + scope.throwIfFailed(); + return ethBlockFuture.resultNow().getBlock(); + } + } + + private EthGetProof.Proof getProof(String blockHash) + throws InterruptedException, ExecutionException { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + Future ehtGetProofFuture = + scope.fork( + () -> { + return new Request<>( + ETH_GET_PROOF, + Arrays.asList( + this.l2ToL1MessagePasser, Collections.emptyList(), blockHash), + this.service, + EthGetProof.class) + .send(); + }); + scope.join(); + scope.throwIfFailed(); + return ehtGetProofFuture.resultNow().getProof(); + } + } } diff --git a/hildr-node/src/test/java/io/optimism/TestConstants.java b/hildr-node/src/test/java/io/optimism/TestConstants.java new file mode 100644 index 00000000..afcd58df --- /dev/null +++ b/hildr-node/src/test/java/io/optimism/TestConstants.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism; + +import io.optimism.config.Config; +import java.util.Map; + +/** + * @author thinkAfCod + * @since 2023.06 + */ +public class TestConstants { + + private TestConstants() {} + + public static boolean isConfiguredApiKeyEnv = false; + + private static final String ETH_API_ENV = "ETH_API_KEY"; + private static final String OPT_API_ENV = "OPT_API_KEY"; + + static String l1RpcUrlFormat = "https://eth-goerli.g.alchemy.com/v2/%s"; + static String l2RpcUrlFormat = "https://opt-goerli.g.alchemy.com/v2/%s"; + + public static Config createConfig() { + Map envs = System.getenv(); + isConfiguredApiKeyEnv = envs.containsKey(ETH_API_ENV) && envs.containsKey(OPT_API_ENV); + if (!isConfiguredApiKeyEnv) { + return null; + } + var l1RpcUrl = l1RpcUrlFormat.formatted(envs.get(ETH_API_ENV)); + var l2RpcUrl = l2RpcUrlFormat.formatted(envs.get(OPT_API_ENV)); + Config.CliConfig cliConfig = + new Config.CliConfig(l1RpcUrl, l2RpcUrl, null, "testjwt", null, null); + return Config.create(null, cliConfig, Config.ChainConfig.optimismGoerli()); + } +} diff --git a/hildr-node/src/test/java/io/optimism/l1/InnerWatcherTest.java b/hildr-node/src/test/java/io/optimism/l1/InnerWatcherTest.java index df0ea1d6..7583ac72 100644 --- a/hildr-node/src/test/java/io/optimism/l1/InnerWatcherTest.java +++ b/hildr-node/src/test/java/io/optimism/l1/InnerWatcherTest.java @@ -18,9 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import io.optimism.TestConstants; import io.optimism.config.Config; import java.math.BigInteger; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -38,21 +38,13 @@ */ public class InnerWatcherTest { - private static final String ETH_API_ENV = "ETH_API_KEY"; - private static final String OPT_API_ENV = "OPT_API_KEY"; - - static String l1RpcUrlFormat = "https://eth-goerli.g.alchemy.com/v2/%s"; - static String l2RpcUrlFormat = "https://opt-goerli.g.alchemy.com/v2/%s"; - - private static boolean isConfiguredApiKeyEnv = false; - private static Config config; private static ExecutorService executor; @BeforeAll static void setUp() { - config = createConfig(); + config = TestConstants.createConfig(); executor = Executors.newSingleThreadExecutor(); } @@ -61,19 +53,6 @@ static void tearDown() { executor.shutdownNow(); } - static Config createConfig() { - Map envs = System.getenv(); - isConfiguredApiKeyEnv = envs.containsKey(ETH_API_ENV) && envs.containsKey(OPT_API_ENV); - if (!isConfiguredApiKeyEnv) { - return null; - } - var l1RpcUrl = l1RpcUrlFormat.formatted(envs.get(ETH_API_ENV)); - var l2RpcUrl = l2RpcUrlFormat.formatted(envs.get(OPT_API_ENV)); - Config.CliConfig cliConfig = - new Config.CliConfig(l1RpcUrl, l2RpcUrl, null, "testjwt", null, null); - return Config.create(null, cliConfig, Config.ChainConfig.optimismGoerli()); - } - InnerWatcher createWatcher( BigInteger l2StartBlock, MessagePassingQueue queue, ExecutorService executor) { var watcherl2StartBlock = l2StartBlock; @@ -86,7 +65,7 @@ InnerWatcher createWatcher( @Test void testCreateInnerWatcher() { - if (!isConfiguredApiKeyEnv) { + if (!TestConstants.isConfiguredApiKeyEnv) { return; } var queue = new MpscGrowableArrayQueue(1024 * 4, 1024 * 64); @@ -98,7 +77,7 @@ void testCreateInnerWatcher() { @Test void testTryIngestBlock() throws ExecutionException, InterruptedException { - if (!isConfiguredApiKeyEnv) { + if (!TestConstants.isConfiguredApiKeyEnv) { return; } ExecutorService executor = Executors.newSingleThreadExecutor(); diff --git a/hildr-node/src/test/java/io/optimism/rpc/RpcServerTest.java b/hildr-node/src/test/java/io/optimism/rpc/RpcServerTest.java new file mode 100644 index 00000000..f18f220d --- /dev/null +++ b/hildr-node/src/test/java/io/optimism/rpc/RpcServerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023-2811 281165273grape@gmail.com + * + * 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 io.optimism.rpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.optimism.TestConstants; +import io.optimism.config.Config; +import io.optimism.rpc.internal.JsonRpcRequest; +import io.optimism.rpc.internal.JsonRpcRequestId; +import io.optimism.rpc.internal.result.OutputRootResult; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.Future; +import jdk.incubator.concurrent.StructuredTaskScope; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * rpc server test. + * + * @author thinkAfCod + * @since 2023.06 + */ +public class RpcServerTest { + + private static Config config; + + @BeforeAll + static void setUp() { + config = TestConstants.createConfig(); + } + + RpcServer createRpcServer(Config config) { + return new RpcServer(config); + } + + @Test + void testRpcServerStart() throws Exception { + if (!TestConstants.isConfiguredApiKeyEnv) { + return; + } + + RpcServer rpcServer = createRpcServer(config); + try { + rpcServer.start(); + + OkHttpClient okHttpClient = + new OkHttpClient.Builder() + .readTimeout(Duration.ofMinutes(5)) + .callTimeout(Duration.ofMinutes(5)) + .build(); + + ObjectMapper mapper = new ObjectMapper(); + JsonRpcRequest jsonRpcRequest = + new JsonRpcRequest( + "2.0", RpcMethod.OP_OUTPUT_AT_BLOCK.getRpcMethodName(), new Object[] {"7900000"}); + jsonRpcRequest.setId(new JsonRpcRequestId("1")); + var postBody = mapper.writeValueAsBytes(jsonRpcRequest); + RequestBody requestBody = RequestBody.create(postBody, MediaType.get("application/json")); + + final Request request = + new Request.Builder().url("http://127.0.0.1:9545").post(requestBody).build(); + + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + Future fork = scope.fork(() -> okHttpClient.newCall(request).execute()); + scope.join(); + Response response = fork.get(); + assertEquals(200, response.code()); + assertNotNull(response.body()); + Map jsonRpcResp = mapper.readValue(response.body().string(), Map.class); + assertEquals(jsonRpcResp.get("id"), "1"); + OutputRootResult outputRootResult = + mapper.readValue( + mapper.writeValueAsString(jsonRpcResp.get("result")), OutputRootResult.class); + assertNotNull(outputRootResult); + } + } finally { + rpcServer.stop(); + } + } +}