From a5f5c28911a4cb6ce94b621db980765ee90d065c Mon Sep 17 00:00:00 2001 From: Miroslav Kovar Date: Fri, 23 Jan 2026 06:13:57 +0100 Subject: [PATCH] Add engine_getPayloadBodiesBy*V2 methods Signed-off-by: Miroslav Kovar --- .../besu/ethereum/api/jsonrpc/RpcMethod.java | 2 + .../EngineGetPayloadBodiesByHashV2.java | 119 ++++++++++ .../EngineGetPayloadBodiesByRangeV2.java | 154 ++++++++++++ .../internal/results/BlockResultFactory.java | 25 +- .../EngineGetPayloadBodiesResultV2.java | 89 +++++++ .../ExecutionEngineJsonRpcMethods.java | 8 + .../EngineGetPayloadBodiesByHashV2Test.java | 213 +++++++++++++++++ .../EngineGetPayloadBodiesByRangeV2Test.java | 223 ++++++++++++++++++ 8 files changed, 830 insertions(+), 3 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV2.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2Test.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2Test.java diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index c206fd4ed7a..08c369526e8 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -72,7 +72,9 @@ public enum RpcMethod { ENGINE_EXCHANGE_TRANSITION_CONFIGURATION("engine_exchangeTransitionConfigurationV1"), ENGINE_GET_CLIENT_VERSION_V1("engine_getClientVersionV1"), ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1("engine_getPayloadBodiesByHashV1"), + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2("engine_getPayloadBodiesByHashV2"), ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1("engine_getPayloadBodiesByRangeV1"), + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2("engine_getPayloadBodiesByRangeV2"), ENGINE_EXCHANGE_CAPABILITIES("engine_exchangeCapabilities"), ENGINE_PREPARE_PAYLOAD_DEBUG("engine_preparePayload_debug"), ETH_ACCOUNTS("eth_accounts"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2.java new file mode 100644 index 00000000000..ee59d844635 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2.java @@ -0,0 +1,119 @@ +/* + * 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.ethereum.api.jsonrpc.internal.methods.engine; + +import org.hyperledger.besu.datatypes.Hash; +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.methods.ExecutionEngineJsonRpcMethod; +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.JsonRpcResponse; +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.jsonrpc.internal.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV2; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.vertx.core.Vertx; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EngineGetPayloadBodiesByHashV2 extends ExecutionEngineJsonRpcMethod { + + private static final int MAX_REQUEST_BLOCKS = 1024; + private static final Logger LOG = LoggerFactory.getLogger(EngineGetPayloadBodiesByHashV2.class); + private final BlockResultFactory blockResultFactory; + + public EngineGetPayloadBodiesByHashV2( + final Vertx vertx, + final ProtocolContext protocolContext, + final BlockResultFactory blockResultFactory, + final EngineCallListener engineCallListener) { + super(vertx, protocolContext, engineCallListener); + this.blockResultFactory = blockResultFactory; + } + + @Override + public String getName() { + return RpcMethod.ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2.getMethodName(); + } + + @Override + public JsonRpcResponse syncResponse(final JsonRpcRequestContext request) { + engineCallListener.executionEngineCalled(); + + final Object reqId = request.getRequest().getId(); + + final Hash[] blockHashes; + try { + blockHashes = request.getRequiredParameter(0, Hash[].class); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid block hash parameters (index 0)", RpcErrorType.INVALID_BLOCK_HASH_PARAMS, e); + } + + LOG.atTrace() + .setMessage("{} parameters: blockHashes {}") + .addArgument(this::getName) + .addArgument(blockHashes) + .log(); + + if (blockHashes.length > getMaxRequestBlocks()) { + return new JsonRpcErrorResponse(reqId, RpcErrorType.INVALID_RANGE_REQUEST_TOO_LARGE); + } + + final Blockchain blockchain = protocolContext.getBlockchain(); + + final List> blockAccessLists = + Arrays.stream(blockHashes) + .map(blockHash -> getBlockAccessList(blockchain, blockHash)) + .collect(Collectors.toList()); + + final List> blockBodies = + Arrays.stream(blockHashes).map(blockchain::getBlockBody).collect(Collectors.toList()); + + final EngineGetPayloadBodiesResultV2 engineGetPayloadBodiesResultV2 = + blockResultFactory.payloadBodiesCompleteV2(blockBodies, blockAccessLists); + + return new JsonRpcSuccessResponse(reqId, engineGetPayloadBodiesResultV2); + } + + protected int getMaxRequestBlocks() { + return MAX_REQUEST_BLOCKS; + } + + private Optional getBlockAccessList(final Blockchain blockchain, final Hash blockHash) { + return blockchain + .getBlockAccessList(blockHash) + .map(EngineGetPayloadBodiesByHashV2::encodeBlockAccessList); + } + + private static String encodeBlockAccessList(final BlockAccessList blockAccessList) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + blockAccessList.writeTo(output); + return output.encoded().toHexString(); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2.java new file mode 100644 index 00000000000..7e25737ed37 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2.java @@ -0,0 +1,154 @@ +/* + * 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.ethereum.api.jsonrpc.internal.methods.engine; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; +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.methods.ExecutionEngineJsonRpcMethod; +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.JsonRpcResponse; +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.jsonrpc.internal.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV2; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import io.vertx.core.Vertx; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EngineGetPayloadBodiesByRangeV2 extends ExecutionEngineJsonRpcMethod { + private static final Logger LOG = LoggerFactory.getLogger(EngineGetPayloadBodiesByRangeV2.class); + private static final int MAX_REQUEST_BLOCKS = 1024; + private final BlockResultFactory blockResultFactory; + + public EngineGetPayloadBodiesByRangeV2( + final Vertx vertx, + final ProtocolContext protocolContext, + final BlockResultFactory blockResultFactory, + final EngineCallListener engineCallListener) { + super(vertx, protocolContext, engineCallListener); + this.blockResultFactory = blockResultFactory; + } + + @Override + public String getName() { + return RpcMethod.ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2.getMethodName(); + } + + @Override + public JsonRpcResponse syncResponse(final JsonRpcRequestContext request) { + engineCallListener.executionEngineCalled(); + + final long startBlockNumber; + try { + startBlockNumber = request.getRequiredParameter(0, UnsignedLongParameter.class).getValue(); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid start block number parameter (index 0)", + RpcErrorType.INVALID_BLOCK_NUMBER_PARAMS, + e); + } + final long count; + try { + count = request.getRequiredParameter(1, UnsignedLongParameter.class).getValue(); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid block count params (index 1)", RpcErrorType.INVALID_BLOCK_COUNT_PARAMS, e); + } + final Object reqId = request.getRequest().getId(); + + LOG.atTrace() + .setMessage("{} parameters: start block number {} count {}") + .addArgument(this::getName) + .addArgument(startBlockNumber) + .addArgument(count) + .log(); + + if (startBlockNumber < 1 || count < 1) { + return new JsonRpcErrorResponse(reqId, RpcErrorType.INVALID_BLOCK_NUMBER_PARAMS); + } + + if (count > getMaxRequestBlocks()) { + return new JsonRpcErrorResponse(reqId, RpcErrorType.INVALID_RANGE_REQUEST_TOO_LARGE); + } + + final Blockchain blockchain = protocolContext.getBlockchain(); + final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber(); + + // request startBlockNumber is beyond head of chain + if (chainHeadBlockNumber < startBlockNumber) { + // Empty List of payloadBodies + return new JsonRpcSuccessResponse(reqId, new EngineGetPayloadBodiesResultV2()); + } + + final long upperBound = startBlockNumber + count; + + // if we've received request from blocks beyond the head we exclude those from the query + final long endExclusiveBlockNumber = + chainHeadBlockNumber < upperBound ? chainHeadBlockNumber + 1 : upperBound; + + final List> blockHashes = + LongStream.range(startBlockNumber, endExclusiveBlockNumber) + .mapToObj(blockchain::getBlockHashByNumber) + .collect(Collectors.toList()); + + final List> blockAccessLists = + blockHashes.stream() + .map( + maybeHash -> + maybeHash.flatMap(blockHash -> getBlockAccessList(blockchain, blockHash))) + .collect(Collectors.toList()); + + final List> blockBodies = + blockHashes.stream() + .map(maybeHash -> maybeHash.flatMap(blockchain::getBlockBody)) + .collect(Collectors.toList()); + + EngineGetPayloadBodiesResultV2 engineGetPayloadBodiesResultV2 = + blockResultFactory.payloadBodiesCompleteV2(blockBodies, blockAccessLists); + + return new JsonRpcSuccessResponse(reqId, engineGetPayloadBodiesResultV2); + } + + protected int getMaxRequestBlocks() { + return MAX_REQUEST_BLOCKS; + } + + private Optional getBlockAccessList(final Blockchain blockchain, final Hash blockHash) { + return blockchain + .getBlockAccessList(blockHash) + .map(EngineGetPayloadBodiesByRangeV2::encodeBlockAccessList); + } + + private static String encodeBlockAccessList(final BlockAccessList blockAccessList) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + blockAccessList.writeTo(output); + return output.encoded().toHexString(); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResultFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResultFactory.java index 3643fe1623f..18dc4e7a39d 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResultFactory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResultFactory.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.consensus.merge.PayloadWrapper; import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV1.PayloadBody; import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; import org.hyperledger.besu.ethereum.core.Block; @@ -33,6 +32,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.IntStream; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -252,12 +252,31 @@ private static Optional> requestsAsHex(final PayloadWrapper payload public EngineGetPayloadBodiesResultV1 payloadBodiesCompleteV1( final List> blockBodies) { - final List payloadBodies = + final List payloadBodies = blockBodies.stream() - .map(maybeBody -> maybeBody.map(PayloadBody::new).orElse(null)) + .map( + maybeBody -> + maybeBody.map(EngineGetPayloadBodiesResultV1.PayloadBody::new).orElse(null)) .collect(Collectors.toList()); return new EngineGetPayloadBodiesResultV1(payloadBodies); } + public EngineGetPayloadBodiesResultV2 payloadBodiesCompleteV2( + final List> blockBodies, final List> blockAccessLists) { + final List payloadBodies = + IntStream.range(0, blockBodies.size()) + .mapToObj( + index -> + blockBodies + .get(index) + .map( + body -> + new EngineGetPayloadBodiesResultV2.PayloadBody( + body, blockAccessLists.get(index).orElse(null))) + .orElse(null)) + .collect(Collectors.toList()); + return new EngineGetPayloadBodiesResultV2(payloadBodies); + } + // endregion EngineGetPayloadBodiesResult } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV2.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV2.java new file mode 100644 index 00000000000..a8d95afa93c --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV2.java @@ -0,0 +1,89 @@ +/* + * 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.ethereum.api.jsonrpc.internal.results; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.WithdrawalParameter; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; +import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; +import org.hyperledger.besu.util.HexUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +@JsonPropertyOrder({"payloadBodies"}) +public class EngineGetPayloadBodiesResultV2 { + + private final List payloadBodies; + + public EngineGetPayloadBodiesResultV2() { + this.payloadBodies = Collections.emptyList(); + } + + public EngineGetPayloadBodiesResultV2(final List payloadBody) { + this.payloadBodies = payloadBody; + } + + @JsonValue + public List getPayloadBodies() { + return payloadBodies; + } + + public static class PayloadBody { + private final List transactions; + private final List withdrawals; + private final String blockAccessList; + + public PayloadBody(final BlockBody blockBody, final String blockAccessList) { + this.transactions = + blockBody.getTransactions().stream() + .map( + transaction -> + TransactionEncoder.encodeOpaqueBytes(transaction, EncodingContext.BLOCK_BODY)) + .map(b -> HexUtils.toFastHex(b, true)) + .collect(Collectors.toList()); + this.withdrawals = + blockBody + .getWithdrawals() + .map( + ws -> + ws.stream() + .map(WithdrawalParameter::fromWithdrawal) + .collect(Collectors.toList())) + .orElse(null); + this.blockAccessList = blockAccessList; + } + + @JsonGetter(value = "transactions") + public List getTransactions() { + return transactions; + } + + @JsonGetter(value = "withdrawals") + public List getWithdrawals() { + return withdrawals; + } + + @JsonGetter(value = "blockAccessList") + public String getBlockAccessList() { + return blockAccessList; + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/ExecutionEngineJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/ExecutionEngineJsonRpcMethods.java index 9e4d98951de..aaefa1322c8 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/ExecutionEngineJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/ExecutionEngineJsonRpcMethods.java @@ -33,7 +33,9 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetBlobsV3; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetClientVersionV1; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByHashV1; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByHashV2; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByRangeV1; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByRangeV2; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadV1; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadV2; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadV3; @@ -253,6 +255,12 @@ protected Map create() { blockResultFactory, engineQosTimer, protocolSchedule)); + executionEngineApisSupported.add( + new EngineGetPayloadBodiesByHashV2( + consensusEngineServer, protocolContext, blockResultFactory, engineQosTimer)); + executionEngineApisSupported.add( + new EngineGetPayloadBodiesByRangeV2( + consensusEngineServer, protocolContext, blockResultFactory, engineQosTimer)); executionEngineApisSupported.add( new EngineNewPayloadV5( consensusEngineServer, diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2Test.java new file mode 100644 index 00000000000..85281dc7a7c --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV2Test.java @@ -0,0 +1,213 @@ +/* + * 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.ethereum.api.jsonrpc.internal.methods.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineTestSupport.fromErrorResp; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType.INVALID_RANGE_REQUEST_TOO_LARGE; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +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.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV2; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotRead; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.StorageChange; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import io.vertx.core.Vertx; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class EngineGetPayloadBodiesByHashV2Test { + private EngineGetPayloadBodiesByHashV2 method; + private static final Vertx vertx = Vertx.vertx(); + private static final BlockResultFactory blockResultFactory = new BlockResultFactory(); + @Mock private ProtocolContext protocolContext; + @Mock private EngineCallListener engineCallListener; + @Mock private org.hyperledger.besu.ethereum.chain.MutableBlockchain blockchain; + + @BeforeEach + public void before() { + lenient().when(protocolContext.getBlockchain()).thenReturn(blockchain); + this.method = + spy( + new EngineGetPayloadBodiesByHashV2( + vertx, protocolContext, blockResultFactory, engineCallListener)); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("engine_getPayloadBodiesByHashV2"); + } + + @Test + public void shouldReturnBlockAccessListWhenAvailable() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash = Hash.wrap(Bytes32.random()); + final BlockBody blockBody = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockAccessList blockAccessList = createSampleBlockAccessList(); + final String encodedBlockAccessList = encodeBlockAccessList(blockAccessList); + + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + when(blockchain.getBlockAccessList(blockHash)).thenReturn(Optional.of(blockAccessList)); + + final var resp = resp(new Hash[] {blockHash}); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(0).getBlockAccessList()) + .isEqualTo(encodedBlockAccessList); + } + + @Test + public void shouldReturnNullBlockAccessListForPreAmsterdamBlock() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash = Hash.wrap(Bytes32.random()); + final BlockBody blockBody = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + + final var resp = resp(new Hash[] {blockHash}); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(0).getBlockAccessList()).isNull(); + } + + @Test + public void shouldReturnNullBlockAccessListWhenPruned() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash = Hash.wrap(Bytes32.random()); + final BlockBody blockBody = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + when(blockchain.getBlockAccessList(blockHash)).thenReturn(Optional.empty()); + + final var resp = resp(new Hash[] {blockHash}); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(0).getBlockAccessList()).isNull(); + } + + @Test + public void shouldReturnEmptyPayloadBodiesWithEmptyHash() { + final var resp = resp(new Hash[] {}); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().isEmpty()).isTrue(); + } + + @Test + public void shouldReturnNullForUnknownHashes() { + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = Hash.wrap(Bytes32.random()); + final var resp = resp(new Hash[] {blockHash1, blockHash2, blockHash3}); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0)).isNull(); + assertThat(result.getPayloadBodies().get(1)).isNull(); + assertThat(result.getPayloadBodies().get(2)).isNull(); + } + + @Test + public void shouldReturnErrorWhenRequestExceedsPermittedNumberOfBlocks() { + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash[] hashes = new Hash[] {blockHash1, blockHash2}; + + doReturn(1).when(method).getMaxRequestBlocks(); + + final JsonRpcResponse resp = resp(hashes); + final var result = fromErrorResp(resp); + assertThat(result.getCode()).isEqualTo(INVALID_RANGE_REQUEST_TOO_LARGE.getCode()); + } + + private JsonRpcResponse resp(final Hash[] hashes) { + return method.response( + new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + RpcMethod.ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2.getMethodName(), + new Object[] {hashes}))); + } + + private EngineGetPayloadBodiesResultV2 fromSuccessResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(RpcResponseType.SUCCESS); + return Optional.of(resp) + .map(JsonRpcSuccessResponse.class::cast) + .map(JsonRpcSuccessResponse::getResult) + .map(EngineGetPayloadBodiesResultV2.class::cast) + .get(); + } + + private static BlockAccessList createSampleBlockAccessList() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final StorageSlotKey slotKey = new StorageSlotKey(UInt256.ONE); + final SlotChanges slotChanges = + new SlotChanges(slotKey, List.of(new StorageChange(0, UInt256.valueOf(2)))); + return new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(slotChanges), + List.of(new SlotRead(slotKey)), + List.of(new BalanceChange(0, Wei.ONE)), + List.of(), + List.of()))); + } + + private static String encodeBlockAccessList(final BlockAccessList blockAccessList) { + final var output = new org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput(); + blockAccessList.writeTo(output); + return output.encoded().toHexString(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2Test.java new file mode 100644 index 00000000000..d22117a1975 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV2Test.java @@ -0,0 +1,223 @@ +/* + * 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.ethereum.api.jsonrpc.internal.methods.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineTestSupport.fromErrorResp; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType.INVALID_PARAMS; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType.INVALID_RANGE_REQUEST_TOO_LARGE; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +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.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV2; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotRead; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.StorageChange; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import io.vertx.core.Vertx; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class EngineGetPayloadBodiesByRangeV2Test { + private EngineGetPayloadBodiesByRangeV2 method; + private static final Vertx vertx = Vertx.vertx(); + private static final BlockResultFactory blockResultFactory = new BlockResultFactory(); + @Mock private ProtocolContext protocolContext; + @Mock private EngineCallListener engineCallListener; + @Mock private MutableBlockchain blockchain; + + @BeforeEach + public void before() { + lenient().when(protocolContext.getBlockchain()).thenReturn(blockchain); + this.method = + spy( + new EngineGetPayloadBodiesByRangeV2( + vertx, protocolContext, blockResultFactory, engineCallListener)); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("engine_getPayloadBodiesByRangeV2"); + } + + @Test + public void shouldReturnBlockAccessListWhenAvailable() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final BlockBody blockBody1 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockBody blockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + + final BlockAccessList blockAccessList = createSampleBlockAccessList(); + final String encodedBlockAccessList = encodeBlockAccessList(blockAccessList); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(2L); + when(blockchain.getBlockHashByNumber(1L)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(2L)).thenReturn(Optional.of(blockHash2)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody1)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(blockBody2)); + when(blockchain.getBlockAccessList(blockHash1)).thenReturn(Optional.of(blockAccessList)); + + final var resp = resp("0x1", "0x2"); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(0).getBlockAccessList()) + .isEqualTo(encodedBlockAccessList); + assertThat(result.getPayloadBodies().get(1).getBlockAccessList()).isNull(); + } + + @Test + public void shouldReturnNullForUnknownNumber() { + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + final var resp = resp("0x7b", "0x3"); + final EngineGetPayloadBodiesResultV2 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0)).isNull(); + assertThat(result.getPayloadBodies().get(1)).isNull(); + assertThat(result.getPayloadBodies().get(2)).isNull(); + } + + @Test + public void shouldNotContainTrailingNullForBlocksPastTheCurrentHead() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + + final BlockBody blockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(Collections.emptyList())); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(123)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + + final var resp = resp("0x7b", "0x3"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(1); + } + + @Test + public void shouldReturnEmptyPayloadForRequestsPastCurrentHead() { + + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(123)); + final JsonRpcResponse resp = resp("0x7d", "0x3"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies()).isEqualTo(Collections.EMPTY_LIST); + } + + @Test + public void shouldReturnErrorWhenRequestExceedsPermittedNumberOfBlocks() { + doReturn(3).when(method).getMaxRequestBlocks(); + final JsonRpcResponse resp = resp("0x539", "0x4"); + final var result = fromErrorResp(resp); + assertThat(result.getCode()).isEqualTo(INVALID_RANGE_REQUEST_TOO_LARGE.getCode()); + } + + @Test + public void shouldReturnInvalidParamsIfStartIsZero() { + final JsonRpcResponse resp = resp("0x0", "0x539"); + final var result = fromErrorResp(resp); + assertThat(result.getCode()).isEqualTo(INVALID_PARAMS.getCode()); + } + + @Test + public void shouldReturnInvalidParamsIfCountIsZero() { + final JsonRpcResponse resp = resp("0x539", "0x0"); + final var result = fromErrorResp(resp); + assertThat(result.getCode()).isEqualTo(INVALID_PARAMS.getCode()); + } + + private JsonRpcResponse resp(final String startBlockNumber, final String range) { + return method.response( + new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + RpcMethod.ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2.getMethodName(), + new Object[] {startBlockNumber, range}))); + } + + private EngineGetPayloadBodiesResultV2 fromSuccessResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(RpcResponseType.SUCCESS); + return Optional.of(resp) + .map(JsonRpcSuccessResponse.class::cast) + .map(JsonRpcSuccessResponse::getResult) + .map(EngineGetPayloadBodiesResultV2.class::cast) + .get(); + } + + private static BlockAccessList createSampleBlockAccessList() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final StorageSlotKey slotKey = new StorageSlotKey(UInt256.ONE); + final SlotChanges slotChanges = + new SlotChanges(slotKey, List.of(new StorageChange(0, UInt256.valueOf(2)))); + return new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(slotChanges), + List.of(new SlotRead(slotKey)), + List.of(new BalanceChange(0, Wei.ONE)), + List.of(), + List.of()))); + } + + private static String encodeBlockAccessList(final BlockAccessList blockAccessList) { + final var output = new org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput(); + blockAccessList.writeTo(output); + return output.encoded().toHexString(); + } +}