mirror of
https://github.com/vacp2p/linea-besu.git
synced 2026-01-09 07:27:55 -05:00
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>
This commit is contained in:
@@ -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;
|
||||
;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user