Skip to content

Commit

Permalink
feat: Expose set finalized/safe block in plugin-api BlockchainService (
Browse files Browse the repository at this point in the history
…hyperledger#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>
  • Loading branch information
usmansaleem authored Jul 31, 2024
1 parent b634b9c commit 9d92ae8
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions acceptance-tests/test-plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand All @@ -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
Expand Down Expand Up @@ -135,6 +136,37 @@ public Optional<Hash> getFinalizedBlock() {
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) {
Expand Down
2 changes: 1 addition & 1 deletion plugin-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
Loading

0 comments on commit 9d92ae8

Please sign in to comment.