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
Usman Saleem 3 months ago committed by GitHub
parent b634b9c66c
commit 9d92ae87df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      acceptance-tests/test-plugins/build.gradle
  3. 149
      acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBlockchainServiceFinalizedPlugin.java
  4. 158
      acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/plugins/BlockchainServiceFinalizedBlockPluginTest.java
  5. 34
      besu/src/main/java/org/hyperledger/besu/services/BlockchainServiceImpl.java
  6. 2
      plugin-api/build.gradle
  7. 20
      plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockchainService.java

@ -13,6 +13,7 @@
- Remove long-deprecated `perm*whitelist*` methods [#7401](https://github.com/hyperledger/besu/pull/7401)
### Additions and Improvements
- Expose set finalized/safe block in plugin api BlockchainService. These method can be used by plugins to set finalized/safe block for a PoA network (such as QBFT, IBFT and Clique).[#7382](https://github.com/hyperledger/besu/pull/7382)
### Bug fixes

@ -7,6 +7,7 @@ dependencies {
implementation project(':datatypes')
implementation project(':ethereum:core')
implementation project(':ethereum:rlp')
implementation project(':ethereum:api')
implementation project(':plugin-api')
implementation 'com.google.auto.service:auto-service'
implementation 'info.picocli:picocli'

@ -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);
}
}

@ -20,6 +20,7 @@ import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.plugin.Unstable;
@ -46,7 +47,7 @@ public class BlockchainServiceImpl implements BlockchainService {
public BlockchainServiceImpl() {}
/**
* Instantiates a new Blockchain service.
* Initialize the Blockchain service.
*
* @param protocolContext the protocol context
* @param protocolSchedule the protocol schedule
@ -135,6 +136,37 @@ public class BlockchainServiceImpl implements BlockchainService {
return blockchain.getFinalized();
}
@Override
public void setFinalizedBlock(final Hash blockHash) {
final var protocolSpec = getProtocolSpec(blockHash);
if (protocolSpec.isPoS()) {
throw new UnsupportedOperationException(
"Marking block as finalized is not supported for PoS networks");
}
blockchain.setFinalized(blockHash);
}
@Override
public void setSafeBlock(final Hash blockHash) {
final var protocolSpec = getProtocolSpec(blockHash);
if (protocolSpec.isPoS()) {
throw new UnsupportedOperationException(
"Marking block as safe is not supported for PoS networks");
}
blockchain.setSafeBlock(blockHash);
}
private ProtocolSpec getProtocolSpec(final Hash blockHash) {
return blockchain
.getBlockByHash(blockHash)
.map(Block::getHeader)
.map(protocolSchedule::getByBlockHeader)
.orElseThrow(() -> new IllegalArgumentException("Block not found: " + blockHash));
}
private static BlockContext blockContext(
final Supplier<BlockHeader> blockHeaderSupplier,
final Supplier<BlockBody> blockBodySupplier) {

@ -70,7 +70,7 @@ Calculated : ${currentHash}
tasks.register('checkAPIChanges', FileStateChecker) {
description = "Checks that the API for the Plugin-API project does not change without deliberate thought"
files = sourceSets.main.allJava.files
knownHash = 'o0IuPVpCvE3YUzuZgVf4NP74q1ECpkbAkeC6u/Nr8yU='
knownHash = 'tXFd8EcMJtD+ZSLJxWJLYRZD0d3njRz+3Ubey2zFM2A='
}
check.dependsOn('checkAPIChanges')

@ -87,4 +87,24 @@ public interface BlockchainService extends BesuService {
* @return the block hash of the finalized block
*/
Optional<Hash> getFinalizedBlock();
/**
* Set the finalized block for non-PoS networks
*
* @param blockHash Hash of the finalized block
* @throws IllegalArgumentException if the block hash is not on the chain
* @throws UnsupportedOperationException if the network is a PoS network
*/
void setFinalizedBlock(Hash blockHash)
throws IllegalArgumentException, UnsupportedOperationException;
/**
* Set the safe block for non-PoS networks
*
* @param blockHash Hash of the finalized block
* @throws IllegalArgumentException if the block hash is not on the chain
* @throws UnsupportedOperationException if the network is a PoS network
*/
void setSafeBlock(Hash blockHash) throws IllegalArgumentException, UnsupportedOperationException;
;
}

Loading…
Cancel
Save