Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expose set finalized/safe block in plugin api BlockchainService #7382

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8868adb
feat: Expose set finalized and safe block in plugin api BlockchainSer…
usmansaleem Jul 26, 2024
364f848
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 26, 2024
97f2fe4
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 29, 2024
b8c01a2
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 30, 2024
0178dcd
check for poa network before setting finalized block
usmansaleem Jul 30, 2024
c65f755
fix init call
usmansaleem Jul 30, 2024
c8f13b8
Check for PoS for finalized and safe block
usmansaleem Jul 30, 2024
df461b2
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 30, 2024
a73a7bb
revert the init method
usmansaleem Jul 30, 2024
44c0735
changelog
usmansaleem Jul 30, 2024
a91166e
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 31, 2024
3e89e43
Add BlockchainService set finalized acceptance test
usmansaleem Jul 31, 2024
a52f185
Add BlockchainService set finalized acceptance test
usmansaleem Jul 31, 2024
77e6d87
fix doc sentence
usmansaleem Jul 31, 2024
e3e4da0
AT for set finalized and set safe
usmansaleem Jul 31, 2024
04a09f5
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 31, 2024
e05ccbc
Merge remote-tracking branch 'upstream/main' into besu_plugin_blockch…
usmansaleem Jul 31, 2024
cbffa16
trigger ci
usmansaleem Jul 31, 2024
bb7ac7d
Merge branch 'main' into besu_plugin_blockchainservice_finalized_block
fab-10 Jul 31, 2024
e2af439
Merge branch 'main' into besu_plugin_blockchainservice_finalized_block
fab-10 Jul 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also test the updateSafeBlock method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

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
Loading