diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cd7dfb51..84e32c4630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ What this means for you: permissions on the directory allow other users and groups to r/w. Ideally this should be set to `besu:besu` and `orion:orion` as the owners. +## 1.4.6 + +### Additions and Improvements + +- Implemented WebSocket logs subscription for private contracts (`priv_subscribe`/`priv_unsubscribe`) [#762] + ## 1.4.5 ### Additions and Improvements diff --git a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java index dc673e8a09..903c8572ba 100644 --- a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -42,6 +42,7 @@ 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; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketService; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.PrivateWebSocketMethodsFactory; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.WebSocketMethodsFactory; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscriptionService; @@ -50,6 +51,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.pending. import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.pending.PendingTransactionSubscriptionService; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.SyncingSubscriptionService; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.api.query.PrivacyQueries; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.MiningParameters; @@ -84,6 +86,7 @@ import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningControl import org.hyperledger.besu.ethereum.permissioning.node.PeerPermissionsAdapter; import org.hyperledger.besu.ethereum.stratum.StratumServer; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.metrics.ObservableMetricsSystem; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.metrics.prometheus.MetricsService; @@ -523,7 +526,11 @@ public class RunnerBuilder { final SubscriptionManager subscriptionManager = createSubscriptionManager(vertx, transactionPool); - createLogsSubscriptionService(context.getBlockchain(), subscriptionManager); + createLogsSubscriptionService( + context.getBlockchain(), + context.getWorldStateArchive(), + subscriptionManager, + privacyParameters); createNewBlockHeadersSubscriptionService( context.getBlockchain(), blockchainQueries, subscriptionManager); @@ -533,7 +540,14 @@ public class RunnerBuilder { webSocketService = Optional.of( createWebsocketService( - vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods)); + vertx, + webSocketConfiguration, + subscriptionManager, + webSocketsJsonRpcMethods, + privacyParameters, + protocolSchedule, + blockchainQueries, + transactionPool)); } Optional metricsService = Optional.empty(); @@ -698,11 +712,31 @@ public class RunnerBuilder { } private void createLogsSubscriptionService( - final Blockchain blockchain, final SubscriptionManager subscriptionManager) { + final Blockchain blockchain, + final WorldStateArchive worldStateArchive, + final SubscriptionManager subscriptionManager, + final PrivacyParameters privacyParameters) { + + Optional privacyQueries = Optional.empty(); + if (privacyParameters.isEnabled()) { + final BlockchainQueries blockchainQueries = + new BlockchainQueries(blockchain, worldStateArchive); + privacyQueries = + Optional.of( + new PrivacyQueries( + blockchainQueries, privacyParameters.getPrivateWorldStateReader())); + } + final LogsSubscriptionService logsSubscriptionService = - new LogsSubscriptionService(subscriptionManager); + new LogsSubscriptionService(subscriptionManager, privacyQueries); + // monitoring public logs blockchain.observeLogs(logsSubscriptionService); + + // monitoring private logs + if (privacyParameters.isEnabled()) { + blockchain.observeBlockAdded(logsSubscriptionService::checkPrivateLogs); + } } private void createSyncingSubscriptionService( @@ -724,9 +758,27 @@ public class RunnerBuilder { final Vertx vertx, final WebSocketConfiguration configuration, final SubscriptionManager subscriptionManager, - final Map jsonRpcMethods) { + final Map jsonRpcMethods, + final PrivacyParameters privacyParameters, + final ProtocolSchedule protocolSchedule, + final BlockchainQueries blockchainQueries, + final TransactionPool transactionPool) { + final WebSocketMethodsFactory websocketMethodsFactory = new WebSocketMethodsFactory(subscriptionManager, jsonRpcMethods); + + if (privacyParameters.isEnabled()) { + final PrivateWebSocketMethodsFactory privateWebSocketMethodsFactory = + new PrivateWebSocketMethodsFactory( + privacyParameters, + subscriptionManager, + protocolSchedule, + blockchainQueries, + transactionPool); + + privateWebSocketMethodsFactory.methods().forEach(websocketMethodsFactory::addMethods); + } + final WebSocketRequestHandler websocketRequestHandler = new WebSocketRequestHandler(vertx, websocketMethodsFactory.methods()); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index 6eb757f6a6..42ea05ecf9 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -52,6 +52,8 @@ public enum RpcMethod { PRIV_UNINSTALL_FILTER("priv_uninstallFilter"), PRIV_GET_FILTER_CHANGES("priv_getFilterChanges"), PRIV_GET_FILTER_LOGS("priv_getFilterLogs"), + PRIV_SUBSCRIBE("priv_subscribe"), + PRIV_UNSUBSCRIBE("priv_unsubscribe"), PRIVX_FIND_PRIVACY_GROUP("privx_findOnChainPrivacyGroup"), EEA_SEND_RAW_TRANSACTION("eea_sendRawTransaction"), ETH_ACCOUNTS("eth_accounts"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/EnclavePublicKeyProvider.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/EnclavePublicKeyProvider.java index 44a3350530..184fa1da56 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/EnclavePublicKeyProvider.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/EnclavePublicKeyProvider.java @@ -14,11 +14,34 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.MultiTenancyUserUtil.enclavePublicKey; + +import org.hyperledger.besu.ethereum.core.PrivacyParameters; + import java.util.Optional; import io.vertx.ext.auth.User; @FunctionalInterface public interface EnclavePublicKeyProvider { + String getEnclaveKey(Optional user); + + static EnclavePublicKeyProvider build(final PrivacyParameters privacyParameters) { + return privacyParameters.isMultiTenancyEnabled() + ? multiTenancyEnclavePublicKeyProvider() + : defaultEnclavePublicKeyProvider(privacyParameters); + } + + private static EnclavePublicKeyProvider multiTenancyEnclavePublicKeyProvider() { + return user -> + enclavePublicKey(user) + .orElseThrow( + () -> new IllegalStateException("Request does not contain an authorization token")); + } + + private static EnclavePublicKeyProvider defaultEnclavePublicKeyProvider( + final PrivacyParameters privacyParameters) { + return user -> privacyParameters.getEnclavePublicKey(); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java index 8b1f091d4b..551bfd15e2 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.methods; -import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.MultiTenancyUserUtil.enclavePublicKey; - import org.hyperledger.besu.ethereum.api.jsonrpc.LatestNonceProvider; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.DisabledPrivacyRpcMethod; @@ -94,7 +92,8 @@ public abstract class PrivacyApiGroupJsonRpcMethods extends ApiGroupJsonRpcMetho final PrivateMarkerTransactionFactory markerTransactionFactory = createPrivateMarkerTransactionFactory( privacyParameters, blockchainQueries, transactionPool.getPendingTransactions()); - final EnclavePublicKeyProvider enclavePublicProvider = createEnclavePublicKeyProvider(); + final EnclavePublicKeyProvider enclavePublicProvider = + EnclavePublicKeyProvider.build(privacyParameters); final PrivacyController privacyController = createPrivacyController(markerTransactionFactory); return create(privacyController, enclavePublicProvider).entrySet().stream() .collect( @@ -123,23 +122,6 @@ public abstract class PrivacyApiGroupJsonRpcMethods extends ApiGroupJsonRpcMetho return new RandomSigningPrivateMarkerTransactionFactory(privateContractAddress); } - private EnclavePublicKeyProvider createEnclavePublicKeyProvider() { - return privacyParameters.isMultiTenancyEnabled() - ? multiTenancyEnclavePublicKeyProvider() - : defaultEnclavePublicKeyProvider(); - } - - private EnclavePublicKeyProvider multiTenancyEnclavePublicKeyProvider() { - return user -> - enclavePublicKey(user) - .orElseThrow( - () -> new IllegalStateException("Request does not contain an authorization token")); - } - - private EnclavePublicKeyProvider defaultEnclavePublicKeyProvider() { - return user -> privacyParameters.getEnclavePublicKey(); - } - private PrivacyController createPrivacyController( final PrivateMarkerTransactionFactory markerTransactionFactory) { final Optional chainId = protocolSchedule.getChainId(); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java index 71d6978010..0838116979 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java @@ -17,6 +17,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService; import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationUtils; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; @@ -83,6 +84,9 @@ public class WebSocketRequestHandler { future.complete( new JsonRpcUnauthorizedResponse(request.getId(), JsonRpcError.UNAUTHORIZED)); } + } catch (final InvalidJsonRpcParameters e) { + LOG.debug("Invalid Params", e); + future.complete(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS)); } catch (final Exception e) { LOG.error(JsonRpcError.INTERNAL_ERROR.getMessage(), e); future.complete(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR)); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/AbstractPrivateSubscriptionMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/AbstractPrivateSubscriptionMethod.java new file mode 100644 index 0000000000..59a740835f --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/AbstractPrivateSubscriptionMethod.java @@ -0,0 +1,43 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; + +abstract class AbstractPrivateSubscriptionMethod extends AbstractSubscriptionMethod { + + private final PrivacyController privacyController; + private final EnclavePublicKeyProvider enclavePublicKeyProvider; + + AbstractPrivateSubscriptionMethod( + final SubscriptionManager subscriptionManager, + final SubscriptionRequestMapper mapper, + final PrivacyController privacyController, + final EnclavePublicKeyProvider enclavePublicKeyProvider) { + super(subscriptionManager, mapper); + this.privacyController = privacyController; + this.enclavePublicKeyProvider = enclavePublicKeyProvider; + } + + void checkIfPrivacyGroupMatchesAuthenticatedEnclaveKey( + final JsonRpcRequestContext request, final String privacyGroupId) { + final String enclavePublicKey = enclavePublicKeyProvider.getEnclaveKey(request.getUser()); + privacyController.verifyPrivacyGroupContainsEnclavePublicKey(privacyGroupId, enclavePublicKey); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribe.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribe.java new file mode 100644 index 0000000000..b324b5ca34 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribe.java @@ -0,0 +1,64 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateSubscribeRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; + +public class PrivSubscribe extends AbstractPrivateSubscriptionMethod { + + public PrivSubscribe( + final SubscriptionManager subscriptionManager, + final SubscriptionRequestMapper mapper, + final PrivacyController privacyController, + final EnclavePublicKeyProvider enclavePublicKeyProvider) { + super(subscriptionManager, mapper, privacyController, enclavePublicKeyProvider); + } + + @Override + public String getName() { + return RpcMethod.PRIV_SUBSCRIBE.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + try { + final PrivateSubscribeRequest subscribeRequest = + getMapper().mapPrivateSubscribeRequest(requestContext); + + checkIfPrivacyGroupMatchesAuthenticatedEnclaveKey( + requestContext, subscribeRequest.getPrivacyGroupId()); + + final Long subscriptionId = subscriptionManager().subscribe(subscribeRequest); + + return new JsonRpcSuccessResponse( + requestContext.getRequest().getId(), Quantity.create(subscriptionId)); + } catch (final InvalidSubscriptionRequestException isEx) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.INVALID_REQUEST); + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribe.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribe.java new file mode 100644 index 0000000000..21cbdb46f5 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribe.java @@ -0,0 +1,66 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionNotFoundException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateUnsubscribeRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; + +public class PrivUnsubscribe extends AbstractPrivateSubscriptionMethod { + + public PrivUnsubscribe( + final SubscriptionManager subscriptionManager, + final SubscriptionRequestMapper mapper, + final PrivacyController privacyController, + final EnclavePublicKeyProvider enclavePublicKeyProvider) { + super(subscriptionManager, mapper, privacyController, enclavePublicKeyProvider); + } + + @Override + public String getName() { + return RpcMethod.PRIV_UNSUBSCRIBE.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + try { + final PrivateUnsubscribeRequest unsubscribeRequest = + getMapper().mapPrivateUnsubscribeRequest(requestContext); + + checkIfPrivacyGroupMatchesAuthenticatedEnclaveKey( + requestContext, unsubscribeRequest.getPrivacyGroupId()); + + final boolean unsubscribed = subscriptionManager().unsubscribe(unsubscribeRequest); + + return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), unsubscribed); + } catch (final InvalidSubscriptionRequestException isEx) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.INVALID_REQUEST); + } catch (final SubscriptionNotFoundException snfEx) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.SUBSCRIPTION_NOT_FOUND); + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivateWebSocketMethodsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivateWebSocketMethodsFactory.java new file mode 100644 index 0000000000..5cf1a4ee40 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivateWebSocketMethodsFactory.java @@ -0,0 +1,127 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.LatestNonceProvider; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.PrivacyParameters; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.privacy.ChainHeadPrivateNonceProvider; +import org.hyperledger.besu.ethereum.privacy.DefaultPrivacyController; +import org.hyperledger.besu.ethereum.privacy.MultiTenancyPrivacyController; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; +import org.hyperledger.besu.ethereum.privacy.PrivateNonceProvider; +import org.hyperledger.besu.ethereum.privacy.PrivateTransactionSimulator; +import org.hyperledger.besu.ethereum.privacy.markertransaction.FixedKeySigningPrivateMarkerTransactionFactory; +import org.hyperledger.besu.ethereum.privacy.markertransaction.PrivateMarkerTransactionFactory; +import org.hyperledger.besu.ethereum.privacy.markertransaction.RandomSigningPrivateMarkerTransactionFactory; + +import java.math.BigInteger; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +public class PrivateWebSocketMethodsFactory { + + private final PrivacyParameters privacyParameters; + private final SubscriptionManager subscriptionManager; + private final ProtocolSchedule protocolSchedule; + private final BlockchainQueries blockchainQueries; + private final TransactionPool transactionPool; + + public PrivateWebSocketMethodsFactory( + final PrivacyParameters privacyParameters, + final SubscriptionManager subscriptionManager, + final ProtocolSchedule protocolSchedule, + final BlockchainQueries blockchainQueries, + final TransactionPool transactionPool) { + this.privacyParameters = privacyParameters; + this.subscriptionManager = subscriptionManager; + this.protocolSchedule = protocolSchedule; + this.blockchainQueries = blockchainQueries; + this.transactionPool = transactionPool; + } + + public Collection methods() { + final SubscriptionRequestMapper subscriptionRequestMapper = new SubscriptionRequestMapper(); + final EnclavePublicKeyProvider enclavePublicKeyProvider = + EnclavePublicKeyProvider.build(privacyParameters); + final PrivacyController privacyController = createPrivacyController(); + + return Set.of( + new PrivSubscribe( + subscriptionManager, + subscriptionRequestMapper, + privacyController, + enclavePublicKeyProvider), + new PrivUnsubscribe( + subscriptionManager, + subscriptionRequestMapper, + privacyController, + enclavePublicKeyProvider)); + } + + private PrivateMarkerTransactionFactory createPrivateMarkerTransactionFactory() { + + final Address privateContractAddress = + Address.privacyPrecompiled(privacyParameters.getPrivacyAddress()); + + if (privacyParameters.getSigningKeyPair().isPresent()) { + return new FixedKeySigningPrivateMarkerTransactionFactory( + privateContractAddress, + new LatestNonceProvider(blockchainQueries, transactionPool.getPendingTransactions()), + privacyParameters.getSigningKeyPair().get()); + } + return new RandomSigningPrivateMarkerTransactionFactory(privateContractAddress); + } + + private PrivacyController createPrivacyController() { + final Optional chainId = protocolSchedule.getChainId(); + final DefaultPrivacyController defaultPrivacyController = + new DefaultPrivacyController( + blockchainQueries.getBlockchain(), + privacyParameters, + chainId, + createPrivateMarkerTransactionFactory(), + createPrivateTransactionSimulator(), + createPrivateNonceProvider(), + privacyParameters.getPrivateWorldStateReader()); + return privacyParameters.isMultiTenancyEnabled() + ? new MultiTenancyPrivacyController( + defaultPrivacyController, chainId, privacyParameters.getEnclave()) + : defaultPrivacyController; + } + + private PrivateTransactionSimulator createPrivateTransactionSimulator() { + return new PrivateTransactionSimulator( + blockchainQueries.getBlockchain(), + blockchainQueries.getWorldStateArchive(), + protocolSchedule, + privacyParameters); + } + + private PrivateNonceProvider createPrivateNonceProvider() { + return new ChainHeadPrivateNonceProvider( + blockchainQueries.getBlockchain(), + privacyParameters.getPrivateStateRootResolver(), + privacyParameters.getPrivateWorldStateArchive()); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/WebSocketMethodsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/WebSocketMethodsFactory.java index f8ef2e7af1..d6d5aef217 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/WebSocketMethodsFactory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/WebSocketMethodsFactory.java @@ -23,31 +23,28 @@ import java.util.Map; public class WebSocketMethodsFactory { - private final SubscriptionManager subscriptionManager; - private final Map jsonRpcMethods; + private final Map methods = new HashMap<>(); public WebSocketMethodsFactory( final SubscriptionManager subscriptionManager, final Map jsonRpcMethods) { - this.subscriptionManager = subscriptionManager; - this.jsonRpcMethods = jsonRpcMethods; + this.methods.putAll(jsonRpcMethods); + buildWebsocketMethods(subscriptionManager); } - public Map methods() { - final Map websocketMethods = new HashMap<>(); - websocketMethods.putAll(jsonRpcMethods); + private void buildWebsocketMethods(final SubscriptionManager subscriptionManager) { addMethods( - websocketMethods, new EthSubscribe(subscriptionManager, new SubscriptionRequestMapper()), new EthUnsubscribe(subscriptionManager, new SubscriptionRequestMapper())); - return websocketMethods; } - public Map addMethods( - final Map methods, final JsonRpcMethod... rpcMethods) { + public Map methods() { + return methods; + } + + public void addMethods(final JsonRpcMethod... rpcMethods) { for (final JsonRpcMethod rpcMethod : rpcMethods) { methods.put(rpcMethod.getName(), rpcMethod); } - return methods; } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilder.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilder.java index d2bce604ff..908091c222 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilder.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilder.java @@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscription; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs.LogsSubscription; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs.PrivateLogsSubscription; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateSubscribeRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscribeRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionType; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.SyncingSubscription; @@ -35,7 +37,7 @@ public class SubscriptionBuilder { } case LOGS: { - return new LogsSubscription(subscriptionId, connectionId, request.getFilterParameter()); + return logsSubscription(subscriptionId, connectionId, request); } case SYNCING: { @@ -48,6 +50,19 @@ public class SubscriptionBuilder { } } + private Subscription logsSubscription( + final long subscriptionId, final String connectionId, final SubscribeRequest request) { + if (request instanceof PrivateSubscribeRequest) { + return new PrivateLogsSubscription( + subscriptionId, + connectionId, + request.getFilterParameter(), + ((PrivateSubscribeRequest) request).getPrivacyGroupId()); + } else { + return new LogsSubscription(subscriptionId, connectionId, request.getFilterParameter()); + } + } + @SuppressWarnings("unchecked") public Function mapToSubscriptionClass(final Class clazz) { return subscription -> { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManager.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManager.java index 74e2886249..bf7913486b 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManager.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManager.java @@ -134,10 +134,10 @@ public class SubscriptionManager extends AbstractVerticle { } public void sendMessage(final Long subscriptionId, final JsonRpcResult msg) { - final SubscriptionResponse response = new SubscriptionResponse(subscriptionId, msg); - final Subscription subscription = subscriptions.get(subscriptionId); + if (subscription != null) { + final SubscriptionResponse response = new SubscriptionResponse(subscription, msg); vertx.eventBus().send(subscription.getConnectionId(), Json.encode(response)); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java index 68128b5e05..36b3cd8c72 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java @@ -18,17 +18,25 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParam import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.LogResult; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionType; +import org.hyperledger.besu.ethereum.api.query.LogsQuery; +import org.hyperledger.besu.ethereum.api.query.PrivacyQueries; +import org.hyperledger.besu.ethereum.chain.BlockAddedEvent; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.LogWithMetadata; +import java.util.Optional; import java.util.function.Consumer; public class LogsSubscriptionService implements Consumer { private final SubscriptionManager subscriptionManager; + private final Optional privacyQueries; - public LogsSubscriptionService(final SubscriptionManager subscriptionManager) { + public LogsSubscriptionService( + final SubscriptionManager subscriptionManager, + final Optional privacyQueries) { this.subscriptionManager = subscriptionManager; + this.privacyQueries = privacyQueries; } @Override @@ -46,9 +54,34 @@ public class LogsSubscriptionService implements Consumer { && filterParameter.getToBlock().getNumber().orElse(Long.MAX_VALUE) >= blockNumber && filterParameter.getLogsQuery().matches(logWithMetadata); }) - .forEach( - logsSubscription -> - subscriptionManager.sendMessage( - logsSubscription.getSubscriptionId(), new LogResult(logWithMetadata))); + .forEach(logsSubscription -> sendLogToSubscription(logWithMetadata, logsSubscription)); + } + + public void checkPrivateLogs(final BlockAddedEvent event) { + privacyQueries.ifPresent( + pq -> + subscriptionManager.subscriptionsOfType(SubscriptionType.LOGS, LogsSubscription.class) + .stream() + .filter(PrivateLogsSubscription.class::isInstance) + .map(PrivateLogsSubscription.class::cast) + .forEach(queryPrivateEventForSubscription(pq, event))); + } + + private Consumer queryPrivateEventForSubscription( + final PrivacyQueries privacyQueries, final BlockAddedEvent event) { + return subscription -> { + final String privacyGroupId = subscription.getPrivacyGroupId(); + final LogsQuery logsQuery = subscription.getFilterParameter().getLogsQuery(); + + privacyQueries + .matchingLogs(privacyGroupId, event.getBlock().getHash(), logsQuery) + .forEach(logWithMetadata -> sendLogToSubscription(logWithMetadata, subscription)); + }; + } + + private void sendLogToSubscription( + final LogWithMetadata logWithMetadata, final LogsSubscription subscription) { + subscriptionManager.sendMessage( + subscription.getSubscriptionId(), new LogResult(logWithMetadata)); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/PrivateLogsSubscription.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/PrivateLogsSubscription.java new file mode 100644 index 0000000000..45d73646c0 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/PrivateLogsSubscription.java @@ -0,0 +1,35 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParameter; + +public class PrivateLogsSubscription extends LogsSubscription { + + private final String privacyGroupId; + + public PrivateLogsSubscription( + final Long subscriptionId, + final String connectionId, + final FilterParameter filterParameter, + final String privacyGroupId) { + super(subscriptionId, connectionId, filterParameter); + this.privacyGroupId = privacyGroupId; + } + + public String getPrivacyGroupId() { + return privacyGroupId; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java index b14ab07009..59208a45f4 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java @@ -20,6 +20,10 @@ public class InvalidSubscriptionRequestException extends RuntimeException { super(); } + public InvalidSubscriptionRequestException(final String message) { + super(message); + } + public InvalidSubscriptionRequestException(final String message, final Throwable cause) { super(message, cause); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateSubscribeRequest.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateSubscribeRequest.java new file mode 100644 index 0000000000..e4502dc843 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateSubscribeRequest.java @@ -0,0 +1,58 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParameter; + +import java.util.Objects; + +public class PrivateSubscribeRequest extends SubscribeRequest { + + private final String privacyGroupId; + + public PrivateSubscribeRequest( + final SubscriptionType subscriptionType, + final FilterParameter filterParameter, + final Boolean includeTransaction, + final String connectionId, + final String privacyGroupId) { + super(subscriptionType, filterParameter, includeTransaction, connectionId); + this.privacyGroupId = privacyGroupId; + } + + public String getPrivacyGroupId() { + return privacyGroupId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + PrivateSubscribeRequest that = (PrivateSubscribeRequest) o; + return privacyGroupId.equals(that.privacyGroupId); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), privacyGroupId); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateUnsubscribeRequest.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateUnsubscribeRequest.java new file mode 100644 index 0000000000..201edfb253 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/PrivateUnsubscribeRequest.java @@ -0,0 +1,52 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request; + +import java.util.Objects; + +public class PrivateUnsubscribeRequest extends UnsubscribeRequest { + + private final String privacyGroupId; + + public PrivateUnsubscribeRequest( + final Long subscriptionId, final String connectionId, final String privacyGroupId) { + super(subscriptionId, connectionId); + this.privacyGroupId = privacyGroupId; + } + + public String getPrivacyGroupId() { + return privacyGroupId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + PrivateUnsubscribeRequest that = (PrivateUnsubscribeRequest) o; + return privacyGroupId.equals(that.privacyGroupId); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), privacyGroupId); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java index 31c3d62225..1048d15f84 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java @@ -83,11 +83,60 @@ public class SubscriptionRequestMapper { final long subscriptionId = webSocketRpcRequestBody.getRequiredParameter(0, UnsignedLongParameter.class).getValue(); return new UnsubscribeRequest(subscriptionId, webSocketRpcRequestBody.getConnectionId()); + } catch (final Exception e) { + throw new InvalidSubscriptionRequestException("Error parsing unsubscribe request", e); + } + } + + public PrivateSubscribeRequest mapPrivateSubscribeRequest( + final JsonRpcRequestContext jsonRpcRequestContext) + throws InvalidSubscriptionRequestException { + try { + final WebSocketRpcRequest webSocketRpcRequestBody = validateRequest(jsonRpcRequestContext); + + final String privacyGroupId = webSocketRpcRequestBody.getRequiredParameter(0, String.class); + final SubscriptionType subscriptionType = + webSocketRpcRequestBody.getRequiredParameter(1, SubscriptionType.class); + + switch (subscriptionType) { + case LOGS: + { + final FilterParameter filterParameter = + jsonRpcRequestContext.getRequiredParameter(2, FilterParameter.class); + return new PrivateSubscribeRequest( + SubscriptionType.LOGS, + filterParameter, + null, + webSocketRpcRequestBody.getConnectionId(), + privacyGroupId); + } + default: + throw new InvalidSubscriptionRequestException( + "Invalid subscribe request. Invalid private subscription type."); + } + } catch (final InvalidSubscriptionRequestException e) { + throw e; } catch (final Exception e) { throw new InvalidSubscriptionRequestException("Error parsing subscribe request", e); } } + public PrivateUnsubscribeRequest mapPrivateUnsubscribeRequest( + final JsonRpcRequestContext jsonRpcRequestContext) + throws InvalidSubscriptionRequestException { + try { + final WebSocketRpcRequest webSocketRpcRequestBody = validateRequest(jsonRpcRequestContext); + + final String privacyGroupId = webSocketRpcRequestBody.getRequiredParameter(0, String.class); + final long subscriptionId = + webSocketRpcRequestBody.getRequiredParameter(1, UnsignedLongParameter.class).getValue(); + return new PrivateUnsubscribeRequest( + subscriptionId, webSocketRpcRequestBody.getConnectionId(), privacyGroupId); + } catch (final Exception e) { + throw new InvalidSubscriptionRequestException("Error parsing unsubscribe request", e); + } + } + private WebSocketRpcRequest validateRequest(final JsonRpcRequestContext jsonRpcRequestContext) { if (jsonRpcRequestContext.getRequest() instanceof WebSocketRpcRequest) { return (WebSocketRpcRequest) jsonRpcRequestContext.getRequest(); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponse.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponse.java index f99ac5cfce..5f71b94f3b 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponse.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponse.java @@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.respons import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.JsonRpcResult; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.Subscription; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs.PrivateLogsSubscription; import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -24,12 +26,24 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; public class SubscriptionResponse { private static final String JSON_RPC_VERSION = "2.0"; - private static final String METHOD_NAME = "eth_subscription"; + private static final String ETH_SUBSCRIPTION_METHOD = "eth_subscription"; + private static final String PRIV_SUBSCRIPTION_METHOD = "priv_subscription"; + private final String methodName; private final SubscriptionResponseResult params; - public SubscriptionResponse(final long subscriptionId, final JsonRpcResult result) { - this.params = new SubscriptionResponseResult(Quantity.create(subscriptionId), result); + public SubscriptionResponse(final Subscription subscription, final JsonRpcResult result) { + if (subscription instanceof PrivateLogsSubscription) { + final String privacyGroupId = ((PrivateLogsSubscription) subscription).getPrivacyGroupId(); + this.methodName = PRIV_SUBSCRIPTION_METHOD; + this.params = + new SubscriptionResponseResult( + Quantity.create(subscription.getSubscriptionId()), result, privacyGroupId); + } else { + this.methodName = ETH_SUBSCRIPTION_METHOD; + this.params = + new SubscriptionResponseResult(Quantity.create(subscription.getSubscriptionId()), result); + } } @JsonGetter("jsonrpc") @@ -39,7 +53,7 @@ public class SubscriptionResponse { @JsonGetter("method") public String getMethod() { - return METHOD_NAME; + return methodName; } @JsonGetter("params") diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java index 337c034ba2..2805c3030e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java @@ -17,17 +17,28 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.respons import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.JsonRpcResult; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder({"subscription", "result"}) +@JsonPropertyOrder({"subscription", "privacyGroupId", "result"}) public class SubscriptionResponseResult { private final String subscription; private final JsonRpcResult result; + @JsonInclude(Include.NON_NULL) + private final String privacyGroupId; + SubscriptionResponseResult(final String subscription, final JsonRpcResult result) { + this(subscription, result, null); + } + + SubscriptionResponseResult( + final String subscription, final JsonRpcResult result, final String privacyGroupId) { this.subscription = subscription; this.result = result; + this.privacyGroupId = privacyGroupId; } @JsonGetter("subscription") @@ -39,4 +50,9 @@ public class SubscriptionResponseResult { public JsonRpcResult getResult() { return result; } + + @JsonGetter("privacyGroupId") + public String getPrivacyGroupId() { + return privacyGroupId; + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandlerTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandlerTest.java index 720a58ba97..3e05d38b4f 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandlerTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandlerTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; @@ -167,6 +168,34 @@ public class WebSocketRequestHandlerTest { async.awaitSuccess(WebSocketRequestHandlerTest.VERTX_AWAIT_TIMEOUT_MILLIS); } + @Test + public void onInvalidJsonRpcParametersExceptionProcessingRequestShouldRespondInvalidParams( + final TestContext context) { + final Async async = context.async(); + + final JsonObject requestJson = new JsonObject().put("id", 1).put("method", "eth_x"); + final JsonRpcRequestContext expectedRequest = + new JsonRpcRequestContext(requestJson.mapTo(WebSocketRpcRequest.class)); + when(jsonRpcMethodMock.response(eq(expectedRequest))) + .thenThrow(new InvalidJsonRpcParameters("")); + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(1, JsonRpcError.INVALID_PARAMS); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(expectedResponse), msg.body()); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, requestJson.toString())); + + async.awaitSuccess(WebSocketRequestHandlerTest.VERTX_AWAIT_TIMEOUT_MILLIS); + } + @Test public void onExceptionProcessingRequestShouldRespondInternalError(final TestContext context) { final Async async = context.async(); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribeTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribeTest.java new file mode 100644 index 0000000000..e6d3b9682b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivSubscribeTest.java @@ -0,0 +1,144 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +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.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateSubscribeRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionType; +import org.hyperledger.besu.ethereum.privacy.MultiTenancyValidationException; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; + +import io.vertx.core.json.Json; +import io.vertx.ext.auth.User; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PrivSubscribeTest { + + private final String ENCLAVE_KEY = "enclave_key"; + private final String PRIVACY_GROUP_ID = "B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="; + + @Mock private SubscriptionManager subscriptionManagerMock; + @Mock private SubscriptionRequestMapper mapperMock; + @Mock private PrivacyController privacyController; + @Mock private EnclavePublicKeyProvider enclavePublicKeyProvider; + + private PrivSubscribe privSubscribe; + + @Before + public void before() { + privSubscribe = + new PrivSubscribe( + subscriptionManagerMock, mapperMock, privacyController, enclavePublicKeyProvider); + } + + @Test + public void expectedMethodName() { + assertThat(privSubscribe.getName()).isEqualTo("priv_subscribe"); + } + + @Test + public void responseContainsSubscriptionId() { + final WebSocketRpcRequest webSocketRequest = createWebSocketRpcRequest(); + final JsonRpcRequestContext jsonRpcrequestContext = new JsonRpcRequestContext(webSocketRequest); + + final PrivateSubscribeRequest subscribeRequest = + new PrivateSubscribeRequest( + SubscriptionType.LOGS, + null, + null, + webSocketRequest.getConnectionId(), + PRIVACY_GROUP_ID); + + when(mapperMock.mapPrivateSubscribeRequest(eq(jsonRpcrequestContext))) + .thenReturn(subscribeRequest); + when(subscriptionManagerMock.subscribe(eq(subscribeRequest))).thenReturn(1L); + + final JsonRpcSuccessResponse expectedResponse = + new JsonRpcSuccessResponse( + jsonRpcrequestContext.getRequest().getId(), Quantity.create((1L))); + + assertThat(privSubscribe.response(jsonRpcrequestContext)).isEqualTo(expectedResponse); + } + + @Test + public void invalidSubscribeRequestRespondsInvalidRequestResponse() { + final WebSocketRpcRequest webSocketRequest = createWebSocketRpcRequest(); + final JsonRpcRequestContext jsonRpcrequestContext = new JsonRpcRequestContext(webSocketRequest); + + when(mapperMock.mapPrivateSubscribeRequest(any())) + .thenThrow(new InvalidSubscriptionRequestException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse( + jsonRpcrequestContext.getRequest().getId(), JsonRpcError.INVALID_REQUEST); + + assertThat(privSubscribe.response(jsonRpcrequestContext)).isEqualTo(expectedResponse); + } + + @Test + public void multiTenancyCheckFailure() { + final User user = mock(User.class); + final WebSocketRpcRequest webSocketRequest = createWebSocketRpcRequest(); + final JsonRpcRequestContext jsonRpcrequestContext = + new JsonRpcRequestContext(webSocketRequest, user); + + final PrivateSubscribeRequest subscribeRequest = + new PrivateSubscribeRequest( + SubscriptionType.LOGS, + null, + null, + webSocketRequest.getConnectionId(), + PRIVACY_GROUP_ID); + + when(mapperMock.mapPrivateSubscribeRequest(any())).thenReturn(subscribeRequest); + when(enclavePublicKeyProvider.getEnclaveKey(any())).thenReturn(ENCLAVE_KEY); + doThrow(new MultiTenancyValidationException("msg")) + .when(privacyController) + .verifyPrivacyGroupContainsEnclavePublicKey(eq(PRIVACY_GROUP_ID), eq(ENCLAVE_KEY)); + + assertThatThrownBy(() -> privSubscribe.response(jsonRpcrequestContext)) + .isInstanceOf(MultiTenancyValidationException.class) + .hasMessageContaining("msg"); + } + + private WebSocketRpcRequest createWebSocketRpcRequest() { + return Json.decodeValue( + "{\"id\": 1, \"method\": \"priv_subscribe\", \"params\": [\"" + + PRIVACY_GROUP_ID + + "\", \"logs\"], \"connectionId\": \"1\"}", + WebSocketRpcRequest.class); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribeTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribeTest.java new file mode 100644 index 0000000000..50db561cfb --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/methods/PrivUnsubscribeTest.java @@ -0,0 +1,150 @@ +/* + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.EnclavePublicKeyProvider; +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.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionNotFoundException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateUnsubscribeRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import org.hyperledger.besu.ethereum.privacy.MultiTenancyValidationException; +import org.hyperledger.besu.ethereum.privacy.PrivacyController; + +import io.vertx.core.json.Json; +import io.vertx.ext.auth.User; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PrivUnsubscribeTest { + + private final String ENCLAVE_KEY = "enclave_key"; + private final String PRIVACY_GROUP_ID = "B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="; + private final String CONNECTION_ID = "test-connection-id"; + + @Mock private SubscriptionManager subscriptionManagerMock; + @Mock private SubscriptionRequestMapper mapperMock; + @Mock private PrivacyController privacyController; + @Mock private EnclavePublicKeyProvider enclavePublicKeyProvider; + + private PrivUnsubscribe privUnsubscribe; + + @Before + public void before() { + privUnsubscribe = + new PrivUnsubscribe( + subscriptionManagerMock, mapperMock, privacyController, enclavePublicKeyProvider); + } + + @Test + public void expectedMethodName() { + assertThat(privUnsubscribe.getName()).isEqualTo("priv_unsubscribe"); + } + + @Test + public void responseContainsUnsubscribeStatus() { + final JsonRpcRequestContext request = createPrivUnsubscribeRequest(); + final PrivateUnsubscribeRequest unsubscribeRequest = + new PrivateUnsubscribeRequest(1L, CONNECTION_ID, PRIVACY_GROUP_ID); + when(mapperMock.mapPrivateUnsubscribeRequest(eq(request))).thenReturn(unsubscribeRequest); + when(subscriptionManagerMock.unsubscribe(eq(unsubscribeRequest))).thenReturn(true); + + final JsonRpcSuccessResponse expectedResponse = + new JsonRpcSuccessResponse(request.getRequest().getId(), true); + + assertThat(privUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void invalidUnsubscribeRequestReturnsInvalidRequestResponse() { + final JsonRpcRequestContext request = createPrivUnsubscribeRequest(); + when(mapperMock.mapPrivateUnsubscribeRequest(any())) + .thenThrow(new InvalidSubscriptionRequestException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getRequest().getId(), JsonRpcError.INVALID_REQUEST); + + assertThat(privUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void whenSubscriptionNotFoundReturnError() { + final JsonRpcRequestContext request = createPrivUnsubscribeRequest(); + when(mapperMock.mapPrivateUnsubscribeRequest(any())) + .thenReturn(mock(PrivateUnsubscribeRequest.class)); + when(subscriptionManagerMock.unsubscribe(any())) + .thenThrow(new SubscriptionNotFoundException(1L)); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getRequest().getId(), JsonRpcError.SUBSCRIPTION_NOT_FOUND); + + assertThat(privUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void multiTenancyCheckFailure() { + final User user = mock(User.class); + final JsonRpcRequestContext jsonRpcrequestContext = createPrivUnsubscribeRequestWithUser(user); + + final PrivateUnsubscribeRequest unsubscribeRequest = + new PrivateUnsubscribeRequest(0L, CONNECTION_ID, PRIVACY_GROUP_ID); + + when(mapperMock.mapPrivateUnsubscribeRequest(any())).thenReturn(unsubscribeRequest); + when(enclavePublicKeyProvider.getEnclaveKey(any())).thenReturn(ENCLAVE_KEY); + doThrow(new MultiTenancyValidationException("msg")) + .when(privacyController) + .verifyPrivacyGroupContainsEnclavePublicKey(eq(PRIVACY_GROUP_ID), eq(ENCLAVE_KEY)); + + assertThatThrownBy(() -> privUnsubscribe.response(jsonRpcrequestContext)) + .isInstanceOf(MultiTenancyValidationException.class) + .hasMessageContaining("msg"); + } + + private JsonRpcRequestContext createPrivUnsubscribeRequest() { + return new JsonRpcRequestContext( + Json.decodeValue( + "{\"id\": 1, \"method\": \"priv_unsubscribe\", \"params\": [\"" + + PRIVACY_GROUP_ID + + "\", \"0x0\"]}", + JsonRpcRequest.class)); + } + + private JsonRpcRequestContext createPrivUnsubscribeRequestWithUser(final User user) { + return new JsonRpcRequestContext( + Json.decodeValue( + "{\"id\": 1, \"method\": \"priv_unsubscribe\", \"params\": [\"" + + PRIVACY_GROUP_ID + + "\", \"0x0\"]}", + JsonRpcRequest.class), + user); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java index 00b0be68d0..ee9bb721cb 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java @@ -21,6 +21,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParame import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParameter; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscription; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs.LogsSubscription; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.logs.PrivateLogsSubscription; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.PrivateSubscribeRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscribeRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionType; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.SyncingSubscription; @@ -54,6 +56,22 @@ public class SubscriptionBuilderTest { assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); } + @Test + public void shouldBuildPrivateLogsSubscriptionWhenSubscribeRequestTypeIsPrivateLogs() { + final String privacyGroupId = "ZDmkMK7CyxA1F1rktItzKFTfRwApg7aWzsTtm2IOZ5Y="; + final FilterParameter filterParameter = filterParameter(); + final PrivateSubscribeRequest subscribeRequest = + new PrivateSubscribeRequest( + SubscriptionType.LOGS, filterParameter, null, CONNECTION_ID, privacyGroupId); + final PrivateLogsSubscription expectedSubscription = + new PrivateLogsSubscription(1L, CONNECTION_ID, filterParameter, privacyGroupId); + + final Subscription builtSubscription = + subscriptionBuilder.build(1L, CONNECTION_ID, subscribeRequest); + + assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); + } + @Test public void shouldBuildNewBlockHeadsSubscriptionWhenSubscribeRequestTypeIsNewBlockHeads() { final SubscribeRequest subscribeRequest = diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java index 7d581248e9..22413337a1 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java @@ -57,7 +57,10 @@ public class SubscriptionManagerSendMessageTest { new SubscribeRequest(SubscriptionType.SYNCING, null, null, connectionId); final JsonRpcResult expectedResult = mock(JsonRpcResult.class); - final SubscriptionResponse expectedResponse = new SubscriptionResponse(1L, expectedResult); + final Subscription subscription = + new Subscription(1L, connectionId, SubscriptionType.SYNCING, false); + final SubscriptionResponse expectedResponse = + new SubscriptionResponse(subscription, expectedResult); final Long subscriptionId = subscriptionManager.subscribe(subscribeRequest); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java index cc88f6a03a..f4a9db1436 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java @@ -26,6 +26,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParam import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.LogResult; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.api.query.PrivacyQueries; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.Block; @@ -33,9 +34,11 @@ import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.BlockDataGenerator.BlockOptions; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockWithReceipts; +import org.hyperledger.besu.ethereum.core.Hash; import org.hyperledger.besu.ethereum.core.InMemoryStorageProvider; import org.hyperledger.besu.ethereum.core.Log; import org.hyperledger.besu.ethereum.core.LogTopic; +import org.hyperledger.besu.ethereum.core.LogWithMetadata; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; @@ -43,6 +46,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -69,10 +73,14 @@ public class LogsSubscriptionServiceTest { @Mock private SubscriptionManager subscriptionManager; + @Mock private PrivacyQueries privacyQueries; + @Before public void before() { - logsSubscriptionService = new LogsSubscriptionService(subscriptionManager); + logsSubscriptionService = + new LogsSubscriptionService(subscriptionManager, Optional.of(privacyQueries)); blockchain.observeLogs(logsSubscriptionService); + blockchain.observeBlockAdded(logsSubscriptionService::checkPrivateLogs); } @Test @@ -283,6 +291,38 @@ public class LogsSubscriptionServiceTest { .sendMessage(eq(subscription.getSubscriptionId()), captor.capture()); } + @Test + public void whenExistsPrivateLogsSubscriptionPrivacyQueriesIsCalled() { + final String privacyGroupId = "privacy_group_id"; + final Address address = Address.fromHexString("0x0"); + final PrivateLogsSubscription subscription = createPrivateSubscription(privacyGroupId, address); + registerSubscriptions(subscription); + + final BlockWithReceipts blockWithReceipts = generateBlock(2, 2, 2); + blockchain.appendBlock(blockWithReceipts.getBlock(), blockWithReceipts.getReceipts()); + + verify(privacyQueries) + .matchingLogs( + eq(subscription.getPrivacyGroupId()), + eq(blockWithReceipts.getHash()), + eq(subscription.getFilterParameter().getLogsQuery())); + } + + @Test + public void whenPrivateLogsSubscriptionMatchesLogNotificationIsSent() { + final String privacyGroupId = "privacy_group_id"; + final Address address = Address.fromHexString("0x0"); + final PrivateLogsSubscription subscription = createPrivateSubscription(privacyGroupId, address); + registerSubscriptions(subscription); + + when(privacyQueries.matchingLogs(any(), any(), any())).thenReturn(List.of(logWithMetadata())); + + final BlockWithReceipts blockWithReceipts = generateBlock(2, 2, 2); + blockchain.appendBlock(blockWithReceipts.getBlock(), blockWithReceipts.getReceipts()); + + verify(subscriptionManager, times(1)).sendMessage(eq(subscription.getSubscriptionId()), any()); + } + private void assertLogResultMatches( final LogResult result, final Block block, @@ -347,6 +387,20 @@ public class LogsSubscriptionServiceTest { return new BlockWithReceipts(block, receipts); } + private PrivateLogsSubscription createPrivateSubscription( + final String privacyGroupId, final Address address) { + return new PrivateLogsSubscription( + nextSubscriptionId.incrementAndGet(), + "conn", + new FilterParameter( + BlockParameter.LATEST, + BlockParameter.LATEST, + Arrays.asList(address), + Collections.emptyList(), + null), + privacyGroupId); + } + private LogsSubscription createSubscription(final Address address) { return createSubscription(Arrays.asList(address), Collections.emptyList()); } @@ -369,4 +423,17 @@ public class LogsSubscriptionServiceTest { when(subscriptionManager.subscriptionsOfType(any(), any())) .thenReturn(Lists.newArrayList(subscriptions)); } + + private LogWithMetadata logWithMetadata() { + return new LogWithMetadata( + 0, + 100L, + Hash.ZERO, + Hash.ZERO, + 0, + Address.fromHexString("0x0"), + Bytes.EMPTY, + Lists.newArrayList(), + false); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java index 5e2d637ac5..4fc37497b0 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java @@ -374,6 +374,59 @@ public class SubscriptionRequestMapperTest { mapper.mapSubscribeRequest(new JsonRpcRequestContext(jsonRpcRequest)); } + @Test + public void mapRequestToPrivateLogsSubscription() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"priv_subscribe\", \"params\": [\"B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=\", \"logs\", {\"address\": \"0x8320fe7702b96808f7bbc0d4a888ed1468216cfd\"}]}"); + + final PrivateSubscribeRequest expectedSubscribeRequest = + new PrivateSubscribeRequest( + SubscriptionType.LOGS, + new FilterParameter( + BlockParameter.LATEST, + BlockParameter.LATEST, + singletonList(Address.fromHexString("0x8320fe7702b96808f7bbc0d4a888ed1468216cfd")), + emptyList(), + null), + null, + null, + "B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); + + final PrivateSubscribeRequest subscribeRequest = + mapper.mapPrivateSubscribeRequest(new JsonRpcRequestContext(jsonRpcRequest)); + + assertThat(subscribeRequest) + .isEqualToComparingFieldByFieldRecursively(expectedSubscribeRequest); + } + + @Test + public void mapRequestToPrivateSubscriptionWithInvalidType() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"priv_subscribe\", \"params\": [\"B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=\", \"syncing\", {\"includeTransactions\": true}]}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectMessage("Invalid subscribe request. Invalid private subscription type."); + + mapper.mapPrivateSubscribeRequest(new JsonRpcRequestContext(jsonRpcRequest)); + } + + @Test + public void mapRequestToPrivateUnsubscribeRequest() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"priv_unsubscribe\", \"params\": [\"B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=\", \"0x1\"]}"); + final PrivateUnsubscribeRequest expectedUnsubscribeRequest = + new PrivateUnsubscribeRequest( + 1L, CONNECTION_ID, "B1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); + + final PrivateUnsubscribeRequest unsubscribeRequest = + mapper.mapPrivateUnsubscribeRequest(new JsonRpcRequestContext(jsonRpcRequest)); + + assertThat(unsubscribeRequest).isEqualTo(expectedUnsubscribeRequest); + } + private WebSocketRpcRequest parseWebSocketRpcRequest(final String json) { return Json.decodeValue(json, WebSocketRpcRequest.class); } diff --git a/ethereum/referencetests/src/test/resources b/ethereum/referencetests/src/test/resources index 5841af6da4..6af0621522 160000 --- a/ethereum/referencetests/src/test/resources +++ b/ethereum/referencetests/src/test/resources @@ -1 +1 @@ -Subproject commit 5841af6da472fb3f19810354cf9a30afd8e72b5f +Subproject commit 6af0621522dd0274525457741291d391c10002be