From 09f55b8a56677684d74f4aef8ed270013253c1f5 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Mon, 28 Oct 2024 14:12:55 -0700 Subject: [PATCH] add optional worldstate move to debug_setHead Signed-off-by: garyschulte --- .../internal/methods/DebugSetHead.java | 46 +++-- .../internal/methods/DebugSetHeadTest.java | 164 ++++++++++++++++++ 2 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHead.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHead.java index 7958f8e759..293ddfd6b5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHead.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHead.java @@ -21,16 +21,17 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.core.BlockHeader; import java.util.Optional; -public class DebugSetHead extends AbstractBlockParameterMethod { +public class DebugSetHead extends AbstractBlockParameterOrBlockHashMethod { private final ProtocolContext protocolContext; public DebugSetHead(final BlockchainQueries blockchain, final ProtocolContext protocolContext) { @@ -45,26 +46,51 @@ public class DebugSetHead extends AbstractBlockParameterMethod { } @Override - protected BlockParameter blockParameter(final JsonRpcRequestContext request) { + protected BlockParameterOrBlockHash blockParameterOrBlockHash( + final JsonRpcRequestContext requestContext) { try { - return request.getRequiredParameter(0, BlockParameter.class); + return requestContext.getRequiredParameter(0, BlockParameterOrBlockHash.class); } catch (JsonRpcParameterException e) { throw new InvalidJsonRpcParameters( - "Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); + "Invalid block or block hash parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); } } @Override - protected Object resultByBlockNumber( - final JsonRpcRequestContext request, final long blockNumber) { - final Optional maybeBlockHash = getBlockchainQueries().getBlockHashByNumber(blockNumber); + protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) { + var blockchainQueries = getBlockchainQueries(); + Optional maybeBlockHeader = blockchainQueries.getBlockHeaderByHash(blockHash); + Optional maybeMoveWorldstate = shouldMoveWorldstate(request); - if (maybeBlockHash.isEmpty()) { + if (maybeBlockHeader.isEmpty()) { return new JsonRpcErrorResponse(request.getRequest().getId(), UNKNOWN_BLOCK); } - protocolContext.getBlockchain().rewindToBlock(maybeBlockHash.get()); + protocolContext.getBlockchain().rewindToBlock(maybeBlockHeader.get().getBlockHash()); + + // Optionally move the worldstate to the specified blockhash, if it is present + if (maybeMoveWorldstate.orElse(Boolean.FALSE)) { + var blockHeader = maybeBlockHeader.get(); + var archive = blockchainQueries.getWorldStateArchive(); + if (archive.isWorldStateAvailable(blockHeader.getStateRoot(), blockHeader.getBlockHash())) { + // WARNING, this can be dangerous for a DiffBasedWorldstate if a concurrent + // process attempts to move or modify the head worldstate. + // for Foreset worldstates, this is essentially a no-op. + archive.getMutable(blockHeader.getStateRoot(), blockHeader.getBlockHash()); + } + } return JsonRpcSuccessResponse.SUCCESS_RESULT; } + + private Optional shouldMoveWorldstate(final JsonRpcRequestContext request) { + try { + return request.getOptionalParameter(1, Boolean.class); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid should move worldstate boolean parameter (index 1)", + RpcErrorType.INVALID_PARAMS, + e); + } + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java new file mode 100644 index 0000000000..bc1bd087b7 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java @@ -0,0 +1,164 @@ +/* + * Copyright contributors to 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.ethereum.api.jsonrpc.internal.methods; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpServiceTest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MiningParameters; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * This test only exercises bonsai worldstate since forest is essentially a no-op for moving the + * worldstate. + */ +public class DebugSetHeadTest extends AbstractJsonRpcHttpServiceTest { + + DebugSetHead debugSetHead; + Blockchain blockchain; + WorldStateArchive archive; + ProtocolContext protocolContext; + ProtocolSchedule protocolSchedule; + + @Override + @BeforeEach + public void setup() throws Exception { + setupBonsaiBlockchain(); + blockchain = blockchainSetupUtil.getBlockchain(); + protocolContext = blockchainSetupUtil.getProtocolContext(); + protocolSchedule = blockchainSetupUtil.getProtocolSchedule(); + ; + archive = blockchainSetupUtil.getWorldArchive(); + debugSetHead = + new DebugSetHead( + new BlockchainQueries( + protocolSchedule, blockchain, archive, MiningParameters.MINING_DISABLED), + protocolContext); + startService(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "0x01", + "0x02", + "0x3d813a0ffc9cd04436e17e3e9c309f1e80df0407078e50355ce0d570b5424812", + "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de" + }) + public void assertOnlyChainHeadMoves(final String blockParam) { + var chainTip = blockchain.getChainHead().getBlockHeader(); + var blockOne = getBlockHeaderForHashOrNumber(blockParam).orElse(null); + + assertThat(blockOne).isNotNull(); + assertThat(blockOne).isNotEqualTo(chainTip); + + // move the head to param val, number or hash + debugSetHead.response(debugSetHead(blockParam, false)); + + // get the new chainTip: + var newChainTip = blockchain.getChainHead().getBlockHeader(); + + // assert the chain moved, and the worldstate did not + assertThat(newChainTip).isEqualTo(blockOne); + assertThat(archive.getMutable().rootHash()).isEqualTo(chainTip.getStateRoot()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "0x01", + "0x02", + "0x3d813a0ffc9cd04436e17e3e9c309f1e80df0407078e50355ce0d570b5424812", + "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de" + }) + public void assertBothChainHeadAndWorldStatByNumber(final String blockParam) { + var chainTip = blockchain.getChainHead().getBlockHeader(); + var blockOne = getBlockHeaderForHashOrNumber(blockParam).orElse(null); + + assertThat(blockOne).isNotNull(); + assertThat(blockOne).isNotEqualTo(chainTip); + + // move the head and worldstate to param val number or hash + debugSetHead.response(debugSetHead(blockParam, true)); + + // get the new chainTip: + var newChainTip = blockchain.getChainHead().getBlockHeader(); + + // assert both the chain and worldstate moved to block one + assertThat(newChainTip).isEqualTo(blockOne); + assertThat(archive.getMutable().rootHash()).isEqualTo(blockOne.getStateRoot()); + } + + @Test + public void assertNotFound() { + var chainTip = blockchain.getChainHead().getBlockHeader(); + + // move the head to number just after chain head + var resp = debugSetHead.response(debugSetHead("" + chainTip.getNumber() + 1, false)); + assertThat(resp.getType()).isEqualTo(RpcResponseType.ERROR); + + // move the head to some arbitrary hash + var resp2 = + debugSetHead.response( + debugSetHead(Hash.keccak256(Bytes.fromHexString("0xdeadbeef")).toHexString(), false)); + assertThat(resp2.getType()).isEqualTo(RpcResponseType.ERROR); + + // get the new chainTip: + var newChainTip = blockchain.getChainHead().getBlockHeader(); + + // assert neither the chain nor the worldstate moved + assertThat(newChainTip).isEqualTo(chainTip); + assertThat(archive.getMutable().rootHash()).isEqualTo(chainTip.getStateRoot()); + } + + private JsonRpcRequestContext debugSetHead( + final String numberOrHash, final Boolean moveWorldState) { + return new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "debug_setHead", new Object[] {numberOrHash, moveWorldState})); + } + + private Optional getBlockHeaderForHashOrNumber(final String input) { + try { + var param = new BlockParameterOrBlockHash(input); + if (param.getHash().isPresent()) { + return blockchain.getBlockHeader(param.getHash().get()); + } else if (param.getNumber().isPresent()) { + return blockchain.getBlockHeader(param.getNumber().getAsLong()); + } + } catch (JsonProcessingException ignored) { + // meh + } + return Optional.empty(); + } +}