diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java index faaed87de7d..c99b01adee7 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.crypto.KeyPairUtil; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.core.MiningParameters; import org.hyperledger.besu.ethereum.core.PrivacyParameters; @@ -102,6 +103,7 @@ public class BesuNode implements NodeConfiguration, RunnableNode, AutoCloseable private final JsonRpcConfiguration jsonRpcConfiguration; private final Optional engineRpcConfiguration; private final WebSocketConfiguration webSocketConfiguration; + private final JsonRpcIpcConfiguration jsonRpcIpcConfiguration; private final MetricsConfiguration metricsConfiguration; private Optional permissioningConfiguration; private final GenesisConfigurationProvider genesisConfigProvider; @@ -132,6 +134,7 @@ public BesuNode( final JsonRpcConfiguration jsonRpcConfiguration, final Optional engineRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, + final JsonRpcIpcConfiguration jsonRpcIpcConfiguration, final MetricsConfiguration metricsConfiguration, final Optional permissioningConfiguration, final Optional keyfilePath, @@ -178,6 +181,7 @@ public BesuNode( this.jsonRpcConfiguration = jsonRpcConfiguration; this.engineRpcConfiguration = engineRpcConfiguration; this.webSocketConfiguration = webSocketConfiguration; + this.jsonRpcIpcConfiguration = jsonRpcIpcConfiguration; this.metricsConfiguration = metricsConfiguration; this.permissioningConfiguration = permissioningConfiguration; this.genesisConfigProvider = genesisConfigProvider; @@ -234,6 +238,10 @@ private boolean isWebSocketsRpcEnabled() { return webSocketConfiguration().isEnabled(); } + public boolean isJsonRpcIpcEnabled() { + return jsonRpcIpcConfiguration().isEnabled(); + } + boolean isMetricsEnabled() { return metricsConfiguration.isEnabled(); } @@ -593,6 +601,10 @@ WebSocketConfiguration webSocketConfiguration() { return webSocketConfiguration; } + JsonRpcIpcConfiguration jsonRpcIpcConfiguration() { + return jsonRpcIpcConfiguration; + } + Optional wsRpcListenHost() { return Optional.of(webSocketConfiguration().getHost()); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index 2f6d46a4849..9699b564fca 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -18,6 +18,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import org.hyperledger.besu.cli.options.unstable.NetworkingOptions; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.netty.TLSConfiguration; import org.hyperledger.besu.ethereum.permissioning.PermissioningConfiguration; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; @@ -230,6 +231,15 @@ public void startNode(final BesuNode node) { params.add("0"); } + if (node.isJsonRpcIpcEnabled()) { + final JsonRpcIpcConfiguration ipcConfiguration = node.jsonRpcIpcConfiguration(); + params.add("--Xrpc-ipc-enabled"); + params.add("--Xrpc-ipc-path"); + params.add(ipcConfiguration.getPath().toString()); + params.add("--Xrpc-ipc-apis"); + params.add(String.join(",", ipcConfiguration.getEnabledApis())); + } + if (node.isMetricsEnabled()) { final MetricsConfiguration metricsConfiguration = node.getMetricsConfiguration(); params.add("--metrics-enabled"); diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java index 99a976f2388..28e4f25f9ba 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java @@ -202,6 +202,7 @@ public void startNode(final BesuNode node) { .networkingConfiguration(node.getNetworkingConfiguration()) .jsonRpcConfiguration(node.jsonRpcConfiguration()) .webSocketConfiguration(node.webSocketConfiguration()) + .jsonRpcIpcConfiguration(node.jsonRpcIpcConfiguration()) .dataDir(node.homeDirectory()) .metricsSystem(metricsSystem) .permissioningService(new PermissioningServiceImpl()) diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java index c65672ff87f..e1fa5c14358 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.cli.config.NetworkName; import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.core.MiningParameters; import org.hyperledger.besu.ethereum.core.PrivacyParameters; @@ -39,6 +40,7 @@ public class BesuNodeConfiguration { private final JsonRpcConfiguration jsonRpcConfiguration; private final Optional engineRpcConfiguration; private final WebSocketConfiguration webSocketConfiguration; + private final JsonRpcIpcConfiguration jsonRpcIpcConfiguration; private final Optional engineWebSocketConfiguration; private final MetricsConfiguration metricsConfiguration; private final Optional permissioningConfiguration; @@ -72,6 +74,7 @@ public class BesuNodeConfiguration { final JsonRpcConfiguration jsonRpcConfiguration, final Optional engineRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, + final JsonRpcIpcConfiguration jsonRpcIpcConfiguration, final Optional engineWebSocketConfiguration, final MetricsConfiguration metricsConfiguration, final Optional permissioningConfiguration, @@ -102,6 +105,7 @@ public class BesuNodeConfiguration { this.jsonRpcConfiguration = jsonRpcConfiguration; this.engineRpcConfiguration = engineRpcConfiguration; this.webSocketConfiguration = webSocketConfiguration; + this.jsonRpcIpcConfiguration = jsonRpcIpcConfiguration; this.engineWebSocketConfiguration = engineWebSocketConfiguration; this.metricsConfiguration = metricsConfiguration; this.permissioningConfiguration = permissioningConfiguration; @@ -150,6 +154,10 @@ public WebSocketConfiguration getWebSocketConfiguration() { return webSocketConfiguration; } + public JsonRpcIpcConfiguration getJsonRpcIpcConfiguration() { + return jsonRpcIpcConfiguration; + } + public Optional getEngineWebSocketConfiguration() { return engineWebSocketConfiguration; } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java index eafd197b456..a95b6e1ae34 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider; import org.hyperledger.besu.ethereum.core.AddressHelpers; @@ -59,6 +60,7 @@ public class BesuNodeConfigurationBuilder { private JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); private JsonRpcConfiguration engineRpcConfiguration = JsonRpcConfiguration.createEngineDefault(); private WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); + private JsonRpcIpcConfiguration jsonRpcIpcConfiguration = new JsonRpcIpcConfiguration(); private WebSocketConfiguration engineWebSocketConfiguration = WebSocketConfiguration.createDefault(); private MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build(); @@ -240,6 +242,12 @@ public BesuNodeConfigurationBuilder webSocketConfiguration( return this; } + public BesuNodeConfigurationBuilder jsonRpcIpcConfiguration( + final JsonRpcIpcConfiguration jsonRpcIpcConfiguration) { + this.jsonRpcIpcConfiguration = jsonRpcIpcConfiguration; + return this; + } + public BesuNodeConfigurationBuilder metricsConfiguration( final MetricsConfiguration metricsConfiguration) { this.metricsConfiguration = metricsConfiguration; @@ -487,6 +495,7 @@ public BesuNodeConfiguration build() { jsonRpcConfiguration, Optional.of(engineRpcConfiguration), webSocketConfiguration, + jsonRpcIpcConfiguration, Optional.of(engineWebSocketConfiguration), metricsConfiguration, permissioningConfiguration, diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index 5be6b9e12f5..6d96581942b 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -64,6 +64,7 @@ public BesuNode create(final BesuNodeConfiguration config) throws IOException { config.getJsonRpcConfiguration(), config.getEngineRpcConfiguration(), config.getWebSocketConfiguration(), + config.getJsonRpcIpcConfiguration(), config.getMetricsConfiguration(), config.getPermissioningConfiguration(), config.getKeyFilePath(), diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java index 732cf5c0921..fb14adb0e37 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java @@ -103,6 +103,7 @@ public PrivacyNode( besuConfig.getJsonRpcConfiguration(), besuConfig.getEngineRpcConfiguration(), besuConfig.getWebSocketConfiguration(), + besuConfig.getJsonRpcIpcConfiguration(), besuConfig.getMetricsConfiguration(), besuConfig.getPermissioningConfiguration(), besuConfig.getKeyFilePath(), diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/ipc/Web3JSupportAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/ipc/Web3JSupportAcceptanceTest.java new file mode 100644 index 00000000000..69adb77efce --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/ipc/Web3JSupportAcceptanceTest.java @@ -0,0 +1,55 @@ +package org.hyperledger.besu.tests.acceptance.jsonrpc.ipc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.node.Node; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.NetVersion; +import org.web3j.protocol.ipc.UnixIpcService; + +public class Web3JSupportAcceptanceTest extends AcceptanceTestBase { + + private Node node; + private Path socketPath; + + @Before + public void setUp() throws Exception { + socketPath = Files.createTempFile("besu-test-", ".ipc"); + node = + besu.createNode( + "node1", + (configurationBuilder) -> + configurationBuilder.jsonRpcIpcConfiguration( + new JsonRpcIpcConfiguration( + true, socketPath, Collections.singletonList(RpcApis.NET.name())))); + cluster.start(node); + } + + @Test + public void netVersionCall() { + final Web3j web3 = Web3j.build(new UnixIpcService(socketPath.toString())); + final Request ethBlockNumberRequest = web3.netVersion(); + node.verify( + node -> { + try { + assertThat(ethBlockNumberRequest.send().getNetVersion()) + .isEqualTo(String.valueOf(2018)); + } catch (IOException e) { + fail("Web3J net_version call failed", e); + } + }); + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/Runner.java b/besu/src/main/java/org/hyperledger/besu/Runner.java index 0468b19b5bf..b22e2c97d68 100644 --- a/besu/src/main/java/org/hyperledger/besu/Runner.java +++ b/besu/src/main/java/org/hyperledger/besu/Runner.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.ethereum.api.graphql.GraphQLHttpService; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcHttpService; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcService; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketService; import org.hyperledger.besu.ethereum.api.query.cache.AutoTransactionLogBloomCachingService; import org.hyperledger.besu.ethereum.api.query.cache.TransactionLogBloomCacher; @@ -64,6 +65,7 @@ public class Runner implements AutoCloseable { private final Optional jsonRpc; private final Optional engineJsonRpc; private final Optional metrics; + private final Optional ipcJsonRpc; private final Optional pidPath; private final Optional webSocketRpc; private final Optional engineWebSocketRpc; @@ -84,6 +86,7 @@ public class Runner implements AutoCloseable { final Optional graphQLHttp, final Optional webSocketRpc, final Optional engineWebSocketRpc, + final Optional ipcJsonRpc, final Optional stratumServer, final Optional metrics, final Optional ethStatsService, @@ -101,6 +104,7 @@ public class Runner implements AutoCloseable { this.engineJsonRpc = engineJsonRpc; this.webSocketRpc = webSocketRpc; this.engineWebSocketRpc = engineWebSocketRpc; + this.ipcJsonRpc = ipcJsonRpc; this.metrics = metrics; this.ethStatsService = ethStatsService; this.besuController = besuController; @@ -123,6 +127,10 @@ public void startExternalServices() { webSocketRpc.ifPresent(service -> waitForServiceToStart("websocketRpc", service.start())); engineWebSocketRpc.ifPresent( service -> waitForServiceToStart("engineWebsocketRpc", service.start())); + ipcJsonRpc.ifPresent( + service -> + waitForServiceToStart( + "ipcJsonRpc", service.start().toCompletionStage().toCompletableFuture())); stratumServer.ifPresent(server -> waitForServiceToStart("stratum", server.start())); autoTransactionLogBloomCachingService.ifPresent(AutoTransactionLogBloomCachingService::start); ethStatsService.ifPresent(EthStatsService::start); @@ -158,6 +166,10 @@ public void stop() { webSocketRpc.ifPresent(service -> waitForServiceToStop("websocketRpc", service.stop())); engineWebSocketRpc.ifPresent( service -> waitForServiceToStop("engineWebsocketRpc", service.stop())); + ipcJsonRpc.ifPresent( + service -> + waitForServiceToStop( + "ipcJsonRpc", service.stop().toCompletionStage().toCompletableFuture())); metrics.ifPresent(service -> waitForServiceToStop("metrics", service.stop())); ethStatsService.ifPresent(EthStatsService::stop); besuController.getMiningCoordinator().stop(); diff --git a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java index eeba4ccb08b..9f7b3285c25 100644 --- a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -48,6 +48,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManager; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManagerBuilder; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcService; import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethodsFactory; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketRequestHandler; @@ -188,6 +190,7 @@ public class RunnerBuilder { private StorageProvider storageProvider; private Supplier> forkIdSupplier; private RpcEndpointServiceImpl rpcEndpointServiceImpl; + private JsonRpcIpcConfiguration jsonRpcIpcConfiguration; public RunnerBuilder vertx(final Vertx vertx) { this.vertx = vertx; @@ -398,6 +401,12 @@ public RunnerBuilder rpcEndpointService(final RpcEndpointServiceImpl rpcEndpoint return this; } + public RunnerBuilder jsonRpcIpcConfiguration( + final JsonRpcIpcConfiguration jsonRpcIpcConfiguration) { + this.jsonRpcIpcConfiguration = jsonRpcIpcConfiguration; + return this; + } + public Runner build() { Preconditions.checkNotNull(besuController); @@ -832,6 +841,45 @@ public Runner build() { ethStatsService = Optional.empty(); } + final Optional jsonRpcIpcService; + if (jsonRpcIpcConfiguration.isEnabled()) { + Map ipcMethods = + jsonRpcMethods( + protocolSchedule, + context, + besuController, + peerNetwork, + blockchainQueries, + synchronizer, + transactionPool, + miningCoordinator, + metricsSystem, + supportedCapabilities, + jsonRpcIpcConfiguration.getEnabledApis().stream() + .filter(apiGroup -> !apiGroup.toLowerCase().startsWith("engine")) + .collect(Collectors.toList()), + filterManager, + accountLocalConfigPermissioningController, + nodeLocalConfigPermissioningController, + privacyParameters, + jsonRpcConfiguration, + webSocketConfiguration, + metricsConfiguration, + natService, + besuPluginContext.getNamedPlugins(), + dataDir, + rpcEndpointServiceImpl); + + jsonRpcIpcService = + Optional.of( + new JsonRpcIpcService( + vertx, + jsonRpcIpcConfiguration.getPath(), + new JsonRpcExecutor(new BaseJsonRpcProcessor(), ipcMethods))); + } else { + jsonRpcIpcService = Optional.empty(); + } + return new Runner( vertx, networkRunner, @@ -841,6 +889,7 @@ public Runner build() { graphQLHttpService, webSocketService, engineWebSocketService, + jsonRpcIpcService, stratumServer, metricsService, ethStatsService, diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index a24536431da..da7ed318206 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -60,6 +60,7 @@ import org.hyperledger.besu.cli.options.unstable.DnsOptions; import org.hyperledger.besu.cli.options.unstable.EthProtocolOptions; import org.hyperledger.besu.cli.options.unstable.EvmOptions; +import org.hyperledger.besu.cli.options.unstable.IpcOptions; import org.hyperledger.besu.cli.options.unstable.LauncherOptions; import org.hyperledger.besu.cli.options.unstable.MergeOptions; import org.hyperledger.besu.cli.options.unstable.MetricsCLIOptions; @@ -111,6 +112,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider; import org.hyperledger.besu.ethereum.api.tls.TlsClientAuthConfiguration; @@ -284,6 +286,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { final LauncherOptions unstableLauncherOptions = LauncherOptions.create(); private final PrivacyPluginOptions unstablePrivacyPluginOptions = PrivacyPluginOptions.create(); private final EvmOptions unstableEvmOptions = EvmOptions.create(); + private final IpcOptions unstableIpcOptions = IpcOptions.create(); // stable CLI options private final DataStorageOptions dataStorageOptions = DataStorageOptions.create(); @@ -1275,6 +1278,7 @@ static class TxPoolOptionGroup { private GraphQLConfiguration graphQLConfiguration; private WebSocketConfiguration webSocketConfiguration; private WebSocketConfiguration engineWebSocketConfiguration; + private JsonRpcIpcConfiguration jsonRpcIpcConfiguration; private ApiConfiguration apiConfiguration; private MetricsConfiguration metricsConfiguration; private Optional permissioningConfiguration; @@ -1486,6 +1490,7 @@ private void handleUnstableOptions() { .put("Launcher", unstableLauncherOptions) .put("Merge", mergeOptions) .put("EVM Options", unstableEvmOptions) + .put("IPC Options", unstableIpcOptions) .build(); UnstableOptionsSubCommand.createUnstableOptions(commandLine, unstableOptions); @@ -1579,6 +1584,7 @@ private Runner buildRunner() { engineJsonRpcConfiguration, webSocketConfiguration, engineWebSocketConfiguration, + jsonRpcIpcConfiguration, apiConfiguration, metricsConfiguration, permissioningConfiguration, @@ -1907,6 +1913,11 @@ private void configure() throws Exception { engineWebSocketConfiguration = engineWebSocketConfiguration( engineRPCOptionGroup.engineRpcWsPort, engineRPCOptionGroup.engineHostsAllowlist); + jsonRpcIpcConfiguration = + jsonRpcIpcConfiguration( + unstableIpcOptions.isEnabled(), + unstableIpcOptions.getIpcPath(), + unstableIpcOptions.getRpcIpcApis()); apiConfiguration = apiConfiguration(); // hostsWhitelist is a hidden option. If it is specified, add the list to hostAllowlist if (!hostsWhitelist.isEmpty()) { @@ -1937,6 +1948,18 @@ private void configure() throws Exception { instantiateSignatureAlgorithmFactory(); } + private JsonRpcIpcConfiguration jsonRpcIpcConfiguration( + final Boolean enabled, final Path ipcPath, final List rpcIpcApis) { + final Path actualPath; + if (ipcPath == null) { + actualPath = IpcOptions.getDefaultPath(dataDir()); + } else { + actualPath = ipcPath; + } + return new JsonRpcIpcConfiguration( + vertx.isNativeTransportEnabled() && enabled, actualPath, rpcIpcApis); + } + private GoQuorumPrivacyParameters configureGoQuorumPrivacy( final KeyValueStorageProvider storageProvider) { return new GoQuorumPrivacyParameters( @@ -2745,6 +2768,7 @@ private Runner synchronize( final JsonRpcConfiguration engineJsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, final WebSocketConfiguration engineWebSocketConfiguration, + final JsonRpcIpcConfiguration jsonRpcIpcConfiguration, final ApiConfiguration apiConfiguration, final MetricsConfiguration metricsConfiguration, final Optional permissioningConfiguration, @@ -2781,6 +2805,7 @@ private Runner synchronize( .engineJsonRpcConfiguration(engineJsonRpcConfiguration) .webSocketConfiguration(webSocketConfiguration) .engineWebSocketConfiguration(engineWebSocketConfiguration) + .jsonRpcIpcConfiguration(jsonRpcIpcConfiguration) .apiConfiguration(apiConfiguration) .pidPath(pidPath) .dataDir(dataDir()) @@ -2810,6 +2835,7 @@ protected Vertx createVertx(final VertxOptions vertxOptions) { private VertxOptions createVertxOptions(final MetricsSystem metricsSystem) { return new VertxOptions() + .setPreferNativeTransport(true) .setMetricsOptions( new MetricsOptions() .setEnabled(true) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/IpcOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/IpcOptions.java new file mode 100644 index 00000000000..580af54cd73 --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/IpcOptions.java @@ -0,0 +1,71 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.cli.options.unstable; + +import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_RPC_APIS; + +import java.nio.file.Path; +import java.util.List; + +import picocli.CommandLine; + +public class IpcOptions { + private static final String DEFAULT_IPC_FILE = "besu.ipc"; + + public static IpcOptions create() { + return new IpcOptions(); + } + + public static Path getDefaultPath(final Path dataDir) { + return dataDir.resolve(DEFAULT_IPC_FILE); + } + + @CommandLine.Option( + names = {"--Xrpc-ipc-enabled"}, + hidden = true, + description = "Set to start the JSON-RPC IPC service (default: ${DEFAULT-VALUE})") + private final Boolean enabled = false; + + @CommandLine.Option( + names = {"--Xrpc-ipc-path"}, + hidden = true, + description = + "IPC socket/pipe file (default: a file named \"" + + DEFAULT_IPC_FILE + + "\" in the Besu data directory)") + private Path ipcPath; + + @CommandLine.Option( + names = {"--Xrpc-ipc-api", "--Xrpc-ipc-apis"}, + hidden = true, + paramLabel = "", + split = " {0,1}, {0,1}", + arity = "1..*", + description = + "Comma separated list of APIs to enable on JSON-RPC IPC service (default: ${DEFAULT-VALUE})") + private final List rpcIpcApis = DEFAULT_RPC_APIS; + + public Boolean isEnabled() { + return enabled; + } + + public Path getIpcPath() { + return ipcPath; + } + + public List getRpcIpcApis() { + return rpcIpcApis; + } +} diff --git a/besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java b/besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java index abcec6e3bf1..917ab7ad7db 100644 --- a/besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java +++ b/besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java @@ -39,6 +39,7 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator; @@ -148,6 +149,7 @@ public void enodeUrlShouldHaveAdvertisedHostWhenDiscoveryDisabled() { .permissioningService(mock(PermissioningServiceImpl.class)) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .webSocketConfiguration(mock(WebSocketConfiguration.class)) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) .vertx(vertx) .dataDir(dataDir.getRoot().toPath()) @@ -192,6 +194,7 @@ public void movingAcrossProtocolSpecsUpdatesNodeRecord() { .jsonRpcConfiguration(mock(JsonRpcConfiguration.class)) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .webSocketConfiguration(mock(WebSocketConfiguration.class)) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) .vertx(Vertx.vertx()) .dataDir(dataDir.getRoot().toPath()) @@ -247,6 +250,7 @@ public void whenEngineApiAddedListensOnDefaultPort() { .engineJsonRpcConfiguration(engine) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .webSocketConfiguration(mock(WebSocketConfiguration.class)) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) .vertx(Vertx.vertx()) .dataDir(dataDir.getRoot().toPath()) @@ -285,6 +289,7 @@ public void whenEngineApiAddedWebSocketReadyOnDefaultPort() { .permissioningService(mock(PermissioningServiceImpl.class)) .jsonRpcConfiguration(JsonRpcConfiguration.createDefault()) .webSocketConfiguration(wsRpc) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .engineWebSocketConfiguration(engineWsRpc) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) @@ -326,6 +331,7 @@ public void noEngineApiNoServiceForMethods() { .jsonRpcConfiguration(defaultRpcConfig) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .webSocketConfiguration(defaultWebSockConfig) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) .vertx(Vertx.vertx()) .dataDir(dataDir.getRoot().toPath()) @@ -372,6 +378,7 @@ public void assertTransitionStratumConfiguration() { .engineJsonRpcConfiguration(engine) .graphQLConfiguration(mock(GraphQLConfiguration.class)) .webSocketConfiguration(mock(WebSocketConfiguration.class)) + .jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class)) .metricsConfiguration(mock(MetricsConfiguration.class)) .vertx(Vertx.vertx()) .dataDir(dataDir.getRoot().toPath()) diff --git a/besu/src/test/java/org/hyperledger/besu/RunnerTest.java b/besu/src/test/java/org/hyperledger/besu/RunnerTest.java index de3079868d8..d3efc558815 100644 --- a/besu/src/test/java/org/hyperledger/besu/RunnerTest.java +++ b/besu/src/test/java/org/hyperledger/besu/RunnerTest.java @@ -30,6 +30,7 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockImporter; @@ -201,6 +202,7 @@ private void syncFromGenesis(final SyncMode mode, final GenesisConfigFile genesi final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); final GraphQLConfiguration aheadGraphQLConfiguration = graphQLConfiguration(); final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); + final JsonRpcIpcConfiguration aheadJsonRpcIpcConfiguration = new JsonRpcIpcConfiguration(); final MetricsConfiguration aheadMetricsConfiguration = metricsConfiguration(); final Path pidPath = temp.getRoot().toPath().resolve("pid"); final RunnerBuilder runnerBuilder = @@ -225,6 +227,7 @@ private void syncFromGenesis(final SyncMode mode, final GenesisConfigFile genesi .jsonRpcConfiguration(aheadJsonRpcConfiguration) .graphQLConfiguration(aheadGraphQLConfiguration) .webSocketConfiguration(aheadWebSocketConfiguration) + .jsonRpcIpcConfiguration(aheadJsonRpcIpcConfiguration) .metricsConfiguration(aheadMetricsConfiguration) .dataDir(dbAhead) .pidPath(pidPath) diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index f4a1acef913..90c446b1dfe 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -268,6 +268,7 @@ public void initMocks() throws Exception { when(mockRunnerBuilder.engineWebSocketConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.graphQLConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.webSocketConfiguration(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.jsonRpcIpcConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.apiConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle index 24de67a263f..b39d7a44776 100644 --- a/ethereum/api/build.gradle +++ b/ethereum/api/build.gradle @@ -76,6 +76,8 @@ dependencies { implementation "org.immutables:value-annotations" runtimeOnly 'org.bouncycastle:bcpkix-jdk15on' + runtimeOnly 'io.netty:netty-transport-native-epoll' + runtimeOnly 'io.netty:netty-transport-native-kqueue' testImplementation project(':config') testImplementation project(path: ':config', configuration: 'testSupportArtifacts') @@ -89,6 +91,7 @@ dependencies { testImplementation 'com.squareup.okhttp3:okhttp' testImplementation 'io.vertx:vertx-auth-jwt' + testImplementation 'io.vertx:vertx-junit5' testImplementation 'io.vertx:vertx-unit' testImplementation 'io.vertx:vertx-web-client' testImplementation 'junit:junit' diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcConfiguration.java new file mode 100644 index 00000000000..ac0de10e601 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.ipc; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; + +public class JsonRpcIpcConfiguration { + + private final boolean enabled; + private final Path ipcPath; + private final Collection enabledApis; + + public JsonRpcIpcConfiguration() { + enabled = false; + ipcPath = null; + enabledApis = Collections.emptyList(); + } + + public JsonRpcIpcConfiguration( + final boolean enabled, final Path ipcPath, final Collection enabledApis) { + this.enabled = enabled; + this.ipcPath = ipcPath; + this.enabledApis = enabledApis; + } + + public boolean isEnabled() { + return enabled; + } + + public Path getPath() { + return ipcPath; + } + + public Collection getEnabledApis() { + return enabledApis; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcService.java new file mode 100644 index 00000000000..d34434c91cb --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcService.java @@ -0,0 +1,210 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.ipc; + +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError.INVALID_REQUEST; + +import org.hyperledger.besu.ethereum.api.jsonrpc.execution.JsonRpcExecutor; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponseType; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.net.SocketAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JsonRpcIpcService { + + private static final Logger LOG = LoggerFactory.getLogger(JsonRpcIpcService.class); + private static final ObjectWriter JSON_OBJECT_WRITER = + new ObjectMapper() + .registerModule(new Jdk8Module()) // Handle JDK8 Optionals (de)serialization + .writer() + .without(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM) + .with(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + + private final Vertx vertx; + private final Path path; + private final JsonRpcExecutor jsonRpcExecutor; + private NetServer netServer; + + public JsonRpcIpcService(final Vertx vertx, final Path path, final JsonRpcExecutor rpcExecutor) { + this.vertx = vertx; + this.path = path; + this.jsonRpcExecutor = rpcExecutor; + } + + public Future start() { + netServer = vertx.createNetServer(buildNetServerOptions()); + netServer.connectHandler( + socket -> { + AtomicBoolean closedSocket = new AtomicBoolean(false); + socket + .closeHandler(unused -> closedSocket.set(true)) + .handler( + buffer -> { + if (buffer.length() == 0) { + errorReturn(socket, null, JsonRpcError.INVALID_REQUEST); + } else { + try { + final JsonObject jsonRpcRequest = buffer.toJsonObject(); + vertx + .executeBlocking( + promise -> { + final JsonRpcResponse jsonRpcResponse = + jsonRpcExecutor.execute( + Optional.empty(), + null, + null, + closedSocket::get, + jsonRpcRequest, + req -> req.mapTo(JsonRpcRequest.class)); + promise.complete(jsonRpcResponse); + }) + .onSuccess( + jsonRpcResponse -> { + try { + socket.write( + JSON_OBJECT_WRITER.writeValueAsString(jsonRpcResponse) + + '\n'); + } catch (JsonProcessingException e) { + LOG.error("Error streaming JSON-RPC response", e); + } + }) + .onFailure( + throwable -> { + try { + final Integer id = jsonRpcRequest.getInteger("id", null); + errorReturn(socket, id, JsonRpcError.INTERNAL_ERROR); + } catch (ClassCastException idNotIntegerException) { + errorReturn(socket, null, JsonRpcError.INTERNAL_ERROR); + } + }); + } catch (DecodeException jsonObjectDecodeException) { + try { + final JsonArray batchJsonRpcRequest = buffer.toJsonArray(); + if (batchJsonRpcRequest.isEmpty()) { + errorReturn(socket, null, JsonRpcError.INVALID_REQUEST); + } else { + vertx + .>executeBlocking( + promise -> { + List responses = new ArrayList<>(); + for (int i = 0; i < batchJsonRpcRequest.size(); i++) { + final JsonObject jsonRequest; + try { + jsonRequest = batchJsonRpcRequest.getJsonObject(i); + } catch (ClassCastException e) { + responses.add( + new JsonRpcErrorResponse(null, INVALID_REQUEST)); + continue; + } + responses.add( + jsonRpcExecutor.execute( + Optional.empty(), + null, + null, + closedSocket::get, + jsonRequest, + req -> req.mapTo(JsonRpcRequest.class))); + } + promise.complete(responses); + }) + .onSuccess( + jsonRpcBatchResponse -> { + try { + final JsonRpcResponse[] completed = + jsonRpcBatchResponse.stream() + .filter( + jsonRpcResponse -> + jsonRpcResponse.getType() + != JsonRpcResponseType.NONE) + .toArray(JsonRpcResponse[]::new); + + socket.write( + JSON_OBJECT_WRITER.writeValueAsString(completed) + + '\n'); + } catch (JsonProcessingException e) { + LOG.error("Error streaming JSON-RPC response", e); + } + }) + .onFailure( + throwable -> + errorReturn(socket, null, JsonRpcError.INTERNAL_ERROR)); + } + } catch (DecodeException jsonArrayDecodeException) { + errorReturn(socket, null, JsonRpcError.PARSE_ERROR); + } + } + } + }); + }); + return netServer + .listen(SocketAddress.domainSocketAddress(path.toString())) + .onSuccess(successServer -> LOG.info("IPC endpoint opened: {}", path)) + .onFailure(throwable -> LOG.error("Unable to open IPC endpoint", throwable)); + } + + public Future stop() { + if (netServer == null) { + return Future.succeededFuture(); + } else { + return netServer + .close() + .onComplete( + closeResult -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + LOG.error("Unable to delete IPC file", e); + } + }); + } + } + + private Future errorReturn( + final NetSocket socket, final Integer id, final JsonRpcError rpcError) { + return socket.write(Buffer.buffer(Json.encode(new JsonRpcErrorResponse(id, rpcError)) + '\n')); + } + + private NetServerOptions buildNetServerOptions() { + return new NetServerOptions(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcServiceTest.java new file mode 100644 index 00000000000..661d8e17a49 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/ipc/JsonRpcIpcServiceTest.java @@ -0,0 +1,195 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.ipc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.api.jsonrpc.execution.BaseJsonRpcProcessor; +import org.hyperledger.besu.ethereum.api.jsonrpc.execution.JsonRpcExecutor; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.SocketAddress; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +@EnabledOnOs({OS.LINUX, OS.MAC}) +@ExtendWith(VertxExtension.class) +class JsonRpcIpcServiceTest { + + @TempDir private Path tempDir; + private Vertx vertx; + private VertxTestContext testContext; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(new VertxOptions().setPreferNativeTransport(true)); + testContext = new VertxTestContext(); + } + + @AfterEach + public void after() throws Throwable { + assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)) + .describedAs("Test completed on time") + .isTrue(); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + } + + @Test + void successfulExecution() { + final Path socketPath = tempDir.resolve("besu-test.ipc"); + final JsonRpcMethod testMethod = mock(JsonRpcMethod.class); + when(testMethod.response(any())).thenReturn(new JsonRpcSuccessResponse(1, "TEST OK")); + final JsonRpcIpcService service = + new JsonRpcIpcService( + vertx, + socketPath, + new JsonRpcExecutor(new BaseJsonRpcProcessor(), Map.of("test_method", testMethod))); + final String expectedResponse = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"TEST OK\"}\n"; + + assertSocketCall( + service, + socketPath, + expectedResponse, + new JsonObject().put("id", 1).put("method", "test_method").toBuffer()); + } + + @Test + void successfulBatchExecution() { + final Path socketPath = tempDir.resolve("besu-test.ipc"); + final JsonRpcMethod fooMethod = mock(JsonRpcMethod.class); + when(fooMethod.response(any())).thenReturn(new JsonRpcSuccessResponse(1, "FOO OK")); + final JsonRpcMethod barMethod = mock(JsonRpcMethod.class); + when(barMethod.response(any())).thenReturn(new JsonRpcSuccessResponse(2, "BAR OK")); + final JsonRpcIpcService service = + new JsonRpcIpcService( + vertx, + socketPath, + new JsonRpcExecutor( + new BaseJsonRpcProcessor(), + Map.of("foo_method", fooMethod, "bar_method", barMethod))); + + assertSocketCall( + service, + socketPath, + "[{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"FOO OK\"},{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\"BAR OK\"}]\n", + new JsonArray( + Arrays.asList( + new JsonObject().put("id", 1).put("method", "foo_method"), + new JsonObject().put("id", 2).put("method", "bar_method"))) + .toBuffer()); + } + + @Test + void validJsonButNotRpcShouldReturnInvalidRequest() { + final Path socketPath = tempDir.resolve("besu-test.ipc"); + final JsonRpcIpcService service = + new JsonRpcIpcService( + vertx, + socketPath, + new JsonRpcExecutor(new BaseJsonRpcProcessor(), Collections.emptyMap())); + final String expectedResponse = + "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Invalid Request\"}}\n"; + + assertSocketCall(service, socketPath, expectedResponse, Buffer.buffer("{\"foo\":\"bar\"}")); + } + + @Test + void nonJsonRequestShouldReturnParseError() { + final Path socketPath = tempDir.resolve("besu-test.ipc"); + final JsonRpcIpcService service = + new JsonRpcIpcService(vertx, socketPath, mock(JsonRpcExecutor.class)); + final String expectedResponse = + "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}\n"; + + assertSocketCall(service, socketPath, expectedResponse, Buffer.buffer("bad request")); + } + + @Test + void shouldDeleteSocketFileOnStop() { + final Path socketPath = tempDir.resolve("besu-test.ipc"); + final JsonRpcIpcService service = + new JsonRpcIpcService(vertx, socketPath, mock(JsonRpcExecutor.class)); + service + .start() + .onComplete( + testContext.succeeding( + server -> + service + .stop() + .onComplete( + testContext.succeeding( + handler -> + testContext.verify( + () -> { + assertThat(socketPath).doesNotExist(); + testContext.completeNow(); + }))))); + } + + private void assertSocketCall( + final JsonRpcIpcService service, + final Path socketPath, + final String expectedResponse, + final Buffer request) { + service + .start() + .onComplete( + testContext.succeeding( + server -> + vertx + .createNetClient() + .connect(SocketAddress.domainSocketAddress(socketPath.toString())) + .onComplete( + testContext.succeeding( + socket -> + socket + .handler( + buffer -> + testContext.verify( + () -> { + assertThat(buffer) + .hasToString(expectedResponse); + service + .stop() + .onComplete( + testContext.succeedingThenComplete()); + })) + .write(request))))); + } +} diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle index db41efcfaf8..026338fe757 100644 --- a/gradle/check-licenses.gradle +++ b/gradle/check-licenses.gradle @@ -72,6 +72,7 @@ downloadLicenses { ext.cc0 = license('Public Domain (CC0) License 1.0', 'https://creativecommons.org/publicdomain/zero/1.0') ext.cddl = license('Common Development and Distribution License 1.0', 'http://opensource.org/licenses/CDDL-1.0') ext.epl1 = license('Eclipse Public License 1.0', 'https://www.eclipse.org/legal/epl-v10.html') + ext.epl2 = license('Eclipse Public License - v 2.0', 'https://www.eclipse.org/legal/epl-v20.html') ext.mit = license('MIT License', 'http://www.opensource.org/licenses/mit-license.php') aliases = [ (apache) : [ @@ -123,6 +124,10 @@ downloadLicenses { 'Eclipse Public License - v 1.0', 'Eclipse Public License 1.0 ', ], + (epl2) : [ + 'Eclipse Public License - v 2.0', + 'Eclipse Public License version 2.0' + ], (mit) : [ 'MIT', 'The MIT License', @@ -161,6 +166,9 @@ downloadLicenses { // Explicitly declare that we are using the EPL 1.0 license (group('javax.persistence')) : epl1, + // jnr-posix is released under a tri EPL v2.0/GPL/LGPL license + 'com.github.jnr:jnr-posix:3.0.47' : epl2, + // io.netty:netty-tcnative-boringssl-static license markings are not machine readable. // io.netty:netty-tcnative-classes license markings are not machine readable. // Explicitly state Apache 2 License for license scanning. diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 6bb70ca0c55..691a774082f 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -69,6 +69,7 @@ dependencyManagement { dependency 'io.netty:netty-all:4.1.74.Final' dependency 'io.netty:netty-tcnative-boringssl-static:2.0.50.Final' dependency group: 'io.netty', name: 'netty-transport-native-epoll', version:'4.1.74.Final', classifier: 'linux-x86_64' + dependency group: 'io.netty', name: 'netty-transport-native-kqueue', version:'4.1.74.Final', classifier: 'osx-x86_64' dependency 'io.opentelemetry:opentelemetry-api:1.6.0' dependency 'io.opentelemetry:opentelemetry-exporter-otlp-metrics:1.6.0-alpha' @@ -97,6 +98,7 @@ dependencyManagement { entry 'vertx-auth-jwt' entry 'vertx-codegen' entry 'vertx-core' + entry 'vertx-junit5' entry 'vertx-unit' entry 'vertx-web' entry 'vertx-web-client' @@ -172,9 +174,7 @@ dependencyManagement { dependency 'org.web3j:abi:4.8.9' dependency 'org.web3j:besu:4.8.9' - dependency('org.web3j:core:4.8.9') { - exclude group: 'com.github.jnr', name: 'jnr-unixsocket' - } + dependency 'org.web3j:core:4.8.9' dependency 'org.web3j:crypto:4.8.9' dependency 'org.web3j:quorum:4.8.4'