mirror of https://github.com/hyperledger/besu
feat: Expose set finalized/safe block in plugin-api BlockchainService (#7382)
* feat: Expose set finalized and safe block in plugin-api BlockchainService * check for poa network before setting finalized block * changelog * Add BlockchainService set finalized acceptance test --------- Signed-off-by: Usman Saleem <usman@usmans.info>pull/7395/head
parent
b634b9c66c
commit
9d92ae87df
@ -0,0 +1,149 @@ |
|||||||
|
/* |
||||||
|
* 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.tests.acceptance.plugins; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; |
||||||
|
import org.hyperledger.besu.plugin.BesuContext; |
||||||
|
import org.hyperledger.besu.plugin.BesuPlugin; |
||||||
|
import org.hyperledger.besu.plugin.data.BlockContext; |
||||||
|
import org.hyperledger.besu.plugin.services.BlockchainService; |
||||||
|
import org.hyperledger.besu.plugin.services.RpcEndpointService; |
||||||
|
import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; |
||||||
|
import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; |
||||||
|
|
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import com.google.auto.service.AutoService; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
@AutoService(BesuPlugin.class) |
||||||
|
public class TestBlockchainServiceFinalizedPlugin implements BesuPlugin { |
||||||
|
private static final Logger LOG = |
||||||
|
LoggerFactory.getLogger(TestBlockchainServiceFinalizedPlugin.class); |
||||||
|
private static final String RPC_NAMESPACE = "updater"; |
||||||
|
private static final String RPC_METHOD_FINALIZED_BLOCK = "updateFinalizedBlockV1"; |
||||||
|
private static final String RPC_METHOD_SAFE_BLOCK = "updateSafeBlockV1"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void register(final BesuContext besuContext) { |
||||||
|
LOG.trace("Registering plugin ..."); |
||||||
|
|
||||||
|
final RpcEndpointService rpcEndpointService = |
||||||
|
besuContext |
||||||
|
.getService(RpcEndpointService.class) |
||||||
|
.orElseThrow( |
||||||
|
() -> |
||||||
|
new RuntimeException( |
||||||
|
"Failed to obtain RpcEndpointService from the BesuContext.")); |
||||||
|
|
||||||
|
final BlockchainService blockchainService = |
||||||
|
besuContext |
||||||
|
.getService(BlockchainService.class) |
||||||
|
.orElseThrow( |
||||||
|
() -> |
||||||
|
new RuntimeException( |
||||||
|
"Failed to obtain BlockchainService from the BesuContext.")); |
||||||
|
|
||||||
|
final FinalizationUpdaterRpcMethod rpcMethod = |
||||||
|
new FinalizationUpdaterRpcMethod(blockchainService); |
||||||
|
rpcEndpointService.registerRPCEndpoint( |
||||||
|
RPC_NAMESPACE, RPC_METHOD_FINALIZED_BLOCK, rpcMethod::setFinalizedBlock); |
||||||
|
rpcEndpointService.registerRPCEndpoint( |
||||||
|
RPC_NAMESPACE, RPC_METHOD_SAFE_BLOCK, rpcMethod::setSafeBlock); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void start() { |
||||||
|
LOG.trace("Starting plugin ..."); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stop() { |
||||||
|
LOG.trace("Stopping plugin ..."); |
||||||
|
} |
||||||
|
|
||||||
|
static class FinalizationUpdaterRpcMethod { |
||||||
|
private final BlockchainService blockchainService; |
||||||
|
private final JsonRpcParameter parameterParser = new JsonRpcParameter(); |
||||||
|
|
||||||
|
FinalizationUpdaterRpcMethod(final BlockchainService blockchainService) { |
||||||
|
this.blockchainService = blockchainService; |
||||||
|
} |
||||||
|
|
||||||
|
Boolean setFinalizedBlock(final PluginRpcRequest request) { |
||||||
|
return setFinalizedOrSafeBlock(request, true); |
||||||
|
} |
||||||
|
|
||||||
|
Boolean setSafeBlock(final PluginRpcRequest request) { |
||||||
|
return setFinalizedOrSafeBlock(request, false); |
||||||
|
} |
||||||
|
|
||||||
|
private Boolean setFinalizedOrSafeBlock( |
||||||
|
final PluginRpcRequest request, final boolean isFinalized) { |
||||||
|
final Long blockNumberToSet = parseResult(request); |
||||||
|
|
||||||
|
// lookup finalized block by number in local chain
|
||||||
|
final Optional<BlockContext> finalizedBlock = |
||||||
|
blockchainService.getBlockByNumber(blockNumberToSet); |
||||||
|
if (finalizedBlock.isEmpty()) { |
||||||
|
throw new PluginRpcEndpointException( |
||||||
|
RpcErrorType.BLOCK_NOT_FOUND, |
||||||
|
"Block not found in the local chain: " + blockNumberToSet); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
final Hash blockHash = finalizedBlock.get().getBlockHeader().getBlockHash(); |
||||||
|
if (isFinalized) { |
||||||
|
blockchainService.setFinalizedBlock(blockHash); |
||||||
|
} else { |
||||||
|
blockchainService.setSafeBlock(blockHash); |
||||||
|
} |
||||||
|
} catch (final IllegalArgumentException e) { |
||||||
|
throw new PluginRpcEndpointException( |
||||||
|
RpcErrorType.BLOCK_NOT_FOUND, |
||||||
|
"Block not found in the local chain: " + blockNumberToSet); |
||||||
|
} catch (final UnsupportedOperationException e) { |
||||||
|
throw new PluginRpcEndpointException( |
||||||
|
RpcErrorType.METHOD_NOT_ENABLED, |
||||||
|
"Method not enabled for PoS network: setFinalizedBlock"); |
||||||
|
} catch (final Exception e) { |
||||||
|
throw new PluginRpcEndpointException( |
||||||
|
RpcErrorType.INTERNAL_ERROR, "Error setting finalized block: " + blockNumberToSet); |
||||||
|
} |
||||||
|
|
||||||
|
return Boolean.TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
private Long parseResult(final PluginRpcRequest request) { |
||||||
|
Long blockNumber; |
||||||
|
try { |
||||||
|
final Object[] params = request.getParams(); |
||||||
|
blockNumber = parameterParser.required(params, 0, Long.class); |
||||||
|
} catch (final Exception e) { |
||||||
|
throw new PluginRpcEndpointException(RpcErrorType.INVALID_PARAMS, e.getMessage()); |
||||||
|
} |
||||||
|
|
||||||
|
if (blockNumber <= 0) { |
||||||
|
throw new PluginRpcEndpointException( |
||||||
|
RpcErrorType.INVALID_PARAMS, "Block number must be greater than 0"); |
||||||
|
} |
||||||
|
|
||||||
|
return blockNumber; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
/* |
||||||
|
* 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.tests.acceptance.plugins; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import org.hyperledger.besu.config.JsonUtil; |
||||||
|
import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; |
||||||
|
import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||||
|
import okhttp3.MediaType; |
||||||
|
import okhttp3.OkHttpClient; |
||||||
|
import okhttp3.Request; |
||||||
|
import okhttp3.RequestBody; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.DisplayName; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||||
|
import org.junit.jupiter.params.provider.ValueSource; |
||||||
|
|
||||||
|
public class BlockchainServiceFinalizedBlockPluginTest extends AcceptanceTestBase { |
||||||
|
|
||||||
|
private BesuNode pluginNode; |
||||||
|
private BesuNode minerNode; |
||||||
|
private OkHttpClient client; |
||||||
|
protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() throws Exception { |
||||||
|
minerNode = besu.createMinerNode("minerNode"); |
||||||
|
pluginNode = |
||||||
|
besu.createPluginsNode("node1", List.of("testPlugins"), List.of("--rpc-http-api=UPDATER")); |
||||||
|
cluster.start(minerNode, pluginNode); |
||||||
|
client = new OkHttpClient(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@DisplayName("Calling update{Finalized/Safe}BlockV1 will set block") |
||||||
|
public void canUpdateFinalizedBlock() throws IOException { |
||||||
|
pluginNode.verify(blockchain.minimumHeight(5)); |
||||||
|
|
||||||
|
// RPC Call. Set the safe block number to 3
|
||||||
|
final ObjectNode resultJson = callTestMethod("updater_updateSafeBlockV1", List.of(3L)); |
||||||
|
assertThat(resultJson.get("result").asBoolean()).isTrue(); |
||||||
|
|
||||||
|
// RPC Call. Set the finalized block number to 4
|
||||||
|
final ObjectNode finalizedResultJson = |
||||||
|
callTestMethod("updater_updateFinalizedBlockV1", List.of(4L)); |
||||||
|
assertThat(finalizedResultJson.get("result").asBoolean()).isTrue(); |
||||||
|
|
||||||
|
final ObjectNode blockNumberSafeResult = |
||||||
|
callTestMethod("eth_getBlockByNumber", List.of("SAFE", true)); |
||||||
|
assertThat(blockNumberSafeResult.get("result").get("number").asText()).isEqualTo("0x3"); |
||||||
|
|
||||||
|
// Verify the value was set
|
||||||
|
final ObjectNode blockNumberFinalizedResult = |
||||||
|
callTestMethod("eth_getBlockByNumber", List.of("FINALIZED", true)); |
||||||
|
assertThat(blockNumberFinalizedResult.get("result").get("number").asText()).isEqualTo("0x4"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@DisplayName("Calling update{Finalized/Safe}BlockV1 with non-existing block number returns error") |
||||||
|
public void nonExistingBlockNumberReturnsError() throws IOException { |
||||||
|
pluginNode.verify(blockchain.minimumHeight(5)); |
||||||
|
|
||||||
|
final ObjectNode[] resultsJson = new ObjectNode[2]; |
||||||
|
resultsJson[0] = callTestMethod("updater_updateFinalizedBlockV1", List.of(250L)); |
||||||
|
resultsJson[1] = callTestMethod("updater_updateSafeBlockV1", List.of(250L)); |
||||||
|
|
||||||
|
for (int i = 0; i < resultsJson.length; i++) { |
||||||
|
assertThat(resultsJson[i].get("error").get("code").asInt()).isEqualTo(-32000); |
||||||
|
assertThat(resultsJson[i].get("error").get("message").asText()).isEqualTo("Block not found"); |
||||||
|
assertThat(resultsJson[i].get("error").get("data").asText()) |
||||||
|
.isEqualTo("Block not found in the local chain: 250"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest(name = "{index} - blockNumber={0}") |
||||||
|
@ValueSource(longs = {-1, 0}) |
||||||
|
@DisplayName("Calling update{Finalized/Safe}BlockV1 with block number <= 0 returns error") |
||||||
|
public void invalidBlockNumberReturnsError(final long blockNumber) throws IOException { |
||||||
|
pluginNode.verify(blockchain.minimumHeight(5)); |
||||||
|
|
||||||
|
final ObjectNode[] resultsJson = new ObjectNode[2]; |
||||||
|
resultsJson[0] = callTestMethod("updater_updateFinalizedBlockV1", List.of(blockNumber)); |
||||||
|
resultsJson[1] = callTestMethod("updater_updateSafeBlockV1", List.of(blockNumber)); |
||||||
|
|
||||||
|
for (int i = 0; i < resultsJson.length; i++) { |
||||||
|
assertThat(resultsJson[i].get("error").get("code").asInt()).isEqualTo(-32602); |
||||||
|
assertThat(resultsJson[i].get("error").get("message").asText()).isEqualTo("Invalid params"); |
||||||
|
assertThat(resultsJson[i].get("error").get("data").asText()) |
||||||
|
.isEqualTo("Block number must be greater than 0"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@DisplayName("Calling update{Finalized/Safe}BlockV1 with invalid block number type returns error") |
||||||
|
public void invalidBlockNumberTypeReturnsError() throws IOException { |
||||||
|
pluginNode.verify(blockchain.minimumHeight(5)); |
||||||
|
|
||||||
|
final ObjectNode[] resultsJson = new ObjectNode[2]; |
||||||
|
resultsJson[0] = callTestMethod("updater_updateFinalizedBlockV1", List.of("testblock")); |
||||||
|
resultsJson[1] = callTestMethod("updater_updateSafeBlockV1", List.of("testblock")); |
||||||
|
|
||||||
|
for (int i = 0; i < resultsJson.length; i++) { |
||||||
|
assertThat(resultsJson[i].get("error").get("code").asInt()).isEqualTo(-32602); |
||||||
|
assertThat(resultsJson[i].get("error").get("message").asText()).isEqualTo("Invalid params"); |
||||||
|
assertThat(resultsJson[i].get("error").get("data").asText()) |
||||||
|
.isEqualTo( |
||||||
|
"Invalid json rpc parameter at index 0. Supplied value was: 'testblock' of type: 'java.lang.String' - expected type: 'java.lang.Long'"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private ObjectNode callTestMethod(final String method, final List<Object> params) |
||||||
|
throws IOException { |
||||||
|
String format = |
||||||
|
String.format( |
||||||
|
"{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":[%s],\"id\":42}", |
||||||
|
method, |
||||||
|
params.stream().map(value -> "\"" + value + "\"").collect(Collectors.joining(","))); |
||||||
|
|
||||||
|
RequestBody body = RequestBody.create(format, JSON); |
||||||
|
|
||||||
|
final String resultString = |
||||||
|
client |
||||||
|
.newCall( |
||||||
|
new Request.Builder() |
||||||
|
.post(body) |
||||||
|
.url( |
||||||
|
"http://" |
||||||
|
+ pluginNode.getHostName() |
||||||
|
+ ":" |
||||||
|
+ pluginNode.getJsonRpcPort().get() |
||||||
|
+ "/") |
||||||
|
.build()) |
||||||
|
.execute() |
||||||
|
.body() |
||||||
|
.string(); |
||||||
|
return JsonUtil.objectNodeFromString(resultString); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue